@final-commerce/command-frame 0.5.0-preprod.5 → 0.5.0-preprod.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.
@@ -10,5 +10,10 @@ import { GetRefundPlan } from './types';
10
10
  * `maxRefundable` as the full captured amount. Against real kaching those
11
11
  * numbers come from `order.refund[]` and the capture's `emv` JSON. Use this
12
12
  * only to shape UI in local/standalone mode — never to assert real capacity.
13
+ *
14
+ * `allocation` is likewise shape-only: the demo has no refund SELECTION state
15
+ * and no company cash-rounding setting, so it always describes a FULL refund
16
+ * with `rounding: 0` and no cash `payout`. Against real kaching the allocation
17
+ * tracks the live selection and carries the drawer snap.
13
18
  */
14
19
  export declare const mockGetRefundPlan: GetRefundPlan;
@@ -10,6 +10,11 @@ import { MOCK_ORDERS } from '../../demo/database';
10
10
  * `maxRefundable` as the full captured amount. Against real kaching those
11
11
  * numbers come from `order.refund[]` and the capture's `emv` JSON. Use this
12
12
  * only to shape UI in local/standalone mode — never to assert real capacity.
13
+ *
14
+ * `allocation` is likewise shape-only: the demo has no refund SELECTION state
15
+ * and no company cash-rounding setting, so it always describes a FULL refund
16
+ * with `rounding: 0` and no cash `payout`. Against real kaching the allocation
17
+ * tracks the live selection and carries the drawer snap.
13
18
  */
14
19
  export const mockGetRefundPlan = async (params) => {
15
20
  console.log('[Mock] getRefundPlan called', params);
@@ -45,10 +50,28 @@ export const mockGetRefundPlan = async (params) => {
45
50
  });
46
51
  const totalCaptured = sources.reduce((sum, s) => sum + s.capturedAmount, 0);
47
52
  const nonRefundableLiability = order.summary?.nonRevenueTotal ?? 0;
53
+ // Full-refund legs: every source returns its whole capture (the demo's
54
+ // `maxRefundable`), which is exactly the shape a full selection produces.
55
+ const legs = sources
56
+ .filter((s) => s.maxRefundable > 0)
57
+ .map((s) => ({
58
+ transactionId: s.transactionId,
59
+ amount: s.maxRefundable,
60
+ paymentType: s.paymentType,
61
+ requiresGiftCardDestination: s.paymentType === 'redeem',
62
+ }));
63
+ const budget = legs.reduce((sum, l) => sum + l.amount, 0);
48
64
  return {
49
65
  success: true,
50
66
  orderId: order._id,
51
67
  sources,
68
+ allocation: {
69
+ budget,
70
+ // No cash rounding in the demo, so the goods value and the budget agree.
71
+ itemTotal: budget,
72
+ rounding: 0,
73
+ legs,
74
+ },
52
75
  // Demo: no prior refunds, so remaining = captured minus the non-revenue load.
53
76
  remainingRefundable: Math.max(0, totalCaptured - nonRefundableLiability),
54
77
  nonRefundableLiability,
@@ -17,10 +17,67 @@ export interface RefundPlanSource {
17
17
  /** For redeem sources: the card number from the payment entry's emv, when present. */
18
18
  cardNumber?: string;
19
19
  }
20
+ /**
21
+ * One ready-to-submit refund leg. Pass these straight to
22
+ * `processPartialRefund({ openUI: false, legs })` — the amounts are the
23
+ * engine's own allocation and already satisfy its Σ-contract.
24
+ */
25
+ export interface RefundPlanLeg {
26
+ /** `transactionId` of the source payment this leg draws from — join key to `sources`. */
27
+ transactionId: string;
28
+ /** Amount to return to this source (minor units). Submit VERBATIM; do not re-derive. */
29
+ amount: number;
30
+ /** `cash` / `card` / `redeem` / etc., copied from the source. */
31
+ paymentType: string;
32
+ /** True when the leg must carry a `giftCard` destination (a `redeem` source). */
33
+ requiresGiftCardDestination: boolean;
34
+ /**
35
+ * Cash legs only: what the drawer actually pays after the company's
36
+ * cash-rounding snap, and the signed delta from `amount`. Display it
37
+ * ("drawer pays 6.50 (+0.01 rounding)") — never apply the snap yourself,
38
+ * and never stage `payout.amount` as the leg (`amount` is the leg).
39
+ */
40
+ payout?: {
41
+ amount: number;
42
+ rounding: number;
43
+ };
44
+ }
45
+ /**
46
+ * The engine's own allocation of the CURRENT refund selection across the
47
+ * order's captures — what a flow renders and submits instead of computing a
48
+ * split of its own.
49
+ *
50
+ * Present only when a refund selection exists on the ACTIVE order (i.e. after
51
+ * `selectAllRefundItems` / `setRefundItemQuantity`); omitted for a pure
52
+ * capacity read of some other order.
53
+ */
54
+ export interface RefundPlanAllocation {
55
+ /**
56
+ * What Σ `legs.amount` MUST equal — `min(itemTotal, Σ maxRefundable)`, which
57
+ * on a FULL selection is the captured total, not the goods value. Staging the
58
+ * goods value instead is rejected with `refund.legSumMismatch`.
59
+ */
60
+ budget: number;
61
+ /** Goods value of the selection (minor units). DISPLAY ONLY — never allocate against it. */
62
+ itemTotal: number;
63
+ /**
64
+ * `budget − itemTotal` — the sale's cash rounding, returned to the tender that
65
+ * took it. Non-zero only on a cash-rounded capture; the engine stamps it as
66
+ * refund residue at commit.
67
+ */
68
+ rounding: number;
69
+ /** One leg per source that receives money. Submit as `legs`, unchanged. */
70
+ legs: RefundPlanLeg[];
71
+ }
20
72
  export interface GetRefundPlanResponse {
21
73
  success: boolean;
22
74
  orderId: string;
23
75
  sources: RefundPlanSource[];
76
+ /**
77
+ * Ready-to-submit allocation of the current selection. Present only when a
78
+ * refund selection exists on the active order. See {@link RefundPlanAllocation}.
79
+ */
80
+ allocation?: RefundPlanAllocation;
24
81
  /** Order-level remaining refundable (minor units) — non-revenue liability already excluded. */
25
82
  remainingRefundable: number;
26
83
  /** Non-refundable liability (gift-card loads etc., minor units). */
@@ -4,4 +4,10 @@
4
4
  // order-level math so flows can PRESENT accurate refund options without
5
5
  // re-deriving the numbers client-side (the mutating commands —
6
6
  // `processPartialRefund` / `redeemRefund` — re-validate at submit time).
7
+ //
8
+ // `allocation` closes the last gap: capacities alone still left a flow to work
9
+ // out WHICH tender gets WHAT, and a flow that split the goods value across the
10
+ // tenders shaved the sale's cash rounding off gift-card legs and was rejected
11
+ // at submit. The engine now returns the legs it would accept — render them,
12
+ // submit them unchanged, compute nothing.
7
13
  export {};
@@ -1,12 +1,20 @@
1
1
  export const mockProcessPartialRefund = async (params) => {
2
2
  // The mock has no split-payment modal or order engine, so `openUI` (default
3
- // true on the real command), `legs` (the headless per-tender allocation) and
4
- // any per-leg `giftCard` destination (mixed returns) are inert here — the
5
- // shape is accepted and echoed, nothing else. No gift card is credited.
3
+ // true on the real command), `legs` (the headless per-tender allocation), a
4
+ // per-leg `giftCard` destination (mixed returns) and the top-level `giftCard`
5
+ // routing are all inert here — the shape is accepted and echoed, nothing
6
+ // else. No gift card is credited.
7
+ //
8
+ // The one rule worth mirroring is the mutual exclusion, so a flow built
9
+ // against the mock fails the same way it will against the runtime.
10
+ if (params?.legs && params?.giftCard) {
11
+ throw new Error('refund.giftCardAndLegs: pass either `legs` (you allocate) or `giftCard` (the engine allocates), not both');
12
+ }
6
13
  console.log("[Mock] processPartialRefund called", {
7
14
  ...params,
8
15
  openUI: params?.openUI ?? true,
9
16
  legs: params?.legs ?? null,
17
+ giftCard: params?.giftCard ?? null,
10
18
  });
11
19
  return {
12
20
  success: true,
@@ -88,6 +88,47 @@ export interface ProcessPartialRefundParams {
88
88
  label?: string;
89
89
  };
90
90
  }[];
91
+ /**
92
+ * Route part (or all) of the refund onto ONE gift-card / store-credit tender
93
+ * and let the engine send whatever is left back to the original payments.
94
+ *
95
+ * This is the declarative alternative to hand-building `legs`: state the card
96
+ * and how much lands on it, and the engine does the allocation — it already
97
+ * owns that math for every other refund path. Prefer it over `legs` for a
98
+ * gift-card destination; a flow that computes its own split is duplicating
99
+ * engine arithmetic that will drift (see "Query, never recompute").
100
+ *
101
+ * - `amount` omitted → the WHOLE refund lands on the card (what an
102
+ * all-`giftCard` `legs` staging does today).
103
+ * - `amount` set → that much lands on the card, in minor units; the
104
+ * remainder returns to the original payments, allocated by the engine.
105
+ *
106
+ * DRAWING ORDER — the card is filled from the tenders that cannot be
107
+ * refunded to source first (a redeem tender has nowhere to return to), then
108
+ * proportionally from the rest. So `amount` can never be lower than what
109
+ * those tenders must contribute: below that the call throws
110
+ * `REFUND_GIFT_AMOUNT_BELOW_MINIMUM`, naming the minimum, and nothing is
111
+ * committed. Surface that message — it is the number to clamp the field to,
112
+ * so the flow never has to derive it.
113
+ *
114
+ * Exactly one destination card, therefore exactly ONE credit for the caller
115
+ * to place and one to reverse. **Credit-first:** credit `referenceId` for
116
+ * `amount` (or the full refund total when omitted) BEFORE calling; on any
117
+ * throw nothing was recorded — reverse it.
118
+ *
119
+ * Requires `openUI: false`. Mutually exclusive with `legs` — passing both
120
+ * throws, since they are two answers to the same question.
121
+ */
122
+ giftCard?: {
123
+ /** Card/account id the flow already credited (stored raw). */
124
+ referenceId: string;
125
+ /** Minor units landing on the card. Omit for the whole refund. */
126
+ amount?: number;
127
+ /** Provider/program name. Defaults to `giftCard`. */
128
+ processor?: string;
129
+ /** Human label for the destination tender. */
130
+ label?: string;
131
+ };
91
132
  /** Optional items to refund. */
92
133
  items?: {
93
134
  /** internalId or variantId or customSaleId. */
package/dist/index.d.ts CHANGED
@@ -141,8 +141,8 @@ export type { CalculateRefundTotal, CalculateRefundTotalParams, CalculateRefundT
141
141
  export type { GetRemainingRefundableQuantities, GetRemainingRefundableQuantitiesParams, GetRemainingRefundableQuantitiesResponse, } from './actions/get-remaining-refundable-quantities/types';
142
142
  export type { ProcessPartialRefund, ProcessPartialRefundParams, ProcessPartialRefundResponse, } from './actions/process-partial-refund/types';
143
143
  export type { RedeemRefund, RedeemRefundParams, RedeemRefundResponse } from './actions/redeem-refund/types';
144
- export type { GetRefundPlan, GetRefundPlanParams, GetRefundPlanResponse, RefundPlanSource, } from './actions/get-refund-plan/types';
145
- export type { CheckPermission, CheckPermissionParams, CheckPermissionResponse, } from './actions/check-permission/types';
144
+ export type { GetRefundPlan, GetRefundPlanParams, GetRefundPlanResponse, RefundPlanSource, RefundPlanAllocation, RefundPlanLeg, } from './actions/get-refund-plan/types';
145
+ export type { CheckPermission, CheckPermissionParams, CheckPermissionResponse } from './actions/check-permission/types';
146
146
  export type { InitiateRefund, InitiateRefundParams, InitiateRefundResponse } from './actions/initiate-refund/types';
147
147
  export type { GetCurrentCart, GetCurrentCartResponse } from './actions/get-current-cart/types';
148
148
  export type { AddProductDiscount, AddProductDiscountParams, AddProductDiscountResponse, } from './actions/add-product-discount/types';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@final-commerce/command-frame",
3
- "version": "0.5.0-preprod.5",
3
+ "version": "0.5.0-preprod.8",
4
4
  "description": "Commands Frame library",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",