@goweekdays/core 2.11.4 → 2.11.5

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,11 @@
1
1
  # @goweekdays/core
2
2
 
3
+ ## 2.11.5
4
+
5
+ ### Patch Changes
6
+
7
+ - cb8aa36: Enhance promo usage tracking and seat limit logic
8
+
3
9
  ## 2.11.4
4
10
 
5
11
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -470,6 +470,33 @@ declare function usePromoController(): {
470
470
  deleteById: (req: Request, res: Response, next: NextFunction) => Promise<void>;
471
471
  };
472
472
 
473
+ type TPromoUsage = {
474
+ _id?: ObjectId;
475
+ promo: ObjectId;
476
+ org: ObjectId | string;
477
+ usedBy: string;
478
+ status?: "active" | "inactive";
479
+ createdAt?: Date | string;
480
+ updatedAt?: Date | string;
481
+ };
482
+
483
+ declare function usePromoUsageRepo(): {
484
+ createIndexes: () => Promise<void>;
485
+ add: (value: TPromoUsage, session?: ClientSession) => Promise<string>;
486
+ getAll: ({ page, limit, promo, org, }?: {
487
+ page?: number | undefined;
488
+ limit?: number | undefined;
489
+ promo?: string | undefined;
490
+ org?: string | undefined;
491
+ }) => Promise<TPaginate<TPromoUsage>>;
492
+ getByPromo: (promo: string | ObjectId) => Promise<TPromoUsage | null>;
493
+ getById: (_id: string | ObjectId) => Promise<TPromoUsage | null>;
494
+ getByOrgId: (orgId: string | ObjectId) => Promise<TPromoUsage[]>;
495
+ countByPromoId: (promoId: string | ObjectId) => Promise<number>;
496
+ updateStatusByOrgId: (orgId: string | ObjectId, status: "active" | "inactive", session?: ClientSession) => Promise<string>;
497
+ deleteById: (_id: string | ObjectId) => Promise<string>;
498
+ };
499
+
473
500
  type TRole = {
474
501
  _id?: ObjectId;
475
502
  id?: string | ObjectId;
@@ -1255,4 +1282,4 @@ declare const XENDIT_BASE_URL: string;
1255
1282
  declare const DOMAIN: string;
1256
1283
  declare const APP_ORG: string;
1257
1284
 
1258
- export { ACCESS_TOKEN_EXPIRY, ACCESS_TOKEN_SECRET, APP_ACCOUNT, APP_MAIN, APP_ORG, DEFAULT_USER_EMAIL, DEFAULT_USER_FIRST_NAME, DEFAULT_USER_LAST_NAME, DEFAULT_USER_PASSWORD, DOMAIN, MAILER_EMAIL, MAILER_PASSWORD, MAILER_TRANSPORT_HOST, MAILER_TRANSPORT_PORT, MAILER_TRANSPORT_SECURE, MBuilding, MBuildingUnit, MFile, MONGO_DB, MONGO_URI, PAYPAL_API_URL, PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET, PAYPAL_WEBHOOK_ID, PORT, PaypalWebhookHeaders, REDIS_HOST, REDIS_PASSWORD, REDIS_PORT, REFRESH_TOKEN_EXPIRY, REFRESH_TOKEN_SECRET, SECRET_KEY, SPACES_ACCESS_KEY, SPACES_BUCKET, SPACES_ENDPOINT, SPACES_REGION, SPACES_SECRET_KEY, TApp, TBuilding, TBuildingUnit, TCounter, TFile, TJobPost, TLedgerBill, TMember, TOrg, TPermission, TPermissionGroup, TPlan, TPromo, TRole, TSubscribe, TSubscription, TSubscriptionTransaction, TUser, TVerification, TVerificationMetadata, VERIFICATION_FORGET_PASSWORD_DURATION, VERIFICATION_USER_INVITE_DURATION, XENDIT_BASE_URL, XENDIT_SECRET_KEY, currencies, isDev, ledgerBillStatuses, ledgerBillTypes, modelApp, modelJobPost, modelLedgerBill, modelMember, modelOrg, modelPermission, modelPermissionGroup, modelPlan, modelPromo, modelRole, modelSubscription, modelSubscriptionTransaction, modelUser, modelVerification, schemaApp, schemaAppUpdate, schemaBuilding, schemaBuildingUnit, schemaInviteMember, schemaJobPost, schemaJobPostUpdate, schemaLedgerBill, schemaLedgerBillingSummary, schemaMember, schemaMemberRole, schemaMemberStatus, schemaOrg, schemaOrgAdd, schemaOrgUpdate, schemaPermission, schemaPermissionGroup, schemaPermissionGroupUpdate, schemaPermissionUpdate, schemaPlan, schemaPromo, schemaRole, schemaRoleUpdate, schemaSubscribe, schemaSubscription, schemaSubscriptionCompute, schemaSubscriptionPromoCode, schemaSubscriptionSeats, schemaSubscriptionTransaction, schemaSubscriptionUpdate, schemaUpdateOptions, schemaUser, schemaVerification, transactionSchema, useAppController, useAppRepo, useAppService, useAuthController, useAuthService, useBuildingController, useBuildingRepo, useBuildingService, useBuildingUnitController, useBuildingUnitRepo, useBuildingUnitService, useCounterModel, useCounterRepo, useFileController, useFileRepo, useFileService, useGitHubService, useJobPostController, useJobPostRepo, useJobPostService, useLedgerBillingController, useLedgerBillingRepo, useMemberController, useMemberRepo, useOrgController, useOrgRepo, useOrgService, usePaypalService, usePermissionController, usePermissionGroupController, usePermissionGroupRepo, usePermissionGroupService, usePermissionRepo, usePermissionService, usePlanController, usePlanRepo, usePlanService, usePromoController, usePromoRepo, useRoleController, useRoleRepo, useRoleService, useSubscriptionController, useSubscriptionRepo, useSubscriptionService, useSubscriptionTransactionController, useSubscriptionTransactionRepo, useUserController, useUserRepo, useUserService, useUtilController, useVerificationController, useVerificationRepo, useVerificationService };
1285
+ export { ACCESS_TOKEN_EXPIRY, ACCESS_TOKEN_SECRET, APP_ACCOUNT, APP_MAIN, APP_ORG, DEFAULT_USER_EMAIL, DEFAULT_USER_FIRST_NAME, DEFAULT_USER_LAST_NAME, DEFAULT_USER_PASSWORD, DOMAIN, MAILER_EMAIL, MAILER_PASSWORD, MAILER_TRANSPORT_HOST, MAILER_TRANSPORT_PORT, MAILER_TRANSPORT_SECURE, MBuilding, MBuildingUnit, MFile, MONGO_DB, MONGO_URI, PAYPAL_API_URL, PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET, PAYPAL_WEBHOOK_ID, PORT, PaypalWebhookHeaders, REDIS_HOST, REDIS_PASSWORD, REDIS_PORT, REFRESH_TOKEN_EXPIRY, REFRESH_TOKEN_SECRET, SECRET_KEY, SPACES_ACCESS_KEY, SPACES_BUCKET, SPACES_ENDPOINT, SPACES_REGION, SPACES_SECRET_KEY, TApp, TBuilding, TBuildingUnit, TCounter, TFile, TJobPost, TLedgerBill, TMember, TOrg, TPermission, TPermissionGroup, TPlan, TPromo, TRole, TSubscribe, TSubscription, TSubscriptionTransaction, TUser, TVerification, TVerificationMetadata, VERIFICATION_FORGET_PASSWORD_DURATION, VERIFICATION_USER_INVITE_DURATION, XENDIT_BASE_URL, XENDIT_SECRET_KEY, currencies, isDev, ledgerBillStatuses, ledgerBillTypes, modelApp, modelJobPost, modelLedgerBill, modelMember, modelOrg, modelPermission, modelPermissionGroup, modelPlan, modelPromo, modelRole, modelSubscription, modelSubscriptionTransaction, modelUser, modelVerification, schemaApp, schemaAppUpdate, schemaBuilding, schemaBuildingUnit, schemaInviteMember, schemaJobPost, schemaJobPostUpdate, schemaLedgerBill, schemaLedgerBillingSummary, schemaMember, schemaMemberRole, schemaMemberStatus, schemaOrg, schemaOrgAdd, schemaOrgUpdate, schemaPermission, schemaPermissionGroup, schemaPermissionGroupUpdate, schemaPermissionUpdate, schemaPlan, schemaPromo, schemaRole, schemaRoleUpdate, schemaSubscribe, schemaSubscription, schemaSubscriptionCompute, schemaSubscriptionPromoCode, schemaSubscriptionSeats, schemaSubscriptionTransaction, schemaSubscriptionUpdate, schemaUpdateOptions, schemaUser, schemaVerification, transactionSchema, useAppController, useAppRepo, useAppService, useAuthController, useAuthService, useBuildingController, useBuildingRepo, useBuildingService, useBuildingUnitController, useBuildingUnitRepo, useBuildingUnitService, useCounterModel, useCounterRepo, useFileController, useFileRepo, useFileService, useGitHubService, useJobPostController, useJobPostRepo, useJobPostService, useLedgerBillingController, useLedgerBillingRepo, useMemberController, useMemberRepo, useOrgController, useOrgRepo, useOrgService, usePaypalService, usePermissionController, usePermissionGroupController, usePermissionGroupRepo, usePermissionGroupService, usePermissionRepo, usePermissionService, usePlanController, usePlanRepo, usePlanService, usePromoController, usePromoRepo, usePromoUsageRepo, useRoleController, useRoleRepo, useRoleService, useSubscriptionController, useSubscriptionRepo, useSubscriptionService, useSubscriptionTransactionController, useSubscriptionTransactionRepo, useUserController, useUserRepo, useUserService, useUtilController, useVerificationController, useVerificationRepo, useVerificationService };
package/dist/index.js CHANGED
@@ -161,6 +161,7 @@ __export(src_exports, {
161
161
  usePlanService: () => usePlanService,
162
162
  usePromoController: () => usePromoController,
163
163
  usePromoRepo: () => usePromoRepo,
164
+ usePromoUsageRepo: () => usePromoUsageRepo,
164
165
  useRoleController: () => useRoleController,
165
166
  useRoleRepo: () => useRoleRepo,
166
167
  useRoleService: () => useRoleService,
@@ -5810,7 +5811,7 @@ function useSubscriptionRepo() {
5810
5811
  const validation = import_joi21.default.object({
5811
5812
  seats: import_joi21.default.number().integer().min(1).optional().allow("", null),
5812
5813
  paidSeats: import_joi21.default.number().integer().min(0).optional(),
5813
- amount: import_joi21.default.number().positive().optional(),
5814
+ amount: import_joi21.default.number().positive().optional().allow(0),
5814
5815
  promoCode: import_joi21.default.string().max(50).optional().allow("", null),
5815
5816
  status: import_joi21.default.string().valid("active", "due", "overdue", "suspended").optional().allow("", null),
5816
5817
  nextBillingDate: import_joi21.default.date().optional().allow("", null)
@@ -7803,7 +7804,8 @@ var import_mongodb23 = require("mongodb");
7803
7804
  var schemaPromoUsage = import_joi34.default.object({
7804
7805
  promo: import_joi34.default.string().hex().length(24).required(),
7805
7806
  org: import_joi34.default.string().hex().length(24).required(),
7806
- usedBy: import_joi34.default.string().email().required()
7807
+ usedBy: import_joi34.default.string().email().required(),
7808
+ status: import_joi34.default.string().valid("active", "inactive").optional()
7807
7809
  });
7808
7810
  function modelPromoUsage(value) {
7809
7811
  const { error } = schemaPromoUsage.validate(value);
@@ -7817,14 +7819,14 @@ function modelPromoUsage(value) {
7817
7819
  throw new import_utils40.BadRequestError("Invalid Promo Usage _id");
7818
7820
  }
7819
7821
  }
7820
- if (typeof value.promo === "string") {
7822
+ if (value.promo && typeof value.promo === "string") {
7821
7823
  try {
7822
7824
  value.promo = new import_mongodb23.ObjectId(value.promo);
7823
7825
  } catch (error2) {
7824
7826
  throw new import_utils40.BadRequestError("Invalid Promo Usage promo");
7825
7827
  }
7826
7828
  }
7827
- if (typeof value.org === "string") {
7829
+ if (value.org && typeof value.org === "string") {
7828
7830
  try {
7829
7831
  value.org = new import_mongodb23.ObjectId(value.org);
7830
7832
  } catch (error2) {
@@ -7836,6 +7838,7 @@ function modelPromoUsage(value) {
7836
7838
  promo: value.promo,
7837
7839
  org: value.org,
7838
7840
  usedBy: value.usedBy,
7841
+ status: value.status ?? "active",
7839
7842
  createdAt: value.createdAt ? new Date(value.createdAt) : /* @__PURE__ */ new Date(),
7840
7843
  updatedAt: value.updatedAt ?? ""
7841
7844
  };
@@ -7868,6 +7871,9 @@ function usePromoUsageRepo() {
7868
7871
  async function createIndexes() {
7869
7872
  try {
7870
7873
  await collection.createIndexes([
7874
+ {
7875
+ key: { status: 1 }
7876
+ },
7871
7877
  {
7872
7878
  key: { promo: 1, org: 1 },
7873
7879
  name: "promo_org_index"
@@ -7880,10 +7886,10 @@ function usePromoUsageRepo() {
7880
7886
  } catch (error) {
7881
7887
  }
7882
7888
  }
7883
- async function add(value) {
7889
+ async function add(value, session) {
7884
7890
  try {
7885
7891
  value = modelPromoUsage(value);
7886
- await collection.insertOne(value);
7892
+ await collection.insertOne(value, { session });
7887
7893
  delCachedData();
7888
7894
  return "Successfully added promo usage.";
7889
7895
  } catch (error) {
@@ -8075,7 +8081,10 @@ function usePromoUsageRepo() {
8075
8081
  if (cachedData !== null && cachedData !== void 0) {
8076
8082
  return cachedData;
8077
8083
  }
8078
- const count = await collection.countDocuments({ promo: promoId });
8084
+ const count = await collection.countDocuments({
8085
+ promo: promoId,
8086
+ status: "active"
8087
+ });
8079
8088
  setCache(cacheKey, count).then(() => {
8080
8089
  import_utils41.logger.log({
8081
8090
  level: "info",
@@ -8092,6 +8101,28 @@ function usePromoUsageRepo() {
8092
8101
  throw new import_utils41.InternalServerError("Failed to count promo usages.");
8093
8102
  }
8094
8103
  }
8104
+ async function updateStatusByOrgId(orgId, status, session) {
8105
+ const { error } = import_joi35.default.string().hex().length(24).required().validate(orgId);
8106
+ if (error) {
8107
+ throw new import_utils41.BadRequestError(`Invalid org ID: ${error.message}`);
8108
+ }
8109
+ try {
8110
+ orgId = new import_mongodb24.ObjectId(orgId);
8111
+ } catch (error2) {
8112
+ throw new import_utils41.BadRequestError("Invalid org ID.");
8113
+ }
8114
+ try {
8115
+ await collection.updateMany(
8116
+ { org: orgId, status: "active" },
8117
+ { $set: { status, updatedAt: /* @__PURE__ */ new Date() } },
8118
+ { session }
8119
+ );
8120
+ delCachedData();
8121
+ return "Successfully updated promo usage status.";
8122
+ } catch (error2) {
8123
+ throw new import_utils41.InternalServerError("Failed to update promo usage status.");
8124
+ }
8125
+ }
8095
8126
  async function deleteById(_id) {
8096
8127
  const { error } = import_joi35.default.string().hex().length(24).required().validate(_id);
8097
8128
  if (error) {
@@ -8124,6 +8155,7 @@ function usePromoUsageRepo() {
8124
8155
  getById,
8125
8156
  getByOrgId,
8126
8157
  countByPromoId,
8158
+ updateStatusByOrgId,
8127
8159
  deleteById
8128
8160
  };
8129
8161
  }
@@ -8300,6 +8332,11 @@ function useSubscriptionService() {
8300
8332
  const { getDefault: getDefaultPlan, getById: getPlanById } = usePlanRepo();
8301
8333
  const { getById: getOrgById, updateStatusById: updateOrgStatusById } = useOrgRepo();
8302
8334
  const { getByCode: getPromoByCode } = usePromoRepo();
8335
+ const {
8336
+ countByPromoId,
8337
+ add: addPromoUsage,
8338
+ updateStatusByOrgId: updatePromoUsageStatusByOrgId
8339
+ } = usePromoUsageRepo();
8303
8340
  const { getByApp: getMembershipByApp } = useMemberRepo();
8304
8341
  function calculateVolumeTierAmount(tiers, startSeat, seatCount, fallbackRate) {
8305
8342
  const sortedTiers = [...tiers].sort((a, b) => a.minSeats - b.minSeats);
@@ -8350,16 +8387,33 @@ function useSubscriptionService() {
8350
8387
  if (!promo) {
8351
8388
  throw new import_utils44.BadRequestError("Promo code not found.");
8352
8389
  }
8390
+ if (promo.usage && promo.usage > 0) {
8391
+ const currentUsageCount = await countByPromoId(
8392
+ promo._id?.toString() ?? ""
8393
+ );
8394
+ const isAlreadyUsingPromo = existingSubscription?.promoCode === promoCode;
8395
+ if (!isAlreadyUsingPromo && currentUsageCount >= promo.usage) {
8396
+ throw new import_utils44.BadRequestError(
8397
+ "Promo code has reached its maximum usage limit."
8398
+ );
8399
+ }
8400
+ }
8353
8401
  }
8354
8402
  let monthlyAmount = plan.price * value.seats;
8355
8403
  if (promo) {
8404
+ const promoSeatLimit = promo.seats && promo.seats > 0 ? promo.seats : Infinity;
8356
8405
  switch (promo.type) {
8357
- case "fixed":
8358
- monthlyAmount = Math.max(promo.fixedRate ?? 0, 0) * value.seats;
8406
+ case "fixed": {
8407
+ const promoSeats = Math.min(value.seats, promoSeatLimit);
8408
+ const standardSeats = Math.max(value.seats - promoSeatLimit, 0);
8409
+ monthlyAmount = Math.max(promo.fixedRate ?? 0, 0) * promoSeats + plan.price * standardSeats;
8359
8410
  break;
8360
- case "flat":
8361
- monthlyAmount = Math.max((promo.flatRate ?? 0) / value.seats, 0) * value.seats;
8411
+ }
8412
+ case "flat": {
8413
+ const standardSeats = Math.max(value.seats - promoSeatLimit, 0);
8414
+ monthlyAmount = (promo.flatRate ?? 0) + plan.price * standardSeats;
8362
8415
  break;
8416
+ }
8363
8417
  case "volume": {
8364
8418
  if (promo.tiers && promo.tiers.length > 0) {
8365
8419
  monthlyAmount = calculateVolumeTierAmount(
@@ -8402,17 +8456,32 @@ function useSubscriptionService() {
8402
8456
  plan.price
8403
8457
  );
8404
8458
  } else if (promo?.type === "fixed") {
8405
- const discountedPricePerSeat = Math.max(
8406
- plan.price - (promo.fixedRate ?? 0),
8407
- 0
8459
+ const promoSeatLimit = promo.seats && promo.seats > 0 ? promo.seats : Infinity;
8460
+ const startSeat = existingSubscription.paidSeats + 1;
8461
+ const endSeat = value.seats;
8462
+ const promoSeatsInRange = Math.max(
8463
+ 0,
8464
+ Math.min(promoSeatLimit, endSeat) - startSeat + 1
8408
8465
  );
8409
- additionalSeatsAmount = discountedPricePerSeat * additionalSeats;
8410
- } else if (promo?.type === "flat") {
8411
- const effectivePricePerSeat = Math.max(
8412
- plan.price - (promo.flatRate ?? 0) / value.seats,
8413
- 0
8466
+ const standardSeatsInRange = Math.max(
8467
+ 0,
8468
+ additionalSeats - promoSeatsInRange
8414
8469
  );
8415
- additionalSeatsAmount = effectivePricePerSeat * additionalSeats;
8470
+ additionalSeatsAmount = (promo.fixedRate ?? 0) * promoSeatsInRange + plan.price * standardSeatsInRange;
8471
+ } else if (promo?.type === "flat") {
8472
+ const promoSeatLimit = promo.seats && promo.seats > 0 ? promo.seats : Infinity;
8473
+ const startSeat = existingSubscription.paidSeats + 1;
8474
+ const endSeat = value.seats;
8475
+ if (startSeat > promoSeatLimit) {
8476
+ additionalSeatsAmount = plan.price * additionalSeats;
8477
+ } else {
8478
+ const seatsStillCoveredByFlat = Math.max(
8479
+ 0,
8480
+ Math.min(promoSeatLimit, endSeat) - startSeat + 1
8481
+ );
8482
+ const seatsAtPlanPrice = additionalSeats - seatsStillCoveredByFlat;
8483
+ additionalSeatsAmount = plan.price * seatsAtPlanPrice;
8484
+ }
8416
8485
  } else {
8417
8486
  additionalSeatsAmount = plan.price * additionalSeats;
8418
8487
  }
@@ -8420,8 +8489,6 @@ function useSubscriptionService() {
8420
8489
  proratedAmount = dailyRate * daysRemaining;
8421
8490
  }
8422
8491
  }
8423
- const isDecrease = existingSubscription && value.seats < existingSubscription.paidSeats;
8424
- const isNoChange = existingSubscription && value.seats === existingSubscription.paidSeats;
8425
8492
  const isIncrease = existingSubscription && value.seats > existingSubscription.paidSeats;
8426
8493
  let subscriptionAmount;
8427
8494
  if (!existingSubscription) {
@@ -8486,6 +8553,13 @@ function useSubscriptionService() {
8486
8553
  org: value.org,
8487
8554
  plan: value.plan
8488
8555
  });
8556
+ let promo = null;
8557
+ if (value.promoCode) {
8558
+ promo = await getPromoByCode(value.promoCode);
8559
+ if (!promo) {
8560
+ throw new import_utils44.BadRequestError("Promo code not found.");
8561
+ }
8562
+ }
8489
8563
  const subId = await _add(
8490
8564
  {
8491
8565
  org: value.org,
@@ -8501,6 +8575,16 @@ function useSubscriptionService() {
8501
8575
  },
8502
8576
  session
8503
8577
  );
8578
+ if (promo && promo._id) {
8579
+ await addPromoUsage(
8580
+ {
8581
+ promo: promo._id,
8582
+ org: value.org,
8583
+ usedBy: userData.email
8584
+ },
8585
+ session
8586
+ );
8587
+ }
8504
8588
  await addTransaction(
8505
8589
  {
8506
8590
  type: "initiate",
@@ -8657,6 +8741,19 @@ function useSubscriptionService() {
8657
8741
  { promoCode: value.promoCode ?? "", amount: subscriptionAmount },
8658
8742
  session
8659
8743
  );
8744
+ if (subscription.promoCode) {
8745
+ await updatePromoUsageStatusByOrgId(value.org, "inactive", session);
8746
+ }
8747
+ if (promo && promo._id) {
8748
+ await addPromoUsage(
8749
+ {
8750
+ promo: promo._id,
8751
+ org: value.org,
8752
+ usedBy: userData.email
8753
+ },
8754
+ session
8755
+ );
8756
+ }
8660
8757
  await addTransaction(
8661
8758
  {
8662
8759
  type: "promo-updated",
@@ -13540,6 +13637,7 @@ function useJobPostController() {
13540
13637
  usePlanService,
13541
13638
  usePromoController,
13542
13639
  usePromoRepo,
13640
+ usePromoUsageRepo,
13543
13641
  useRoleController,
13544
13642
  useRoleRepo,
13545
13643
  useRoleService,