@goweekdays/core 2.11.10 → 2.11.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @goweekdays/core
2
2
 
3
+ ## 2.11.12
4
+
5
+ ### Patch Changes
6
+
7
+ - ab6a66f: Update dependencies
8
+
9
+ ## 2.11.11
10
+
11
+ ### Patch Changes
12
+
13
+ - ae3339b: Refactor and simplify subscription fee computation logic
14
+
3
15
  ## 2.11.10
4
16
 
5
17
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -930,7 +930,7 @@ declare function useSubscriptionService(): {
930
930
  promoCode?: string;
931
931
  org: string;
932
932
  plan: string;
933
- }, skipProration?: boolean) => Promise<{
933
+ }) => Promise<{
934
934
  monthlyAmount: number;
935
935
  proratedAmount: number;
936
936
  subscriptionAmount: number;
package/dist/index.js CHANGED
@@ -6020,7 +6020,7 @@ var schemaSubscriptionTransaction = import_joi22.default.object({
6020
6020
  "promo-expired",
6021
6021
  "promo-updated"
6022
6022
  ).required(),
6023
- description: import_joi22.default.string().max(255).optional().allow("", null),
6023
+ description: import_joi22.default.string().optional().allow("", null),
6024
6024
  createdBy: import_joi22.default.string().hex().length(24).required(),
6025
6025
  createdByName: import_joi22.default.string().optional().allow("", null)
6026
6026
  });
@@ -8417,147 +8417,114 @@ function useSubscriptionService() {
8417
8417
  }
8418
8418
  return total;
8419
8419
  }
8420
- async function computeFee(value, skipProration) {
8420
+ function computeMonthlyAmount(seats, planPrice, promo) {
8421
+ if (!promo)
8422
+ return planPrice * seats;
8423
+ const limit = promo.seats && promo.seats > 0 ? promo.seats : Infinity;
8424
+ switch (promo.type) {
8425
+ case "fixed":
8426
+ return Math.min(seats, limit) * (promo.fixedRate ?? 0) + Math.max(seats - limit, 0) * planPrice;
8427
+ case "flat":
8428
+ return (promo.flatRate ?? 0) + Math.max(0, seats - limit) * planPrice;
8429
+ case "volume":
8430
+ return promo.tiers?.length ? calculateVolumeTierAmount(promo.tiers, 1, seats, planPrice) : planPrice * seats;
8431
+ default:
8432
+ return planPrice * seats;
8433
+ }
8434
+ }
8435
+ function computeAdditionalSeatAmount(additionalSeats, planPrice, promo, paidSeats) {
8436
+ if (additionalSeats <= 0)
8437
+ return 0;
8438
+ if (!promo) {
8439
+ return additionalSeats * planPrice;
8440
+ }
8441
+ const limit = promo.seats && promo.seats > 0 ? promo.seats : Infinity;
8442
+ switch (promo.type) {
8443
+ case "flat": {
8444
+ const chargeableSeats = Math.max(0, paidSeats + additionalSeats - limit) - Math.max(0, paidSeats - limit);
8445
+ return chargeableSeats * planPrice;
8446
+ }
8447
+ case "fixed": {
8448
+ const promoSeatsLeft = Math.max(0, limit - paidSeats);
8449
+ const promoSeats = Math.min(additionalSeats, promoSeatsLeft);
8450
+ const normalSeats = additionalSeats - promoSeats;
8451
+ return promoSeats * (promo.fixedRate ?? 0) + normalSeats * planPrice;
8452
+ }
8453
+ case "volume":
8454
+ return promo.tiers?.length ? calculateVolumeTierAmount(
8455
+ promo.tiers,
8456
+ paidSeats + 1,
8457
+ additionalSeats,
8458
+ planPrice
8459
+ ) : additionalSeats * planPrice;
8460
+ default:
8461
+ return additionalSeats * planPrice;
8462
+ }
8463
+ }
8464
+ function computeProration(subscription, seats, plan, promo) {
8465
+ const additionalSeats = Math.max(0, seats - subscription.paidSeats);
8466
+ if (additionalSeats === 0)
8467
+ return 0;
8468
+ const today = /* @__PURE__ */ new Date();
8469
+ const nextBilling = new Date(subscription.nextBillingDate);
8470
+ const daysRemaining = Math.max(
8471
+ 0,
8472
+ (nextBilling.getTime() - today.getTime()) / 864e5
8473
+ );
8474
+ const cycleDays = plan.billingCycle === "yearly" ? 365 : 30;
8475
+ if (daysRemaining === 0)
8476
+ return 0;
8477
+ const additionalAmount = computeAdditionalSeatAmount(
8478
+ additionalSeats,
8479
+ plan.price,
8480
+ promo,
8481
+ subscription.paidSeats
8482
+ );
8483
+ return additionalAmount / cycleDays * daysRemaining;
8484
+ }
8485
+ async function computeFee(value) {
8421
8486
  const { error } = schemaSubscriptionCompute.validate(value);
8422
8487
  if (error) {
8423
8488
  throw new import_utils44.BadRequestError(`Invalid subscription data: ${error.message}`);
8424
8489
  }
8425
8490
  const existingSubscription = await getByOrg(value.org);
8426
8491
  const plan = await getPlanById(value.plan);
8427
- if (!plan) {
8492
+ if (!plan)
8428
8493
  throw new import_utils44.BadRequestError("Plan not found.");
8494
+ const isNew = !existingSubscription;
8495
+ const isPromoChange = !!existingSubscription && value.promoCode !== void 0 && value.promoCode !== existingSubscription.promoCode;
8496
+ const isSeatIncrease = !!existingSubscription && value.seats > existingSubscription.seats;
8497
+ if (isPromoChange && isSeatIncrease) {
8498
+ throw new import_utils44.BadRequestError(
8499
+ "Cannot change promo code while increasing seats. Perform actions separately."
8500
+ );
8429
8501
  }
8430
- const org = await getOrgById(value.org);
8431
- if (!org) {
8432
- throw new import_utils44.BadRequestError("Organization not found.");
8433
- }
8434
- let promo = null;
8435
- const promoCode = value.promoCode;
8436
- if (promoCode) {
8437
- promo = await getPromoByCode(promoCode);
8438
- if (!promo) {
8439
- throw new import_utils44.BadRequestError("Promo code not found.");
8440
- }
8441
- if (promo.usage && promo.usage > 0) {
8442
- const currentUsageCount = await countByPromoId(
8443
- promo._id?.toString() ?? ""
8444
- );
8445
- const isAlreadyUsingPromo = existingSubscription?.promoCode === promoCode;
8446
- if (!isAlreadyUsingPromo && currentUsageCount >= promo.usage) {
8447
- throw new import_utils44.BadRequestError(
8448
- "Promo code has reached its maximum usage limit."
8449
- );
8450
- }
8451
- }
8452
- }
8453
- let monthlyAmount = plan.price * value.seats;
8454
- if (promo) {
8455
- const promoSeatLimit = promo.seats && promo.seats > 0 ? promo.seats : Infinity;
8456
- switch (promo.type) {
8457
- case "fixed": {
8458
- const promoSeats = Math.min(value.seats, promoSeatLimit);
8459
- const standardSeats = Math.max(value.seats - promoSeatLimit, 0);
8460
- monthlyAmount = Math.max(promo.fixedRate ?? 0, 0) * promoSeats + plan.price * standardSeats;
8461
- break;
8462
- }
8463
- case "flat": {
8464
- const standardSeats = Math.max(value.seats - promoSeatLimit, 0);
8465
- monthlyAmount = (promo.flatRate ?? 0) + plan.price * standardSeats;
8466
- break;
8467
- }
8468
- case "volume": {
8469
- if (promo.tiers && promo.tiers.length > 0) {
8470
- monthlyAmount = calculateVolumeTierAmount(
8471
- promo.tiers,
8472
- 1,
8473
- value.seats,
8474
- plan.price
8475
- );
8476
- }
8477
- break;
8478
- }
8479
- default:
8480
- monthlyAmount = plan.price * value.seats;
8481
- }
8482
- }
8502
+ const effectivePromoCode = isPromoChange ? value.promoCode : existingSubscription?.promoCode;
8503
+ const promo = effectivePromoCode ? await getPromoByCode(effectivePromoCode) : null;
8504
+ const monthlyAmount = computeMonthlyAmount(value.seats, plan.price, promo);
8483
8505
  let proratedAmount = 0;
8484
- let daysRemaining = 0;
8485
- let totalDaysInCycle = 30;
8486
- if (existingSubscription) {
8487
- const today = /* @__PURE__ */ new Date();
8488
- const nextBillingDate = new Date(existingSubscription.nextBillingDate);
8489
- const timeDiff = nextBillingDate.getTime() - today.getTime();
8490
- daysRemaining = Math.max(0, Math.ceil(timeDiff / (1e3 * 60 * 60 * 24)));
8491
- if (plan.billingCycle === "yearly") {
8492
- totalDaysInCycle = 365;
8493
- }
8494
- daysRemaining = Math.min(daysRemaining, totalDaysInCycle);
8495
- const daysElapsed = totalDaysInCycle - daysRemaining;
8496
- const additionalSeats = Math.max(
8497
- 0,
8498
- value.seats - existingSubscription.paidSeats
8506
+ if (existingSubscription && isSeatIncrease && !isPromoChange) {
8507
+ proratedAmount = computeProration(
8508
+ existingSubscription,
8509
+ value.seats,
8510
+ plan,
8511
+ promo
8499
8512
  );
8500
- if (additionalSeats > 0 && daysRemaining > 0 && daysElapsed > 0 && !skipProration) {
8501
- let additionalSeatsAmount = 0;
8502
- if (promo?.type === "volume" && promo.tiers && promo.tiers.length > 0) {
8503
- additionalSeatsAmount = calculateVolumeTierAmount(
8504
- promo.tiers,
8505
- existingSubscription.paidSeats + 1,
8506
- additionalSeats,
8507
- plan.price
8508
- );
8509
- } else if (promo?.type === "fixed") {
8510
- const promoSeatLimit = promo.seats && promo.seats > 0 ? promo.seats : Infinity;
8511
- const startSeat = existingSubscription.paidSeats + 1;
8512
- const endSeat = value.seats;
8513
- const promoSeatsInRange = Math.max(
8514
- 0,
8515
- Math.min(promoSeatLimit, endSeat) - startSeat + 1
8516
- );
8517
- const standardSeatsInRange = Math.max(
8518
- 0,
8519
- additionalSeats - promoSeatsInRange
8520
- );
8521
- additionalSeatsAmount = (promo.fixedRate ?? 0) * promoSeatsInRange + plan.price * standardSeatsInRange;
8522
- } else if (promo?.type === "flat") {
8523
- const promoSeatLimit = promo.seats && promo.seats > 0 ? promo.seats : Infinity;
8524
- const startSeat = existingSubscription.paidSeats + 1;
8525
- const endSeat = value.seats;
8526
- if (startSeat > promoSeatLimit) {
8527
- additionalSeatsAmount = plan.price * additionalSeats;
8528
- } else {
8529
- const seatsStillCoveredByFlat = Math.max(
8530
- 0,
8531
- Math.min(promoSeatLimit, endSeat) - startSeat + 1
8532
- );
8533
- const seatsAtPlanPrice = additionalSeats - seatsStillCoveredByFlat;
8534
- additionalSeatsAmount = plan.price * seatsAtPlanPrice;
8535
- }
8536
- } else {
8537
- additionalSeatsAmount = plan.price * additionalSeats;
8538
- }
8539
- const dailyRate = additionalSeatsAmount / totalDaysInCycle;
8540
- proratedAmount = dailyRate * daysRemaining;
8541
- }
8542
8513
  }
8543
- const isIncrease = existingSubscription && value.seats > existingSubscription.paidSeats;
8544
8514
  let subscriptionAmount;
8545
- if (!existingSubscription) {
8546
- subscriptionAmount = Math.round(monthlyAmount * 100) / 100;
8547
- } else if (proratedAmount > 0) {
8548
- subscriptionAmount = Math.round((existingSubscription.amount + proratedAmount) * 100) / 100;
8549
- } else if (isIncrease) {
8550
- subscriptionAmount = Math.round(monthlyAmount * 100) / 100;
8515
+ if (isNew) {
8516
+ subscriptionAmount = monthlyAmount;
8517
+ } else if (isPromoChange) {
8518
+ subscriptionAmount = monthlyAmount;
8519
+ } else if (isSeatIncrease) {
8520
+ subscriptionAmount = existingSubscription.amount + proratedAmount;
8551
8521
  } else {
8552
- subscriptionAmount = Math.round(existingSubscription.amount * 100) / 100;
8522
+ subscriptionAmount = existingSubscription.amount;
8553
8523
  }
8554
8524
  return {
8555
- // The monthly fee for the next billing cycle
8556
8525
  monthlyAmount: Math.round(monthlyAmount * 100) / 100,
8557
- // The prorated amount to charge now for mid-cycle seat additions
8558
8526
  proratedAmount: Math.round(proratedAmount * 100) / 100,
8559
- // The new subscription.amount for updates
8560
- subscriptionAmount,
8527
+ subscriptionAmount: Math.round(subscriptionAmount * 100) / 100,
8561
8528
  currency: plan.currency
8562
8529
  };
8563
8530
  }
@@ -8699,20 +8666,23 @@ function useSubscriptionService() {
8699
8666
  const { subscriptionAmount, proratedAmount, currency } = await computeFee(
8700
8667
  {
8701
8668
  seats: value.seats,
8702
- promoCode: value.promoCode ?? "",
8703
8669
  plan: value.plan ?? "",
8704
8670
  org: value.org
8705
8671
  }
8706
8672
  );
8707
8673
  const seatIncreased = value.seats > subscription.paidSeats;
8708
- const additionalSeats = value.seats - subscription.paidSeats;
8709
- const paidSeats = seatIncreased ? value.seats : subscription.paidSeats;
8674
+ const additionalSeats = seatIncreased ? value.seats - subscription.paidSeats : 0;
8675
+ const paidSeats = seatIncreased ? subscription.paidSeats + additionalSeats : subscription.paidSeats;
8710
8676
  await updateById(
8711
8677
  subscription._id?.toString() ?? "",
8712
- { seats: value.seats, amount: subscriptionAmount, paidSeats },
8678
+ {
8679
+ seats: value.seats,
8680
+ amount: subscriptionAmount,
8681
+ paidSeats
8682
+ },
8713
8683
  session
8714
8684
  );
8715
- if (seatIncreased) {
8685
+ if (seatIncreased && proratedAmount > 0) {
8716
8686
  await addTransaction(
8717
8687
  {
8718
8688
  type: "add-seat",
@@ -8778,15 +8748,12 @@ function useSubscriptionService() {
8778
8748
  if (!plan) {
8779
8749
  throw new import_utils44.BadRequestError("Plan not found.");
8780
8750
  }
8781
- const { subscriptionAmount, currency } = await computeFee(
8782
- {
8783
- seats: subscription.seats,
8784
- promoCode: value.promoCode ?? "",
8785
- plan: plan._id?.toString() ?? "",
8786
- org: value.org
8787
- },
8788
- true
8789
- );
8751
+ const { subscriptionAmount, currency } = await computeFee({
8752
+ seats: subscription.seats,
8753
+ promoCode: value.promoCode ?? "",
8754
+ plan: plan._id?.toString() ?? "",
8755
+ org: value.org
8756
+ });
8790
8757
  await updateById(
8791
8758
  subscription._id?.toString() ?? "",
8792
8759
  { promoCode: value.promoCode ?? "", amount: subscriptionAmount },
@@ -8795,6 +8762,9 @@ function useSubscriptionService() {
8795
8762
  if (subscription.promoCode) {
8796
8763
  await updatePromoUsageStatusByOrgId(value.org, "inactive", session);
8797
8764
  }
8765
+ let description = `Promo code updated to ${value.promoCode ?? "none"}. At the time of update, ${(0, import_utils44.formatNumber)(subscription.paidSeats, {
8766
+ decimalPlaces: 0
8767
+ })} seat(s) were already included in the subscription.`;
8798
8768
  if (promo && promo._id) {
8799
8769
  await addPromoUsage(
8800
8770
  {
@@ -8804,11 +8774,44 @@ function useSubscriptionService() {
8804
8774
  },
8805
8775
  session
8806
8776
  );
8777
+ const additionalDescription = `${promo.seats ? `Seats beyond ${(0, import_utils44.formatNumber)(promo.seats, {
8778
+ decimalPlaces: 0
8779
+ })} are charged at the standard rate.` : ""}`;
8780
+ if (promo.type === "flat") {
8781
+ description += ` ${(0, import_utils44.formatNumber)(promo.flatRate ?? 0, {
8782
+ currency,
8783
+ useSymbol: true
8784
+ })} ${promo.seats ? `limited to ${(0, import_utils44.formatNumber)(promo.seats, {
8785
+ decimalPlaces: 0
8786
+ })} seat(s)` : " for all seats"}. ${additionalDescription}`;
8787
+ }
8788
+ if (promo.type === "fixed") {
8789
+ description += ` ${(0, import_utils44.formatNumber)(promo.fixedRate ?? 0, {
8790
+ currency,
8791
+ useSymbol: true
8792
+ })} per seat${promo.seats ? `, limited to ${(0, import_utils44.formatNumber)(promo.seats, {
8793
+ decimalPlaces: 0
8794
+ })} seat(s)` : ""}. ${additionalDescription}`;
8795
+ }
8796
+ if (promo.type === "volume") {
8797
+ if (promo.tiers && promo.tiers.length > 0) {
8798
+ const tierDescriptions = promo.tiers.map((tier) => {
8799
+ const maxSeatsDesc = tier.maxSeats === 0 ? "and above" : `to ${(0, import_utils44.formatNumber)(tier.maxSeats, { decimalPlaces: 0 })}`;
8800
+ return `${(0, import_utils44.formatNumber)(tier.minSeats, {
8801
+ decimalPlaces: 0
8802
+ })} ${maxSeatsDesc} ${(0, import_utils44.formatNumber)(tier.rate, {
8803
+ currency,
8804
+ useSymbol: true
8805
+ })} per seat`;
8806
+ }).join(", ");
8807
+ description += ` Volume tiers, ${tierDescriptions}.`;
8808
+ }
8809
+ }
8807
8810
  }
8808
8811
  await addTransaction(
8809
8812
  {
8810
8813
  type: "promo-updated",
8811
- description: `Updated promo code to ${value.promoCode || "none"}.`,
8814
+ description,
8812
8815
  amount: 0,
8813
8816
  currency,
8814
8817
  subscription: subscription._id?.toString() ?? "",