sahab-utils 1.8.5 → 1.8.8

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.
@@ -152,6 +152,10 @@ export type Order = {
152
152
  customerNotificationTaskId?: string;
153
153
  /** Set on an order created by re-ordering, pointing at the one it was copied from. */
154
154
  originalOrderId?: string;
155
+ /** Multi-visit package only: 1-based position of this order in the package. Every visit is its own order. */
156
+ visitNumber?: number;
157
+ /** Multi-visit package only: what the customer pays for the whole package, stamped on every visit. */
158
+ originalPrice?: number;
155
159
  };
156
160
  export type ReduxOrder = {
157
161
  orderId?: string;
package/dist/index.d.ts CHANGED
@@ -10,3 +10,4 @@ export * from './ts/orderActivity';
10
10
  export * from './ts/push';
11
11
  export * from './ts/utils';
12
12
  export * from './ts/wheel';
13
+ export * from './ts/workingHours';
package/dist/index.js CHANGED
@@ -26,3 +26,4 @@ __exportStar(require("./ts/orderActivity"), exports);
26
26
  __exportStar(require("./ts/push"), exports);
27
27
  __exportStar(require("./ts/utils"), exports);
28
28
  __exportStar(require("./ts/wheel"), exports);
29
+ __exportStar(require("./ts/workingHours"), exports);
@@ -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
- return (order === null || order === void 0 ? void 0 : order.paymentType) === types_1.OrderPaymentType.CASH ? (0, utils_1.preventNaN)((_a = order === null || order === void 0 ? void 0 : order.paymentAmount) !== null && _a !== void 0 ? _a : 0) : getCustomerTotalToPay(order);
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 / (1 + (0, utils_1.preventNaN)((_a = order === null || order === void 0 ? void 0 : order.sahabClientPercentage) !== null && _a !== void 0 ? _a : 0));
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 / (1 + (0, utils_1.preventNaN)((_a = order === null || order === void 0 ? void 0 : order.sahabClientPercentage) !== null && _a !== void 0 ? _a : 0));
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.
@@ -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 a finished order earns a stamp. The minimum is compared against what the customer
16
- * actually paid, not the list price, so an order discounted below it doesn't qualify. */
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. */
@@ -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 a finished order earns a stamp. The minimum is compared against what the customer
62
- * actually paid, not the list price, so an order discounted below it doesn't qualify. */
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 && (0, calcs_1.getCustomerTotalToPay)(order) < min)
99
+ if (min > 0 && loyaltyQualifyingAmount(order) < min)
79
100
  return false;
80
101
  return true;
81
102
  }
@@ -0,0 +1,8 @@
1
+ import { Provider, WorkingHours } from '../constants/types';
2
+ export declare const emptyWorkingHours: () => WorkingHours;
3
+ export declare const allDayWorkingHours: () => WorkingHours;
4
+ /**
5
+ * The hours a provider can be booked in. Slot listing and booking validation both read this, so they cannot disagree: a provider
6
+ * who works 24/7 is open every hour of every day whatever weekly schedule is also stored, and one with none stored is never open.
7
+ */
8
+ export declare function resolveWorkingHours(provider?: Pick<Provider, 'working247' | 'workingHoursV2'> | null): WorkingHours;
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.allDayWorkingHours = exports.emptyWorkingHours = void 0;
4
+ exports.resolveWorkingHours = resolveWorkingHours;
5
+ /** One shift that covers a whole calendar day; slot building caps a day at midnight, so this is as long as a shift can be. */
6
+ const ALL_DAY_SHIFT = { start: '00:00', end: '23:59' };
7
+ const DAYS = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
8
+ const weekOf = (shifts) => {
9
+ const week = {};
10
+ DAYS.forEach((day) => {
11
+ week[day] = shifts();
12
+ });
13
+ return week;
14
+ };
15
+ const emptyWorkingHours = () => weekOf(() => []);
16
+ exports.emptyWorkingHours = emptyWorkingHours;
17
+ const allDayWorkingHours = () => weekOf(() => [Object.assign({}, ALL_DAY_SHIFT)]);
18
+ exports.allDayWorkingHours = allDayWorkingHours;
19
+ /**
20
+ * The hours a provider can be booked in. Slot listing and booking validation both read this, so they cannot disagree: a provider
21
+ * who works 24/7 is open every hour of every day whatever weekly schedule is also stored, and one with none stored is never open.
22
+ */
23
+ function resolveWorkingHours(provider) {
24
+ var _a;
25
+ if ((provider === null || provider === void 0 ? void 0 : provider.working247) === true)
26
+ return (0, exports.allDayWorkingHours)();
27
+ return (_a = provider === null || provider === void 0 ? void 0 : provider.workingHoursV2) !== null && _a !== void 0 ? _a : (0, exports.emptyWorkingHours)();
28
+ }
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "sahab-utils",
3
- "version": "1.8.5",
3
+ "version": "1.8.8",
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 && 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"
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
11
  },
12
12
  "repository": {
13
13
  "type": "git",