sahab-utils 1.7.4 → 1.7.6

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.
@@ -129,8 +129,7 @@ export type Order = {
129
129
  cashbackOffer?: CashbackOffer;
130
130
  cashbackAmount?: number;
131
131
  /** Set by calculateCashbackOnOrderDone once the credit lands — its own idempotency guard,
132
- * independent of movementsCreated, so a retried COMPLETED trigger cannot double-credit even
133
- * if calculateBalanceOnOrderDone's own guard fails to fire for an unrelated reason. */
132
+ * independent of movementsCreated, so a retried COMPLETED trigger cannot double-credit. */
134
133
  cashbackCreditedAt?: TimestampType;
135
134
  isDummyOrder?: boolean;
136
135
  ratingNote?: string;
@@ -146,12 +145,8 @@ export type Order = {
146
145
  providerLogoUrl?: string;
147
146
  paymentGateway?: 'PAYLINK' | 'CHECKOUT' | 'TAP';
148
147
  visitDates?: number[];
149
- /**
150
- * Cloud Tasks ids, written by the backend so a scheduled job can be cancelled.
151
- *
152
- * Already declared in cloudFun's local `Order`; added here because the customer app reads
153
- * them too, and was doing so against a type that did not admit they exist.
154
- */
148
+ /** Cloud Tasks ids for cancelling a scheduled job — mirrors cloudFun's local `Order`; added here
149
+ * because the customer app reads them too, against a type that previously didn't admit they exist. */
155
150
  arrivalExpirationTaskId?: string;
156
151
  customerNotificationTaskId?: string;
157
152
  /** Set on an order created by re-ordering, pointing at the one it was copied from. */
@@ -250,23 +245,14 @@ export type Complaint = {
250
245
  type: ComplaintType;
251
246
  resolvedBy?: string;
252
247
  resolveDate?: string;
253
- /**
254
- * Who raised it and against whom.
255
- *
256
- * These are written by the customer app and read by the back office, but were missing from
257
- * this type — so the app cast its way past the mismatch and the dashboard could not see the
258
- * fields at all. Optional because older complaints predate them.
259
- */
248
+ /** Who raised it and against whom — written by the app, read by the back office. Optional because
249
+ * older complaints predate these fields (missing here before, so the app cast past the type mismatch). */
260
250
  complaintClientId?: string;
261
251
  complaintClientName?: string;
262
252
  complaintProviderId?: string;
263
253
  complaintProviderName?: string;
264
- /**
265
- * The order the complaint is about, when it is about one.
266
- *
267
- * Named "number" but holds the order's document id — the back office looks the order up with
268
- * it directly. Kept as-is because existing complaints are stored this way.
269
- */
254
+ /** The order the complaint is about, when it is about one. Named "number" but holds the order's
255
+ * document id — kept as-is because existing complaints are already stored this way. */
270
256
  orderNumber?: string;
271
257
  /** Hidden from the customer's list without deleting the record. */
272
258
  isDismissed?: boolean;
@@ -543,13 +529,8 @@ export type TapApplicationError = {
543
529
  at: number;
544
530
  endpoint: 'files' | 'lead' | 'account' | 'update_lead';
545
531
  };
546
- /**
547
- * Stored at: `service_providers/{providerId}/tapApplications/{applicationId}`.
548
- *
549
- * One document per onboarding attempt. On rejection a new application may be created;
550
- * history is preserved. Invariant: at most one non-terminal application per provider.
551
- * Terminal states: `payout_enabled`, `rejected`.
552
- */
532
+ /** Stored at `service_providers/{providerId}/tapApplications/{applicationId}` — one document per
533
+ * onboarding attempt (history preserved on rejection). Invariant: at most one non-terminal application per provider. */
553
534
  export type TapApplication = {
554
535
  id: string;
555
536
  providerId: string;
@@ -634,55 +615,71 @@ export type Discount = {
634
615
  providerPercentage?: number;
635
616
  /** Present only on "buy N, get one free" offers. Absent means an ordinary discount. */
636
617
  loyalty?: LoyaltyOffer;
637
- /**
638
- * Restricts the offer to customers who have not ordered before.
639
- *
640
- * Sahab-only by nature: it is a platform acquisition offer, so `sahabPercentage` should be
641
- * 100. Eligibility is decided by the server — the client cannot be trusted to report its own
642
- * order history — and re-checked when the order is created.
643
- *
644
- * "Has not ordered before" means no order that is still live or already completed. An order
645
- * the customer cancelled does not burn the offer, but one that is in flight does, which is
646
- * what stops the same first-order discount being claimed on two carts at once.
647
- */
618
+ /** Restricts to customers who have never ordered before (cancelled orders don't count, in-flight
619
+ * ones do). Server-decided and re-checked at order creation — the client cannot be trusted to report this. */
648
620
  firstOrderOnly?: boolean;
649
- /**
650
- * Providers this offer is valid at. Empty or absent means every provider.
651
- *
652
- * Distinct from `providerUid`, which marks an offer a *provider* created and funds. This is
653
- * a Sahab offer that Sahab funds, restricted to a chosen set of carwashes.
654
- */
621
+ /** Providers this offer is valid at (empty/absent = every provider). Distinct from `providerUid`,
622
+ * which marks an offer a *provider* created and funds — this is a Sahab offer restricted to a chosen set of carwashes. */
655
623
  providerUids?: string[];
656
- /**
657
- * Present only on the voucher issued for a gift. Absent means an ordinary discount.
658
- *
659
- * The back-pointer is what lets the order pipeline recognise a gift redemption without
660
- * loading anything — used to keep the loyalty stamp away from the recipient, who did not pay
661
- * for the wash.
662
- */
624
+ /** Present only on the voucher issued for a gift; absent means an ordinary discount. The back-pointer
625
+ * lets the order pipeline recognise a gift redemption without a load, to keep the loyalty stamp off the recipient. */
663
626
  gift?: {
664
627
  giftId: string;
665
628
  };
666
- /**
667
- * Which environment this offer belongs to. Absent means prod, matching every offer created
668
- * before this field existed — mirrors `Client.isTest`/`Order.isTest`/`Provider.isTest`.
669
- *
670
- * Only meaningful for an offer that doesn't otherwise resolve to one environment (see
671
- * `loyaltyOfferMatchesTestMode` in `ts/loyalty.ts`): a provider-scoped offer's environment is
672
- * really the provider's own `isTest`, and a gift voucher's is really the gift's — this field
673
- * is what lets a genuinely Sahab-wide, no-single-provider offer (or a promo code with no
674
- * gift/provider scope at all) still declare one explicitly.
675
- */
629
+ /** Present only on a prize issued by a wheel-of-fortune spin; absent means an ordinary discount. */
630
+ wheel?: {
631
+ campaignId: string;
632
+ segmentId: string;
633
+ };
634
+ /** Which environment this offer belongs to. Absent means prod, mirroring `Client`/`Order`/`Provider` `isTest`.
635
+ * Only needed when the offer doesn't already resolve to one environment on its own (see `loyaltyOfferMatchesTestMode`). */
676
636
  isTest?: boolean;
677
637
  };
678
- /**
679
- * A cashback campaign — a percentage- or flat-amount credit to a customer's balance after a
680
- * completed purchase. Kept separate from `Discount`: a `Discount` reduces the order's price at
681
- * checkout, cashback credits balance afterward, and never touches the pricing pipeline.
682
- *
683
- * No `offerId` — unlike `Discount`/`promoCodes`, this is not its own Firestore document; it lives
684
- * nested inside the singleton `configs/app_settings` doc (see `AppSettings` below).
685
- */
638
+ /** One prize slice on a wheel-of-fortune wheel. `weight` is relative, not a percentage — a segment
639
+ * with weight 2 is twice as likely as one with weight 1, whatever the other weights sum to. */
640
+ export type WheelSegment = {
641
+ segmentId: string;
642
+ label: {
643
+ ar: string;
644
+ en: string;
645
+ };
646
+ amount: number;
647
+ isPercentage?: boolean;
648
+ maxAmount?: number;
649
+ weight: number;
650
+ };
651
+ /** A wheel-of-fortune campaign, configured by Sahab staff. `cadence: 'once'` grants exactly one
652
+ * spin per customer for the campaign's whole `validFrom`–`validTo` window; `'daily'`/`'weekly'`
653
+ * grant one spin per customer per UTC day/ISO week the campaign is live. */
654
+ export type WheelCampaign = {
655
+ campaignId: string;
656
+ name?: string;
657
+ segments: WheelSegment[];
658
+ cadence: 'once' | 'daily' | 'weekly';
659
+ validFrom?: TimestampType;
660
+ validTo?: TimestampType;
661
+ active?: boolean;
662
+ /** Carwashes a won prize is valid at (empty/absent = every provider), mirroring `Discount.providerUids`. */
663
+ providerUids?: string[];
664
+ sahabPercentage?: number;
665
+ providerPercentage?: number;
666
+ createdAt?: TimestampType;
667
+ createrEmail?: string;
668
+ isTest?: boolean;
669
+ };
670
+ /** One customer's claim on a campaign cycle — flat doc id `${clientId}__${campaignId}__${cycleKey}`,
671
+ * so "has this customer already spun this cycle" is a direct read, not a query. */
672
+ export type WheelSpin = {
673
+ spinId: string;
674
+ clientId: string;
675
+ campaignId: string;
676
+ cycleKey: string;
677
+ segmentId: string;
678
+ promoCodeId: string;
679
+ spunAt: TimestampType;
680
+ };
681
+ /** A cashback campaign — percentage or flat credit to balance after a completed purchase. Kept
682
+ * separate from `Discount`, which prices at checkout; cashback never touches the pricing pipeline. */
686
683
  export type CashbackOffer = {
687
684
  amount?: number;
688
685
  isPercentage?: boolean;
@@ -695,41 +692,20 @@ export type CashbackOffer = {
695
692
  createrEmail?: string;
696
693
  isTest?: boolean;
697
694
  };
698
- /**
699
- * "Buy N washes, get one free" configuration, carried on the `Discount` that represents the
700
- * offer.
701
- *
702
- * The reward is redeemed as an ordinary fixed-amount discount: at redemption the earned amount
703
- * is written onto `order.promoCode.amount`, so it flows through `getSahabDiscountsAmount` /
704
- * `getProviderDiscountsAmount` and the `sahabPercentage` / `providerPercentage` split exactly
705
- * like any other promo. Nothing in the pricing pipeline needs to know loyalty exists.
706
- */
695
+ /** "Buy N washes, get one free" config, carried on the `Discount` representing the offer.
696
+ * Redeemed as an ordinary fixed discount, so it flows through the normal split unchanged. */
707
697
  export type LoyaltyOffer = {
708
698
  /** Qualifying orders needed to earn one free wash. */
709
699
  buyQuantity: number;
710
- /**
711
- * Minimum the customer must actually pay for an order to count as a stamp.
712
- *
713
- * Without it a customer can collect stamps on the cheapest possible wash and redeem against
714
- * an expensive one.
715
- */
700
+ /** Minimum the customer must actually pay for an order to count as a stamp — without it they could
701
+ * stamp on the cheapest wash and redeem against an expensive one. */
716
702
  minOrderAmount?: number;
717
- /**
718
- * Providers whose completed orders count toward this offer.
719
- *
720
- * Only meaningful for Sahab-created offers, which may span some or all providers. A
721
- * provider-created offer is always scoped to its own `providerUid`, whatever this says.
722
- * Absent or empty on a Sahab offer means every provider counts.
723
- */
703
+ /** Providers whose orders count toward a Sahab-created offer (empty/absent = every provider).
704
+ * A provider-created offer ignores this and is always scoped to its own `providerUid`. */
724
705
  providerUids?: string[];
725
706
  };
726
- /**
727
- * A customer's progress toward one loyalty offer.
728
- *
729
- * Server-owned: only cloud functions may write it, or a customer could award themselves washes.
730
- * Rewards do not expire — the campaign's own `validFrom`/`validTo` bounds when stamps are
731
- * earned, not how long an earned reward lasts.
732
- */
707
+ /** A customer's progress toward one loyalty offer. Server-owned — only cloud functions may write
708
+ * it, or a customer could award themselves washes. Earned rewards do not expire. */
733
709
  export type LoyaltyProgress = {
734
710
  clientId: string;
735
711
  /** The offer this progress belongs to. */
@@ -742,25 +718,13 @@ export type LoyaltyProgress = {
742
718
  earnedRewards: number;
743
719
  /** Orders already counted, so a status flapping COMPLETED->RATED cannot double-stamp. */
744
720
  countedOrderIds: string[];
745
- /**
746
- * Gifts already counted, for stamps earned by *buying* a gift rather than by ordering.
747
- *
748
- * Kept separate from `countedOrderIds` because a gift has no order id at the moment it is
749
- * paid for, and mixing the two id spaces would make a collision silently skip a stamp.
750
- */
721
+ /** Gifts counted for stamps earned by *buying* one, kept separate from `countedOrderIds` because
722
+ * a gift has no order id at purchase — mixing the two id spaces would silently skip a stamp. */
751
723
  countedGiftIds?: string[];
752
724
  updatedAt?: TimestampType;
753
725
  };
754
- /**
755
- * One paid-for gift: a basket of services and addons bought by one customer for another.
756
- *
757
- * The voucher the recipient actually spends is an ordinary `Discount` carrying `gift.giftId`.
758
- * This document owns everything the voucher deliberately does not — payment, delivery and
759
- * lifecycle — so the voucher stays a pure pricing object that the calcs can read without
760
- * knowing gifting exists.
761
- *
762
- * Server-owned. A client that could write this could issue itself free washes.
763
- */
726
+ /** One paid-for gift. The voucher the recipient spends is an ordinary `Discount` carrying
727
+ * `gift.giftId` — this document owns payment, delivery and lifecycle so the voucher stays pure. Server-owned. */
764
728
  export type Gift = {
765
729
  giftId: string;
766
730
  status: GiftStatus;
@@ -769,10 +733,8 @@ export type Gift = {
769
733
  giverName?: string;
770
734
  /** E.164, normalised at write. The recipient may not have an account yet. */
771
735
  recipientPhone: string;
772
- /**
773
- * Resolved at issuance, before the recipient has ever opened the app — Firebase Auth mints a
774
- * stable uid for a phone number on demand, so the voucher can be scoped to them immediately.
775
- */
736
+ /** Resolved at issuance, before the recipient has opened the app — Firebase Auth mints a stable
737
+ * uid for a phone number on demand, so the voucher can be scoped to them immediately. */
776
738
  recipientUid?: string;
777
739
  /** Whether they were already a customer. Analytics, and useful to support. */
778
740
  recipientHadAccount?: boolean;
@@ -801,11 +763,8 @@ export type Gift = {
801
763
  expiresAt?: TimestampType;
802
764
  notifiedAt?: TimestampType;
803
765
  notifyChannel?: GiftNotifyChannel;
804
- /**
805
- * Whether the giver was a test client at purchase — mirrors `Client.isTest`. Stamped once at
806
- * creation and never recomputed, same reasoning as `expiresAt`: a client's test/prod status
807
- * changing later must not retroactively move a gift someone already paid for.
808
- */
766
+ /** Whether the giver was a test client at purchase — mirrors `Client.isTest`. Stamped once and
767
+ * never recomputed, so a client's test/prod status changing later can't retroactively move a paid gift. */
809
768
  isTest?: boolean;
810
769
  };
811
770
  /** One line of a gifted basket, priced when the gift was bought. */
@@ -839,11 +798,8 @@ export declare enum GiftNotifyChannel {
839
798
  }
840
799
  /** Back-office settings for gifting. */
841
800
  export type GiftConfig = {
842
- /**
843
- * How long an issued gift stays spendable. Applied at issuance only.
844
- *
845
- * Bounded on read: a 0 would expire every gift the moment it was sold.
846
- */
801
+ /** How long an issued gift stays spendable, applied at issuance only. Bounded on read — a 0
802
+ * would expire every gift the moment it was sold. */
847
803
  giftExpiryMonths?: number;
848
804
  /** Charged when the giver adds a personal message to the SMS. Defaults to `GIFT_MESSAGE_SURCHARGE_DEFAULT` when unset. */
849
805
  giftMessageSurcharge?: number;
package/dist/index.d.ts CHANGED
@@ -4,3 +4,4 @@ export * from './ts/cashback';
4
4
  export * from './ts/gifts';
5
5
  export * from './ts/loyalty';
6
6
  export * from './ts/utils';
7
+ export * from './ts/wheel';
package/dist/index.js CHANGED
@@ -20,3 +20,4 @@ __exportStar(require("./ts/cashback"), exports);
20
20
  __exportStar(require("./ts/gifts"), exports);
21
21
  __exportStar(require("./ts/loyalty"), exports);
22
22
  __exportStar(require("./ts/utils"), exports);
23
+ __exportStar(require("./ts/wheel"), exports);
@@ -2,6 +2,9 @@ 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
+ export declare const PROVIDER_ORDERS_PERCENTAGE_DEFAULT = 0.1;
6
+ export declare const PROVIDER_ORDERS_PERCENTAGE_WITH_SAHAB_LOGO = 0.07;
7
+ export declare function providerOrdersPercentage(hasSahabLogoOnCar: boolean | undefined, storedValue: unknown): number;
5
8
  export declare function getServicesTotal(order?: Order): number;
6
9
  export declare function getSahabDiscountsAmount(order: Order): number;
7
10
  export declare function getProviderDiscountsAmount(order: Order): number;
package/dist/ts/calcs.js CHANGED
@@ -3,7 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.CLIENT_SAHAB_PERCENTAGE = exports.ONLINE_SAHAB_PERCENTAGE = exports.SAHAB_PERCENTAGE = void 0;
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.providerOrdersPercentage = providerOrdersPercentage;
7
8
  exports.getServicesTotal = getServicesTotal;
8
9
  exports.getSahabDiscountsAmount = getSahabDiscountsAmount;
9
10
  exports.getProviderDiscountsAmount = getProviderDiscountsAmount;
@@ -22,10 +23,21 @@ const utils_1 = require("./utils");
22
23
  exports.SAHAB_PERCENTAGE = 0;
23
24
  exports.ONLINE_SAHAB_PERCENTAGE = 0.03;
24
25
  exports.CLIENT_SAHAB_PERCENTAGE = 0.03;
25
- // Type guard functions
26
- // `type` is not present on every historical order, so fall back to the id field that only
27
- // one of the two shapes carries. Without this, legacy line items match neither guard and
28
- // scoped promos silently resolved to a zero discount.
26
+ // `Provider.ordersPercentage` — the provider's own cut of an order, factored into
27
+ // `sahabProviderPercentage` server-side. Only two values are ever valid: the default, or the
28
+ // reduced rate for a provider that's put the Sahab logo on their car.
29
+ exports.PROVIDER_ORDERS_PERCENTAGE_DEFAULT = 0.1;
30
+ exports.PROVIDER_ORDERS_PERCENTAGE_WITH_SAHAB_LOGO = 0.07;
31
+ // Never trusts a stored `ordersPercentage` verbatim — it may have arrived without going through
32
+ // this constraint (a stray Firestore edit, a migration). Clamps to whichever of the two valid
33
+ // values actually applies instead.
34
+ function providerOrdersPercentage(hasSahabLogoOnCar, storedValue) {
35
+ if (!hasSahabLogoOnCar)
36
+ return exports.PROVIDER_ORDERS_PERCENTAGE_DEFAULT;
37
+ return storedValue === exports.PROVIDER_ORDERS_PERCENTAGE_WITH_SAHAB_LOGO ? exports.PROVIDER_ORDERS_PERCENTAGE_WITH_SAHAB_LOGO : exports.PROVIDER_ORDERS_PERCENTAGE_DEFAULT;
38
+ }
39
+ // `type` is missing on legacy orders, so fall back to the id field only one shape carries —
40
+ // without it, legacy line items match neither guard and promos silently zero out.
29
41
  function isProvidedService(service) {
30
42
  if (service === null || service === void 0 ? void 0 : service.type)
31
43
  return service.type === 'service';
@@ -36,7 +48,6 @@ function isProvidedAddon(service) {
36
48
  return service.type === 'addon';
37
49
  return (service === null || service === void 0 ? void 0 : service.addonId) !== undefined;
38
50
  }
39
- // Helper function to check if service/addon is applicable for promo codes
40
51
  function isServiceApplicableForPromo(service, servicesIds, addonsIds) {
41
52
  if (isProvidedService(service)) {
42
53
  return (servicesIds === null || servicesIds === void 0 ? void 0 : servicesIds.includes(service.serviceId || '')) || false;
@@ -46,7 +57,6 @@ function isServiceApplicableForPromo(service, servicesIds, addonsIds) {
46
57
  }
47
58
  return false;
48
59
  }
49
- // Helper function to check if service/addon is applicable for discounts
50
60
  function isServiceApplicableForDiscount(service, discount) {
51
61
  var _a, _b, _c, _d;
52
62
  const scopedToServices = !!((_a = discount === null || discount === void 0 ? void 0 : discount.servicesIds) === null || _a === void 0 ? void 0 : _a.length);
@@ -63,14 +73,8 @@ function isServiceApplicableForDiscount(service, discount) {
63
73
  }
64
74
  return false;
65
75
  }
66
- /**
67
- * How a discount is funded, as fractions of the gross discount that always sum to exactly 1.
68
- *
69
- * Previously the Sahab and Provider sides each decided independently, so a partially
70
- * configured discount could be funded at 150% (both a percentage and `createdByProvider`)
71
- * or at 60% (one percentage set, the remainder funded by nobody). Deriving both shares in
72
- * one place makes over/under-funding structurally impossible.
73
- */
76
+ /** Funding split for a discount, as fractions that always sum to 1. Previously each side computed
77
+ * its own share independently and could total 150% or 60%; deriving both here makes that impossible. */
74
78
  function responsibilitySplit(discount) {
75
79
  var _a, _b;
76
80
  const s = (0, utils_1.preventNaN)((_a = discount === null || discount === void 0 ? void 0 : discount.sahabPercentage) !== null && _a !== void 0 ? _a : 0);
@@ -180,25 +184,19 @@ function getOrderTotalDiscountsAmount(order) {
180
184
  return getSahabDiscountsAmount(order) + getProviderDiscountsAmount(order);
181
185
  }
182
186
  function getCustomerTotalToPay(order) {
183
- const sahabExtraPaymentAmount = (order === null || order === void 0 ? void 0 : order.paymentType) === types_1.OrderPaymentType.CASH ? (order === null || order === void 0 ? void 0 : order.sahabExtraPaymentAmount) || 0 : 0;
184
- const orderTotal = getServicesTotal(order) + sahabExtraPaymentAmount;
187
+ const orderTotal = getServicesTotal(order) + cashSurcharge(order);
185
188
  const discounts = getSahabDiscountsAmount(order) + getProviderDiscountsAmount(order);
186
189
  return orderTotal - discounts;
187
190
  }
188
- /** Sahab's cash surcharge on this order (zero for online orders). */
191
+ /** Sahab's cash surcharge on this order (zero for online orders). Clamped to non-negative — a
192
+ * negative surcharge would let a client undercut the real service total on a cash order, which
193
+ * isn't caught by a payment gateway the way an online charge is. */
189
194
  function cashSurcharge(order) {
190
195
  var _a;
191
- 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.sahabExtraPaymentAmount) !== null && _a !== void 0 ? _a : 0) : 0;
196
+ 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;
192
197
  }
193
- /**
194
- * The part of a cash collection that settles against the provider's entitlement.
195
- *
196
- * `paymentAmount` for a cash order is the full amount the customer hands over, which
197
- * includes Sahab's cash surcharge. That surcharge is Sahab's revenue, not the provider's,
198
- * so it must come off before comparing against `minimumProviderAmount` — otherwise the
199
- * surcharge is counted twice in `getSahabFees` and deducted from the provider twice in
200
- * `getProviderProfits`.
201
- */
198
+ /** Cash `paymentAmount` includes Sahab's surcharge, which is Sahab's revenue not the provider's —
199
+ * it must come off before comparing to `minimumProviderAmount`, or it's double-counted in getSahabFees/getProviderProfits. */
202
200
  function cashSettlementBase(order) {
203
201
  var _a;
204
202
  return Math.max(0, (0, utils_1.preventNaN)((_a = order === null || order === void 0 ? void 0 : order.paymentAmount) !== null && _a !== void 0 ? _a : 0) - cashSurcharge(order));
@@ -219,9 +217,8 @@ function getProviderTransferAmount(order) {
219
217
  }
220
218
  }
221
219
  function getProviderProfits(order) {
222
- const sahabExtraPaymentAmount = (order === null || order === void 0 ? void 0 : order.paymentType) === types_1.OrderPaymentType.CASH ? (order === null || order === void 0 ? void 0 : order.sahabExtraPaymentAmount) || 0 : 0;
223
220
  return (order === null || order === void 0 ? void 0 : order.paymentType) === types_1.OrderPaymentType.CASH
224
- ? getCustomerTotalToPay(order) - sahabExtraPaymentAmount + getProviderTransferAmount(order)
221
+ ? getCustomerTotalToPay(order) - cashSurcharge(order) + getProviderTransferAmount(order)
225
222
  : getProviderTransferAmount(order);
226
223
  }
227
224
  function getOnlineProcessingFees(order) {
@@ -238,14 +235,12 @@ function getSahabFees(order) {
238
235
  const paymentAmount = (order === null || order === void 0 ? void 0 : order.paymentType) === types_1.OrderPaymentType.CASH ? cashSettlementBase(order) : getCustomerTotalToPay(order);
239
236
  const originalPrice = orderTotal / (1 + (0, utils_1.preventNaN)((_a = order === null || order === void 0 ? void 0 : order.sahabClientPercentage) !== null && _a !== void 0 ? _a : 0));
240
237
  const minimumProviderAmount = originalPrice - originalPrice * ((_b = order === null || order === void 0 ? void 0 : order.sahabProviderPercentage) !== null && _b !== void 0 ? _b : 0);
241
- // When a Sahab-funded discount pushes the customer below the provider's entitlement,
242
- // Sahab is genuinely subsidising the order. Reporting a floor of 0 there overstated
243
- // margin on every discounted order, so the negative figure is returned as-is.
238
+ // When a Sahab-funded discount pushes the customer below the provider's entitlement, Sahab is
239
+ // genuinely subsidising the order — reporting a floor of 0 here overstated margin, so the negative figure stands.
244
240
  return paymentAmount - minimumProviderAmount + cashSurcharge(order);
245
241
  }
246
242
  function calcOrdersData(orders) {
247
243
  var _a, _b, _c, _d, _e, _f, _g, _h;
248
- //Filter Orders
249
244
  const cashOrders = orders.filter((o) => o.paymentType === types_1.OrderPaymentType.CASH);
250
245
  const onlineOrders = orders.filter((o) => o.paymentType !== types_1.OrderPaymentType.CASH);
251
246
  const sahabOnlineRevenue = (0, sum_1.default)(onlineOrders.map((o) => getSahabFees(o)));
@@ -254,7 +249,6 @@ function calcOrdersData(orders) {
254
249
  const sahabCashRevenue = (0, sum_1.default)(cashOrders.map((o) => getSahabFees(o)));
255
250
  const totalCashTransfer = (0, sum_1.default)(cashOrders.map((o) => getProviderTransferAmount(o)));
256
251
  const providerCashRevenue = (0, sum_1.default)(cashOrders.map((o) => getProviderProfits(o)));
257
- //Calculate Percentages
258
252
  const totalTransfer = totalOnlineTransfer + totalCashTransfer;
259
253
  const totalSahabRevenue = sahabOnlineRevenue + sahabCashRevenue;
260
254
  const totalProviderRevenue = providerOnlineRevenue + providerCashRevenue;
@@ -1,15 +1,8 @@
1
1
  import { CashbackOffer, Order } from '../constants/types';
2
- /**
3
- * Whether a provider-scoped offer covers a given provider. Empty/absent `scope` means every
4
- * carwash. Shared by every offer type with a `providerUids`-style restriction — do not let a
5
- * second copy of this exist; see the CLAUDE.md warning about the gift-flow eligibility
6
- * duplication this exact pattern caused before.
7
- */
2
+ /** Whether a provider-scoped offer covers a provider (empty/absent scope = every provider). Shared
3
+ * by every offer type with this restriction — don't duplicate it; see CLAUDE.md's gift-flow eligibility warning. */
8
4
  export declare function offerCoversProvider(scope: string[] | undefined, providerId?: string): boolean;
9
5
  export declare function cashbackOfferCoversProvider(offer: CashbackOffer | undefined, providerId?: string): boolean;
10
- /**
11
- * The cashback amount this order earns, or 0. Percentage is of `getCustomerTotalToPay`, which
12
- * already nets out other discounts and any balance the customer used — never pay cashback on
13
- * money the customer didn't spend.
14
- */
6
+ /** The cashback amount this order earns, or 0. Percentage is of `getCustomerTotalToPay`, which
7
+ * already nets out discounts and used balance — never pay cashback on money the customer didn't spend. */
15
8
  export declare function cashbackAmountFor(offer: CashbackOffer | undefined, order: Order): number;
@@ -5,12 +5,8 @@ exports.cashbackOfferCoversProvider = cashbackOfferCoversProvider;
5
5
  exports.cashbackAmountFor = cashbackAmountFor;
6
6
  const calcs_1 = require("./calcs");
7
7
  const utils_1 = require("./utils");
8
- /**
9
- * Whether a provider-scoped offer covers a given provider. Empty/absent `scope` means every
10
- * carwash. Shared by every offer type with a `providerUids`-style restriction — do not let a
11
- * second copy of this exist; see the CLAUDE.md warning about the gift-flow eligibility
12
- * duplication this exact pattern caused before.
13
- */
8
+ /** Whether a provider-scoped offer covers a provider (empty/absent scope = every provider). Shared
9
+ * by every offer type with this restriction — don't duplicate it; see CLAUDE.md's gift-flow eligibility warning. */
14
10
  function offerCoversProvider(scope, providerId) {
15
11
  if (!scope || !scope.length)
16
12
  return true;
@@ -19,11 +15,8 @@ function offerCoversProvider(scope, providerId) {
19
15
  function cashbackOfferCoversProvider(offer, providerId) {
20
16
  return offerCoversProvider(offer === null || offer === void 0 ? void 0 : offer.providerUids, providerId);
21
17
  }
22
- /**
23
- * The cashback amount this order earns, or 0. Percentage is of `getCustomerTotalToPay`, which
24
- * already nets out other discounts and any balance the customer used — never pay cashback on
25
- * money the customer didn't spend.
26
- */
18
+ /** The cashback amount this order earns, or 0. Percentage is of `getCustomerTotalToPay`, which
19
+ * already nets out discounts and used balance — never pay cashback on money the customer didn't spend. */
27
20
  function cashbackAmountFor(offer, order) {
28
21
  if (!offer)
29
22
  return 0;