sahab-utils 1.8.10 → 1.9.2
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 +3 -16
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/ts/capacity.d.ts +12 -0
- package/dist/ts/capacity.js +23 -0
- package/dist/ts/cash.d.ts +3 -0
- package/dist/ts/cash.js +11 -0
- package/dist/ts/offerings.d.ts +54 -0
- package/dist/ts/offerings.js +216 -0
- package/dist/ts/payLink.js +9 -32
- package/package.json +2 -2
|
@@ -145,7 +145,7 @@ export type Order = {
|
|
|
145
145
|
};
|
|
146
146
|
providerPhoneNumber?: string;
|
|
147
147
|
providerLogoUrl?: string;
|
|
148
|
-
paymentGateway?: '
|
|
148
|
+
paymentGateway?: 'CHECKOUT' | 'TAP';
|
|
149
149
|
visitDates?: number[];
|
|
150
150
|
/** Cloud Tasks ids for cancelling a scheduled job — mirrors cloudFun's local `Order`; added here
|
|
151
151
|
* because the customer app reads them too, against a type that previously didn't admit they exist. */
|
|
@@ -912,6 +912,8 @@ export type CheckoutToken = {
|
|
|
912
912
|
name?: string;
|
|
913
913
|
};
|
|
914
914
|
export type SPECIFIED_TIME = 'anytime' | number;
|
|
915
|
+
/** What the customer app sends to create an order: choices only. Who is paying, whether it is paid, the carwash's own
|
|
916
|
+
* details, the cash fee and every price are decided by the server, so none of them is in the payload. */
|
|
915
917
|
export type PlaceOrder = {
|
|
916
918
|
clientID: string;
|
|
917
919
|
providerId: string;
|
|
@@ -922,31 +924,16 @@ export type PlaceOrder = {
|
|
|
922
924
|
long: number;
|
|
923
925
|
willClientBeThere: boolean;
|
|
924
926
|
paymentType: OrderPaymentType;
|
|
925
|
-
isPaid: boolean;
|
|
926
|
-
/** Set once the charge succeeds, not when the payload is first built. */
|
|
927
|
-
paymentId?: string;
|
|
928
|
-
isProviderTest: boolean;
|
|
929
|
-
providerReceiveOrders: boolean;
|
|
930
927
|
orderNote: string;
|
|
931
928
|
/** Absent when no discount applies. */
|
|
932
929
|
promoCode?: Discount;
|
|
933
930
|
driverIds: string[];
|
|
934
|
-
providerName: {
|
|
935
|
-
ar: string;
|
|
936
|
-
en: string;
|
|
937
|
-
};
|
|
938
|
-
paymentAmount: number;
|
|
939
931
|
usedBalance: number;
|
|
940
|
-
providerPhoneNumber: string;
|
|
941
932
|
isAddonStructure: boolean;
|
|
942
933
|
isMonthlyPackage: boolean;
|
|
943
|
-
sahabExtraPaymentAmount: number;
|
|
944
934
|
orderId?: string;
|
|
945
|
-
paymentGateway?: 'PAYLINK' | 'CHECKOUT' | 'TAP';
|
|
946
935
|
/** Explicit dates for a multi-visit package; the server derives them when omitted. */
|
|
947
936
|
visitDates?: number[];
|
|
948
|
-
/** Mirrors Order economics for server-side Tap split; optional override of client fee % */
|
|
949
|
-
sahabClientPercentage?: number;
|
|
950
937
|
};
|
|
951
938
|
export type WeatherData = {
|
|
952
939
|
date: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
export * from './constants/types';
|
|
2
2
|
export * from './ts/broadcast';
|
|
3
|
+
export * from './ts/capacity';
|
|
3
4
|
export * from './ts/calcs';
|
|
4
5
|
export * from './ts/cash';
|
|
5
6
|
export * from './ts/cashback';
|
|
6
7
|
export * from './ts/gifts';
|
|
7
8
|
export * from './ts/iban';
|
|
8
9
|
export * from './ts/loyalty';
|
|
10
|
+
export * from './ts/offerings';
|
|
9
11
|
export * from './ts/orderActivity';
|
|
10
12
|
export * from './ts/payLink';
|
|
11
13
|
export * from './ts/push';
|
package/dist/index.js
CHANGED
|
@@ -16,12 +16,14 @@ 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/broadcast"), exports);
|
|
19
|
+
__exportStar(require("./ts/capacity"), exports);
|
|
19
20
|
__exportStar(require("./ts/calcs"), exports);
|
|
20
21
|
__exportStar(require("./ts/cash"), exports);
|
|
21
22
|
__exportStar(require("./ts/cashback"), exports);
|
|
22
23
|
__exportStar(require("./ts/gifts"), exports);
|
|
23
24
|
__exportStar(require("./ts/iban"), exports);
|
|
24
25
|
__exportStar(require("./ts/loyalty"), exports);
|
|
26
|
+
__exportStar(require("./ts/offerings"), exports);
|
|
25
27
|
__exportStar(require("./ts/orderActivity"), exports);
|
|
26
28
|
__exportStar(require("./ts/payLink"), exports);
|
|
27
29
|
__exportStar(require("./ts/push"), exports);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Provider } from '../constants/types';
|
|
2
|
+
/** Whether a carwash takes orders itself: on unless the owner switched it off, so a record with no flag counts as on. */
|
|
3
|
+
export declare const providerReceivesOrders: (provider?: Pick<Provider, "receiveOrders"> | null) => boolean;
|
|
4
|
+
/** How many orders a slot can hold: the explicit drivers on the booking, else the drivers near the customer, else every
|
|
5
|
+
* active driver, plus the owner when they take orders themselves. The server lists slots and validates bookings with this, and
|
|
6
|
+
* the provider app shows it, so they cannot disagree. */
|
|
7
|
+
export declare function computeAllowedOrdersPerSlot(params: {
|
|
8
|
+
providerReceiveOrders?: boolean;
|
|
9
|
+
totalActiveDrivers: number;
|
|
10
|
+
driverIdsFromClient?: string[];
|
|
11
|
+
nearbyDriverCount?: number;
|
|
12
|
+
}): number;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.providerReceivesOrders = void 0;
|
|
4
|
+
exports.computeAllowedOrdersPerSlot = computeAllowedOrdersPerSlot;
|
|
5
|
+
/** Whether a carwash takes orders itself: on unless the owner switched it off, so a record with no flag counts as on. */
|
|
6
|
+
const providerReceivesOrders = (provider) => (provider === null || provider === void 0 ? void 0 : provider.receiveOrders) !== false;
|
|
7
|
+
exports.providerReceivesOrders = providerReceivesOrders;
|
|
8
|
+
/** How many orders a slot can hold: the explicit drivers on the booking, else the drivers near the customer, else every
|
|
9
|
+
* active driver, plus the owner when they take orders themselves. The server lists slots and validates bookings with this, and
|
|
10
|
+
* the provider app shows it, so they cannot disagree. */
|
|
11
|
+
function computeAllowedOrdersPerSlot(params) {
|
|
12
|
+
const { providerReceiveOrders, totalActiveDrivers, driverIdsFromClient, nearbyDriverCount } = params;
|
|
13
|
+
if (driverIdsFromClient && driverIdsFromClient.length > 0) {
|
|
14
|
+
return providerReceiveOrders ? driverIdsFromClient.length + 1 : driverIdsFromClient.length;
|
|
15
|
+
}
|
|
16
|
+
if (nearbyDriverCount != null && nearbyDriverCount > 0) {
|
|
17
|
+
return providerReceiveOrders ? nearbyDriverCount + 1 : nearbyDriverCount;
|
|
18
|
+
}
|
|
19
|
+
if (providerReceiveOrders) {
|
|
20
|
+
return totalActiveDrivers + 1;
|
|
21
|
+
}
|
|
22
|
+
return totalActiveDrivers === 0 ? 1 : totalActiveDrivers;
|
|
23
|
+
}
|
package/dist/ts/cash.d.ts
CHANGED
|
@@ -11,3 +11,6 @@ export declare const isFreeOrder: (order?: Partial<Order>) => boolean;
|
|
|
11
11
|
export declare function resolveCashFee(configs?: Pick<PaymentConfigs, 'cashFee'> | null): number;
|
|
12
12
|
/** The fee this order would carry if paid in cash: none when there is nothing to collect. */
|
|
13
13
|
export declare function cashFeeForOrder(order: Partial<Order> | undefined, configs?: Pick<PaymentConfigs, 'cashFee'> | null): number;
|
|
14
|
+
/** What the washer takes from the customer for a cash order, and what the server records as collected when the order is finished:
|
|
15
|
+
* nothing for a free order, else the order's total to pay. Undefined for an online order, which was paid up front. */
|
|
16
|
+
export declare function cashAmountToCollect(order?: Partial<Order>): number | undefined;
|
package/dist/ts/cash.js
CHANGED
|
@@ -3,8 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.isFreeOrder = exports.getOrderTotalWithoutCashFee = exports.MAX_CASH_FEE = void 0;
|
|
4
4
|
exports.resolveCashFee = resolveCashFee;
|
|
5
5
|
exports.cashFeeForOrder = cashFeeForOrder;
|
|
6
|
+
exports.cashAmountToCollect = cashAmountToCollect;
|
|
6
7
|
const types_1 = require("../constants/types");
|
|
7
8
|
const calcs_1 = require("./calcs");
|
|
9
|
+
const utils_1 = require("./utils");
|
|
8
10
|
/** Cash payment — the shared rules. The fee is set from `payment_methods/configs`; the app only shows it
|
|
9
11
|
* and cloudFun is what charges it, so both must reach the same number from the same config. */
|
|
10
12
|
/** A fee above this is read as a typo (5000 for 5.00) and ignored rather than charged to customers. */
|
|
@@ -26,3 +28,12 @@ function resolveCashFee(configs) {
|
|
|
26
28
|
function cashFeeForOrder(order, configs) {
|
|
27
29
|
return (0, exports.isFreeOrder)(order) ? 0 : resolveCashFee(configs);
|
|
28
30
|
}
|
|
31
|
+
/** What the washer takes from the customer for a cash order, and what the server records as collected when the order is finished:
|
|
32
|
+
* nothing for a free order, else the order's total to pay. Undefined for an online order, which was paid up front. */
|
|
33
|
+
function cashAmountToCollect(order) {
|
|
34
|
+
if ((order === null || order === void 0 ? void 0 : order.paymentType) !== types_1.OrderPaymentType.CASH)
|
|
35
|
+
return undefined;
|
|
36
|
+
if ((0, exports.isFreeOrder)(order))
|
|
37
|
+
return 0;
|
|
38
|
+
return Math.max(0, (0, utils_1.preventNaN)((0, calcs_1.getCustomerTotalToPay)(order)));
|
|
39
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Car, CarService, CarSize, Discount, Price, ProvidedAddon, ProvidedService, Provider, ProviderAddon, ProviderService } from '../constants/types';
|
|
2
|
+
export declare function serviceVisitCount(service?: ProviderService): number;
|
|
3
|
+
/** A package sold as several visits, rather than a single wash. */
|
|
4
|
+
export declare function isMultipleVisitService(service?: ProviderService): boolean;
|
|
5
|
+
/** Single-wash services the customer may browse. */
|
|
6
|
+
export declare function normalServices(provider?: Provider): ProviderService[];
|
|
7
|
+
/** Multi-visit packages the customer may browse. */
|
|
8
|
+
export declare function multipleVisitServices(provider?: Provider): ProviderService[];
|
|
9
|
+
/** Add-ons the customer may browse. */
|
|
10
|
+
export declare function visibleAddons(provider?: Provider): ProviderAddon[];
|
|
11
|
+
export declare function isAvailableForSize(prices?: Price[], size?: CarSize): boolean;
|
|
12
|
+
export declare function startingPrice(provider?: Provider): number | null;
|
|
13
|
+
/** The discounts that apply to one service. */
|
|
14
|
+
export declare function discountsForService(discounts?: Discount[], serviceId?: string): Discount[];
|
|
15
|
+
/** The discounts that apply to one add-on. */
|
|
16
|
+
export declare function discountsForAddon(discounts?: Discount[], addonId?: string): Discount[];
|
|
17
|
+
/** Why an offering cannot be selected right now, or `null` when it can. */
|
|
18
|
+
export type OfferingBlockedReason = 'unavailable_for_size' | 'package_selected' | 'needs_service';
|
|
19
|
+
export declare function hasPackageSelected(selectedServices: (ProviderService | undefined)[]): boolean;
|
|
20
|
+
export declare function offeringBlockedReason(args: {
|
|
21
|
+
kind: 'service' | 'addon';
|
|
22
|
+
availableForSize: boolean;
|
|
23
|
+
packageSelected: boolean;
|
|
24
|
+
hasServiceSelected: boolean;
|
|
25
|
+
}): OfferingBlockedReason | null;
|
|
26
|
+
export declare function nextSelectionAfterToggle<T extends {
|
|
27
|
+
kind: 'service' | 'addon';
|
|
28
|
+
id: string;
|
|
29
|
+
}>(current: T[], toggled: T, opts: {
|
|
30
|
+
isPackage: boolean;
|
|
31
|
+
packageSelected: boolean;
|
|
32
|
+
}): T[];
|
|
33
|
+
/** A provider price shown to the customer, less the first of `discounts`; never below zero. */
|
|
34
|
+
export declare const getServicePrice: (prices?: Price[], size?: CarSize, discounts?: Discount[]) => number;
|
|
35
|
+
/** The customer's price for a catalogue price at a car size, before any discount. */
|
|
36
|
+
export declare const setServicePrice: (prices?: Price[], size?: CarSize) => number;
|
|
37
|
+
/** An order line for a service, as the customer app builds it and as the server rebuilds it. */
|
|
38
|
+
export declare const setProvidedService: (service: ProviderService, car: Pick<Car, "size">, discounts?: Discount[]) => ProvidedService;
|
|
39
|
+
/** An order line for an add-on, as the customer app builds it and as the server rebuilds it. */
|
|
40
|
+
export declare const setProvidedAddon: (addon: ProviderAddon, car: Pick<Car, "size">, discounts?: Discount[]) => ProvidedAddon;
|
|
41
|
+
export type BasketRejection = 'empty_basket' | 'line_invalid' | 'line_unavailable' | 'line_not_priced' | 'package_not_alone';
|
|
42
|
+
export type RepricedBasket = {
|
|
43
|
+
ok: true;
|
|
44
|
+
services: CarService[];
|
|
45
|
+
} | {
|
|
46
|
+
ok: false;
|
|
47
|
+
reason: BasketRejection;
|
|
48
|
+
detail?: string;
|
|
49
|
+
};
|
|
50
|
+
export declare const withoutUndefined: <T extends object>(line: T) => T;
|
|
51
|
+
/** A basket rebuilt from the provider's catalogue: every line's price at its car's size, duration, visit count and
|
|
52
|
+
* discount come from `provider` and `discounts` (the offers this customer may use now), whatever the basket carried.
|
|
53
|
+
* A line the provider does not sell, or not at that size, is refused rather than dropped. */
|
|
54
|
+
export declare function repriceBasket(services: CarService[] | undefined, provider: Pick<Provider, 'services' | 'addons'>, discounts: Discount[]): RepricedBasket;
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.withoutUndefined = exports.setProvidedAddon = exports.setProvidedService = exports.setServicePrice = exports.getServicePrice = void 0;
|
|
4
|
+
exports.serviceVisitCount = serviceVisitCount;
|
|
5
|
+
exports.isMultipleVisitService = isMultipleVisitService;
|
|
6
|
+
exports.normalServices = normalServices;
|
|
7
|
+
exports.multipleVisitServices = multipleVisitServices;
|
|
8
|
+
exports.visibleAddons = visibleAddons;
|
|
9
|
+
exports.isAvailableForSize = isAvailableForSize;
|
|
10
|
+
exports.startingPrice = startingPrice;
|
|
11
|
+
exports.discountsForService = discountsForService;
|
|
12
|
+
exports.discountsForAddon = discountsForAddon;
|
|
13
|
+
exports.hasPackageSelected = hasPackageSelected;
|
|
14
|
+
exports.offeringBlockedReason = offeringBlockedReason;
|
|
15
|
+
exports.nextSelectionAfterToggle = nextSelectionAfterToggle;
|
|
16
|
+
exports.repriceBasket = repriceBasket;
|
|
17
|
+
const types_1 = require("../constants/types");
|
|
18
|
+
const calcs_1 = require("./calcs");
|
|
19
|
+
const utils_1 = require("./utils");
|
|
20
|
+
// These rules used to live inline in the provider screen, so the gift flow re-derived a looser
|
|
21
|
+
// copy and offered services the customer couldn't book. Read offerings from here, not `provider.services`.
|
|
22
|
+
// The server prices an order with the same functions the app builds its lines with (`repriceBasket`).
|
|
23
|
+
// Two field names exist in the wild: `numberOfVisits` is current, `numberOfServices` is what
|
|
24
|
+
// older provider records carry — both are honoured so an old package isn't sold at one visit's price.
|
|
25
|
+
function serviceVisitCount(service) {
|
|
26
|
+
var _a, _b;
|
|
27
|
+
const s = service;
|
|
28
|
+
return (_b = (_a = s === null || s === void 0 ? void 0 : s.numberOfVisits) !== null && _a !== void 0 ? _a : s === null || s === void 0 ? void 0 : s.numberOfServices) !== null && _b !== void 0 ? _b : 1;
|
|
29
|
+
}
|
|
30
|
+
/** A package sold as several visits, rather than a single wash. */
|
|
31
|
+
function isMultipleVisitService(service) {
|
|
32
|
+
return serviceVisitCount(service) > 1;
|
|
33
|
+
}
|
|
34
|
+
/** Single-wash services the customer may browse. */
|
|
35
|
+
function normalServices(provider) {
|
|
36
|
+
var _a;
|
|
37
|
+
return ((_a = provider === null || provider === void 0 ? void 0 : provider.services) !== null && _a !== void 0 ? _a : []).filter((s) => !(s === null || s === void 0 ? void 0 : s.hide) && !isMultipleVisitService(s));
|
|
38
|
+
}
|
|
39
|
+
/** Multi-visit packages the customer may browse. */
|
|
40
|
+
function multipleVisitServices(provider) {
|
|
41
|
+
var _a;
|
|
42
|
+
return ((_a = provider === null || provider === void 0 ? void 0 : provider.services) !== null && _a !== void 0 ? _a : []).filter((s) => !(s === null || s === void 0 ? void 0 : s.hide) && isMultipleVisitService(s));
|
|
43
|
+
}
|
|
44
|
+
/** Add-ons the customer may browse. */
|
|
45
|
+
function visibleAddons(provider) {
|
|
46
|
+
var _a;
|
|
47
|
+
return ((_a = provider === null || provider === void 0 ? void 0 : provider.addons) !== null && _a !== void 0 ? _a : []).filter((a) => !(a === null || a === void 0 ? void 0 : a.hide));
|
|
48
|
+
}
|
|
49
|
+
// A size with no price is not offered at all — "cannot be selected", not "costs nothing". Kept
|
|
50
|
+
// local rather than calling `isCarAllowedToSelectService` since `ts/utils.ts` pulls in `expo-location`.
|
|
51
|
+
function isAvailableForSize(prices, size) {
|
|
52
|
+
return (prices !== null && prices !== void 0 ? prices : []).some((p) => (p === null || p === void 0 ? void 0 : p.size) === size);
|
|
53
|
+
}
|
|
54
|
+
// X_SMALL is a motorbike, priced well below any car — quoting it as the starting price would
|
|
55
|
+
// undercut what a customer with a car actually gets charged.
|
|
56
|
+
const SIZES_EXCLUDED_FROM_STARTING_PRICE = [types_1.CarSize.X_SMALL];
|
|
57
|
+
// The "starts from" figure — reads only what the customer can see (hidden services excluded) and
|
|
58
|
+
// excludes multi-visit packages, whose price is a total for several visits, not one wash.
|
|
59
|
+
function startingPrice(provider) {
|
|
60
|
+
const prices = normalServices(provider)
|
|
61
|
+
.reduce((all, s) => { var _a; return all.concat((_a = s === null || s === void 0 ? void 0 : s.prices) !== null && _a !== void 0 ? _a : []); }, [])
|
|
62
|
+
.filter((p) => !SIZES_EXCLUDED_FROM_STARTING_PRICE.includes(p === null || p === void 0 ? void 0 : p.size) && Number.isFinite(p === null || p === void 0 ? void 0 : p.price))
|
|
63
|
+
.map((p) => p.price);
|
|
64
|
+
return prices.length ? Math.min(...prices) : null;
|
|
65
|
+
}
|
|
66
|
+
/** The discounts that apply to one service. */
|
|
67
|
+
function discountsForService(discounts, serviceId) {
|
|
68
|
+
if (!serviceId)
|
|
69
|
+
return [];
|
|
70
|
+
return (discounts !== null && discounts !== void 0 ? discounts : []).filter((d) => { var _a; return (_a = d === null || d === void 0 ? void 0 : d.servicesIds) === null || _a === void 0 ? void 0 : _a.includes(serviceId); });
|
|
71
|
+
}
|
|
72
|
+
/** The discounts that apply to one add-on. */
|
|
73
|
+
function discountsForAddon(discounts, addonId) {
|
|
74
|
+
if (!addonId)
|
|
75
|
+
return [];
|
|
76
|
+
return (discounts !== null && discounts !== void 0 ? discounts : []).filter((d) => { var _a; return (_a = d === null || d === void 0 ? void 0 : d.addonsIds) === null || _a === void 0 ? void 0 : _a.includes(addonId); });
|
|
77
|
+
}
|
|
78
|
+
// A package is sold alone — exactly one selected line covering several visits. Two selected
|
|
79
|
+
// lines never count, which stops a package plus an add-on from reading as a package basket.
|
|
80
|
+
function hasPackageSelected(selectedServices) {
|
|
81
|
+
if (selectedServices.length !== 1)
|
|
82
|
+
return false;
|
|
83
|
+
return isMultipleVisitService(selectedServices[0]);
|
|
84
|
+
}
|
|
85
|
+
// The provider screen's rules, lifted here so the gift flow reaches the same verdict instead of
|
|
86
|
+
// approximating it: size must be priced, no package + anything else, add-ons need a service.
|
|
87
|
+
function offeringBlockedReason(args) {
|
|
88
|
+
if (!args.availableForSize)
|
|
89
|
+
return 'unavailable_for_size';
|
|
90
|
+
if (args.packageSelected)
|
|
91
|
+
return 'package_selected';
|
|
92
|
+
if (args.kind === 'addon' && !args.hasServiceSelected)
|
|
93
|
+
return 'needs_service';
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
// Matches `Services.tsx`'s add/remove rule (package replaces selection; nothing else while one is
|
|
97
|
+
// selected) — never lifting this here is how the gift flow shipped holding both at once (see root CLAUDE.md).
|
|
98
|
+
function nextSelectionAfterToggle(current, toggled, opts) {
|
|
99
|
+
const alreadySelected = current.some((s) => s.kind === toggled.kind && s.id === toggled.id);
|
|
100
|
+
if (alreadySelected) {
|
|
101
|
+
return current.filter((s) => !(s.kind === toggled.kind && s.id === toggled.id));
|
|
102
|
+
}
|
|
103
|
+
// A package can't coexist with anything else — selecting one replaces the whole basket,
|
|
104
|
+
// and selecting a normal service/addon while a package is selected does the same in reverse.
|
|
105
|
+
if (opts.isPackage || opts.packageSelected)
|
|
106
|
+
return [toggled];
|
|
107
|
+
return [...current, toggled];
|
|
108
|
+
}
|
|
109
|
+
/** A provider price shown to the customer, less the first of `discounts`; never below zero. */
|
|
110
|
+
const getServicePrice = (prices, size, discounts) => {
|
|
111
|
+
const price = (prices || []).find((p) => p.size === size);
|
|
112
|
+
let total = (price === null || price === void 0 ? void 0 : price.price) || 0;
|
|
113
|
+
let discountsTotal = 0;
|
|
114
|
+
if (discounts === null || discounts === void 0 ? void 0 : discounts.length) {
|
|
115
|
+
const discount = discounts[0];
|
|
116
|
+
discountsTotal += (discount === null || discount === void 0 ? void 0 : discount.isPercentage) ? ((0, utils_1.preventNaN)(discount === null || discount === void 0 ? void 0 : discount.amount) / 100) * ((price === null || price === void 0 ? void 0 : price.price) || 0) : (0, utils_1.preventNaN)(discount === null || discount === void 0 ? void 0 : discount.amount);
|
|
117
|
+
}
|
|
118
|
+
total = total - discountsTotal > 0 ? total - discountsTotal : 0;
|
|
119
|
+
return (0, calcs_1.customerPriceFromProviderPrice)(total);
|
|
120
|
+
};
|
|
121
|
+
exports.getServicePrice = getServicePrice;
|
|
122
|
+
/** The customer's price for a catalogue price at a car size, before any discount. */
|
|
123
|
+
const setServicePrice = (prices, size) => {
|
|
124
|
+
const price = (prices || []).find((p) => p.size === size);
|
|
125
|
+
return (0, calcs_1.customerPriceFromProviderPrice)((price === null || price === void 0 ? void 0 : price.price) || 0);
|
|
126
|
+
};
|
|
127
|
+
exports.setServicePrice = setServicePrice;
|
|
128
|
+
/** An order line for a service, as the customer app builds it and as the server rebuilds it. */
|
|
129
|
+
const setProvidedService = (service, car, discounts) => {
|
|
130
|
+
const oneDiscount = (discounts === null || discounts === void 0 ? void 0 : discounts.length) ? discounts[0] : undefined;
|
|
131
|
+
return {
|
|
132
|
+
serviceId: service.serviceId,
|
|
133
|
+
name: service.name,
|
|
134
|
+
imageUrl: service.imageUrl,
|
|
135
|
+
providerId: service.providerId,
|
|
136
|
+
description: service.description,
|
|
137
|
+
price: {
|
|
138
|
+
size: car.size,
|
|
139
|
+
price: (0, exports.setServicePrice)(service.prices, car.size),
|
|
140
|
+
},
|
|
141
|
+
quantity: 1,
|
|
142
|
+
type: 'service',
|
|
143
|
+
time: service.time || 0,
|
|
144
|
+
includedServices: service.includedServices,
|
|
145
|
+
numberOfVisits: service.numberOfVisits || 1,
|
|
146
|
+
isDone: false,
|
|
147
|
+
discount: oneDiscount,
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
exports.setProvidedService = setProvidedService;
|
|
151
|
+
/** An order line for an add-on, as the customer app builds it and as the server rebuilds it. */
|
|
152
|
+
const setProvidedAddon = (addon, car, discounts) => {
|
|
153
|
+
const oneDiscount = (discounts === null || discounts === void 0 ? void 0 : discounts.length) ? discounts[0] : undefined;
|
|
154
|
+
return {
|
|
155
|
+
addonId: addon.addonId,
|
|
156
|
+
name: addon.name,
|
|
157
|
+
providerId: addon.providerId,
|
|
158
|
+
description: addon.description,
|
|
159
|
+
price: {
|
|
160
|
+
size: car.size,
|
|
161
|
+
price: (0, exports.setServicePrice)(addon.prices, car.size),
|
|
162
|
+
},
|
|
163
|
+
quantity: 1,
|
|
164
|
+
type: 'addon',
|
|
165
|
+
time: addon.time || 0,
|
|
166
|
+
isDone: false,
|
|
167
|
+
discount: oneDiscount,
|
|
168
|
+
};
|
|
169
|
+
};
|
|
170
|
+
exports.setProvidedAddon = setProvidedAddon;
|
|
171
|
+
const withoutUndefined = (line) => {
|
|
172
|
+
const kept = {};
|
|
173
|
+
Object.keys(line).forEach((key) => {
|
|
174
|
+
const value = line[key];
|
|
175
|
+
if (value !== undefined)
|
|
176
|
+
kept[key] = value;
|
|
177
|
+
});
|
|
178
|
+
return kept;
|
|
179
|
+
};
|
|
180
|
+
exports.withoutUndefined = withoutUndefined;
|
|
181
|
+
/** A basket rebuilt from the provider's catalogue: every line's price at its car's size, duration, visit count and
|
|
182
|
+
* discount come from `provider` and `discounts` (the offers this customer may use now), whatever the basket carried.
|
|
183
|
+
* A line the provider does not sell, or not at that size, is refused rather than dropped. */
|
|
184
|
+
function repriceBasket(services, provider, discounts) {
|
|
185
|
+
var _a, _b, _c, _d, _e;
|
|
186
|
+
if (!Array.isArray(services) || !services.some((section) => Array.isArray(section === null || section === void 0 ? void 0 : section.services) && section.services.length > 0))
|
|
187
|
+
return { ok: false, reason: 'empty_basket' };
|
|
188
|
+
const sections = [];
|
|
189
|
+
for (const section of services) {
|
|
190
|
+
const size = (_a = section === null || section === void 0 ? void 0 : section.car) === null || _a === void 0 ? void 0 : _a.size;
|
|
191
|
+
if (!Array.isArray(section === null || section === void 0 ? void 0 : section.services) || size === undefined || size === null)
|
|
192
|
+
return { ok: false, reason: 'line_invalid', detail: 'car or size' };
|
|
193
|
+
const lines = [];
|
|
194
|
+
for (const line of section.services) {
|
|
195
|
+
const { serviceId, addonId } = line;
|
|
196
|
+
const offering = serviceId ? (_b = provider.services) === null || _b === void 0 ? void 0 : _b.find((s) => s.serviceId === serviceId) : addonId ? (_c = provider.addons) === null || _c === void 0 ? void 0 : _c.find((a) => a.addonId === addonId) : undefined;
|
|
197
|
+
if (!serviceId && !addonId)
|
|
198
|
+
return { ok: false, reason: 'line_invalid', detail: 'line has no serviceId or addonId' };
|
|
199
|
+
if (!offering || offering.hide)
|
|
200
|
+
return { ok: false, reason: 'line_unavailable', detail: serviceId !== null && serviceId !== void 0 ? serviceId : addonId };
|
|
201
|
+
if (!isAvailableForSize(offering.prices, size))
|
|
202
|
+
return { ok: false, reason: 'line_not_priced', detail: `${serviceId !== null && serviceId !== void 0 ? serviceId : addonId} size ${size}` };
|
|
203
|
+
const built = serviceId
|
|
204
|
+
? (0, exports.setProvidedService)(offering, section.car, discountsForService(discounts, serviceId))
|
|
205
|
+
: (0, exports.setProvidedAddon)(offering, section.car, discountsForAddon(discounts, addonId));
|
|
206
|
+
const builtPrice = (_e = (_d = built.price) === null || _d === void 0 ? void 0 : _d.price) !== null && _e !== void 0 ? _e : 0;
|
|
207
|
+
if (!(builtPrice > 0) || !Number.isFinite(builtPrice))
|
|
208
|
+
return { ok: false, reason: 'line_not_priced', detail: `${serviceId !== null && serviceId !== void 0 ? serviceId : addonId} size ${size}` };
|
|
209
|
+
lines.push((0, exports.withoutUndefined)(built));
|
|
210
|
+
}
|
|
211
|
+
if (lines.length > 1 && lines.some((line) => { var _a; return line.type === 'service' && ((_a = line.numberOfVisits) !== null && _a !== void 0 ? _a : 1) > 1; }))
|
|
212
|
+
return { ok: false, reason: 'package_not_alone' };
|
|
213
|
+
sections.push(Object.assign(Object.assign({}, section), { services: lines }));
|
|
214
|
+
}
|
|
215
|
+
return { ok: true, services: sections };
|
|
216
|
+
}
|
package/dist/ts/payLink.js
CHANGED
|
@@ -5,7 +5,7 @@ exports.whatsAppShareUrl = whatsAppShareUrl;
|
|
|
5
5
|
exports.parseLatLng = parseLatLng;
|
|
6
6
|
exports.buildPayLinkOrderLines = buildPayLinkOrderLines;
|
|
7
7
|
const types_1 = require("../constants/types");
|
|
8
|
-
const
|
|
8
|
+
const offerings_1 = require("./offerings");
|
|
9
9
|
/** Back-office orders paid by a link — the shared contract between sahab-website and cloudFun. */
|
|
10
10
|
exports.PAY_LINK_WINDOW_MINUTES = 15;
|
|
11
11
|
exports.PAY_LINK_CANCEL_REASON = 'payment not completed';
|
|
@@ -51,7 +51,7 @@ exports.isPayLinkAmountExact = isPayLinkAmountExact;
|
|
|
51
51
|
/** Order lines priced exactly as the customer app's setProvidedService/setProvidedAddon build them: the
|
|
52
52
|
* provider's price for this car size plus the client percentage. Hidden items and packages are refused. */
|
|
53
53
|
function buildPayLinkOrderLines(provider, carSize, serviceIds, addonIds = []) {
|
|
54
|
-
var _a, _b, _c, _d, _e
|
|
54
|
+
var _a, _b, _c, _d, _e;
|
|
55
55
|
if (!serviceIds.length)
|
|
56
56
|
return { ok: false, reason: 'NO_SERVICE' };
|
|
57
57
|
const lines = [];
|
|
@@ -61,42 +61,19 @@ function buildPayLinkOrderLines(provider, carSize, serviceIds, addonIds = []) {
|
|
|
61
61
|
return { ok: false, reason: 'UNKNOWN_ITEM' };
|
|
62
62
|
if (((_b = service.numberOfVisits) !== null && _b !== void 0 ? _b : 1) > 1)
|
|
63
63
|
return { ok: false, reason: 'PACKAGE_NOT_SUPPORTED' };
|
|
64
|
-
const
|
|
65
|
-
if (!price)
|
|
64
|
+
const line = (0, offerings_1.setProvidedService)(service, { size: carSize });
|
|
65
|
+
if (!((_c = line.price) === null || _c === void 0 ? void 0 : _c.price))
|
|
66
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
|
-
});
|
|
67
|
+
lines.push((0, offerings_1.withoutUndefined)(line));
|
|
81
68
|
}
|
|
82
69
|
for (const id of addonIds) {
|
|
83
|
-
const addon = (
|
|
70
|
+
const addon = (_d = provider.addons) === null || _d === void 0 ? void 0 : _d.find((a) => a.addonId === id && !a.hide);
|
|
84
71
|
if (!addon)
|
|
85
72
|
return { ok: false, reason: 'UNKNOWN_ITEM' };
|
|
86
|
-
const
|
|
87
|
-
if (!price)
|
|
73
|
+
const line = (0, offerings_1.setProvidedAddon)(addon, { size: carSize });
|
|
74
|
+
if (!((_e = line.price) === null || _e === void 0 ? void 0 : _e.price))
|
|
88
75
|
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
|
-
});
|
|
76
|
+
lines.push((0, offerings_1.withoutUndefined)(line));
|
|
100
77
|
}
|
|
101
78
|
return { ok: true, lines };
|
|
102
79
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sahab-utils",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.2",
|
|
4
4
|
"description": "This package contains types and utils functions",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"build": "tsc",
|
|
9
9
|
"prepublishOnly": "npm run build",
|
|
10
10
|
"deploy": "npm run build && npm publish",
|
|
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
|
+
"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 && node tests/offerings.test.js && node tests/capacity.test.js"
|
|
12
12
|
},
|
|
13
13
|
"repository": {
|
|
14
14
|
"type": "git",
|