@goweekdays/core 2.11.12 → 2.11.14

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.14
4
+
5
+ ### Patch Changes
6
+
7
+ - 9fd2d52: Improve promo usage and subscription transaction handling
8
+
9
+ ## 2.11.13
10
+
11
+ ### Patch Changes
12
+
13
+ - 0458614: Add support for next promo code and billing period start
14
+
3
15
  ## 2.11.12
4
16
 
5
17
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -866,8 +866,10 @@ type TSubscription = {
866
866
  currency: string;
867
867
  billingCycle: "monthly" | "yearly";
868
868
  promoCode?: string;
869
+ nextPromoCode?: string;
869
870
  retry?: number;
870
871
  status?: string;
872
+ billingPeriodStart: Date | string;
871
873
  nextBillingDate: Date | string;
872
874
  createdAt?: Date | string;
873
875
  updatedAt?: Date | string;
@@ -904,7 +906,9 @@ declare function useSubscriptionRepo(): {
904
906
  paidSeats?: number;
905
907
  amount?: number;
906
908
  promoCode?: string;
909
+ nextPromoCode?: string;
907
910
  status?: string;
911
+ billingPeriodStart?: Date;
908
912
  nextBillingDate?: Date;
909
913
  }, session?: ClientSession) => Promise<string>;
910
914
  getByStatus: (status?: string, limit?: number, retry?: number) => Promise<TSubscription[]>;
@@ -949,12 +953,23 @@ declare function useSubscriptionService(): {
949
953
  processPaidInvoice: (invoiceId: string) => Promise<string>;
950
954
  };
951
955
 
956
+ type TSubscriptionTransactionMetadata = {
957
+ additionalSeats?: number;
958
+ seats?: number;
959
+ paidSeats?: number;
960
+ plan?: string | ObjectId;
961
+ promoCode?: string;
962
+ nextPromoCode?: string;
963
+ billingPeriodStart?: Date | string;
964
+ nextBillingDate?: Date | string;
965
+ };
952
966
  type TSubscriptionTransaction = {
953
967
  _id?: ObjectId;
954
968
  subscription: string | ObjectId;
955
969
  amount: number;
956
970
  currency: string;
957
971
  type: "initiate" | "add-seat" | "remove-seat" | "renewal" | "promo-applied" | "promo-removed" | "promo-expired" | "promo-updated";
972
+ metadata?: TSubscriptionTransactionMetadata;
958
973
  description?: string;
959
974
  createdBy: string | ObjectId;
960
975
  createdByName?: string;
package/dist/index.js CHANGED
@@ -5486,6 +5486,7 @@ var schemaSubscriptionCompute = import_joi20.default.object({
5486
5486
  seats: import_joi20.default.number().integer().min(1).required(),
5487
5487
  plan: import_joi20.default.string().hex().length(24).required(),
5488
5488
  promoCode: import_joi20.default.string().optional().allow("", null),
5489
+ nextPromoCode: import_joi20.default.string().optional().allow("", null),
5489
5490
  org: import_joi20.default.string().hex().length(24).required()
5490
5491
  });
5491
5492
  var schemaSubscribe = import_joi20.default.object({
@@ -5504,6 +5505,7 @@ var schema2 = {
5504
5505
  };
5505
5506
  var schemaSubscription = import_joi20.default.object({
5506
5507
  ...schema2,
5508
+ billingPeriodStart: import_joi20.default.date().optional().allow("", null),
5507
5509
  org: import_joi20.default.string().hex().length(24).required(),
5508
5510
  orgName: import_joi20.default.string().optional().allow("", null),
5509
5511
  currency: import_joi20.default.string().length(3).required(),
@@ -5550,9 +5552,11 @@ function modelSubscription(data) {
5550
5552
  amount: data.amount,
5551
5553
  currency: data.currency,
5552
5554
  billingCycle: data.billingCycle,
5553
- promoCode: data.promoCode,
5555
+ promoCode: data.promoCode ?? "",
5556
+ nextPromoCode: data.nextPromoCode ?? "",
5554
5557
  status: data.status ?? "active",
5555
5558
  retry: data.retry ?? 0,
5559
+ billingPeriodStart: data.billingPeriodStart,
5556
5560
  nextBillingDate: data.nextBillingDate,
5557
5561
  createdAt: data.createdAt ?? /* @__PURE__ */ new Date(),
5558
5562
  updatedAt: data.updatedAt ?? ""
@@ -5864,7 +5868,9 @@ function useSubscriptionRepo() {
5864
5868
  paidSeats: import_joi21.default.number().integer().min(0).optional(),
5865
5869
  amount: import_joi21.default.number().positive().optional().allow(0),
5866
5870
  promoCode: import_joi21.default.string().max(50).optional().allow("", null),
5871
+ nextPromoCode: import_joi21.default.string().max(50).optional().allow("", null),
5867
5872
  status: import_joi21.default.string().valid("active", "due", "overdue", "suspended").optional().allow("", null),
5873
+ billingPeriodStart: import_joi21.default.date().optional().allow("", null),
5868
5874
  nextBillingDate: import_joi21.default.date().optional().allow("", null)
5869
5875
  });
5870
5876
  const { error } = validation.validate(options);
@@ -6020,6 +6026,16 @@ var schemaSubscriptionTransaction = import_joi22.default.object({
6020
6026
  "promo-expired",
6021
6027
  "promo-updated"
6022
6028
  ).required(),
6029
+ metadata: import_joi22.default.object({
6030
+ additionalSeats: import_joi22.default.number().integer().min(1).optional(),
6031
+ seats: import_joi22.default.number().integer().min(0).optional(),
6032
+ paidSeats: import_joi22.default.number().integer().min(0).optional(),
6033
+ plan: import_joi22.default.string().hex().length(24).optional(),
6034
+ promoCode: import_joi22.default.string().optional().allow("", null),
6035
+ nextPromoCode: import_joi22.default.string().optional().allow("", null),
6036
+ billingPeriodStart: import_joi22.default.date().optional().allow("", null),
6037
+ nextBillingDate: import_joi22.default.date().optional().allow("", null)
6038
+ }).optional(),
6023
6039
  description: import_joi22.default.string().optional().allow("", null),
6024
6040
  createdBy: import_joi22.default.string().hex().length(24).required(),
6025
6041
  createdByName: import_joi22.default.string().optional().allow("", null)
@@ -6052,12 +6068,22 @@ function modelSubscriptionTransaction(data) {
6052
6068
  throw new import_utils29.BadRequestError("Invalid createdBy ID.");
6053
6069
  }
6054
6070
  }
6071
+ if (data.metadata) {
6072
+ if (data.metadata.plan && typeof data.metadata.plan === "string" && data.metadata.plan.length === 24) {
6073
+ try {
6074
+ data.metadata.plan = new import_mongodb17.ObjectId(data.metadata.plan);
6075
+ } catch (error2) {
6076
+ throw new import_utils29.BadRequestError("Invalid plan ID in metadata.");
6077
+ }
6078
+ }
6079
+ }
6055
6080
  return {
6056
6081
  _id: data._id,
6057
6082
  subscription: data.subscription,
6058
6083
  amount: data.amount,
6059
6084
  currency: data.currency,
6060
6085
  type: data.type,
6086
+ metadata: data.metadata ?? {},
6061
6087
  description: data.description ?? "",
6062
6088
  createdBy: data.createdBy,
6063
6089
  createdByName: data.createdByName,
@@ -7861,7 +7887,7 @@ var schemaPromoUsage = import_joi34.default.object({
7861
7887
  function modelPromoUsage(value) {
7862
7888
  const { error } = schemaPromoUsage.validate(value);
7863
7889
  if (error) {
7864
- throw new Error(`Invalid Promo Usage model: ${error.message}`);
7890
+ throw new import_utils40.BadRequestError(`Invalid Promo Usage model: ${error.message}`);
7865
7891
  }
7866
7892
  if (value._id && typeof value._id === "string") {
7867
7893
  try {
@@ -7944,6 +7970,9 @@ function usePromoUsageRepo() {
7944
7970
  delCachedData();
7945
7971
  return "Successfully added promo usage.";
7946
7972
  } catch (error) {
7973
+ if (error instanceof import_utils41.AppError) {
7974
+ throw error;
7975
+ }
7947
7976
  throw new import_utils41.InternalServerError("Failed to add promo usage.");
7948
7977
  }
7949
7978
  }
@@ -8384,7 +8413,6 @@ function useSubscriptionService() {
8384
8413
  const { getById: getOrgById, updateStatusById: updateOrgStatusById } = useOrgRepo();
8385
8414
  const { getByCode: getPromoByCode } = usePromoRepo();
8386
8415
  const {
8387
- countByPromoId,
8388
8416
  add: addPromoUsage,
8389
8417
  updateStatusByOrgId: updatePromoUsageStatusByOrgId
8390
8418
  } = usePromoUsageRepo();
@@ -8499,7 +8527,7 @@ function useSubscriptionService() {
8499
8527
  "Cannot change promo code while increasing seats. Perform actions separately."
8500
8528
  );
8501
8529
  }
8502
- const effectivePromoCode = isPromoChange ? value.promoCode : existingSubscription?.promoCode;
8530
+ const effectivePromoCode = isNew ? value.promoCode : isPromoChange ? value.promoCode : existingSubscription?.promoCode;
8503
8531
  const promo = effectivePromoCode ? await getPromoByCode(effectivePromoCode) : null;
8504
8532
  const monthlyAmount = computeMonthlyAmount(value.seats, plan.price, promo);
8505
8533
  let proratedAmount = 0;
@@ -8563,7 +8591,8 @@ function useSubscriptionService() {
8563
8591
  if (!membership) {
8564
8592
  throw new import_utils44.BadRequestError("User is not a member of the organization.");
8565
8593
  }
8566
- const nextBillingDate = /* @__PURE__ */ new Date();
8594
+ const billingPeriodStart = /* @__PURE__ */ new Date();
8595
+ const nextBillingDate = new Date(billingPeriodStart);
8567
8596
  nextBillingDate.setDate(nextBillingDate.getDate() + 30);
8568
8597
  const { subscriptionAmount, currency } = await computeFee({
8569
8598
  seats: value.seats,
@@ -8588,6 +8617,7 @@ function useSubscriptionService() {
8588
8617
  amount: subscriptionAmount,
8589
8618
  currency,
8590
8619
  billingCycle: plan.billingCycle,
8620
+ billingPeriodStart,
8591
8621
  nextBillingDate,
8592
8622
  promoCode: value.promoCode
8593
8623
  },
@@ -8691,7 +8721,40 @@ function useSubscriptionService() {
8691
8721
  currency,
8692
8722
  subscription: subscription._id?.toString() ?? "",
8693
8723
  createdBy: value.user,
8694
- createdByName: `${userData.firstName} ${userData.lastName}`
8724
+ createdByName: `${userData.firstName} ${userData.lastName}`,
8725
+ metadata: {
8726
+ additionalSeats,
8727
+ seats: value.seats,
8728
+ paidSeats,
8729
+ plan: value.plan ?? "",
8730
+ promoCode: subscription.promoCode ?? "",
8731
+ nextPromoCode: subscription.nextPromoCode ?? "",
8732
+ billingPeriodStart: subscription.billingPeriodStart,
8733
+ nextBillingDate: subscription.nextBillingDate
8734
+ }
8735
+ },
8736
+ session
8737
+ );
8738
+ }
8739
+ if (!seatIncreased) {
8740
+ await addTransaction(
8741
+ {
8742
+ type: "remove-seat",
8743
+ description: `Removed ${subscription.seats - value.seats} seats.`,
8744
+ amount: 0,
8745
+ currency,
8746
+ subscription: subscription._id?.toString() ?? "",
8747
+ createdBy: value.user,
8748
+ createdByName: `${userData.firstName} ${userData.lastName}`,
8749
+ metadata: {
8750
+ seats: subscription.seats - value.seats,
8751
+ paidSeats,
8752
+ plan: value.plan ?? "",
8753
+ promoCode: subscription.promoCode ?? "",
8754
+ nextPromoCode: subscription.nextPromoCode ?? "",
8755
+ billingPeriodStart: subscription.billingPeriodStart,
8756
+ nextBillingDate: subscription.nextBillingDate
8757
+ }
8695
8758
  },
8696
8759
  session
8697
8760
  );
@@ -8748,23 +8811,14 @@ function useSubscriptionService() {
8748
8811
  if (!plan) {
8749
8812
  throw new import_utils44.BadRequestError("Plan not found.");
8750
8813
  }
8751
- const { subscriptionAmount, currency } = await computeFee({
8752
- seats: subscription.seats,
8753
- promoCode: value.promoCode ?? "",
8754
- plan: plan._id?.toString() ?? "",
8755
- org: value.org
8756
- });
8757
8814
  await updateById(
8758
8815
  subscription._id?.toString() ?? "",
8759
- { promoCode: value.promoCode ?? "", amount: subscriptionAmount },
8816
+ { nextPromoCode: value.promoCode ?? "" },
8760
8817
  session
8761
8818
  );
8762
8819
  if (subscription.promoCode) {
8763
8820
  await updatePromoUsageStatusByOrgId(value.org, "inactive", session);
8764
8821
  }
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.`;
8768
8822
  if (promo && promo._id) {
8769
8823
  await addPromoUsage(
8770
8824
  {
@@ -8774,49 +8828,23 @@ function useSubscriptionService() {
8774
8828
  },
8775
8829
  session
8776
8830
  );
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
- }
8810
8831
  }
8811
8832
  await addTransaction(
8812
8833
  {
8813
8834
  type: "promo-updated",
8814
- description,
8835
+ description: `Promo code ${value.promoCode || "removed"} scheduled to take effect on the next billing cycle.`,
8815
8836
  amount: 0,
8816
- currency,
8837
+ currency: subscription.currency,
8817
8838
  subscription: subscription._id?.toString() ?? "",
8818
8839
  createdBy: value.user,
8819
- createdByName: `${userData.firstName} ${userData.lastName}`
8840
+ metadata: {
8841
+ seats: subscription.seats,
8842
+ paidSeats: subscription.paidSeats,
8843
+ promoCode: subscription.promoCode,
8844
+ nextPromoCode: value.promoCode ?? "",
8845
+ billingPeriodStart: subscription.billingPeriodStart,
8846
+ nextBillingDate: subscription.nextBillingDate
8847
+ }
8820
8848
  },
8821
8849
  session
8822
8850
  );
@@ -8901,9 +8929,11 @@ function useSubscriptionService() {
8901
8929
  }
8902
8930
  for (let index = 0; index < subscriptions.length; index++) {
8903
8931
  const subscription = subscriptions[index];
8904
- const today = /* @__PURE__ */ new Date();
8905
- const nextBillingDate = new Date(subscription.updatedAt ?? "");
8906
- if (nextBillingDate.getFullYear() === today.getFullYear() && nextBillingDate.getMonth() === today.getMonth() && nextBillingDate.getDate() === today.getDate() || subscription.retry && subscription.retry >= retry) {
8932
+ const now = /* @__PURE__ */ new Date();
8933
+ if (now < new Date(subscription.nextBillingDate)) {
8934
+ continue;
8935
+ }
8936
+ if ((subscription.retry ?? 0) >= retry) {
8907
8937
  continue;
8908
8938
  }
8909
8939
  try {
@@ -8976,39 +9006,43 @@ function useSubscriptionService() {
8976
9006
  }
8977
9007
  const ledgerBill = await getByInvoice(invoiceId);
8978
9008
  if (!ledgerBill) {
8979
- throw new import_utils44.BadRequestError(
8980
- "Ledger bill not found for the given invoice ID."
8981
- );
9009
+ throw new import_utils44.BadRequestError("Ledger bill not found.");
8982
9010
  }
8983
9011
  const orgId = String(ledgerBill.org);
8984
- const org = await getOrgById(orgId);
8985
- if (!org) {
8986
- throw new import_utils44.BadRequestError("Organization not found for the ledger bill.");
8987
- }
8988
9012
  const subscription = await getByOrg(orgId);
8989
9013
  if (!subscription) {
8990
- throw new import_utils44.BadRequestError("Subscription not found for the organization.");
9014
+ throw new import_utils44.BadRequestError("Subscription not found.");
8991
9015
  }
8992
9016
  const plan = await getDefaultPlan();
8993
9017
  if (!plan) {
8994
- throw new import_utils44.BadRequestError("Default plan not found.");
9018
+ throw new import_utils44.BadRequestError("Plan not found.");
8995
9019
  }
8996
9020
  const session = import_utils44.useAtlas.getClient()?.startSession();
8997
9021
  if (!session) {
8998
- throw new Error("Unable to start database session.");
9022
+ throw new import_utils44.InternalServerError("Unable to start database session.");
8999
9023
  }
9000
9024
  try {
9001
9025
  session.startTransaction();
9002
- const subscriptionId = String(subscription._id);
9003
- const nextBillingDate = /* @__PURE__ */ new Date();
9026
+ const billingPeriodStart = /* @__PURE__ */ new Date();
9027
+ const nextBillingDate = new Date(billingPeriodStart);
9004
9028
  nextBillingDate.setMonth(nextBillingDate.getMonth() + 1);
9029
+ const effectivePromoCode = subscription.nextPromoCode || subscription.promoCode;
9030
+ const promo = effectivePromoCode ? await getPromoByCode(effectivePromoCode) : null;
9031
+ const monthlyAmount = computeMonthlyAmount(
9032
+ subscription.seats,
9033
+ plan.price,
9034
+ promo
9035
+ );
9005
9036
  await updateById(
9006
- subscriptionId,
9037
+ subscription._id?.toString() ?? "",
9007
9038
  {
9008
9039
  status: "active",
9040
+ billingPeriodStart,
9009
9041
  nextBillingDate,
9010
9042
  paidSeats: subscription.seats,
9011
- amount: plan.price * subscription.seats
9043
+ amount: Math.round(monthlyAmount * 100) / 100,
9044
+ promoCode: effectivePromoCode,
9045
+ nextPromoCode: ""
9012
9046
  },
9013
9047
  session
9014
9048
  );
@@ -9019,14 +9053,7 @@ function useSubscriptionService() {
9019
9053
  return "Successfully processed paid invoice.";
9020
9054
  } catch (error2) {
9021
9055
  await session.abortTransaction();
9022
- import_utils44.logger.log({
9023
- level: "error",
9024
- message: `Failed to process paid invoice ${invoiceId}: ${error2 instanceof Error ? error2.message : String(error2)}`
9025
- });
9026
- if (error2 instanceof import_utils44.AppError) {
9027
- throw error2;
9028
- }
9029
- throw new import_utils44.InternalServerError("Failed to process paid invoice.");
9056
+ throw error2;
9030
9057
  } finally {
9031
9058
  session.endSession();
9032
9059
  }
@@ -9562,6 +9589,7 @@ function useOrgService() {
9562
9589
  paidSeats: value.seats,
9563
9590
  currency: plan.currency,
9564
9591
  billingCycle: plan.billingCycle,
9592
+ billingPeriodStart: currentDate,
9565
9593
  nextBillingDate
9566
9594
  },
9567
9595
  session