@final-commerce/command-frame 0.5.0-preprod.8 → 0.5.0
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/actions/add-cart-discount/mock.js +8 -5
- package/dist/actions/add-cart-fee/mock.js +8 -5
- package/dist/actions/add-custom-sale/mock.js +4 -4
- package/dist/actions/edit-custom-sale/mock.js +4 -4
- package/dist/actions/get-refund-plan/mock.js +1 -0
- package/dist/actions/get-refund-plan/types.d.ts +39 -4
- package/dist/actions/partial-payment/mock.js +6 -4
- package/dist/actions/process-partial-refund/mock.d.ts +1 -1
- package/dist/actions/process-partial-refund/mock.js +3 -2
- package/dist/actions/process-partial-refund/types.d.ts +19 -1
- package/dist/actions/remove-cart-fee/mock.js +4 -1
- package/dist/demo/units.d.ts +18 -0
- package/dist/demo/units.js +29 -0
- package/package.json +2 -1
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
import { MOCK_CART, mockPublishEvent } from "../../demo/database";
|
|
2
|
+
import { percentToFraction, requireMinorUnitsInteger } from "../../demo/units";
|
|
2
3
|
export const mockAddCartDiscount = async (params) => {
|
|
3
4
|
console.log("[Mock] addCartDiscount called", params);
|
|
4
5
|
if (params) {
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
const value = params.isPercent
|
|
6
|
+
// FI-6991: a fixed amount arrives as an INTEGER in MINOR units (500 =
|
|
7
|
+
// $5.00) and is stored directly; a percent arrives raw (50 = 50%) and is
|
|
8
|
+
// stored as a fraction (0.5). Same as the real handler.
|
|
9
|
+
const value = params.isPercent
|
|
10
|
+
? percentToFraction(params.amount)
|
|
11
|
+
: requireMinorUnitsInteger(params.amount, "Discount amount");
|
|
9
12
|
MOCK_CART.discount = {
|
|
10
13
|
value,
|
|
11
14
|
isPercent: params.isPercent,
|
|
12
15
|
label: params.label
|
|
13
16
|
};
|
|
14
17
|
if (params.isPercent) {
|
|
15
|
-
MOCK_CART.total = MOCK_CART.subtotal * (1 -
|
|
18
|
+
MOCK_CART.total = MOCK_CART.subtotal * (1 - value);
|
|
16
19
|
}
|
|
17
20
|
else {
|
|
18
21
|
MOCK_CART.total = MOCK_CART.subtotal - value;
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { MOCK_CART } from "../../demo/database";
|
|
2
|
+
import { percentToFraction, requireMinorUnitsInteger } from "../../demo/units";
|
|
2
3
|
export const mockAddCartFee = async (params) => {
|
|
3
4
|
console.log("[Mock] addCartFee called", params);
|
|
4
5
|
if (params) {
|
|
5
6
|
if (!MOCK_CART.customFee)
|
|
6
7
|
MOCK_CART.customFee = [];
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
const storedAmount = params.isPercent
|
|
8
|
+
// FI-6991: a fixed amount arrives as an INTEGER in MINOR units (500 =
|
|
9
|
+
// $5.00) and is stored directly; a percent arrives raw (50 = 50%) and is
|
|
10
|
+
// stored as a fraction (0.5). Same as the real handler.
|
|
11
|
+
const storedAmount = params.isPercent
|
|
12
|
+
? percentToFraction(params.amount)
|
|
13
|
+
: requireMinorUnitsInteger(params.amount, "Fee amount");
|
|
11
14
|
MOCK_CART.customFee.push({
|
|
12
15
|
label: params.label || "Fee",
|
|
13
16
|
amount: storedAmount,
|
|
@@ -15,7 +18,7 @@ export const mockAddCartFee = async (params) => {
|
|
|
15
18
|
applyTaxes: params.applyTaxes || false,
|
|
16
19
|
taxTableId: params.taxTableId
|
|
17
20
|
});
|
|
18
|
-
const feeAmount = params.isPercent ? MOCK_CART.subtotal *
|
|
21
|
+
const feeAmount = params.isPercent ? MOCK_CART.subtotal * storedAmount : storedAmount;
|
|
19
22
|
MOCK_CART.total += feeAmount;
|
|
20
23
|
MOCK_CART.amountToBeCharged = MOCK_CART.total;
|
|
21
24
|
MOCK_CART.remainingBalance = MOCK_CART.total;
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { MOCK_CART, mockPublishEvent } from '../../demo/database';
|
|
2
|
+
import { requireMinorUnitsInteger } from '../../demo/units';
|
|
2
3
|
export const mockAddCustomSale = async (params) => {
|
|
3
4
|
console.log('[Mock] addCustomSale called', params);
|
|
4
5
|
if (!params)
|
|
5
6
|
throw new Error('Params required');
|
|
6
7
|
// Simple mock ID generation
|
|
7
8
|
const mockId = 'sale_' + Math.random().toString(36).substr(2, 9);
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
const
|
|
11
|
-
const price = Math.round(Number(params.price) * minorFactor);
|
|
9
|
+
// FI-6991: price arrives as an INTEGER in MINOR units (500 = $5.00) and is
|
|
10
|
+
// stored directly — the engine throws on a fraction, so the mock does too.
|
|
11
|
+
const price = requireMinorUnitsInteger(params.price, 'price');
|
|
12
12
|
const quantity = params.quantity ?? 1;
|
|
13
13
|
const customSale = {
|
|
14
14
|
id: mockId,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { MOCK_CART, mockPublishEvent } from '../../demo/database';
|
|
2
|
+
import { requireMinorUnitsInteger } from '../../demo/units';
|
|
2
3
|
export const mockEditCustomSale = async (params) => {
|
|
3
4
|
console.log('[Mock] editCustomSale called', params);
|
|
4
5
|
if (!params)
|
|
@@ -13,10 +14,9 @@ export const mockEditCustomSale = async (params) => {
|
|
|
13
14
|
if (params.label !== undefined)
|
|
14
15
|
sale.name = params.label;
|
|
15
16
|
if (params.price !== undefined) {
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
sale.price = Math.round(Number(params.price) * minorFactor);
|
|
17
|
+
// FI-6991: price arrives as an INTEGER in MINOR units and is stored
|
|
18
|
+
// directly — the engine throws on a fraction, so the mock does too.
|
|
19
|
+
sale.price = requireMinorUnitsInteger(params.price, 'price');
|
|
20
20
|
}
|
|
21
21
|
if (params.quantity !== undefined)
|
|
22
22
|
sale.quantity = params.quantity;
|
|
@@ -58,6 +58,7 @@ export const mockGetRefundPlan = async (params) => {
|
|
|
58
58
|
transactionId: s.transactionId,
|
|
59
59
|
amount: s.maxRefundable,
|
|
60
60
|
paymentType: s.paymentType,
|
|
61
|
+
requiresDestination: s.paymentType === 'redeem',
|
|
61
62
|
requiresGiftCardDestination: s.paymentType === 'redeem',
|
|
62
63
|
}));
|
|
63
64
|
const budget = legs.reduce((sum, l) => sum + l.amount, 0);
|
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
export interface GetRefundPlanParams {
|
|
2
2
|
/** Order to inspect; defaults to the active order. */
|
|
3
3
|
orderId?: string;
|
|
4
|
+
/**
|
|
5
|
+
* The selection to allocate — the SAME array you will pass to
|
|
6
|
+
* `processPartialRefund({ items })`, so the plan you render and the refund
|
|
7
|
+
* you submit are computed from one input.
|
|
8
|
+
*
|
|
9
|
+
* A flow that owns its own refund UI holds the selection in its own state and
|
|
10
|
+
* never stages it on the POS, so without this there is nothing for the engine
|
|
11
|
+
* to allocate. Pass it here on every selection change to get the matching
|
|
12
|
+
* {@link RefundPlanAllocation} back. **Purely a read** — unlike
|
|
13
|
+
* `processPartialRefund`, this never stages the selection or touches POS
|
|
14
|
+
* state, so it is safe to call as the cashier ticks rows.
|
|
15
|
+
*
|
|
16
|
+
* Omit it to fall back to the selection already staged on the POS (what
|
|
17
|
+
* `selectAllRefundItems` sets) — the in-POS modal's path. Omitted with
|
|
18
|
+
* nothing staged, no `allocation` comes back.
|
|
19
|
+
*/
|
|
20
|
+
items?: {
|
|
21
|
+
/** `internalId` / `variantId` for a product, `customSaleId`, cart-fee id, or tip `transactionId`. */
|
|
22
|
+
itemKey: string;
|
|
23
|
+
quantity: number;
|
|
24
|
+
/** Optional hint; inferred from the order when omitted. */
|
|
25
|
+
type?: 'product' | 'customSale' | 'fee' | 'tip';
|
|
26
|
+
}[];
|
|
4
27
|
}
|
|
5
28
|
export interface RefundPlanSource {
|
|
6
29
|
transactionId: string;
|
|
@@ -29,7 +52,18 @@ export interface RefundPlanLeg {
|
|
|
29
52
|
amount: number;
|
|
30
53
|
/** `cash` / `card` / `redeem` / etc., copied from the source. */
|
|
31
54
|
paymentType: string;
|
|
32
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* True when the leg must carry a destination tender (a `redeem` source
|
|
57
|
+
* cannot be refunded to itself — the money needs somewhere to land, credited
|
|
58
|
+
* by the flow FIRST). The destination is usually a gift card but redeem is
|
|
59
|
+
* the general rail: loyalty and store-credit extensions ride it too.
|
|
60
|
+
*/
|
|
61
|
+
requiresDestination: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* @deprecated Same value as {@link RefundPlanLeg.requiresDestination} — the
|
|
64
|
+
* old name baked one extension (gift card) into a general redeem concept.
|
|
65
|
+
* Kept populated for existing callers; prefer `requiresDestination`.
|
|
66
|
+
*/
|
|
33
67
|
requiresGiftCardDestination: boolean;
|
|
34
68
|
/**
|
|
35
69
|
* Cash legs only: what the drawer actually pays after the company's
|
|
@@ -47,9 +81,10 @@ export interface RefundPlanLeg {
|
|
|
47
81
|
* order's captures — what a flow renders and submits instead of computing a
|
|
48
82
|
* split of its own.
|
|
49
83
|
*
|
|
50
|
-
* Present
|
|
51
|
-
*
|
|
52
|
-
*
|
|
84
|
+
* Present when the call carries a selection: either `params.items` (a flow
|
|
85
|
+
* holding its own selection — the usual case) or a selection already staged on
|
|
86
|
+
* the POS for the active order (`selectAllRefundItems`). Omitted for a bare
|
|
87
|
+
* capacity read with neither.
|
|
53
88
|
*/
|
|
54
89
|
export interface RefundPlanAllocation {
|
|
55
90
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { MOCK_CART, mockPublishEvent } from "../../demo/database";
|
|
2
|
+
import { percentToFraction, requireMinorUnitsInteger } from "../../demo/units";
|
|
2
3
|
export const mockPartialPayment = async (params) => {
|
|
3
4
|
console.log("[Mock] partialPayment called", params);
|
|
4
5
|
const openUI = params?.openUI ?? true;
|
|
@@ -18,10 +19,11 @@ export const mockPartialPayment = async (params) => {
|
|
|
18
19
|
// untouched until the payment is actually taken (see applyMockPayment).
|
|
19
20
|
const remaining = MOCK_CART.remainingBalance ?? MOCK_CART.total;
|
|
20
21
|
const raw = params?.amount ?? 0;
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
const
|
|
24
|
-
|
|
22
|
+
// FI-6991: a fixed amount is already an INTEGER in MINOR units and is used
|
|
23
|
+
// directly; a percent is raw 0-100 against the remaining total.
|
|
24
|
+
const charge = params?.isPercent
|
|
25
|
+
? Math.round(remaining * percentToFraction(raw))
|
|
26
|
+
: requireMinorUnitsInteger(raw, "Payment amount");
|
|
25
27
|
MOCK_CART.amountToBeCharged = Math.min(Math.max(0, charge), remaining);
|
|
26
28
|
mockPublishEvent("cart", "partial-payment-set", { amountToBeCharged: MOCK_CART.amountToBeCharged });
|
|
27
29
|
return {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { ProcessPartialRefund } from
|
|
1
|
+
import { ProcessPartialRefund } from './types';
|
|
2
2
|
export declare const mockProcessPartialRefund: ProcessPartialRefund;
|
|
@@ -10,7 +10,7 @@ export const mockProcessPartialRefund = async (params) => {
|
|
|
10
10
|
if (params?.legs && params?.giftCard) {
|
|
11
11
|
throw new Error('refund.giftCardAndLegs: pass either `legs` (you allocate) or `giftCard` (the engine allocates), not both');
|
|
12
12
|
}
|
|
13
|
-
console.log(
|
|
13
|
+
console.log('[Mock] processPartialRefund called', {
|
|
14
14
|
...params,
|
|
15
15
|
openUI: params?.openUI ?? true,
|
|
16
16
|
legs: params?.legs ?? null,
|
|
@@ -19,6 +19,7 @@ export const mockProcessPartialRefund = async (params) => {
|
|
|
19
19
|
return {
|
|
20
20
|
success: true,
|
|
21
21
|
refundId: 'mock_refund_' + Date.now(),
|
|
22
|
-
|
|
22
|
+
modalRaised: false,
|
|
23
|
+
timestamp: new Date().toISOString(),
|
|
23
24
|
};
|
|
24
25
|
};
|
|
@@ -153,7 +153,25 @@ export interface ProcessPartialRefundParams {
|
|
|
153
153
|
}
|
|
154
154
|
export interface ProcessPartialRefundResponse {
|
|
155
155
|
success: boolean;
|
|
156
|
-
|
|
156
|
+
/**
|
|
157
|
+
* The persisted Refund document's id — the REAL one, usable to look the
|
|
158
|
+
* refund up. `null` means nothing has committed: the split-payment modal
|
|
159
|
+
* was raised (`modalRaised: true`) and owns the commit from there.
|
|
160
|
+
*
|
|
161
|
+
* Before kaching 1.9.5-preprod.17 this was the hardcoded string
|
|
162
|
+
* `'processed'` regardless of outcome, so a truthiness check could not
|
|
163
|
+
* detect a refund that silently didn't happen. Guard on it now:
|
|
164
|
+
* `if (!res.refundId && !res.modalRaised) …` is unreachable (such paths
|
|
165
|
+
* throw instead), so `res.refundId` alone answers "did money move".
|
|
166
|
+
*/
|
|
167
|
+
refundId: string | null;
|
|
168
|
+
/**
|
|
169
|
+
* True when a multi-tender order raised the split-payment refund modal
|
|
170
|
+
* (`openUI` omitted or `true`): the cashier allocates there and the modal
|
|
171
|
+
* drives the commit — this call wrote nothing. Headless calls
|
|
172
|
+
* (`openUI: false`) never raise it.
|
|
173
|
+
*/
|
|
174
|
+
modalRaised: boolean;
|
|
157
175
|
timestamp: string;
|
|
158
176
|
}
|
|
159
177
|
export type ProcessPartialRefund = (params?: ProcessPartialRefundParams) => Promise<ProcessPartialRefundResponse>;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { MOCK_CART, mockPublishEvent } from "../../demo/database";
|
|
2
2
|
function feeContributionToTotal(fee, subtotal) {
|
|
3
|
+
// A percent fee is STORED as a fraction (10% -> 0.1), so it scales the
|
|
4
|
+
// subtotal directly. Dividing by 100 again credited back a hundredth of the
|
|
5
|
+
// fee, leaving the total permanently inflated after a remove.
|
|
3
6
|
if (fee.isPercent) {
|
|
4
|
-
return subtotal *
|
|
7
|
+
return subtotal * fee.amount;
|
|
5
8
|
}
|
|
6
9
|
return fee.amount;
|
|
7
10
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FI-6991 money contract, mirrored for the mock host.
|
|
3
|
+
*
|
|
4
|
+
* The POS engine a published build boots (kaching) takes every FIXED money
|
|
5
|
+
* amount as an INTEGER in MINOR currency units and stores it DIRECTLY, throwing
|
|
6
|
+
* on a fraction; a percent arrives raw 0-100 and is stored as a FRACTION
|
|
7
|
+
* (10 -> 0.1). See kaching's `src/command-frame/utils/adjustmentValue.ts`.
|
|
8
|
+
*
|
|
9
|
+
* The mocks used to convert fixed amounts from MAJOR units — the pre-FI-6991
|
|
10
|
+
* contract — so an app that was correct against a real register read 100x wrong
|
|
11
|
+
* in preview, and an app tuned until preview looked right shipped 100x wrong.
|
|
12
|
+
* A mock host is only worth having if it stores, and rejects, exactly what the
|
|
13
|
+
* engine does.
|
|
14
|
+
*/
|
|
15
|
+
/** A fixed money amount must be an integer count of minor units. */
|
|
16
|
+
export declare function requireMinorUnitsInteger(amount: number | string, what: string): number;
|
|
17
|
+
/** A percent arrives raw 0-100 on the wire and is STORED as a fraction. */
|
|
18
|
+
export declare function percentToFraction(amount: number): number;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FI-6991 money contract, mirrored for the mock host.
|
|
3
|
+
*
|
|
4
|
+
* The POS engine a published build boots (kaching) takes every FIXED money
|
|
5
|
+
* amount as an INTEGER in MINOR currency units and stores it DIRECTLY, throwing
|
|
6
|
+
* on a fraction; a percent arrives raw 0-100 and is stored as a FRACTION
|
|
7
|
+
* (10 -> 0.1). See kaching's `src/command-frame/utils/adjustmentValue.ts`.
|
|
8
|
+
*
|
|
9
|
+
* The mocks used to convert fixed amounts from MAJOR units — the pre-FI-6991
|
|
10
|
+
* contract — so an app that was correct against a real register read 100x wrong
|
|
11
|
+
* in preview, and an app tuned until preview looked right shipped 100x wrong.
|
|
12
|
+
* A mock host is only worth having if it stores, and rejects, exactly what the
|
|
13
|
+
* engine does.
|
|
14
|
+
*/
|
|
15
|
+
/** A fixed money amount must be an integer count of minor units. */
|
|
16
|
+
export function requireMinorUnitsInteger(amount, what) {
|
|
17
|
+
const n = Number(amount);
|
|
18
|
+
if (!Number.isFinite(n)) {
|
|
19
|
+
throw new Error(`${what} must be a valid number`);
|
|
20
|
+
}
|
|
21
|
+
if (!Number.isInteger(n)) {
|
|
22
|
+
throw new Error(`${what} must be an integer amount in minor currency units (e.g. 1575 = $15.75)`);
|
|
23
|
+
}
|
|
24
|
+
return n;
|
|
25
|
+
}
|
|
26
|
+
/** A percent arrives raw 0-100 on the wire and is STORED as a fraction. */
|
|
27
|
+
export function percentToFraction(amount) {
|
|
28
|
+
return Number(amount) / 100;
|
|
29
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@final-commerce/command-frame",
|
|
3
|
-
"version": "0.5.0
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Commands Frame library",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
"@commitlint/cli": "^19.0.0",
|
|
64
64
|
"@commitlint/config-conventional": "^19.0.0",
|
|
65
65
|
"@eslint/js": "^9.0.0",
|
|
66
|
+
"@vitest/coverage-v8": "^4.1.11",
|
|
66
67
|
"eslint": "^9.0.0",
|
|
67
68
|
"husky": "^9.1.7",
|
|
68
69
|
"jira-prepare-commit-msg": "^1.7.2",
|