sahab-utils 1.7.2 → 1.7.4

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.
@@ -125,6 +125,13 @@ export type Order = {
125
125
  isTest?: boolean;
126
126
  clientNumber?: string;
127
127
  usedBalance?: number;
128
+ /** Snapshotted at order creation; the exact amount later credited on COMPLETED. */
129
+ cashbackOffer?: CashbackOffer;
130
+ cashbackAmount?: number;
131
+ /** Set by calculateCashbackOnOrderDone once the credit lands — its own idempotency guard,
132
+ * independent of movementsCreated, so a retried COMPLETED trigger cannot double-credit even
133
+ * if calculateBalanceOnOrderDone's own guard fails to fire for an unrelated reason. */
134
+ cashbackCreditedAt?: TimestampType;
128
135
  isDummyOrder?: boolean;
129
136
  ratingNote?: string;
130
137
  isAddonStructure?: boolean;
@@ -222,7 +229,8 @@ export declare enum MovementType {
222
229
  ORDER_REFUND = 4,
223
230
  SAHAB_PAYMENT_TO_PROVIDER = 5,
224
231
  SAHAB_RECEIVE_FROM_PROVIDER = 6,
225
- USING_BALANCE = 7
232
+ USING_BALANCE = 7,
233
+ CASHBACK = 8
226
234
  }
227
235
  export declare enum ComplaintType {
228
236
  BUG = 0,
@@ -667,6 +675,26 @@ export type Discount = {
667
675
  */
668
676
  isTest?: boolean;
669
677
  };
678
+ /**
679
+ * A cashback campaign — a percentage- or flat-amount credit to a customer's balance after a
680
+ * completed purchase. Kept separate from `Discount`: a `Discount` reduces the order's price at
681
+ * checkout, cashback credits balance afterward, and never touches the pricing pipeline.
682
+ *
683
+ * No `offerId` — unlike `Discount`/`promoCodes`, this is not its own Firestore document; it lives
684
+ * nested inside the singleton `configs/app_settings` doc (see `AppSettings` below).
685
+ */
686
+ export type CashbackOffer = {
687
+ amount?: number;
688
+ isPercentage?: boolean;
689
+ maxAmount?: number;
690
+ /** Carwashes this campaign is valid at. Empty or absent means every provider. */
691
+ providerUids?: string[];
692
+ validFrom?: TimestampType;
693
+ validTo?: TimestampType;
694
+ createdAt?: TimestampType;
695
+ createrEmail?: string;
696
+ isTest?: boolean;
697
+ };
670
698
  /**
671
699
  * "Buy N washes, get one free" configuration, carried on the `Discount` that represents the
672
700
  * offer.
@@ -820,6 +848,14 @@ export type GiftConfig = {
820
848
  /** Charged when the giver adds a personal message to the SMS. Defaults to `GIFT_MESSAGE_SURCHARGE_DEFAULT` when unset. */
821
849
  giftMessageSurcharge?: number;
822
850
  };
851
+ /** App-wide settings, stored as the singleton `configs/app_settings` doc. */
852
+ export type AppSettings = {
853
+ walletBackground?: string;
854
+ /** Balance a customer must have before the balance checkbox at checkout is usable. */
855
+ minimumBalanceToUse?: number;
856
+ /** The one live cashback campaign, if any — nested here rather than its own collection. */
857
+ cashback?: CashbackOffer;
858
+ };
823
859
  export type OrderReview = {
824
860
  ratingId: string;
825
861
  ratingNote: string;
@@ -42,6 +42,7 @@ var MovementType;
42
42
  MovementType[MovementType["SAHAB_PAYMENT_TO_PROVIDER"] = 5] = "SAHAB_PAYMENT_TO_PROVIDER";
43
43
  MovementType[MovementType["SAHAB_RECEIVE_FROM_PROVIDER"] = 6] = "SAHAB_RECEIVE_FROM_PROVIDER";
44
44
  MovementType[MovementType["USING_BALANCE"] = 7] = "USING_BALANCE";
45
+ MovementType[MovementType["CASHBACK"] = 8] = "CASHBACK";
45
46
  })(MovementType || (exports.MovementType = MovementType = {}));
46
47
  var ComplaintType;
47
48
  (function (ComplaintType) {
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export * from './constants/types';
2
2
  export * from './ts/calcs';
3
+ export * from './ts/cashback';
3
4
  export * from './ts/gifts';
4
5
  export * from './ts/loyalty';
6
+ export * from './ts/utils';
package/dist/index.js CHANGED
@@ -16,5 +16,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./constants/types"), exports);
18
18
  __exportStar(require("./ts/calcs"), exports);
19
+ __exportStar(require("./ts/cashback"), exports);
19
20
  __exportStar(require("./ts/gifts"), exports);
20
21
  __exportStar(require("./ts/loyalty"), exports);
22
+ __exportStar(require("./ts/utils"), exports);
@@ -0,0 +1,15 @@
1
+ import { CashbackOffer, Order } from '../constants/types';
2
+ /**
3
+ * Whether a provider-scoped offer covers a given provider. Empty/absent `scope` means every
4
+ * carwash. Shared by every offer type with a `providerUids`-style restriction — do not let a
5
+ * second copy of this exist; see the CLAUDE.md warning about the gift-flow eligibility
6
+ * duplication this exact pattern caused before.
7
+ */
8
+ export declare function offerCoversProvider(scope: string[] | undefined, providerId?: string): boolean;
9
+ export declare function cashbackOfferCoversProvider(offer: CashbackOffer | undefined, providerId?: string): boolean;
10
+ /**
11
+ * The cashback amount this order earns, or 0. Percentage is of `getCustomerTotalToPay`, which
12
+ * already nets out other discounts and any balance the customer used — never pay cashback on
13
+ * money the customer didn't spend.
14
+ */
15
+ export declare function cashbackAmountFor(offer: CashbackOffer | undefined, order: Order): number;
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.offerCoversProvider = offerCoversProvider;
4
+ exports.cashbackOfferCoversProvider = cashbackOfferCoversProvider;
5
+ exports.cashbackAmountFor = cashbackAmountFor;
6
+ const calcs_1 = require("./calcs");
7
+ const utils_1 = require("./utils");
8
+ /**
9
+ * Whether a provider-scoped offer covers a given provider. Empty/absent `scope` means every
10
+ * carwash. Shared by every offer type with a `providerUids`-style restriction — do not let a
11
+ * second copy of this exist; see the CLAUDE.md warning about the gift-flow eligibility
12
+ * duplication this exact pattern caused before.
13
+ */
14
+ function offerCoversProvider(scope, providerId) {
15
+ if (!scope || !scope.length)
16
+ return true;
17
+ return !!providerId && scope.includes(providerId);
18
+ }
19
+ function cashbackOfferCoversProvider(offer, providerId) {
20
+ return offerCoversProvider(offer === null || offer === void 0 ? void 0 : offer.providerUids, providerId);
21
+ }
22
+ /**
23
+ * The cashback amount this order earns, or 0. Percentage is of `getCustomerTotalToPay`, which
24
+ * already nets out other discounts and any balance the customer used — never pay cashback on
25
+ * money the customer didn't spend.
26
+ */
27
+ function cashbackAmountFor(offer, order) {
28
+ if (!offer)
29
+ return 0;
30
+ const base = Math.max(0, (0, calcs_1.getCustomerTotalToPay)(order));
31
+ const gross = offer.isPercentage ? ((0, utils_1.preventNaN)(offer.amount) / 100) * base : (0, utils_1.preventNaN)(offer.amount);
32
+ const capped = offer.maxAmount ? Math.min(gross, offer.maxAmount) : gross;
33
+ return Math.max(0, capped);
34
+ }
@@ -1,3 +1,10 @@
1
1
  export declare function preventNaN(x?: number | string): number;
2
2
  export declare const toFixed: (num: any) => string;
3
3
  export declare const isEmpty: (text: any) => boolean;
4
+ export declare const compareVersions: (version1: string, version2: string) => number;
5
+ /**
6
+ * Fails open (no block) on a missing or malformed version — the admin UI that writes `minVer`
7
+ * validates the same shape before saving, so this is a second, independent guard against a hand
8
+ * edit (or a future write path) briefly bricking every install with a typo'd value.
9
+ */
10
+ export declare function shouldForceUpdate(minVer?: string, currentVersion?: string): boolean;
package/dist/ts/utils.js CHANGED
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isEmpty = exports.toFixed = void 0;
3
+ exports.compareVersions = exports.isEmpty = exports.toFixed = void 0;
4
4
  exports.preventNaN = preventNaN;
5
+ exports.shouldForceUpdate = shouldForceUpdate;
5
6
  function preventNaN(x) {
6
7
  if (x === undefined || x === null || isNaN(x)) {
7
8
  return 0;
@@ -22,3 +23,31 @@ const isEmpty = (text) => {
22
23
  return false;
23
24
  };
24
25
  exports.isEmpty = isEmpty;
26
+ const compareVersions = (version1, version2) => {
27
+ const v1Parts = version1.split('.').map(Number);
28
+ const v2Parts = version2.split('.').map(Number);
29
+ const maxLength = Math.max(v1Parts.length, v2Parts.length);
30
+ for (let i = 0; i < maxLength; i++) {
31
+ const v1Part = v1Parts[i] || 0;
32
+ const v2Part = v2Parts[i] || 0;
33
+ if (v1Part > v2Part)
34
+ return 1;
35
+ if (v1Part < v2Part)
36
+ return -1;
37
+ }
38
+ return 0;
39
+ };
40
+ exports.compareVersions = compareVersions;
41
+ const VERSION_RE = /^\d+(\.\d+)*$/;
42
+ /**
43
+ * Fails open (no block) on a missing or malformed version — the admin UI that writes `minVer`
44
+ * validates the same shape before saving, so this is a second, independent guard against a hand
45
+ * edit (or a future write path) briefly bricking every install with a typo'd value.
46
+ */
47
+ function shouldForceUpdate(minVer, currentVersion) {
48
+ if (!minVer || !currentVersion)
49
+ return false;
50
+ if (!VERSION_RE.test(minVer) || !VERSION_RE.test(currentVersion))
51
+ return false;
52
+ return (0, exports.compareVersions)(minVer, currentVersion) > 0;
53
+ }
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "sahab-utils",
3
- "version": "1.7.2",
3
+ "version": "1.7.4",
4
4
  "description": "This package contains types and utils functions",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "scripts": {
8
8
  "build": "tsc",
9
9
  "deploy": "npm run build && npm publish",
10
- "test": "npm run build && node tests/loyalty.test.js && node tests/gifts.test.js"
10
+ "test": "npm run build && node tests/loyalty.test.js && node tests/gifts.test.js && node tests/cashback.test.js && node tests/utils.test.js"
11
11
  },
12
12
  "repository": {
13
13
  "type": "git",