"use strict";
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const index = require("./orgDomains-W_rFTMBa.cjs");
const index$1 = require("./email-manager-w-Fd9mb6.cjs");
const stripeBilllingService = require("./stripe-billling-service-x7NIEoOR.cjs");
const internalTeamBilling = require("./internal-team-billing-k5IeYFRN.cjs");
const reminderScheduler = require("./reminderScheduler-ikn0HaU9.cjs");
const dayjs = require("dayjs");
const membership = require("./membership-fZvsiGJr.cjs");
const team = require("./team-HqS6vjRW.cjs");
class CreditsRepository {
  static async findCreditBalance({ teamId, userId }, tx) {
    const prismaClient = tx ?? index.prisma;
    const select = {
      id: true,
      additionalCredits: true,
      limitReachedAt: true,
      warningSentAt: true
    };
    if (teamId) {
      return await prismaClient.creditBalance.findUnique({
        where: {
          teamId
        },
        select
      });
    }
    if (userId) {
      return await prismaClient.creditBalance.findUnique({
        where: { userId },
        select
      });
    }
  }
  static async findCreditBalanceWithTeamOrUser({
    teamId,
    userId
  }, tx) {
    const prismaClient = tx ?? index.prisma;
    const select = {
      id: true,
      additionalCredits: true,
      limitReachedAt: true,
      warningSentAt: true,
      team: {
        select: {
          id: true,
          name: true,
          members: {
            select: {
              user: {
                select: {
                  id: true,
                  name: true,
                  email: true,
                  locale: true
                }
              }
            }
          }
        }
      },
      user: {
        select: {
          id: true,
          name: true,
          email: true,
          locale: true
        }
      }
    };
    if (teamId) {
      return await prismaClient.creditBalance.findUnique({
        where: {
          teamId
        },
        select
      });
    }
    if (userId) {
      return await prismaClient.creditBalance.findUnique({
        where: { userId },
        select
      });
    }
  }
  static async findCreditBalanceWithExpenseLogs({
    teamId,
    userId,
    startDate = dayjs().startOf("month").toDate(),
    endDate = /* @__PURE__ */ new Date(),
    creditType
  }, tx) {
    if (!teamId && !userId)
      return null;
    const prismaClient = tx ?? index.prisma;
    return await prismaClient.creditBalance.findUnique({
      where: {
        teamId,
        ...!teamId ? { userId } : {}
      },
      select: {
        additionalCredits: true,
        expenseLogs: {
          where: {
            date: {
              gte: startDate,
              lte: endDate
            },
            ...creditType ? { creditType } : {}
          },
          orderBy: {
            date: "desc"
          },
          select: {
            date: true,
            credits: true,
            creditType: true,
            bookingUid: true,
            smsSid: true,
            smsSegments: true
          }
        }
      }
    });
  }
  static async updateCreditBalance({
    id,
    teamId,
    userId,
    data
  }, tx) {
    const prismaClient = tx ?? index.prisma;
    if (id) {
      return prismaClient.creditBalance.update({
        where: { id },
        data
      });
    }
    if (teamId) {
      return prismaClient.creditBalance.update({
        where: { teamId },
        data
      });
    }
    if (userId) {
      return prismaClient.creditBalance.update({
        where: { userId },
        data
      });
    }
    return null;
  }
  static async createCreditBalance(data, tx) {
    const { teamId, userId } = data;
    if (!teamId && !userId) {
      throw new Error("Team or user ID is required");
    }
    return (tx ?? index.prisma).creditBalance.create({
      data: {
        ...data,
        ...!teamId ? { userId } : {}
      }
    });
  }
  static async createCreditExpenseLog(data, tx) {
    return (tx ?? index.prisma).creditExpenseLog.create({
      data
    });
  }
  static async createCreditPurchaseLog(data) {
    const { credits, creditBalanceId } = data;
    return index.prisma.creditPurchaseLog.create({
      data: {
        credits,
        creditBalanceId
      }
    });
  }
}
const log = index.logger.getSubLogger({ prefix: ["[CreditService]"] });
class CreditService {
  async chargeCredits({
    userId,
    teamId,
    credits,
    bookingUid,
    smsSid,
    smsSegments
  }) {
    return await index.prisma.$transaction(async (tx) => {
      let teamIdToCharge = credits === 0 && teamId ? teamId : void 0;
      let creditType = index.CreditType.ADDITIONAL;
      let remainingCredits;
      let userIdToCharge;
      if (!teamIdToCharge) {
        const result = await this._getUserOrTeamToCharge({
          credits: credits ?? 1,
          // if we don't have exact credits, we check for at east 1 credit available
          userId,
          teamId,
          tx
        });
        teamIdToCharge = result?.teamId;
        userIdToCharge = result?.userId;
        creditType = result?.creditType ?? creditType;
        remainingCredits = result?.remainingCredits;
      }
      if (!teamIdToCharge && !userIdToCharge) {
        log.error("No team or user found to charge. No credit expense log created");
        return null;
      }
      await this._createExpenseLog({
        bookingUid,
        smsSid,
        teamId: teamIdToCharge,
        userId: userIdToCharge,
        credits,
        creditType,
        smsSegments,
        tx
      });
      let lowCreditBalanceResult = null;
      if (credits) {
        lowCreditBalanceResult = await this._handleLowCreditBalance({
          teamId: teamIdToCharge,
          userId: userIdToCharge,
          remainingCredits: remainingCredits ?? 0,
          tx
        });
      }
      return {
        teamId: teamIdToCharge,
        userId: userIdToCharge,
        lowCreditBalanceResult
      };
    }).then(async (result) => {
      if (result?.lowCreditBalanceResult) {
        await this._handleLowCreditBalanceResult(result.lowCreditBalanceResult);
      }
      return {
        teamId: result?.teamId,
        userId: result?.userId
      };
    });
  }
  /*
    also returns true if team has no available credits but limitReachedAt is not yet set
  */
  async hasAvailableCredits({ userId, teamId }) {
    return await index.prisma.$transaction(async (tx) => {
      if (!index.IS_SMS_CREDITS_ENABLED)
        return true;
      if (teamId) {
        const creditBalance = await CreditsRepository.findCreditBalance({ teamId }, tx);
        const limitReached = creditBalance?.limitReachedAt && dayjs(creditBalance.limitReachedAt).isAfter(dayjs().startOf("month"));
        if (!limitReached)
          return true;
        const teamCredits = await this._getAllCreditsForTeam({ teamId, tx });
        const availableCredits = teamCredits.totalRemainingMonthlyCredits + teamCredits.additionalCredits;
        if (availableCredits > 0) {
          await CreditsRepository.updateCreditBalance(
            {
              teamId,
              data: {
                limitReachedAt: null,
                warningSentAt: null
              }
            },
            tx
          );
          return true;
        }
        return false;
      }
      if (userId) {
        const teamWithAvailableCredits = await this._getTeamWithAvailableCredits({ userId, tx });
        if (teamWithAvailableCredits && !teamWithAvailableCredits.limitReached)
          return true;
        const userCredits = await this._getAllCredits({ userId, tx });
        return userCredits.additionalCredits > 0;
      }
      return false;
    });
  }
  async getTeamWithAvailableCredits(userId) {
    return index.prisma.$transaction(async (tx) => {
      return this._getTeamWithAvailableCredits({ userId, tx });
    });
  }
  /*
    If user has memberships, it always returns a team, even if all have limit reached. In that case, limitReached: true is returned
  */
  async _getTeamWithAvailableCredits({ userId, tx }) {
    const memberships = await membership.MembershipRepository.findAllAcceptedPublishedTeamMemberships(userId, tx);
    if (!memberships || memberships.length === 0) {
      return null;
    }
    for (const membership2 of memberships) {
      const creditBalance = await CreditsRepository.findCreditBalance({ teamId: membership2.teamId }, tx);
      const allCredits = await this._getAllCreditsForTeam({ teamId: membership2.teamId, tx });
      const limitReached = creditBalance?.limitReachedAt && dayjs(creditBalance.limitReachedAt).isAfter(dayjs().startOf("month"));
      const availableCredits = allCredits.totalRemainingMonthlyCredits + allCredits.additionalCredits;
      if (!limitReached || availableCredits > 0) {
        if (limitReached) {
          await CreditsRepository.updateCreditBalance(
            {
              teamId: membership2.teamId,
              data: {
                limitReachedAt: null,
                warningSentAt: null
              }
            },
            tx
          );
        }
        return {
          teamId: membership2.teamId,
          availableCredits,
          creditType: allCredits.totalRemainingMonthlyCredits > 0 ? index.CreditType.MONTHLY : index.CreditType.ADDITIONAL
        };
      }
    }
    return {
      teamId: memberships[0].teamId,
      availableCredits: 0,
      creditType: index.CreditType.ADDITIONAL,
      limitReached: true
    };
  }
  /*
    always returns a team, even if all teams are out of credits
  */
  async getUserOrTeamToCharge({
    credits,
    userId,
    teamId
  }) {
    return index.prisma.$transaction(async (tx) => {
      return this._getUserOrTeamToCharge({ credits, userId, teamId, tx });
    });
  }
  async _getUserOrTeamToCharge({
    credits,
    userId,
    teamId,
    tx
  }) {
    if (teamId) {
      const teamCredits = await this._getAllCreditsForTeam({ teamId, tx });
      const remaningMonthlyCredits = teamCredits.totalRemainingMonthlyCredits > 0 ? teamCredits.totalRemainingMonthlyCredits : 0;
      return {
        teamId,
        remainingCredits: remaningMonthlyCredits + teamCredits.additionalCredits - credits,
        creditType: remaningMonthlyCredits > 0 ? index.CreditType.MONTHLY : index.CreditType.ADDITIONAL
      };
    }
    if (userId) {
      const team2 = await this._getTeamWithAvailableCredits({ userId, tx });
      if (team2) {
        return { ...team2, remainingCredits: team2.availableCredits - credits };
      }
      const userCredits = await this._getAllCredits({ userId, tx });
      return {
        userId,
        remainingCredits: userCredits.additionalCredits - credits,
        creditType: index.CreditType.ADDITIONAL
      };
    }
    return null;
  }
  async _createExpenseLog(props) {
    const { credits, creditType, bookingUid, smsSid, teamId, userId, smsSegments, tx } = props;
    let creditBalance = await CreditsRepository.findCreditBalance({ teamId, userId }, tx);
    if (!creditBalance) {
      creditBalance = await CreditsRepository.createCreditBalance(
        {
          teamId,
          userId
        },
        tx
      );
    }
    if (credits && creditType === index.CreditType.ADDITIONAL) {
      const decrementValue = credits <= creditBalance.additionalCredits ? credits : creditBalance.additionalCredits;
      await CreditsRepository.updateCreditBalance(
        {
          id: creditBalance.id,
          data: {
            additionalCredits: {
              decrement: decrementValue
            }
          }
        },
        tx
      );
    }
    if (creditBalance) {
      await CreditsRepository.createCreditExpenseLog(
        {
          creditBalanceId: creditBalance.id,
          credits,
          creditType,
          date: /* @__PURE__ */ new Date(),
          bookingUid,
          smsSid,
          smsSegments
        },
        tx
      );
    }
  }
  /*
  Called when we know the exact amount of credits to be charged:
  - Sets `limitReachedAt` and `warningSentAt`
  - Sends warning email if balance is low
  - Sends limit reached email
  - cancels all already scheduled SMS (from the next two hours)
  */
  async _handleLowCreditBalance({
    teamId,
    userId,
    remainingCredits,
    tx
  }) {
    let warningLimit = 0;
    if (teamId) {
      const { totalMonthlyCredits } = await this._getAllCreditsForTeam({ teamId, tx });
      warningLimit = totalMonthlyCredits * 0.2;
    } else if (userId) {
      const billingService = new stripeBilllingService.StripeBillingService();
      const teamMonthlyPrice = await billingService.getPrice(process.env.STRIPE_TEAM_MONTHLY_PRICE_ID || "");
      const pricePerSeat = teamMonthlyPrice.unit_amount ?? 0;
      warningLimit = pricePerSeat / 2 * 0.2;
    }
    if (remainingCredits < warningLimit) {
      const creditBalance = await CreditsRepository.findCreditBalanceWithTeamOrUser({ teamId, userId }, tx);
      if (creditBalance?.limitReachedAt && (!teamId || dayjs(creditBalance?.limitReachedAt).isAfter(dayjs().startOf("month")))) {
        return null;
      }
      const teamWithAdmins = creditBalance?.team ? {
        ...creditBalance.team,
        adminAndOwners: await Promise.all(
          creditBalance.team.members.map(async (member) => ({
            id: member.user.id,
            name: member.user.name,
            email: member.user.email,
            t: await index.getTranslation(member.user.locale ?? "en", "common")
          }))
        )
      } : void 0;
      const user = creditBalance?.user ? {
        ...creditBalance.user,
        t: await index.getTranslation(creditBalance.user.locale ?? "en", "common")
      } : void 0;
      if ((!teamWithAdmins || !teamWithAdmins.adminAndOwners?.length) && !user) {
        log.error("Team or user not found to send warning email");
        return null;
      }
      if (remainingCredits <= 0) {
        await CreditsRepository.updateCreditBalance(
          {
            teamId,
            userId,
            data: {
              limitReachedAt: /* @__PURE__ */ new Date(),
              warningSentAt: null
            }
          },
          tx
        );
        return {
          type: "LIMIT_REACHED",
          team: teamWithAdmins,
          user,
          teamId,
          userId
        };
      }
      if (creditBalance?.warningSentAt && (!teamId || dayjs(creditBalance?.warningSentAt).isAfter(dayjs().startOf("month")))) {
        return null;
      }
      await CreditsRepository.updateCreditBalance(
        {
          teamId,
          userId,
          data: {
            warningSentAt: /* @__PURE__ */ new Date()
          }
        },
        tx
      );
      return {
        type: "WARNING",
        balance: remainingCredits,
        team: teamWithAdmins,
        user
      };
    }
    await CreditsRepository.updateCreditBalance(
      {
        teamId,
        userId,
        data: {
          warningSentAt: null,
          limitReachedAt: null
        }
      },
      tx
    );
    return null;
  }
  async _handleLowCreditBalanceResult(result) {
    if (!result)
      return;
    try {
      if (result.type === "LIMIT_REACHED") {
        await Promise.all([
          index$1.sendCreditBalanceLimitReachedEmails({
            team: result.team,
            user: result.user
          }).catch((error) => {
            log.error("Failed to send credit limit reached email", error, { result });
          }),
          reminderScheduler.cancelScheduledMessagesAndScheduleEmails({ teamId: result.teamId, userId: result.userId }).catch(
            (error) => {
              log.error("Failed to cancel scheduled messages", error, { result });
            }
          )
        ]);
      } else if (result.type === "WARNING") {
        await index$1.sendCreditBalanceLowWarningEmails({
          balance: result.balance,
          team: result.team,
          user: result.user
        }).catch((error) => {
          log.error("Failed to send credit warning email", error, { result });
        });
      }
    } catch (error) {
      log.error("Unexpected error handling low credit balance result", error, { result });
    }
  }
  async handleLowCreditBalance({
    teamId,
    userId,
    remainingCredits
  }) {
    return index.prisma.$transaction(async (tx) => {
      const result = await this._handleLowCreditBalance({ teamId, userId, remainingCredits, tx });
      return result;
    }).then(async (result) => {
      await this._handleLowCreditBalanceResult(result);
    });
  }
  async getMonthlyCredits(teamId) {
    const teamRepo = new team.TeamRepository(index.prisma);
    const team$1 = await teamRepo.findTeamWithMembers(teamId);
    if (!team$1)
      return 0;
    let totalMonthlyCredits = 0;
    const teamBillingService = new internalTeamBilling.InternalTeamBilling(team$1);
    const subscriptionStatus = await teamBillingService.getSubscriptionStatus();
    if (subscriptionStatus !== "active" && subscriptionStatus !== "past_due") {
      return 0;
    }
    const activeMembers = team$1.members.filter((member) => member.accepted).length;
    const billingService = new stripeBilllingService.StripeBillingService();
    const teamMonthlyPrice = await billingService.getPrice(process.env.STRIPE_TEAM_MONTHLY_PRICE_ID || "");
    const pricePerSeat = teamMonthlyPrice.unit_amount ?? 0;
    totalMonthlyCredits = activeMembers * pricePerSeat / 2;
    return totalMonthlyCredits;
  }
  calculateCreditsFromPrice(price) {
    const twilioPrice = price;
    const priceWithMarkUp = twilioPrice * 1.8;
    const credits = Math.ceil(priceWithMarkUp * 100);
    return credits || null;
  }
  async getAllCredits({ userId, teamId }) {
    return index.prisma.$transaction(async (tx) => {
      return this._getAllCredits({ userId, teamId, tx });
    });
  }
  async _getAllCredits({
    userId,
    teamId,
    tx
  }) {
    if (teamId) {
      return this._getAllCreditsForTeam({ teamId, tx });
    }
    if (userId) {
      const creditBalance = await CreditsRepository.findCreditBalance({ userId }, tx);
      return {
        totalMonthlyCredits: 0,
        totalRemainingMonthlyCredits: 0,
        additionalCredits: creditBalance?.additionalCredits ?? 0
      };
    }
    return {
      totalMonthlyCredits: 0,
      totalRemainingMonthlyCredits: 0,
      additionalCredits: 0
    };
  }
  async getAllCreditsForTeam(teamId) {
    return index.prisma.$transaction(async (tx) => {
      return this._getAllCreditsForTeam({ teamId, tx });
    });
  }
  async _getAllCreditsForTeam({ teamId, tx }) {
    const creditBalance = await CreditsRepository.findCreditBalanceWithExpenseLogs(
      { teamId, creditType: index.CreditType.MONTHLY },
      tx
    );
    const totalMonthlyCredits = await this.getMonthlyCredits(teamId);
    const totalMonthlyCreditsUsed = creditBalance?.expenseLogs.reduce((sum, log2) => sum + (log2?.credits ?? 0), 0) || 0;
    return {
      totalMonthlyCredits,
      totalRemainingMonthlyCredits: Math.max(totalMonthlyCredits - totalMonthlyCreditsUsed, 0),
      additionalCredits: creditBalance?.additionalCredits ?? 0
    };
  }
}
exports.CreditService = CreditService;
