sahab-utils 1.8.8 → 1.8.10
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 +9 -3
- package/dist/constants/types.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/ts/payLink.d.ts +71 -0
- package/dist/ts/payLink.js +106 -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;
|
|
@@ -156,6 +157,11 @@ export type Order = {
|
|
|
156
157
|
visitNumber?: number;
|
|
157
158
|
/** Multi-visit package only: what the customer pays for the whole package, stamped on every visit. */
|
|
158
159
|
originalPrice?: number;
|
|
160
|
+
paymentLinkId?: string;
|
|
161
|
+
paymentLinkUrl?: string;
|
|
162
|
+
payLinkDeadline?: TimestampType;
|
|
163
|
+
payLinkSmsCount?: number;
|
|
164
|
+
createdByStaff?: string;
|
|
159
165
|
};
|
|
160
166
|
export type ReduxOrder = {
|
|
161
167
|
orderId?: string;
|
|
@@ -190,7 +196,7 @@ export type ReduxOrder = {
|
|
|
190
196
|
paymentCancelationId?: string;
|
|
191
197
|
paymentRefundId?: string;
|
|
192
198
|
paymentCaptureId?: string;
|
|
193
|
-
canceledBy?: 'provider' | 'client';
|
|
199
|
+
canceledBy?: 'provider' | 'client' | 'system' | 'admin';
|
|
194
200
|
movementsCreated?: boolean;
|
|
195
201
|
orderNote?: string;
|
|
196
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);
|
|
@@ -0,0 +1,71 @@
|
|
|
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` — send this or `coordinates`, not both. */
|
|
18
|
+
locationIndex?: number;
|
|
19
|
+
/** A spot typed in by staff, for a customer with no saved location that fits. */
|
|
20
|
+
coordinates?: LatLng;
|
|
21
|
+
providerId: string;
|
|
22
|
+
serviceIds: string[];
|
|
23
|
+
addonIds?: string[];
|
|
24
|
+
/** Start of the chosen slot, epoch ms. */
|
|
25
|
+
orderDate: number;
|
|
26
|
+
promoCode?: string;
|
|
27
|
+
/** Text the link from the server. Off by default: staff copy it or share it on WhatsApp. */
|
|
28
|
+
sendSms?: boolean;
|
|
29
|
+
};
|
|
30
|
+
export type CreatePayLinkOrderResponse = {
|
|
31
|
+
orderId: string;
|
|
32
|
+
orderStatus: OrderStatus;
|
|
33
|
+
/** Absent when the order was free and confirmed without a payment. */
|
|
34
|
+
paymentLinkUrl?: string;
|
|
35
|
+
/** Epoch ms after which the unpaid order is cancelled. */
|
|
36
|
+
payLinkDeadline?: number;
|
|
37
|
+
smsSent: boolean;
|
|
38
|
+
};
|
|
39
|
+
export type ResendPayLinkRequest = {
|
|
40
|
+
orderId: string;
|
|
41
|
+
};
|
|
42
|
+
/** The message carrying the link — one wording for the server's SMS and the back office's copy/WhatsApp share. */
|
|
43
|
+
export declare const payLinkMessage: (lang: string | undefined, url: string) => string;
|
|
44
|
+
export declare const payLinkCancelledMessage: (lang: string | undefined) => string;
|
|
45
|
+
/** wa.me wants the international number as digits only; local 05… numbers are Saudi. */
|
|
46
|
+
export declare function whatsAppShareUrl(phone: string | undefined, text: string): string;
|
|
47
|
+
export type LatLng = {
|
|
48
|
+
lat: number;
|
|
49
|
+
lng: number;
|
|
50
|
+
};
|
|
51
|
+
export declare const isValidLatLng: (p?: Partial<LatLng> | null) => p is LatLng;
|
|
52
|
+
/** "24.7136, 46.6753" (comma, space or both; a pasted Google Maps pair) → coordinates, or undefined when it does not
|
|
53
|
+
* read as one valid point. 0,0 is refused: it is what an empty or failed lookup produces, never a real wash. */
|
|
54
|
+
export declare function parseLatLng(text: string | undefined): LatLng | undefined;
|
|
55
|
+
export declare const payLinkDeadlineMs: (createdMs: number) => number;
|
|
56
|
+
/** Checkout amounts are integer minor units; rounding here keeps the link and the webhook check in agreement. */
|
|
57
|
+
export declare const toMinorUnits: (sar: number) => number;
|
|
58
|
+
/** The webhook amount matches the order only to the halala, never "close enough". */
|
|
59
|
+
export declare const isPayLinkAmountExact: (orderTotalSar: number, paidMinor: number, currency?: string) => boolean;
|
|
60
|
+
export type PayLinkLinesResult = {
|
|
61
|
+
ok: true;
|
|
62
|
+
lines: Array<ProvidedService | ProvidedAddon>;
|
|
63
|
+
} | {
|
|
64
|
+
ok: false;
|
|
65
|
+
reason: 'NO_SERVICE' | 'UNKNOWN_ITEM' | 'NO_PRICE_FOR_SIZE' | 'PACKAGE_NOT_SUPPORTED';
|
|
66
|
+
};
|
|
67
|
+
/** Order lines priced exactly as the customer app's setProvidedService/setProvidedAddon build them: the
|
|
68
|
+
* provider's price for this car size plus the client percentage. Hidden items and packages are refused. */
|
|
69
|
+
export declare function buildPayLinkOrderLines(provider: Pick<Provider, 'services' | 'addons'>, carSize: CarSize, serviceIds: string[], addonIds?: string[]): PayLinkLinesResult;
|
|
70
|
+
/** A pay-link order that never took money — still waiting, or cancelled for non-payment. Statements and KPIs leave these out. */
|
|
71
|
+
export declare const isNeverPaidPayLinkOrder: (order?: Pick<Order, "orderStatus" | "cancelReason" | "isPaid">) => boolean;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isNeverPaidPayLinkOrder = exports.isPayLinkAmountExact = exports.toMinorUnits = exports.payLinkDeadlineMs = exports.isValidLatLng = exports.payLinkCancelledMessage = exports.payLinkMessage = exports.PAY_LINK_CANCEL_REASON = exports.PAY_LINK_WINDOW_MINUTES = void 0;
|
|
4
|
+
exports.whatsAppShareUrl = whatsAppShareUrl;
|
|
5
|
+
exports.parseLatLng = parseLatLng;
|
|
6
|
+
exports.buildPayLinkOrderLines = buildPayLinkOrderLines;
|
|
7
|
+
const types_1 = require("../constants/types");
|
|
8
|
+
const calcs_1 = require("./calcs");
|
|
9
|
+
/** Back-office orders paid by a link — the shared contract between sahab-website and cloudFun. */
|
|
10
|
+
exports.PAY_LINK_WINDOW_MINUTES = 15;
|
|
11
|
+
exports.PAY_LINK_CANCEL_REASON = 'payment not completed';
|
|
12
|
+
/** The message carrying the link — one wording for the server's SMS and the back office's copy/WhatsApp share. */
|
|
13
|
+
const payLinkMessage = (lang, url) => lang === 'en'
|
|
14
|
+
? `Sahab: pay within ${exports.PAY_LINK_WINDOW_MINUTES} minutes to confirm your car wash booking\n${url}`
|
|
15
|
+
: `سحاب: ادفع خلال ${exports.PAY_LINK_WINDOW_MINUTES} دقيقة لتأكيد حجز غسيل سيارتك\n${url}`;
|
|
16
|
+
exports.payLinkMessage = payLinkMessage;
|
|
17
|
+
const payLinkCancelledMessage = (lang) => lang === 'en' ? 'Sahab: your booking was cancelled because the payment was not completed.' : 'سحاب: تم إلغاء حجزك لعدم إتمام الدفع.';
|
|
18
|
+
exports.payLinkCancelledMessage = payLinkCancelledMessage;
|
|
19
|
+
/** wa.me wants the international number as digits only; local 05… numbers are Saudi. */
|
|
20
|
+
function whatsAppShareUrl(phone, text) {
|
|
21
|
+
const digits = (phone !== null && phone !== void 0 ? phone : '').replace(/[^\d]/g, '');
|
|
22
|
+
const international = digits.startsWith('0') ? `966${digits.slice(1)}` : digits;
|
|
23
|
+
return `https://wa.me/${international}?text=${encodeURIComponent(text)}`;
|
|
24
|
+
}
|
|
25
|
+
const isValidLatLng = (p) => !!p &&
|
|
26
|
+
typeof p.lat === 'number' &&
|
|
27
|
+
typeof p.lng === 'number' &&
|
|
28
|
+
Number.isFinite(p.lat) &&
|
|
29
|
+
Number.isFinite(p.lng) &&
|
|
30
|
+
Math.abs(p.lat) <= 90 &&
|
|
31
|
+
Math.abs(p.lng) <= 180 &&
|
|
32
|
+
!(p.lat === 0 && p.lng === 0);
|
|
33
|
+
exports.isValidLatLng = isValidLatLng;
|
|
34
|
+
/** "24.7136, 46.6753" (comma, space or both; a pasted Google Maps pair) → coordinates, or undefined when it does not
|
|
35
|
+
* read as one valid point. 0,0 is refused: it is what an empty or failed lookup produces, never a real wash. */
|
|
36
|
+
function parseLatLng(text) {
|
|
37
|
+
const parts = (text !== null && text !== void 0 ? text : '').trim().replace(/[()]/g, '').split(/[\s,]+/).filter(Boolean);
|
|
38
|
+
if (parts.length !== 2 || !parts.every((x) => /^[-+]?\d+(\.\d+)?$/.test(x)))
|
|
39
|
+
return undefined;
|
|
40
|
+
const point = { lat: Number(parts[0]), lng: Number(parts[1]) };
|
|
41
|
+
return (0, exports.isValidLatLng)(point) ? point : undefined;
|
|
42
|
+
}
|
|
43
|
+
const payLinkDeadlineMs = (createdMs) => createdMs + exports.PAY_LINK_WINDOW_MINUTES * 60000;
|
|
44
|
+
exports.payLinkDeadlineMs = payLinkDeadlineMs;
|
|
45
|
+
/** Checkout amounts are integer minor units; rounding here keeps the link and the webhook check in agreement. */
|
|
46
|
+
const toMinorUnits = (sar) => Math.round(sar * 100);
|
|
47
|
+
exports.toMinorUnits = toMinorUnits;
|
|
48
|
+
/** The webhook amount matches the order only to the halala, never "close enough". */
|
|
49
|
+
const isPayLinkAmountExact = (orderTotalSar, paidMinor, currency) => (currency !== null && currency !== void 0 ? currency : '').toUpperCase() === 'SAR' && Number.isInteger(paidMinor) && paidMinor === (0, exports.toMinorUnits)(orderTotalSar);
|
|
50
|
+
exports.isPayLinkAmountExact = isPayLinkAmountExact;
|
|
51
|
+
/** Order lines priced exactly as the customer app's setProvidedService/setProvidedAddon build them: the
|
|
52
|
+
* provider's price for this car size plus the client percentage. Hidden items and packages are refused. */
|
|
53
|
+
function buildPayLinkOrderLines(provider, carSize, serviceIds, addonIds = []) {
|
|
54
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
55
|
+
if (!serviceIds.length)
|
|
56
|
+
return { ok: false, reason: 'NO_SERVICE' };
|
|
57
|
+
const lines = [];
|
|
58
|
+
for (const id of serviceIds) {
|
|
59
|
+
const service = (_a = provider.services) === null || _a === void 0 ? void 0 : _a.find((s) => s.serviceId === id && !s.hide);
|
|
60
|
+
if (!service)
|
|
61
|
+
return { ok: false, reason: 'UNKNOWN_ITEM' };
|
|
62
|
+
if (((_b = service.numberOfVisits) !== null && _b !== void 0 ? _b : 1) > 1)
|
|
63
|
+
return { ok: false, reason: 'PACKAGE_NOT_SUPPORTED' };
|
|
64
|
+
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;
|
|
65
|
+
if (!price)
|
|
66
|
+
return { ok: false, reason: 'NO_PRICE_FOR_SIZE' };
|
|
67
|
+
lines.push({
|
|
68
|
+
serviceId: service.serviceId,
|
|
69
|
+
name: service.name,
|
|
70
|
+
imageUrl: service.imageUrl,
|
|
71
|
+
providerId: service.providerId,
|
|
72
|
+
description: service.description,
|
|
73
|
+
price: { size: carSize, price: (0, calcs_1.customerPriceFromProviderPrice)(price) },
|
|
74
|
+
quantity: 1,
|
|
75
|
+
type: 'service',
|
|
76
|
+
time: service.time || 0,
|
|
77
|
+
includedServices: service.includedServices,
|
|
78
|
+
numberOfVisits: 1,
|
|
79
|
+
isDone: false,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
for (const id of addonIds) {
|
|
83
|
+
const addon = (_e = provider.addons) === null || _e === void 0 ? void 0 : _e.find((a) => a.addonId === id && !a.hide);
|
|
84
|
+
if (!addon)
|
|
85
|
+
return { ok: false, reason: 'UNKNOWN_ITEM' };
|
|
86
|
+
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;
|
|
87
|
+
if (!price)
|
|
88
|
+
return { ok: false, reason: 'NO_PRICE_FOR_SIZE' };
|
|
89
|
+
lines.push({
|
|
90
|
+
addonId: addon.addonId,
|
|
91
|
+
name: addon.name,
|
|
92
|
+
providerId: addon.providerId,
|
|
93
|
+
description: addon.description,
|
|
94
|
+
price: { size: carSize, price: (0, calcs_1.customerPriceFromProviderPrice)(price) },
|
|
95
|
+
quantity: 1,
|
|
96
|
+
type: 'addon',
|
|
97
|
+
time: addon.time || 0,
|
|
98
|
+
isDone: false,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return { ok: true, lines };
|
|
102
|
+
}
|
|
103
|
+
/** A pay-link order that never took money — still waiting, or cancelled for non-payment. Statements and KPIs leave these out. */
|
|
104
|
+
const isNeverPaidPayLinkOrder = (order) => (order === null || order === void 0 ? void 0 : order.orderStatus) === types_1.OrderStatus.PENDING_PAYMENT ||
|
|
105
|
+
((order === null || order === void 0 ? void 0 : order.orderStatus) === types_1.OrderStatus.CANCELLED && order.cancelReason === exports.PAY_LINK_CANCEL_REASON && !order.isPaid);
|
|
106
|
+
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.10",
|
|
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",
|