sahab-utils 1.7.1 → 1.7.3
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/dist/constants/types.d.ts +39 -1
- package/dist/constants/types.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/ts/cashback.d.ts +15 -0
- package/dist/ts/cashback.js +34 -0
- package/dist/ts/gifts.d.ts +11 -0
- package/dist/ts/gifts.js +22 -3
- package/package.json +2 -2
|
@@ -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.
|
|
@@ -817,6 +845,16 @@ export type GiftConfig = {
|
|
|
817
845
|
* Bounded on read: a 0 would expire every gift the moment it was sold.
|
|
818
846
|
*/
|
|
819
847
|
giftExpiryMonths?: number;
|
|
848
|
+
/** Charged when the giver adds a personal message to the SMS. Defaults to `GIFT_MESSAGE_SURCHARGE_DEFAULT` when unset. */
|
|
849
|
+
giftMessageSurcharge?: number;
|
|
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;
|
|
820
858
|
};
|
|
821
859
|
export type OrderReview = {
|
|
822
860
|
ratingId: string;
|
package/dist/constants/types.js
CHANGED
|
@@ -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
package/dist/index.js
CHANGED
|
@@ -16,5 +16,6 @@ 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);
|
|
@@ -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
|
+
}
|
package/dist/ts/gifts.d.ts
CHANGED
|
@@ -17,6 +17,17 @@ export declare const MIN_GIFT_EXPIRY_MONTHS = 1;
|
|
|
17
17
|
export declare const MAX_GIFT_EXPIRY_MONTHS = 36;
|
|
18
18
|
/** What the giver pays: every line in the basket. */
|
|
19
19
|
export declare function giftTotal(items?: GiftItem[]): number;
|
|
20
|
+
/** A message longer than this spans more than one Arabic (UCS-2) SMS segment on its own. */
|
|
21
|
+
export declare const GIFT_MESSAGE_MAX_CHARS = 70;
|
|
22
|
+
/** Used when the back office has not set `GiftConfig.giftMessageSurcharge`. */
|
|
23
|
+
export declare const GIFT_MESSAGE_SURCHARGE_DEFAULT = 3;
|
|
24
|
+
/**
|
|
25
|
+
* What a personal message, if any, adds to the gift's price.
|
|
26
|
+
*
|
|
27
|
+
* Zero for a blank or whitespace-only message — writing nothing costs nothing. Otherwise the
|
|
28
|
+
* back-office amount if one is configured, else the default, same shape as `giftExpiryMonths`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function giftMessageSurcharge(message?: string, config?: GiftConfig): number;
|
|
20
31
|
/** The gifted service ids. */
|
|
21
32
|
export declare function giftServiceIds(gift?: Pick<Gift, 'items'>): string[];
|
|
22
33
|
/** The gifted addon ids. */
|
package/dist/ts/gifts.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.MAX_GIFT_EXPIRY_MONTHS = exports.MIN_GIFT_EXPIRY_MONTHS = exports.DEFAULT_GIFT_EXPIRY_MONTHS = void 0;
|
|
3
|
+
exports.GIFT_MESSAGE_SURCHARGE_DEFAULT = exports.GIFT_MESSAGE_MAX_CHARS = exports.MAX_GIFT_EXPIRY_MONTHS = exports.MIN_GIFT_EXPIRY_MONTHS = exports.DEFAULT_GIFT_EXPIRY_MONTHS = void 0;
|
|
4
4
|
exports.giftTotal = giftTotal;
|
|
5
|
+
exports.giftMessageSurcharge = giftMessageSurcharge;
|
|
5
6
|
exports.giftServiceIds = giftServiceIds;
|
|
6
7
|
exports.giftAddonIds = giftAddonIds;
|
|
7
8
|
exports.giftExpiryMonths = giftExpiryMonths;
|
|
@@ -36,6 +37,22 @@ exports.MAX_GIFT_EXPIRY_MONTHS = 36;
|
|
|
36
37
|
function giftTotal(items) {
|
|
37
38
|
return (items !== null && items !== void 0 ? items : []).reduce((sum, item) => sum + (0, utils_1.preventNaN)(item === null || item === void 0 ? void 0 : item.price), 0);
|
|
38
39
|
}
|
|
40
|
+
/** A message longer than this spans more than one Arabic (UCS-2) SMS segment on its own. */
|
|
41
|
+
exports.GIFT_MESSAGE_MAX_CHARS = 70;
|
|
42
|
+
/** Used when the back office has not set `GiftConfig.giftMessageSurcharge`. */
|
|
43
|
+
exports.GIFT_MESSAGE_SURCHARGE_DEFAULT = 3;
|
|
44
|
+
/**
|
|
45
|
+
* What a personal message, if any, adds to the gift's price.
|
|
46
|
+
*
|
|
47
|
+
* Zero for a blank or whitespace-only message — writing nothing costs nothing. Otherwise the
|
|
48
|
+
* back-office amount if one is configured, else the default, same shape as `giftExpiryMonths`.
|
|
49
|
+
*/
|
|
50
|
+
function giftMessageSurcharge(message, config) {
|
|
51
|
+
if (!(message !== null && message !== void 0 ? message : '').trim().length)
|
|
52
|
+
return 0;
|
|
53
|
+
const raw = config === null || config === void 0 ? void 0 : config.giftMessageSurcharge;
|
|
54
|
+
return raw === undefined || raw === null || !isFinite(raw) || raw < 0 ? exports.GIFT_MESSAGE_SURCHARGE_DEFAULT : raw;
|
|
55
|
+
}
|
|
39
56
|
/** The gifted service ids. */
|
|
40
57
|
function giftServiceIds(gift) {
|
|
41
58
|
var _a;
|
|
@@ -105,8 +122,10 @@ function giftDiscount(gift, dates) {
|
|
|
105
122
|
return {
|
|
106
123
|
isPercentage: true,
|
|
107
124
|
amount: 100,
|
|
108
|
-
// Bounds the discount to the
|
|
109
|
-
|
|
125
|
+
// Bounds the discount to what the basket is actually worth, whatever the recipient
|
|
126
|
+
// orders — never to `gift.amount`, which can include a message surcharge that bought
|
|
127
|
+
// no redeemable service and must not inflate how much the voucher can discount.
|
|
128
|
+
maxAmount: giftTotal(gift.items),
|
|
110
129
|
clientIds: gift.recipientUid ? [gift.recipientUid] : [],
|
|
111
130
|
providerUids: gift.providerUid ? [gift.providerUid] : [],
|
|
112
131
|
servicesIds: giftServiceIds(gift),
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sahab-utils",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.3",
|
|
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"
|
|
11
11
|
},
|
|
12
12
|
"repository": {
|
|
13
13
|
"type": "git",
|