"use strict";
const library = require("./library-r5_XT1LO.cjs");
const z = require("zod");
const stripeBilllingService = require("./stripe-billling-service-x7NIEoOR.cjs");
const Stripe = require("stripe");
const index = require("./orgDomains-W_rFTMBa.cjs");
function getRequestedSlugError(error, requestedSlug) {
  let message = `Unknown error`;
  let statusCode = 500;
  if (error instanceof library.library.PrismaClientKnownRequestError && error.code === "P2002") {
    statusCode = 400;
    message = `It seems like the requestedSlug: '${requestedSlug}' is already taken. Please contact support at help@cal.com so we can resolve this issue.`;
  } else if (error instanceof Error) {
    message = error.message;
  }
  return { message, statusCode };
}
const stripe = globalThis.stripe || new Stripe(process.env.STRIPE_PRIVATE_KEY, {
  apiVersion: "2020-08-27"
});
if (process.env.NODE_ENV !== "production") {
  globalThis.stripe = stripe;
}
const stripe$1 = stripe;
const log$1 = index.logger.getSubLogger({ prefix: ["teams/lib/payments"] });
z.z.object({
  // Redefine paymentId, subscriptionId and subscriptionItemId to ensure that they are present and nonNullable
  paymentId: z.z.string(),
  subscriptionId: z.z.string(),
  subscriptionItemId: z.z.string(),
  orgSeats: index.teamMetadataSchema.unwrap().shape.orgSeats
});
const checkIfTeamPaymentRequired = async ({ teamId = -1 }) => {
  const team = await index.prisma.team.findUniqueOrThrow({
    where: { id: teamId },
    select: { metadata: true }
  });
  const metadata = index.teamMetadataSchema.parse(team.metadata);
  if (!metadata?.paymentId)
    return { url: null };
  const checkoutSession = await stripe$1.checkout.sessions.retrieve(metadata.paymentId);
  if (checkoutSession.payment_status !== "paid")
    return { url: null };
  return { url: `${index.WEBAPP_URL}/api/teams/${teamId}/upgrade?session_id=${metadata.paymentId}` };
};
const purchaseTeamOrOrgSubscription = async (input) => {
  const {
    teamId,
    seatsToChargeFor,
    seatsUsed,
    userId,
    isOrg,
    pricePerSeat,
    billingPeriod = index.BillingPeriod.MONTHLY
  } = input;
  const { url } = await checkIfTeamPaymentRequired({ teamId });
  if (url)
    return { url };
  const seats = isOrg ? Math.max(seatsUsed, index.MINIMUM_NUMBER_OF_ORG_SEATS) : seatsUsed;
  const quantity = seatsToChargeFor ? seatsToChargeFor : seats;
  const customer = await stripeBilllingService.getStripeCustomerIdFromUserId(userId);
  const fixedPrice = await getFixedPrice();
  let priceId;
  if (pricePerSeat) {
    if (isOrg && pricePerSeat === index.ORGANIZATION_SELF_SERVE_PRICE && seats === index.ORGANIZATION_SELF_SERVE_MIN_SEATS) {
      priceId = fixedPrice;
    } else {
      const customPriceObj = await getPriceObject(fixedPrice);
      priceId = await createPrice({
        isOrg: !!isOrg,
        teamId,
        pricePerSeat,
        billingPeriod,
        product: customPriceObj.product,
        // We don't expand the object from stripe so just use the product as ID
        currency: customPriceObj.currency
      });
    }
  } else {
    priceId = fixedPrice;
  }
  const session = await stripe$1.checkout.sessions.create({
    customer,
    mode: "subscription",
    allow_promotion_codes: true,
    success_url: `${index.WEBAPP_URL}/api/teams/${teamId}/upgrade?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${index.WEBAPP_URL}/settings/my-account/profile`,
    line_items: [
      {
        price: priceId,
        quantity
      }
    ],
    customer_update: {
      address: "auto"
    },
    // Disabled when testing locally as usually developer doesn't setup Tax in Stripe Test mode
    automatic_tax: {
      enabled: index.IS_PRODUCTION
    },
    metadata: {
      teamId
    },
    subscription_data: {
      metadata: {
        teamId,
        dubCustomerId: userId
      }
    }
  });
  return { url: session.url };
  async function createPrice({
    isOrg: isOrg2,
    teamId: teamId2,
    pricePerSeat: pricePerSeat2,
    billingPeriod: billingPeriod2,
    product,
    currency
  }) {
    try {
      const pricePerSeatInCents = pricePerSeat2 * 100;
      const occurrence = billingPeriod2 === "MONTHLY" ? 1 : 12;
      const yearlyPrice = pricePerSeatInCents * occurrence;
      const customPriceObj = await stripe$1.prices.create({
        nickname: `Custom price for ${isOrg2 ? "Organization" : "Team"} ID: ${teamId2}`,
        unit_amount: yearlyPrice,
        // Stripe expects the amount in cents
        // Use the same currency as in the fixed price to avoid hardcoding it.
        currency,
        recurring: { interval: billingPeriod2 === "MONTHLY" ? "month" : "year" },
        // Define your subscription interval
        product: typeof product === "string" ? product : product.id,
        tax_behavior: "exclusive"
      });
      return customPriceObj.id;
    } catch (e) {
      log$1.error(
        `Error creating custom price for ${isOrg2 ? "Organization" : "Team"} ID: ${teamId2}`,
        index.safeStringify(e)
      );
      throw new Error("Error in creation of custom price");
    }
  }
  async function getFixedPrice() {
    const fixedPriceId = isOrg ? process.env.STRIPE_ORG_MONTHLY_PRICE_ID : process.env.STRIPE_TEAM_MONTHLY_PRICE_ID;
    if (!fixedPriceId) {
      throw new Error(
        "You need to have STRIPE_ORG_MONTHLY_PRICE_ID and STRIPE_TEAM_MONTHLY_PRICE_ID env variables set"
      );
    }
    log$1.debug("Getting price ID", index.safeStringify({ fixedPriceId, isOrg, teamId, pricePerSeat }));
    return fixedPriceId;
  }
};
async function getPriceObject(priceId) {
  const priceObj = await stripe$1.prices.retrieve(priceId);
  if (!priceObj)
    throw new Error(`No price found for ID ${priceId}`);
  return priceObj;
}
const objectKeys = (obj) => Object.keys(obj);
const objectKeys$1 = objectKeys;
function getMetadataHelpers(schema, rawMetadata) {
  const metadata = schema.parse(rawMetadata);
  return {
    metadata,
    get: (key) => metadata[key],
    /** This method prevents overwriting the metadata fields that you don't want to change. */
    mergeMetadata: (newMetadata) => {
      const newMetadataToReturn = { ...metadata, ...newMetadata };
      objectKeys$1(newMetadata).forEach((key) => {
        if (newMetadata[key] === void 0) {
          delete newMetadataToReturn[key];
        }
      });
      return newMetadataToReturn;
    }
  };
}
class Redirect {
  constructor(code, url) {
    this.code = code;
    this.url = url;
  }
}
class OrganizationOnboardingRepository {
  static async create(data) {
    index.logger.debug("Creating organization onboarding", index.safeStringify(data));
    return await index.prisma.organizationOnboarding.create({
      // HEKOP
      data: {
        billingPeriod: data.billingPeriod,
        pricePerSeat: data.pricePerSeat,
        seats: data.seats,
        orgOwnerEmail: data.orgOwnerEmail,
        name: data.name,
        slug: data.slug,
        logo: data.logo,
        bio: data.bio,
        stripeCustomerId: data.stripeCustomerId,
        stripeSubscriptionId: data.stripeSubscriptionId,
        invitedMembers: data.invitedMembers || [],
        teams: data.teams || [],
        createdById: data.createdById
      }
    });
  }
  static async findByStripeCustomerId(stripeCustomerId) {
    index.logger.debug(
      "Finding organization onboarding by stripe customer id",
      index.safeStringify({ stripeCustomerId })
    );
    return await index.prisma.organizationOnboarding.findUnique({
      where: {
        stripeCustomerId
      }
    });
  }
  static async findById(id) {
    index.logger.debug("Finding organization onboarding by id", index.safeStringify({ id }));
    return await index.prisma.organizationOnboarding.findUnique({
      where: {
        id
      }
    });
  }
  static async findByOrgOwnerEmail(email) {
    index.logger.debug("Finding organization onboarding by org owner email", index.safeStringify({ email }));
    return await index.prisma.organizationOnboarding.findUnique({
      where: {
        orgOwnerEmail: email
      }
    });
  }
  // TODO: This method should be moved to OrganizationOnboardingService
  static async markAsComplete(id) {
    index.logger.debug("Marking organization onboarding as complete", { id });
    return await index.prisma.organizationOnboarding.update({
      where: {
        id
      },
      data: {
        error: null,
        isComplete: true
      }
    });
  }
  static async update(id, data) {
    index.logger.debug("Updating organization onboarding", index.safeStringify({ id, data }));
    const { organizationId, createdById: _, ...rest } = data;
    return await index.prisma.organizationOnboarding.update({
      where: {
        id
      },
      data: {
        ...rest,
        ...organizationId ? { organization: { connect: { id: organizationId } } } : {},
        updatedAt: /* @__PURE__ */ new Date()
      }
    });
  }
  static async findByOrganizationId(organizationId) {
    index.logger.debug("Finding organization onboarding by organization id", index.safeStringify({ organizationId }));
    return await index.prisma.organizationOnboarding.findUnique({
      where: {
        organizationId
      }
    });
  }
  static async findAllBySlug(slug) {
    index.logger.debug("Finding all organization onboardings by slug", index.safeStringify({ slug }));
    return await index.prisma.organizationOnboarding.findMany({
      where: {
        slug
      }
    });
  }
  static async delete(id) {
    index.logger.debug("Deleting organization onboarding", { id });
    return await index.prisma.organizationOnboarding.delete({
      where: {
        id
      }
    });
  }
}
class BillingFactory {
  constructor() {
    return new stripeBilllingService.StripeBillingService();
  }
}
const globalForBilling = global;
const billing = globalForBilling.billingService || new BillingFactory();
if (process.env.NODE_ENV !== "production") {
  globalForBilling.billingService = billing;
}
const TeamBillingPublishResponseStatus = {
  REQUIRES_PAYMENT: "REQUIRES_PAYMENT",
  REQUIRES_UPGRADE: "REQUIRES_UPGRADE",
  SUCCESS: "SUCCESS"
};
const log = index.logger.getSubLogger({ prefix: ["TeamBilling"] });
const teamPaymentMetadataSchema = index.teamMetadataSchema.unwrap();
class InternalTeamBilling {
  constructor(team) {
    this.team = team;
  }
  set team(team) {
    const metadata = teamPaymentMetadataSchema.parse(team.metadata || {});
    this._team = { ...team, metadata };
  }
  get team() {
    return this._team;
  }
  async getOrgIfNeeded() {
    if (!this.team.parentId)
      return;
    const parentTeam = await index.prisma.team.findUniqueOrThrow({
      where: { id: this.team.parentId },
      select: { metadata: true, id: true, parentId: true, isOrganization: true }
    });
    this.team = parentTeam;
  }
  logErrorFromUnknown(error) {
    let message = "Unknown error on InternalTeamBilling.";
    if (error instanceof Error)
      message = error.message;
    log.error(message);
  }
  async cancel() {
    try {
      const { subscriptionId } = this.team.metadata;
      log.info(`Cancelling subscription ${subscriptionId} for team ${this.team.id}`);
      if (!subscriptionId)
        throw Error("missing subscriptionId");
      await billing.handleSubscriptionCancel(subscriptionId);
      await this.downgrade();
      log.info(`Cancelled subscription ${subscriptionId} for team ${this.team.id}`);
    } catch (error) {
      this.logErrorFromUnknown(error);
    }
  }
  // New teams are published on creation, this is for backwards compatibility
  async publish() {
    const { url } = await this.checkIfTeamPaymentRequired();
    const teamId = this.team.id;
    if (url) {
      return { redirectUrl: url, status: TeamBillingPublishResponseStatus.REQUIRES_UPGRADE };
    }
    const requestedSlug = this.team.metadata?.requestedSlug || "";
    const membershipCount = await index.prisma.membership.count({ where: { teamId } });
    const owner = await index.prisma.membership.findFirstOrThrow({
      where: { teamId, role: "OWNER" },
      select: {
        userId: true
      }
    });
    try {
      const checkoutSession = await purchaseTeamOrOrgSubscription({
        teamId,
        seatsUsed: membershipCount,
        userId: owner.userId,
        pricePerSeat: null
      });
      if (checkoutSession.url) {
        return {
          redirectUrl: checkoutSession.url,
          status: TeamBillingPublishResponseStatus.REQUIRES_PAYMENT
        };
      }
      const { mergeMetadata } = getMetadataHelpers(teamPaymentMetadataSchema, this.team.metadata);
      const data = {
        metadata: mergeMetadata({ requestedSlug: void 0 })
      };
      if (requestedSlug)
        data.slug = requestedSlug;
      await index.prisma.team.update({ where: { id: teamId }, data });
      return { status: TeamBillingPublishResponseStatus.SUCCESS, redirectUrl: null };
    } catch (error) {
      if (error instanceof Redirect)
        throw error;
      const { message } = getRequestedSlugError(error, requestedSlug);
      throw Error(message);
    }
  }
  async downgrade() {
    try {
      const { mergeMetadata } = getMetadataHelpers(teamPaymentMetadataSchema, this.team.metadata);
      const metadata = mergeMetadata({
        paymentId: void 0,
        subscriptionId: void 0,
        subscriptionItemId: void 0
      });
      await index.prisma.team.update({ where: { id: this.team.id }, data: { metadata } });
      log.info(`Downgraded team ${this.team.id}`);
    } catch (error) {
      this.logErrorFromUnknown(error);
    }
  }
  async updateQuantity() {
    try {
      await this.getOrgIfNeeded();
      const { id: teamId, metadata, isOrganization } = this.team;
      const { url } = await this.checkIfTeamPaymentRequired();
      const organizationOnboarding = await OrganizationOnboardingRepository.findByOrganizationId(
        this.team.id
      );
      log.debug("updateQuantity", index.safeStringify({ url, team: this.team }));
      if (!url && !isOrganization)
        return;
      const { subscriptionId, subscriptionItemId, orgSeats } = metadata;
      const orgMinimumSubscriptionQuantity = orgSeats || index.MINIMUM_NUMBER_OF_ORG_SEATS;
      const membershipCount = await index.prisma.membership.count({ where: { teamId } });
      if (isOrganization && membershipCount < orgMinimumSubscriptionQuantity) {
        log.info(
          `Org ${teamId} has less members than the min required ${orgMinimumSubscriptionQuantity}, skipping updating subscription.`
        );
        return;
      }
      if (!subscriptionId)
        throw Error("missing subscriptionId");
      if (!subscriptionItemId)
        throw Error("missing subscriptionItemId");
      await billing.handleSubscriptionUpdate({ subscriptionId, subscriptionItemId, membershipCount });
      log.info(`Updated subscription ${subscriptionId} for team ${teamId} to ${membershipCount} seats.`);
    } catch (error) {
      this.logErrorFromUnknown(error);
    }
  }
  /** Used to prevent double charges for the same team */
  async checkIfTeamPaymentRequired() {
    const { paymentId } = this.team.metadata || {};
    if (!paymentId)
      return { url: null, paymentId: null, paymentRequired: true };
    const checkoutSessionIsPaid = await billing.checkoutSessionIsPaid(paymentId);
    if (!checkoutSessionIsPaid)
      return { url: null, paymentId, paymentRequired: true };
    return {
      url: `${index.WEBAPP_URL}/api/teams/${this.team.id}/upgrade?session_id=${paymentId}`,
      paymentId,
      paymentRequired: false
    };
  }
  /** Returns the subscription status (active, past_due, trialing, ...) */
  async getSubscriptionStatus() {
    const { subscriptionId } = this.team.metadata;
    if (!subscriptionId)
      return null;
    return await billing.getSubscriptionStatus(subscriptionId);
  }
  /**
   * Ends the trial period for a team subscription by converting it to a regular subscription
   * @returns {Promise<boolean>} True if successful, false otherwise
   */
  async endTrial() {
    try {
      const { subscriptionId } = this.team.metadata;
      log.info(`Ending trial for subscription ${subscriptionId} of team ${this.team.id}`);
      if (!subscriptionId) {
        log.warn(`No subscription ID found for team ${this.team.id}`);
        return false;
      }
      await billing.handleEndTrial(subscriptionId);
      log.info(`Successfully ended trial for team ${this.team.id}`);
      return true;
    } catch (error) {
      this.logErrorFromUnknown(error);
      return false;
    }
  }
}
exports.InternalTeamBilling = InternalTeamBilling;
