@classytic/pos-ui 0.2.0 → 1.3.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.
Files changed (87) hide show
  1. package/LICENSE +75 -0
  2. package/dist/components/CartSidebar.d.ts +56 -0
  3. package/dist/components/CartSidebar.js +10 -9
  4. package/dist/components/PosTopBar.d.ts +19 -0
  5. package/dist/components/PosTopBar.js +4 -4
  6. package/dist/components/PrinterSettingsDialog.d.ts +8 -0
  7. package/dist/components/PrinterSettingsDialog.js +506 -20
  8. package/dist/components/ReceiptDeliveryDialog.d.ts +11 -0
  9. package/dist/components/ReceiptDeliveryDialog.js +98 -0
  10. package/dist/components/ReceiptReprintDialog.d.ts +10 -0
  11. package/dist/components/ReceiptReprintDialog.js +49 -22
  12. package/dist/components/index.d.ts +6 -0
  13. package/dist/components/index.js +7 -0
  14. package/dist/dashboard/components/CustomerLookupDialog.js +108 -90
  15. package/dist/dashboard/components/CustomerQuickAddDialog.js +3 -1
  16. package/dist/dashboard/components/DeliveryPanel.js +134 -0
  17. package/dist/dashboard/components/ProductCard.js +17 -7
  18. package/dist/dashboard/components/ProductsPanel.js +4 -4
  19. package/dist/dashboard/components/SplitPaymentPanel.js +5 -5
  20. package/dist/dashboard/components/VariantSelectorDialog.js +53 -11
  21. package/dist/dashboard/components/cart/AddChargeDialog.js +3 -2
  22. package/dist/dashboard/components/cart/CartItems.js +5 -5
  23. package/dist/dashboard/components/cart/CartSummary.js +6 -6
  24. package/dist/dashboard/components/cart/CustomerSection.js +3 -3
  25. package/dist/dashboard/components/cart/DiscountSection.js +4 -3
  26. package/dist/dashboard/components/cart/PointsRedemptionSection.js +5 -4
  27. package/dist/dashboard/pos.types.d.ts +32 -0
  28. package/dist/hardware/agent-adapter.js +127 -0
  29. package/dist/hardware/agent-pairing.d.ts +18 -0
  30. package/dist/hardware/agent-pairing.js +63 -0
  31. package/dist/hardware/context.d.ts +1 -5
  32. package/dist/hardware/index.d.ts +4 -3
  33. package/dist/hardware/index.js +3 -2
  34. package/dist/hardware/ports.d.ts +21 -4
  35. package/dist/hardware/tauri-adapter.d.ts +0 -1
  36. package/dist/hardware/tauri-adapter.js +19 -5
  37. package/dist/hardware/web-adapter.d.ts +22 -7
  38. package/dist/hardware/web-adapter.js +56 -14
  39. package/dist/hooks/branch-scoped-key.js +64 -0
  40. package/dist/hooks/index.d.ts +6 -0
  41. package/dist/hooks/index.js +7 -0
  42. package/dist/hooks/sale-attempt.js +62 -0
  43. package/dist/hooks/useManagerAuth.js +9 -1
  44. package/dist/hooks/usePosCart.d.ts +55 -0
  45. package/dist/hooks/usePosCart.js +224 -117
  46. package/dist/hooks/usePosCustomer.d.ts +31 -0
  47. package/dist/hooks/usePosCustomer.js +32 -78
  48. package/dist/hooks/usePosDelivery.d.ts +28 -0
  49. package/dist/hooks/usePosDelivery.js +133 -0
  50. package/dist/hooks/usePosMultiOrder.d.ts +22 -0
  51. package/dist/hooks/usePosMultiOrder.js +14 -4
  52. package/dist/hooks/usePosPayment.d.ts +22 -0
  53. package/dist/lib/delivery-payload.js +83 -0
  54. package/dist/lib/money.js +96 -23
  55. package/dist/node_modules/react-hook-form/dist/index.esm.js +468 -332
  56. package/dist/runtime/auth-port.d.ts +1 -5
  57. package/dist/runtime/branch-port.d.ts +1 -5
  58. package/dist/runtime/config.d.ts +43 -6
  59. package/dist/runtime/config.js +18 -1
  60. package/dist/runtime/index.d.ts +2 -2
  61. package/dist/runtime/index.js +2 -2
  62. package/dist/screens/OrderHistoryDrawer.d.ts +8 -0
  63. package/dist/screens/OrderHistoryDrawer.js +15 -15
  64. package/dist/screens/ParkedOrdersDrawer.d.ts +12 -0
  65. package/dist/screens/ParkedOrdersDrawer.js +3 -3
  66. package/dist/screens/PaymentScreen.d.ts +4 -0
  67. package/dist/screens/PaymentScreen.js +194 -33
  68. package/dist/screens/ProductScreen.d.ts +4 -0
  69. package/dist/screens/ProductScreen.js +13 -10
  70. package/dist/screens/ReceiptScreen.d.ts +4 -0
  71. package/dist/screens/ReceiptScreen.js +96 -72
  72. package/dist/screens/ShiftCloseScreen.d.ts +4 -0
  73. package/dist/screens/ShiftCloseScreen.js +18 -15
  74. package/dist/screens/ShiftOpenScreen.d.ts +4 -0
  75. package/dist/screens/ShiftOpenScreen.js +75 -19
  76. package/dist/screens/index.d.ts +8 -0
  77. package/dist/screens/index.js +9 -0
  78. package/dist/shell/index.d.ts +2 -0
  79. package/dist/shell/pos-shell.d.ts +41 -8
  80. package/dist/shell/pos-shell.js +25 -18
  81. package/dist/state/index.d.ts +3 -0
  82. package/dist/state/index.js +4 -0
  83. package/dist/state/pos-context.d.ts +13 -0
  84. package/dist/state/pos-state.d.ts +49 -0
  85. package/dist/state/pos-state.js +6 -7
  86. package/dist/utils/pos-helpers.js +137 -34
  87. package/package.json +112 -92
@@ -0,0 +1,133 @@
1
+ "use client";
2
+
3
+ import { branchScopedKey } from "./branch-scoped-key.js";
4
+ import { useCallback } from "react";
5
+ import { useLocalStorage } from "@classytic/fluid/client/hooks";
6
+ import { toast } from "sonner";
7
+
8
+ //#region src/hooks/usePosDelivery.ts
9
+ /**
10
+ * Delivery and payment TERM for the sale being rung.
11
+ *
12
+ * Two independent decisions that a till has never been able to express, and which
13
+ * the backend now models end to end:
14
+ *
15
+ * - **where the goods go** — carryout (the cashier hands them over) or delivery
16
+ * (a courier does, later, to an address);
17
+ * - **when the customer pays** — now, or on delivery.
18
+ *
19
+ * They are orthogonal. A phone order can be prepaid by bKash and shipped; a
20
+ * walk-in can pay cash for something delivered tomorrow; and a COD order is the
21
+ * one where no money exists yet.
22
+ *
23
+ * ## Persisted, branch-scoped, for the reason the cart is
24
+ *
25
+ * `ProductScreen` and `PaymentScreen` each mount their own hook instances — the POS
26
+ * state machine swaps one screen for the other — so per-instance `useState` is wiped
27
+ * on navigation. A delivery address typed on one screen and silently empty on the
28
+ * next is the same class of loss `usePosCustomer` records, and here it would land as
29
+ * a refused sale (`DELIVERY_ADDRESS_UNUSABLE`) with the customer at the counter.
30
+ *
31
+ * The namespace goes through `branchScopedKey`, so an unresolved branch gets the
32
+ * sentinel and every mutator refuses rather than writing to a namespace about to be
33
+ * abandoned. See `branch-scoped-key.ts`.
34
+ *
35
+ * ## The address shape
36
+ *
37
+ * `line1` + `city` are what the host's normalizer REQUIRES (be-prod's
38
+ * `toFulfillmentAddress`; country defaults). The rest is evidence a courier and a
39
+ * dispatcher actually need. The server normalizes and REFUSES an undispatchable
40
+ * delivery before it takes payment or holds stock, so this form's job is to collect
41
+ * enough that the refusal never fires — not to re-implement the validation.
42
+ *
43
+ * NOTE: this is the seventh hand-rolled address surface in the repo. A shared
44
+ * `addressFields()` builder is planned (`tasks/pos-plan.md`); pos-ui depends on
45
+ * neither `@classytic/formkit` nor `@classytic/commerce-ui` today, so it cannot
46
+ * consume it yet. When it lands, this component is a migration target — the shape
47
+ * below is deliberately the same field set.
48
+ */
49
+ /** Match the cart's window: an abandoned sale's delivery details expire with it. */
50
+ const DELIVERY_TTL = 864e5;
51
+ const EMPTY_ADDRESS = {
52
+ recipientName: "",
53
+ recipientPhone: "",
54
+ addressLine1: "",
55
+ city: "",
56
+ area: "",
57
+ note: ""
58
+ };
59
+ /** Major-unit fee. A blank field is no fee, not a zero-valued fee row. */
60
+ function parseFee(input) {
61
+ const n = Number.parseFloat(input);
62
+ return Number.isFinite(n) && n > 0 ? n : 0;
63
+ }
64
+ function usePosDelivery(branchId) {
65
+ const { key, ready } = branchScopedKey("pos:delivery", branchId);
66
+ const [method, setMethodState] = useLocalStorage(`${key}:method`, "pickup", DELIVERY_TTL);
67
+ const [address, setAddress] = useLocalStorage(`${key}:address`, EMPTY_ADDRESS, DELIVERY_TTL);
68
+ const [feeInput, setFeeInputState] = useLocalStorage(`${key}:fee`, "", DELIVERY_TTL);
69
+ const [paymentTerm, setPaymentTermState] = useLocalStorage(`${key}:paymentTerm`, "prepaid", DELIVERY_TTL);
70
+ /**
71
+ * The one guard every mutator calls first — the same shape `usePosCart` uses, and
72
+ * loud on purpose. The alternative is a typed address that is about to be orphaned
73
+ * when the branch resolves and the key changes under it.
74
+ */
75
+ const refuseIfBranchUnresolved = useCallback(() => {
76
+ if (ready) return false;
77
+ toast.error("Branch not ready", { description: "The register is still resolving its branch. Try again in a moment." });
78
+ return true;
79
+ }, [ready]);
80
+ return {
81
+ method,
82
+ address,
83
+ feeInput,
84
+ paymentTerm,
85
+ setMethod: useCallback((next) => {
86
+ if (refuseIfBranchUnresolved()) return;
87
+ setMethodState(next);
88
+ /**
89
+ * Switching back to carryout clears collect-on-delivery.
90
+ *
91
+ * "Collect on delivery" names a delivery that has not happened yet. On a
92
+ * carryout sale there is no later collection — the customer is standing there
93
+ * — so leaving the term set would submit a sale that owes money to nobody, and
94
+ * the server would refuse it after the cashier had already moved on.
95
+ */
96
+ if (next === "pickup") setPaymentTermState("prepaid");
97
+ }, [
98
+ refuseIfBranchUnresolved,
99
+ setMethodState,
100
+ setPaymentTermState
101
+ ]),
102
+ setAddressField: useCallback((field, value) => {
103
+ if (refuseIfBranchUnresolved()) return;
104
+ setAddress((prev) => ({
105
+ ...prev,
106
+ [field]: value
107
+ }));
108
+ }, [refuseIfBranchUnresolved, setAddress]),
109
+ setFeeInput: useCallback((value) => {
110
+ if (refuseIfBranchUnresolved()) return;
111
+ setFeeInputState(value);
112
+ }, [refuseIfBranchUnresolved, setFeeInputState]),
113
+ setPaymentTerm: useCallback((term) => {
114
+ if (refuseIfBranchUnresolved()) return;
115
+ setPaymentTermState(term);
116
+ }, [refuseIfBranchUnresolved, setPaymentTermState]),
117
+ reset: useCallback(() => {
118
+ setMethodState("pickup");
119
+ setAddress(EMPTY_ADDRESS);
120
+ setFeeInputState("");
121
+ setPaymentTermState("prepaid");
122
+ }, [
123
+ setMethodState,
124
+ setAddress,
125
+ setFeeInputState,
126
+ setPaymentTermState
127
+ ]),
128
+ fee: parseFee(feeInput)
129
+ };
130
+ }
131
+
132
+ //#endregion
133
+ export { usePosDelivery };
@@ -0,0 +1,22 @@
1
+ import { PosCartItem } from "../dashboard/pos.types.js";
2
+ //#region src/hooks/usePosMultiOrder.d.ts
3
+ interface ParkedOrder {
4
+ id: string;
5
+ label: string;
6
+ cart: PosCartItem[];
7
+ customerName: string;
8
+ customerPhone: string;
9
+ membershipCardId: string;
10
+ discountInput: string;
11
+ parkedAt: number;
12
+ }
13
+ interface UsePosMultiOrderReturn {
14
+ orders: ParkedOrder[];
15
+ parkOrder: (order: Omit<ParkedOrder, "id" | "parkedAt">) => string;
16
+ resumeOrder: (id: string) => ParkedOrder | null;
17
+ deleteOrder: (id: string) => void;
18
+ canPark: boolean;
19
+ }
20
+ declare function usePosMultiOrder(branchId?: string): UsePosMultiOrderReturn;
21
+ //#endregion
22
+ export { ParkedOrder, UsePosMultiOrderReturn, usePosMultiOrder };
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { branchScopedKey } from "./branch-scoped-key.js";
3
4
  import { useCallback } from "react";
4
5
  import { useLocalStorage } from "@classytic/fluid/client/hooks";
5
6
 
@@ -11,13 +12,22 @@ import { useLocalStorage } from "@classytic/fluid/client/hooks";
11
12
  * Each parked order saves: cart items, customer info, discount state.
12
13
  */
13
14
  const MAX_PARKED_ORDERS = 5;
14
- const PARKED_TTL = 480 * 60 * 1e3;
15
+ const PARKED_TTL = 288e5;
15
16
  function usePosMultiOrder(branchId) {
16
- const [orders, setOrders] = useLocalStorage(branchId ? `pos:parked:${branchId}` : "pos:parked:default", [], PARKED_TTL);
17
- const canPark = orders.length < MAX_PARKED_ORDERS;
17
+ /**
18
+ * Per-BRANCH, and an unresolved branch is not a branch — the same defect the
19
+ * cart had, with a longer fuse: a sale parked under `"default"` was orphaned the
20
+ * moment the branch resolved, and parked sales are the ONE cart the cashier
21
+ * expects to still be there after walking away. `branch-scoped-key.ts` has the
22
+ * full account.
23
+ */
24
+ const { key, ready } = branchScopedKey("pos:parked", branchId);
25
+ const [orders, setOrders] = useLocalStorage(key, [], PARKED_TTL);
26
+ const canPark = ready && orders.length < MAX_PARKED_ORDERS;
18
27
  return {
19
28
  orders,
20
29
  parkOrder: useCallback((order) => {
30
+ if (!ready) return "";
21
31
  const id = `parked_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
22
32
  const parked = {
23
33
  ...order,
@@ -26,7 +36,7 @@ function usePosMultiOrder(branchId) {
26
36
  };
27
37
  setOrders((prev) => [...prev, parked]);
28
38
  return id;
29
- }, [setOrders]),
39
+ }, [setOrders, ready]),
30
40
  resumeOrder: useCallback((id) => {
31
41
  let found = null;
32
42
  setOrders((prev) => {
@@ -0,0 +1,22 @@
1
+ import { PaymentOption, PaymentState, PosPaymentMethod, SplitPaymentEntry } from "@classytic/commerce-sdk/sales";
2
+ //#region src/hooks/usePosPayment.d.ts
3
+ declare function paymentNeedsReference(posMethod: PosPaymentMethod): boolean;
4
+ interface UsePosPaymentReturn {
5
+ options: PaymentOption[];
6
+ isLoading: boolean;
7
+ state: PaymentState;
8
+ selectedOption: PaymentOption | null;
9
+ selectPayment: (key: string) => void;
10
+ setMode: (mode: "single" | "split") => void;
11
+ setCashReceived: (value: string) => void;
12
+ setReference: (value: string) => void;
13
+ addSplit: (paymentKey?: string) => void;
14
+ updateSplit: (id: string, patch: Partial<Pick<SplitPaymentEntry, "paymentKey" | "amount" | "reference">>) => void;
15
+ removeSplit: (id: string) => void;
16
+ validateSplitEntry: (id: string) => string | undefined;
17
+ validateAllSplits: () => boolean;
18
+ reset: () => void;
19
+ }
20
+ declare function usePosPayment(token?: string, total?: number): UsePosPaymentReturn;
21
+ //#endregion
22
+ export { type PaymentOption, type SplitPaymentEntry, UsePosPaymentReturn, paymentNeedsReference, usePosPayment };
@@ -0,0 +1,83 @@
1
+ //#region src/lib/delivery-payload.ts
2
+ /**
3
+ * What the customer owes: goods (after discounts) PLUS the delivery charge.
4
+ *
5
+ * The server computes the same sum and validates the tender against it, so a screen
6
+ * quoting only the cart total collects too little and the sale is refused AFTER the
7
+ * cashier has counted cash. Every payable figure on the screen reads this — numpad
8
+ * target, split check, cash suggestions, both button labels.
9
+ *
10
+ * The fee only counts on a DELIVERY sale: a charge left in the box from a previous
11
+ * selection must not silently inflate a carryout.
12
+ */
13
+ function payableTotalOf(cartTotal, decision) {
14
+ return decision.method === "delivery" && decision.fee > 0 ? cartTotal + decision.fee : cartTotal;
15
+ }
16
+ /**
17
+ * Does this sale take money at the till?
18
+ *
19
+ * Both conditions, not just the term. "Collect on delivery" names a delivery that
20
+ * has not happened yet; on a carryout the customer is standing at the counter, so a
21
+ * COD flag there would tell the cashier not to collect money that is owed right now.
22
+ */
23
+ function isCollectOnDeliverySale(decision) {
24
+ return decision.paymentTerm === "collect_on_delivery" && decision.method === "delivery";
25
+ }
26
+ /**
27
+ * Why this sale cannot be submitted, or `null`.
28
+ *
29
+ * Mirrors the server's refusal so the cashier is stopped at the field rather than at
30
+ * the end of checkout. `addressLine1` + `city` are what the host normalizer requires;
31
+ * the server re-checks and refuses (`DELIVERY_ADDRESS_UNUSABLE`) before taking a
32
+ * tender or holding stock, so this is an affordance, never the enforcement.
33
+ */
34
+ function deliveryBlocker(decision) {
35
+ if (decision.method !== "delivery") return null;
36
+ if (!decision.address.addressLine1?.trim()) return "Enter the delivery address";
37
+ if (!decision.address.city?.trim()) return "Enter the delivery city or town";
38
+ return null;
39
+ }
40
+ /**
41
+ * The delivery fields for the create payload.
42
+ *
43
+ * Three omissions are deliberate and each has a consequence:
44
+ *
45
+ * - **no `deliveryAddress` on a carryout** — there is nothing to dispatch to, and
46
+ * a stale address from a previous selection would ride along;
47
+ * - **no `deliveryPrice` when the fee is zero** — free delivery is real (a
48
+ * promotion, a loyalty perk), and an explicit `0` writes a priced shipping row
49
+ * for a sale that has none rather than leaving it identical to a carryout;
50
+ * - **blank address fields dropped** — an empty string is not a recipient name.
51
+ *
52
+ * `paymentTerm` is ALWAYS sent, prepaid included: the till knows the answer, and a
53
+ * stated fact beats one every downstream reader infers from absence — absence is
54
+ * also what an order written before the field existed looks like, and those are
55
+ * different claims.
56
+ */
57
+ function deliveryPayloadOf(decision) {
58
+ const base = {
59
+ deliveryMethod: decision.method,
60
+ paymentTerm: decision.paymentTerm
61
+ };
62
+ if (decision.method !== "delivery") return base;
63
+ const a = decision.address;
64
+ const address = {};
65
+ const put = (key, value) => {
66
+ const trimmed = value?.trim();
67
+ if (trimmed) address[key] = trimmed;
68
+ };
69
+ put("addressLine1", a.addressLine1);
70
+ put("city", a.city);
71
+ put("recipientName", a.recipientName);
72
+ put("recipientPhone", a.recipientPhone);
73
+ put("area", a.area);
74
+ put("note", a.note);
75
+ return {
76
+ ...base,
77
+ deliveryAddress: address,
78
+ ...decision.fee > 0 ? { deliveryPrice: decision.fee } : {}
79
+ };
80
+ }
81
+
82
+ //#endregion
83
+ export { deliveryBlocker, deliveryPayloadOf, isCollectOnDeliverySale, payableTotalOf };
package/dist/lib/money.js CHANGED
@@ -1,25 +1,62 @@
1
- import { MinorUnits, money, toMajor } from "@classytic/primitives/money";
1
+ import { minorUnitFactor } from "@classytic/primitives/currency";
2
+ import { MinorUnits, money, toMajor, toMajor as toMajor$1 } from "@classytic/primitives/money";
2
3
 
3
4
  //#region src/lib/money.ts
4
5
  /**
5
6
  * Money for POS — composed on `@classytic/primitives` (the canonical Money type
6
- * + arithmetic authority), with thin BDT display formatters on top. The package
7
- * is BD-tenant (BDT / `৳` / `en-BD`); a non-BD deployment forks these three
8
- * constants. Arithmetic and the `Money` type always come from primitives — this
9
- * module never re-implements them.
7
+ * + arithmetic authority), with thin display formatters on top. Arithmetic and
8
+ * the `Money` type always come from primitives this module never
9
+ * re-implements them.
10
+ *
11
+ * ## The tenant seam is the ONE fork knob
12
+ *
13
+ * `@classytic/pos-ui` is a country-agnostic package: it is published under the
14
+ * neutral `@classytic` scope and mounted by hosts in any jurisdiction. The
15
+ * deployment's currency, locale and symbol therefore live in the three
16
+ * constants below, env-driven with a BD default, exactly as
17
+ * `commerce-ui/src/lib/tenant.ts` does it. A fork sets the env vars; it does
18
+ * not edit code.
19
+ *
20
+ * ## Why the exported names are `Minor` / `Major`, never `Paisa` / `Bdt`
21
+ *
22
+ * This module used to export `Paisa`, `formatPaisa`, `formatPaisaPlain`,
23
+ * `paisaToMoney`, `formatBdt` and `formatBdtFixed`. Every one of those is
24
+ * currency-neutral arithmetic wearing a Bangladeshi name, and the names are
25
+ * what the next author reads. A `formatBdt` in a package that claims to serve
26
+ * any country reads as though the package knows what currency it holds — so a
27
+ * USD deployment either believes it must fork, or calls `formatBdt` on dollars
28
+ * and ships a function name that lies about the money in it.
29
+ *
30
+ * The subunit name belongs to the DEPLOYMENT (paisa for BDT, cents for USD,
31
+ * fils for KWD). The package only ever knows "minor units" and "major units".
32
+ * Hosts that want a local alias declare their own — `be-prod/src/shared/money.ts`
33
+ * is the reference: it is the Bangladesh host, so `Paisa` is correct there.
34
+ */
35
+ /**
36
+ * Exported so callers that need the CURRENCY itself (a minor-unit factor, an
37
+ * `Intl` call) read it from the seam instead of re-declaring a literal — a
38
+ * second `"BDT"` elsewhere is a fork knob nobody knows to turn.
10
39
  */
11
40
  const TENANT_CURRENCY = "BDT";
12
- /** Cast a raw number to `Paisa`. Use at SDK boundaries when the response
13
- * type isn't yet branded, or for arithmetic results that guarantee minor
14
- * units. One cast per source not per callsite. */
15
- function Paisa(n) {
16
- return MinorUnits(n, TENANT_CURRENCY);
17
- }
18
- const TENANT_LOCALE = "en-BD";
19
- const TENANT_SYMBOL = "৳";
20
- /** Adapt a paisa (minor-unit) wire value into a structured `Money`. */
21
- function paisaToMoney(paisa, currency = TENANT_CURRENCY) {
22
- return money(paisa, currency);
41
+ const TENANT_LOCALE = process.env.NEXT_PUBLIC_LOCALE ?? "en-BD";
42
+ /**
43
+ * Exported so a LABEL or placeholder can name the currency without a literal.
44
+ * `"Amount (BDT)"` hardcoded in JSX is the same defect as a `Paisa` type name:
45
+ * it survives a fork silently and then lies to the cashier.
46
+ */
47
+ const TENANT_CURRENCY_SYMBOL = process.env.NEXT_PUBLIC_CURRENCY_SYMBOL ?? "";
48
+ const TENANT_SYMBOL = TENANT_CURRENCY_SYMBOL;
49
+ /**
50
+ * Cast a raw number to `TenantMinorUnits`. Use at SDK boundaries when the
51
+ * response type isn't yet branded, or for arithmetic results that guarantee
52
+ * minor units. One cast per source — not per callsite.
53
+ */
54
+ function TenantMinorUnits(n) {
55
+ return MinorUnits(n, "BDT");
56
+ }
57
+ /** Adapt a minor-unit wire value into a structured `Money`. */
58
+ function minorToMoney(minor, currency = "BDT") {
59
+ return money(minor, currency);
23
60
  }
24
61
  /** Canonical formatter — structured `Money` → `৳1,234.00`. */
25
62
  function formatMoney(m) {
@@ -28,14 +65,50 @@ function formatMoney(m) {
28
65
  currency: m.currency
29
66
  }).format(toMajor(m));
30
67
  }
31
- /** Format a wire-format paisa value: `formatPaisa(489900)` → `৳4,899.00`. */
32
- function formatPaisa(paisa, currency = TENANT_CURRENCY) {
33
- return formatMoney(paisaToMoney(paisa, currency));
68
+ /** Format a wire-format minor-unit value: `formatMinor(489900)` → `৳4,899.00`. */
69
+ function formatMinor(minor, currency = "BDT") {
70
+ return formatMoney(minorToMoney(minor, currency));
71
+ }
72
+ /** Compact MAJOR-unit formatter, no forced decimals: `formatMajor(4899)` → `৳4,899`. */
73
+ function formatMajor(amount) {
74
+ return `${TENANT_SYMBOL}${(Number.isFinite(amount) ? amount : 0).toLocaleString(TENANT_LOCALE)}`;
34
75
  }
35
- /** Compact major-unit formatter, no forced decimals: `formatBdt(4899)` → `৳4,899`. */
36
- function formatBdt(bdt) {
37
- return `${TENANT_SYMBOL}${(Number.isFinite(bdt) ? bdt : 0).toLocaleString(TENANT_LOCALE)}`;
76
+ /**
77
+ * Minor units in ONE major unit of the tenant currency, from
78
+ * `@classytic/primitives/currency` never a hardcoded `100`. BDT/USD are 100,
79
+ * KWD is 1000, JPY is 1, so any rate derived by dividing by a literal is wrong
80
+ * for a third of the world.
81
+ */
82
+ function minorPerMajorUnit(currency = "BDT") {
83
+ return minorUnitFactor(currency);
84
+ }
85
+ /**
86
+ * Minor units -> major units, via the CURRENCY's factor.
87
+ *
88
+ * Replaces a hand-written `/ 100` at five call sites (variant absolute prices in the
89
+ * cart and the variant dialog, cash counted at shift open/close, the hardware
90
+ * adapter's amount). `100` is right for BDT and USD and wrong for KWD (1000) and JPY
91
+ * (1) — and each site had derived it independently, so they could drift apart as
92
+ * easily as they were all wrong together.
93
+ */
94
+ function minorToMajor(minor, currency = "BDT") {
95
+ return toMajor(minorToMoney(Number.isFinite(minor) ? minor : 0, currency));
96
+ }
97
+ /**
98
+ * Major units -> integer minor units.
99
+ *
100
+ * Rounds, because the input is typically a parsed decimal from an operator's keypad
101
+ * and IEEE division leaves values like `4899.999999999999`. A truncating conversion
102
+ * there loses a minor unit on ordinary amounts.
103
+ *
104
+ * The `currency` parameter exists to make the factor OBSERVABLE. Without it the tests
105
+ * could not tell this apart from a hardcoded `100` — BDT's factor is 100, so
106
+ * substituting the literal kept every assertion green. A guard that cannot fail for
107
+ * the reason it exists is decoration; callers still omit the argument.
108
+ */
109
+ function majorToMinor(major, currency = "BDT") {
110
+ return Math.round((Number.isFinite(major) ? major : 0) * minorUnitFactor(currency));
38
111
  }
39
112
 
40
113
  //#endregion
41
- export { Paisa, formatBdt, formatMoney, formatPaisa, paisaToMoney };
114
+ export { TENANT_CURRENCY_SYMBOL, TenantMinorUnits, formatMajor, formatMinor, formatMoney, majorToMinor, minorPerMajorUnit, minorToMajor, minorToMoney, toMajor$1 as toMajor };