sahab-utils 1.8.6 → 1.8.9
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 +13 -3
- package/dist/constants/types.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/ts/calcs.d.ts +8 -0
- package/dist/ts/calcs.js +22 -4
- package/dist/ts/loyalty.d.ts +9 -2
- package/dist/ts/loyalty.js +24 -3
- package/dist/ts/payLink.d.ts +61 -0
- package/dist/ts/payLink.js +87 -0
- package/package.json +3 -2
|
@@ -64,7 +64,8 @@ export declare enum OrderStatus {
|
|
|
64
64
|
COMPLETED = 5,// 5
|
|
65
65
|
CANCELLED = 6,// 6
|
|
66
66
|
RATED = 7,// 7
|
|
67
|
-
STARTED = 8
|
|
67
|
+
STARTED = 8,// 8
|
|
68
|
+
PENDING_PAYMENT = 9
|
|
68
69
|
}
|
|
69
70
|
export declare enum OrderPaymentType {
|
|
70
71
|
APPLE_PAY = 0,
|
|
@@ -116,7 +117,7 @@ export type Order = {
|
|
|
116
117
|
paymentCancelationId?: string;
|
|
117
118
|
paymentRefundId?: string;
|
|
118
119
|
paymentCaptureId?: string;
|
|
119
|
-
canceledBy?: 'provider' | 'client';
|
|
120
|
+
canceledBy?: 'provider' | 'client' | 'system' | 'admin';
|
|
120
121
|
movementsCreated?: boolean;
|
|
121
122
|
orderNote?: string;
|
|
122
123
|
cancelReason?: string;
|
|
@@ -152,6 +153,15 @@ export type Order = {
|
|
|
152
153
|
customerNotificationTaskId?: string;
|
|
153
154
|
/** Set on an order created by re-ordering, pointing at the one it was copied from. */
|
|
154
155
|
originalOrderId?: string;
|
|
156
|
+
/** Multi-visit package only: 1-based position of this order in the package. Every visit is its own order. */
|
|
157
|
+
visitNumber?: number;
|
|
158
|
+
/** Multi-visit package only: what the customer pays for the whole package, stamped on every visit. */
|
|
159
|
+
originalPrice?: number;
|
|
160
|
+
paymentLinkId?: string;
|
|
161
|
+
paymentLinkUrl?: string;
|
|
162
|
+
payLinkDeadline?: TimestampType;
|
|
163
|
+
payLinkSmsCount?: number;
|
|
164
|
+
createdByStaff?: string;
|
|
155
165
|
};
|
|
156
166
|
export type ReduxOrder = {
|
|
157
167
|
orderId?: string;
|
|
@@ -186,7 +196,7 @@ export type ReduxOrder = {
|
|
|
186
196
|
paymentCancelationId?: string;
|
|
187
197
|
paymentRefundId?: string;
|
|
188
198
|
paymentCaptureId?: string;
|
|
189
|
-
canceledBy?: 'provider' | 'client';
|
|
199
|
+
canceledBy?: 'provider' | 'client' | 'system' | 'admin';
|
|
190
200
|
movementsCreated?: boolean;
|
|
191
201
|
orderNote?: string;
|
|
192
202
|
cancelReason?: string;
|
package/dist/constants/types.js
CHANGED
|
@@ -20,6 +20,7 @@ var OrderStatus;
|
|
|
20
20
|
OrderStatus[OrderStatus["CANCELLED"] = 6] = "CANCELLED";
|
|
21
21
|
OrderStatus[OrderStatus["RATED"] = 7] = "RATED";
|
|
22
22
|
OrderStatus[OrderStatus["STARTED"] = 8] = "STARTED";
|
|
23
|
+
OrderStatus[OrderStatus["PENDING_PAYMENT"] = 9] = "PENDING_PAYMENT";
|
|
23
24
|
})(OrderStatus || (exports.OrderStatus = OrderStatus = {}));
|
|
24
25
|
var OrderPaymentType;
|
|
25
26
|
(function (OrderPaymentType) {
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -23,6 +23,7 @@ __exportStar(require("./ts/gifts"), exports);
|
|
|
23
23
|
__exportStar(require("./ts/iban"), exports);
|
|
24
24
|
__exportStar(require("./ts/loyalty"), exports);
|
|
25
25
|
__exportStar(require("./ts/orderActivity"), exports);
|
|
26
|
+
__exportStar(require("./ts/payLink"), exports);
|
|
26
27
|
__exportStar(require("./ts/push"), exports);
|
|
27
28
|
__exportStar(require("./ts/utils"), exports);
|
|
28
29
|
__exportStar(require("./ts/wheel"), exports);
|
package/dist/ts/calcs.d.ts
CHANGED
|
@@ -2,6 +2,10 @@ import { Order } from '../constants/types';
|
|
|
2
2
|
export declare const SAHAB_PERCENTAGE = 0;
|
|
3
3
|
export declare const ONLINE_SAHAB_PERCENTAGE = 0.03;
|
|
4
4
|
export declare const CLIENT_SAHAB_PERCENTAGE = 0.03;
|
|
5
|
+
/** What the customer sees for a provider's price: `p + pct * p`. The operand order is kept as the apps and server always wrote it. */
|
|
6
|
+
export declare function customerPriceFromProviderPrice(providerPrice: number, percentage?: number): number;
|
|
7
|
+
/** Inverse of `customerPriceFromProviderPrice`: the provider's own price inside a customer-facing figure. */
|
|
8
|
+
export declare function providerPriceFromCustomerPrice(customerPrice: number, percentage?: number): number;
|
|
5
9
|
export declare const PROVIDER_ORDERS_PERCENTAGE_DEFAULT = 0.1;
|
|
6
10
|
export declare const PROVIDER_ORDERS_PERCENTAGE_WITH_SAHAB_LOGO = 0.07;
|
|
7
11
|
export declare function providerOrdersPercentage(hasSahabLogoOnCar: boolean | undefined, storedValue: unknown): number;
|
|
@@ -10,6 +14,10 @@ export declare function getSahabDiscountsAmount(order: Order): number;
|
|
|
10
14
|
export declare function getProviderDiscountsAmount(order: Order): number;
|
|
11
15
|
export declare function getOrderTotalDiscountsAmount(order: Order): number;
|
|
12
16
|
export declare function getCustomerTotalToPay(order: Order): number;
|
|
17
|
+
/** What was actually collected: the washer's cash, which already includes Sahab's surcharge, or the online charge.
|
|
18
|
+
* The provider holds all of the cash, so the surcharge must stay in this figure for the transfer to take it back.
|
|
19
|
+
* A finished cash order with no recorded amount (every order before the server wrote it) is treated as fully paid. */
|
|
20
|
+
export declare function settledPaymentAmount(order: Order): number;
|
|
13
21
|
export declare function getProviderTransferAmount(order: Order): number;
|
|
14
22
|
export declare function getProviderProfits(order: Order): number;
|
|
15
23
|
export declare function getOnlineProcessingFees(order: Order): number;
|
package/dist/ts/calcs.js
CHANGED
|
@@ -4,12 +4,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.PROVIDER_ORDERS_PERCENTAGE_WITH_SAHAB_LOGO = exports.PROVIDER_ORDERS_PERCENTAGE_DEFAULT = exports.CLIENT_SAHAB_PERCENTAGE = exports.ONLINE_SAHAB_PERCENTAGE = exports.SAHAB_PERCENTAGE = void 0;
|
|
7
|
+
exports.customerPriceFromProviderPrice = customerPriceFromProviderPrice;
|
|
8
|
+
exports.providerPriceFromCustomerPrice = providerPriceFromCustomerPrice;
|
|
7
9
|
exports.providerOrdersPercentage = providerOrdersPercentage;
|
|
8
10
|
exports.getServicesTotal = getServicesTotal;
|
|
9
11
|
exports.getSahabDiscountsAmount = getSahabDiscountsAmount;
|
|
10
12
|
exports.getProviderDiscountsAmount = getProviderDiscountsAmount;
|
|
11
13
|
exports.getOrderTotalDiscountsAmount = getOrderTotalDiscountsAmount;
|
|
12
14
|
exports.getCustomerTotalToPay = getCustomerTotalToPay;
|
|
15
|
+
exports.settledPaymentAmount = settledPaymentAmount;
|
|
13
16
|
exports.getProviderTransferAmount = getProviderTransferAmount;
|
|
14
17
|
exports.getProviderProfits = getProviderProfits;
|
|
15
18
|
exports.getOnlineProcessingFees = getOnlineProcessingFees;
|
|
@@ -23,6 +26,14 @@ const utils_1 = require("./utils");
|
|
|
23
26
|
exports.SAHAB_PERCENTAGE = 0;
|
|
24
27
|
exports.ONLINE_SAHAB_PERCENTAGE = 0.03;
|
|
25
28
|
exports.CLIENT_SAHAB_PERCENTAGE = 0.03;
|
|
29
|
+
/** What the customer sees for a provider's price: `p + pct * p`. The operand order is kept as the apps and server always wrote it. */
|
|
30
|
+
function customerPriceFromProviderPrice(providerPrice, percentage = exports.CLIENT_SAHAB_PERCENTAGE) {
|
|
31
|
+
return providerPrice + percentage * providerPrice;
|
|
32
|
+
}
|
|
33
|
+
/** Inverse of `customerPriceFromProviderPrice`: the provider's own price inside a customer-facing figure. */
|
|
34
|
+
function providerPriceFromCustomerPrice(customerPrice, percentage = exports.CLIENT_SAHAB_PERCENTAGE) {
|
|
35
|
+
return customerPrice / (1 + percentage);
|
|
36
|
+
}
|
|
26
37
|
// `Provider.ordersPercentage` — the provider's own cut of an order, factored into
|
|
27
38
|
// `sahabProviderPercentage` server-side. Only two values are ever valid: the default, or the
|
|
28
39
|
// reduced rate for a provider that's put the Sahab logo on their car.
|
|
@@ -196,16 +207,23 @@ function cashSurcharge(order) {
|
|
|
196
207
|
return (order === null || order === void 0 ? void 0 : order.paymentType) === types_1.OrderPaymentType.CASH ? Math.max(0, (0, utils_1.preventNaN)((_a = order === null || order === void 0 ? void 0 : order.sahabExtraPaymentAmount) !== null && _a !== void 0 ? _a : 0)) : 0;
|
|
197
208
|
}
|
|
198
209
|
/** What was actually collected: the washer's cash, which already includes Sahab's surcharge, or the online charge.
|
|
199
|
-
* The provider holds all of the cash, so the surcharge must stay in this figure for the transfer to take it back.
|
|
210
|
+
* The provider holds all of the cash, so the surcharge must stay in this figure for the transfer to take it back.
|
|
211
|
+
* A finished cash order with no recorded amount (every order before the server wrote it) is treated as fully paid. */
|
|
200
212
|
function settledPaymentAmount(order) {
|
|
201
213
|
var _a;
|
|
202
|
-
|
|
214
|
+
if ((order === null || order === void 0 ? void 0 : order.paymentType) !== types_1.OrderPaymentType.CASH)
|
|
215
|
+
return getCustomerTotalToPay(order);
|
|
216
|
+
const recorded = (0, utils_1.preventNaN)((_a = order === null || order === void 0 ? void 0 : order.paymentAmount) !== null && _a !== void 0 ? _a : 0);
|
|
217
|
+
if (recorded > 0)
|
|
218
|
+
return recorded;
|
|
219
|
+
const finished = (order === null || order === void 0 ? void 0 : order.orderStatus) === types_1.OrderStatus.COMPLETED || (order === null || order === void 0 ? void 0 : order.orderStatus) === types_1.OrderStatus.RATED;
|
|
220
|
+
return finished ? Math.max(0, (0, utils_1.preventNaN)(getCustomerTotalToPay(order))) : recorded;
|
|
203
221
|
}
|
|
204
222
|
function getProviderTransferAmount(order) {
|
|
205
223
|
var _a, _b;
|
|
206
224
|
const orderTotal = getServicesTotal(order) - getProviderDiscountsAmount(order);
|
|
207
225
|
const paymentAmount = settledPaymentAmount(order);
|
|
208
|
-
const originalPrice = orderTotal
|
|
226
|
+
const originalPrice = providerPriceFromCustomerPrice(orderTotal, (0, utils_1.preventNaN)((_a = order === null || order === void 0 ? void 0 : order.sahabClientPercentage) !== null && _a !== void 0 ? _a : 0));
|
|
209
227
|
const minimumProviderAmount = originalPrice - originalPrice * ((_b = order === null || order === void 0 ? void 0 : order.sahabProviderPercentage) !== null && _b !== void 0 ? _b : 0);
|
|
210
228
|
if ((order === null || order === void 0 ? void 0 : order.paymentType) === types_1.OrderPaymentType.CASH) {
|
|
211
229
|
return minimumProviderAmount - paymentAmount;
|
|
@@ -231,7 +249,7 @@ function getSahabFees(order) {
|
|
|
231
249
|
var _a, _b;
|
|
232
250
|
const orderTotal = getServicesTotal(order) - getProviderDiscountsAmount(order);
|
|
233
251
|
const paymentAmount = settledPaymentAmount(order);
|
|
234
|
-
const originalPrice = orderTotal
|
|
252
|
+
const originalPrice = providerPriceFromCustomerPrice(orderTotal, (0, utils_1.preventNaN)((_a = order === null || order === void 0 ? void 0 : order.sahabClientPercentage) !== null && _a !== void 0 ? _a : 0));
|
|
235
253
|
const minimumProviderAmount = originalPrice - originalPrice * ((_b = order === null || order === void 0 ? void 0 : order.sahabProviderPercentage) !== null && _b !== void 0 ? _b : 0);
|
|
236
254
|
// When a Sahab-funded discount pushes the customer below the provider's entitlement, Sahab is
|
|
237
255
|
// genuinely subsidising the order — reporting a floor of 0 here overstated margin, so the negative figure stands.
|
package/dist/ts/loyalty.d.ts
CHANGED
|
@@ -12,8 +12,15 @@ export declare function loyaltyCoversProvider(offer: Discount | undefined, provi
|
|
|
12
12
|
/** Environment priority: explicit `offer.isTest`, else `providerIsTest` (only valid when the offer
|
|
13
13
|
* scopes to one provider), else prod — matching every pre-existing offer's implicit shape. */
|
|
14
14
|
export declare function loyaltyOfferMatchesTestMode(offer: Discount | undefined, isTest: boolean, providerIsTest?: boolean): boolean;
|
|
15
|
-
/** Whether
|
|
16
|
-
|
|
15
|
+
/** Whether this order spends a loyalty free wash. Redeeming is not buying, so it earns no stamp. */
|
|
16
|
+
export declare function isLoyaltyRedemption(order?: Order): boolean;
|
|
17
|
+
/** Whether this order is one visit of a multi-visit package. Each visit is its own order. */
|
|
18
|
+
export declare function isPackageVisit(order?: Order): boolean;
|
|
19
|
+
/** What the minimum is compared against: what the customer paid. A visit holds only a slice of the
|
|
20
|
+
* package, so it uses the package price — otherwise later visits of a qualifying package would not stamp. */
|
|
21
|
+
export declare function loyaltyQualifyingAmount(order: Order): number;
|
|
22
|
+
/** Whether a finished order earns a stamp. For a multi-visit package this is asked per visit: every
|
|
23
|
+
* completed visit earns one, not the package once. A redemption order earns none. */
|
|
17
24
|
export declare function orderEarnsLoyaltyStamp(order: Order | undefined, offer: Discount | undefined): boolean;
|
|
18
25
|
/** Whether buying a gift earns the giver a stamp. Awarded at purchase, not redemption — the reward
|
|
19
26
|
* shouldn't hinge on whether the recipient ever spends it. The minimum is fair here since the giver really paid it. */
|
package/dist/ts/loyalty.js
CHANGED
|
@@ -5,6 +5,9 @@ exports.isProviderLoyaltyOffer = isProviderLoyaltyOffer;
|
|
|
5
5
|
exports.isSahabLoyaltyOffer = isSahabLoyaltyOffer;
|
|
6
6
|
exports.loyaltyCoversProvider = loyaltyCoversProvider;
|
|
7
7
|
exports.loyaltyOfferMatchesTestMode = loyaltyOfferMatchesTestMode;
|
|
8
|
+
exports.isLoyaltyRedemption = isLoyaltyRedemption;
|
|
9
|
+
exports.isPackageVisit = isPackageVisit;
|
|
10
|
+
exports.loyaltyQualifyingAmount = loyaltyQualifyingAmount;
|
|
8
11
|
exports.orderEarnsLoyaltyStamp = orderEarnsLoyaltyStamp;
|
|
9
12
|
exports.giftEarnsLoyaltyStamp = giftEarnsLoyaltyStamp;
|
|
10
13
|
exports.emptyLoyaltyProgress = emptyLoyaltyProgress;
|
|
@@ -58,8 +61,24 @@ function loyaltyOfferMatchesTestMode(offer, isTest, providerIsTest) {
|
|
|
58
61
|
return providerIsTest === isTest;
|
|
59
62
|
return !isTest;
|
|
60
63
|
}
|
|
61
|
-
/** Whether
|
|
62
|
-
|
|
64
|
+
/** Whether this order spends a loyalty free wash. Redeeming is not buying, so it earns no stamp. */
|
|
65
|
+
function isLoyaltyRedemption(order) {
|
|
66
|
+
var _a;
|
|
67
|
+
return !!((_a = order === null || order === void 0 ? void 0 : order.promoCode) === null || _a === void 0 ? void 0 : _a.loyalty);
|
|
68
|
+
}
|
|
69
|
+
/** Whether this order is one visit of a multi-visit package. Each visit is its own order. */
|
|
70
|
+
function isPackageVisit(order) {
|
|
71
|
+
return (0, utils_1.preventNaN)(order === null || order === void 0 ? void 0 : order.visitNumber) >= 1;
|
|
72
|
+
}
|
|
73
|
+
/** What the minimum is compared against: what the customer paid. A visit holds only a slice of the
|
|
74
|
+
* package, so it uses the package price — otherwise later visits of a qualifying package would not stamp. */
|
|
75
|
+
function loyaltyQualifyingAmount(order) {
|
|
76
|
+
if (isPackageVisit(order) && (0, utils_1.preventNaN)(order.originalPrice) > 0)
|
|
77
|
+
return (0, utils_1.preventNaN)(order.originalPrice);
|
|
78
|
+
return (0, calcs_1.getCustomerTotalToPay)(order);
|
|
79
|
+
}
|
|
80
|
+
/** Whether a finished order earns a stamp. For a multi-visit package this is asked per visit: every
|
|
81
|
+
* completed visit earns one, not the package once. A redemption order earns none. */
|
|
63
82
|
function orderEarnsLoyaltyStamp(order, offer) {
|
|
64
83
|
var _a, _b;
|
|
65
84
|
if (!order || !(offer === null || offer === void 0 ? void 0 : offer.loyalty))
|
|
@@ -74,8 +93,10 @@ function orderEarnsLoyaltyStamp(order, offer) {
|
|
|
74
93
|
// Without this, the recipient's total is 0, so any programme with no minimum would wrongly stamp them.
|
|
75
94
|
if ((0, gifts_1.isGiftRedemption)(order))
|
|
76
95
|
return false;
|
|
96
|
+
if (isLoyaltyRedemption(order))
|
|
97
|
+
return false;
|
|
77
98
|
const min = (0, utils_1.preventNaN)((_b = offer.loyalty.minOrderAmount) !== null && _b !== void 0 ? _b : 0);
|
|
78
|
-
if (min > 0 && (
|
|
99
|
+
if (min > 0 && loyaltyQualifyingAmount(order) < min)
|
|
79
100
|
return false;
|
|
80
101
|
return true;
|
|
81
102
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { CarSize, Order, OrderStatus, Provider, ProvidedAddon, ProvidedService } from '../constants/types';
|
|
2
|
+
/** Back-office orders paid by a link — the shared contract between sahab-website and cloudFun. */
|
|
3
|
+
export declare const PAY_LINK_WINDOW_MINUTES = 15;
|
|
4
|
+
export declare const PAY_LINK_CANCEL_REASON = "payment not completed";
|
|
5
|
+
/** Fields a pay-link order carries. Generic over the timestamp because cloudFun stores firebase-admin Timestamps and the
|
|
6
|
+
* apps/website read firebase web ones. Server-only bookkeeping (task name, run token) stays in cloudFun. */
|
|
7
|
+
export type PayLinkOrderFields<TTimestamp> = {
|
|
8
|
+
paymentLinkId?: string;
|
|
9
|
+
paymentLinkUrl?: string;
|
|
10
|
+
payLinkDeadline?: TTimestamp;
|
|
11
|
+
payLinkSmsCount?: number;
|
|
12
|
+
createdByStaff?: string;
|
|
13
|
+
};
|
|
14
|
+
export type CreatePayLinkOrderRequest = {
|
|
15
|
+
clientUid: string;
|
|
16
|
+
carId: string;
|
|
17
|
+
/** Index into the client's saved `locations`. */
|
|
18
|
+
locationIndex: number;
|
|
19
|
+
providerId: string;
|
|
20
|
+
serviceIds: string[];
|
|
21
|
+
addonIds?: string[];
|
|
22
|
+
/** Start of the chosen slot, epoch ms. */
|
|
23
|
+
orderDate: number;
|
|
24
|
+
promoCode?: string;
|
|
25
|
+
/** Text the link from the server. Off by default: staff copy it or share it on WhatsApp. */
|
|
26
|
+
sendSms?: boolean;
|
|
27
|
+
};
|
|
28
|
+
export type CreatePayLinkOrderResponse = {
|
|
29
|
+
orderId: string;
|
|
30
|
+
orderStatus: OrderStatus;
|
|
31
|
+
/** Absent when the order was free and confirmed without a payment. */
|
|
32
|
+
paymentLinkUrl?: string;
|
|
33
|
+
/** Epoch ms after which the unpaid order is cancelled. */
|
|
34
|
+
payLinkDeadline?: number;
|
|
35
|
+
smsSent: boolean;
|
|
36
|
+
};
|
|
37
|
+
export type ResendPayLinkRequest = {
|
|
38
|
+
orderId: string;
|
|
39
|
+
};
|
|
40
|
+
/** The message carrying the link — one wording for the server's SMS and the back office's copy/WhatsApp share. */
|
|
41
|
+
export declare const payLinkMessage: (lang: string | undefined, url: string) => string;
|
|
42
|
+
export declare const payLinkCancelledMessage: (lang: string | undefined) => string;
|
|
43
|
+
/** wa.me wants the international number as digits only; local 05… numbers are Saudi. */
|
|
44
|
+
export declare function whatsAppShareUrl(phone: string | undefined, text: string): string;
|
|
45
|
+
export declare const payLinkDeadlineMs: (createdMs: number) => number;
|
|
46
|
+
/** Checkout amounts are integer minor units; rounding here keeps the link and the webhook check in agreement. */
|
|
47
|
+
export declare const toMinorUnits: (sar: number) => number;
|
|
48
|
+
/** The webhook amount matches the order only to the halala, never "close enough". */
|
|
49
|
+
export declare const isPayLinkAmountExact: (orderTotalSar: number, paidMinor: number, currency?: string) => boolean;
|
|
50
|
+
export type PayLinkLinesResult = {
|
|
51
|
+
ok: true;
|
|
52
|
+
lines: Array<ProvidedService | ProvidedAddon>;
|
|
53
|
+
} | {
|
|
54
|
+
ok: false;
|
|
55
|
+
reason: 'NO_SERVICE' | 'UNKNOWN_ITEM' | 'NO_PRICE_FOR_SIZE' | 'PACKAGE_NOT_SUPPORTED';
|
|
56
|
+
};
|
|
57
|
+
/** Order lines priced exactly as the customer app's setProvidedService/setProvidedAddon build them: the
|
|
58
|
+
* provider's price for this car size plus the client percentage. Hidden items and packages are refused. */
|
|
59
|
+
export declare function buildPayLinkOrderLines(provider: Pick<Provider, 'services' | 'addons'>, carSize: CarSize, serviceIds: string[], addonIds?: string[]): PayLinkLinesResult;
|
|
60
|
+
/** A pay-link order that never took money — still waiting, or cancelled for non-payment. Statements and KPIs leave these out. */
|
|
61
|
+
export declare const isNeverPaidPayLinkOrder: (order?: Pick<Order, "orderStatus" | "cancelReason" | "isPaid">) => boolean;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isNeverPaidPayLinkOrder = exports.isPayLinkAmountExact = exports.toMinorUnits = exports.payLinkDeadlineMs = exports.payLinkCancelledMessage = exports.payLinkMessage = exports.PAY_LINK_CANCEL_REASON = exports.PAY_LINK_WINDOW_MINUTES = void 0;
|
|
4
|
+
exports.whatsAppShareUrl = whatsAppShareUrl;
|
|
5
|
+
exports.buildPayLinkOrderLines = buildPayLinkOrderLines;
|
|
6
|
+
const types_1 = require("../constants/types");
|
|
7
|
+
const calcs_1 = require("./calcs");
|
|
8
|
+
/** Back-office orders paid by a link — the shared contract between sahab-website and cloudFun. */
|
|
9
|
+
exports.PAY_LINK_WINDOW_MINUTES = 15;
|
|
10
|
+
exports.PAY_LINK_CANCEL_REASON = 'payment not completed';
|
|
11
|
+
/** The message carrying the link — one wording for the server's SMS and the back office's copy/WhatsApp share. */
|
|
12
|
+
const payLinkMessage = (lang, url) => lang === 'en'
|
|
13
|
+
? `Sahab: pay within ${exports.PAY_LINK_WINDOW_MINUTES} minutes to confirm your car wash booking\n${url}`
|
|
14
|
+
: `سحاب: ادفع خلال ${exports.PAY_LINK_WINDOW_MINUTES} دقيقة لتأكيد حجز غسيل سيارتك\n${url}`;
|
|
15
|
+
exports.payLinkMessage = payLinkMessage;
|
|
16
|
+
const payLinkCancelledMessage = (lang) => lang === 'en' ? 'Sahab: your booking was cancelled because the payment was not completed.' : 'سحاب: تم إلغاء حجزك لعدم إتمام الدفع.';
|
|
17
|
+
exports.payLinkCancelledMessage = payLinkCancelledMessage;
|
|
18
|
+
/** wa.me wants the international number as digits only; local 05… numbers are Saudi. */
|
|
19
|
+
function whatsAppShareUrl(phone, text) {
|
|
20
|
+
const digits = (phone !== null && phone !== void 0 ? phone : '').replace(/[^\d]/g, '');
|
|
21
|
+
const international = digits.startsWith('0') ? `966${digits.slice(1)}` : digits;
|
|
22
|
+
return `https://wa.me/${international}?text=${encodeURIComponent(text)}`;
|
|
23
|
+
}
|
|
24
|
+
const payLinkDeadlineMs = (createdMs) => createdMs + exports.PAY_LINK_WINDOW_MINUTES * 60000;
|
|
25
|
+
exports.payLinkDeadlineMs = payLinkDeadlineMs;
|
|
26
|
+
/** Checkout amounts are integer minor units; rounding here keeps the link and the webhook check in agreement. */
|
|
27
|
+
const toMinorUnits = (sar) => Math.round(sar * 100);
|
|
28
|
+
exports.toMinorUnits = toMinorUnits;
|
|
29
|
+
/** The webhook amount matches the order only to the halala, never "close enough". */
|
|
30
|
+
const isPayLinkAmountExact = (orderTotalSar, paidMinor, currency) => (currency !== null && currency !== void 0 ? currency : '').toUpperCase() === 'SAR' && Number.isInteger(paidMinor) && paidMinor === (0, exports.toMinorUnits)(orderTotalSar);
|
|
31
|
+
exports.isPayLinkAmountExact = isPayLinkAmountExact;
|
|
32
|
+
/** Order lines priced exactly as the customer app's setProvidedService/setProvidedAddon build them: the
|
|
33
|
+
* provider's price for this car size plus the client percentage. Hidden items and packages are refused. */
|
|
34
|
+
function buildPayLinkOrderLines(provider, carSize, serviceIds, addonIds = []) {
|
|
35
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
36
|
+
if (!serviceIds.length)
|
|
37
|
+
return { ok: false, reason: 'NO_SERVICE' };
|
|
38
|
+
const lines = [];
|
|
39
|
+
for (const id of serviceIds) {
|
|
40
|
+
const service = (_a = provider.services) === null || _a === void 0 ? void 0 : _a.find((s) => s.serviceId === id && !s.hide);
|
|
41
|
+
if (!service)
|
|
42
|
+
return { ok: false, reason: 'UNKNOWN_ITEM' };
|
|
43
|
+
if (((_b = service.numberOfVisits) !== null && _b !== void 0 ? _b : 1) > 1)
|
|
44
|
+
return { ok: false, reason: 'PACKAGE_NOT_SUPPORTED' };
|
|
45
|
+
const price = (_d = (_c = service.prices) === null || _c === void 0 ? void 0 : _c.find((p) => p.size === carSize)) === null || _d === void 0 ? void 0 : _d.price;
|
|
46
|
+
if (!price)
|
|
47
|
+
return { ok: false, reason: 'NO_PRICE_FOR_SIZE' };
|
|
48
|
+
lines.push({
|
|
49
|
+
serviceId: service.serviceId,
|
|
50
|
+
name: service.name,
|
|
51
|
+
imageUrl: service.imageUrl,
|
|
52
|
+
providerId: service.providerId,
|
|
53
|
+
description: service.description,
|
|
54
|
+
price: { size: carSize, price: (0, calcs_1.customerPriceFromProviderPrice)(price) },
|
|
55
|
+
quantity: 1,
|
|
56
|
+
type: 'service',
|
|
57
|
+
time: service.time || 0,
|
|
58
|
+
includedServices: service.includedServices,
|
|
59
|
+
numberOfVisits: 1,
|
|
60
|
+
isDone: false,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
for (const id of addonIds) {
|
|
64
|
+
const addon = (_e = provider.addons) === null || _e === void 0 ? void 0 : _e.find((a) => a.addonId === id && !a.hide);
|
|
65
|
+
if (!addon)
|
|
66
|
+
return { ok: false, reason: 'UNKNOWN_ITEM' };
|
|
67
|
+
const price = (_g = (_f = addon.prices) === null || _f === void 0 ? void 0 : _f.find((p) => p.size === carSize)) === null || _g === void 0 ? void 0 : _g.price;
|
|
68
|
+
if (!price)
|
|
69
|
+
return { ok: false, reason: 'NO_PRICE_FOR_SIZE' };
|
|
70
|
+
lines.push({
|
|
71
|
+
addonId: addon.addonId,
|
|
72
|
+
name: addon.name,
|
|
73
|
+
providerId: addon.providerId,
|
|
74
|
+
description: addon.description,
|
|
75
|
+
price: { size: carSize, price: (0, calcs_1.customerPriceFromProviderPrice)(price) },
|
|
76
|
+
quantity: 1,
|
|
77
|
+
type: 'addon',
|
|
78
|
+
time: addon.time || 0,
|
|
79
|
+
isDone: false,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return { ok: true, lines };
|
|
83
|
+
}
|
|
84
|
+
/** A pay-link order that never took money — still waiting, or cancelled for non-payment. Statements and KPIs leave these out. */
|
|
85
|
+
const isNeverPaidPayLinkOrder = (order) => (order === null || order === void 0 ? void 0 : order.orderStatus) === types_1.OrderStatus.PENDING_PAYMENT ||
|
|
86
|
+
((order === null || order === void 0 ? void 0 : order.orderStatus) === types_1.OrderStatus.CANCELLED && order.cancelReason === exports.PAY_LINK_CANCEL_REASON && !order.isPaid);
|
|
87
|
+
exports.isNeverPaidPayLinkOrder = isNeverPaidPayLinkOrder;
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sahab-utils",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.9",
|
|
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
|
+
"prepublishOnly": "npm run build",
|
|
9
10
|
"deploy": "npm run build && npm publish",
|
|
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 && node tests/wheel.test.js && node tests/calcs.test.js && node tests/orderActivity.test.js && node tests/push.test.js && node tests/broadcast.test.js && node tests/iban.test.js && node tests/cash.test.js && node tests/workingHours.test.js"
|
|
11
|
+
"test": "npm run build && node tests/loyalty.test.js && node tests/gifts.test.js && node tests/cashback.test.js && node tests/utils.test.js && node tests/wheel.test.js && node tests/calcs.test.js && node tests/orderActivity.test.js && node tests/push.test.js && node tests/broadcast.test.js && node tests/iban.test.js && node tests/cash.test.js && node tests/workingHours.test.js && node tests/payLink.test.js"
|
|
11
12
|
},
|
|
12
13
|
"repository": {
|
|
13
14
|
"type": "git",
|