@final-commerce/common 2.1.2 → 2.1.4

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/index.mjs CHANGED
@@ -1152,6 +1152,8 @@ var STATE_SCHEMA = Object.freeze({
1152
1152
  // order-state-machine/financial-invariants.ts
1153
1153
  var REFUND_ALLOWED_FULFILLMENT = /* @__PURE__ */ new Set(["fulfilled", "returned", "partially_returned", "cancelled"]);
1154
1154
  var PAYMENT_COLLECTION_ORDER = ["unpaid", "partially_paid", "paid"];
1155
+ var UNRETURNED_MONEY_STATES = /* @__PURE__ */ new Set(["partially_paid", "paid", "partially_refunded"]);
1156
+ var REFUNDED_FULFILLMENT_LANDINGS = /* @__PURE__ */ new Set(["returned", "cancelled"]);
1155
1157
  var FINANCIAL_INVARIANTS = [
1156
1158
  {
1157
1159
  id: "no-leave-refunded",
@@ -1182,6 +1184,26 @@ var FINANCIAL_INVARIANTS = [
1182
1184
  const toIdx = PAYMENT_COLLECTION_ORDER.indexOf(to.payment);
1183
1185
  return fromIdx !== -1 && toIdx !== -1 && fromIdx > toIdx;
1184
1186
  }
1187
+ },
1188
+ {
1189
+ id: "no-cancel-with-unreturned-payments",
1190
+ description: "Fulfillment cannot enter cancelled while the payment state carries unreturned captured money (paid / partially_paid / partially_refunded) \u2014 return the money first: a full refund of an unfulfilled order lands refunded \xD7 cancelled, a void lands voided \xD7 cancelled. Scoped to the cancelling MOVE (from.fulfillment !== cancelled) so payment-axis edges evaluated against an already-cancelled fulfillment do not trip it",
1191
+ check: (from, to) => from !== null && from.fulfillment !== "cancelled" && to.fulfillment === "cancelled" && UNRETURNED_MONEY_STATES.has(to.payment)
1192
+ },
1193
+ {
1194
+ id: "no-draft-regression-with-captured-payments",
1195
+ description: "Fulfillment cannot regress to draft once money has been captured \u2014 a money-carrying order is no longer a cart (bug #31: resume lands deposit orders on in_progress for the same reason). New orders and draft-to-draft moves are exempt, so partially_paid \xD7 draft remains declarable as an initial state",
1196
+ check: (from, to) => from !== null && from.fulfillment !== "draft" && to.fulfillment === "draft" && to.payment !== "unpaid"
1197
+ },
1198
+ {
1199
+ id: "no-reopen-voided-fulfillment",
1200
+ description: "A voided order exists only at cancelled fulfillment: a voided order cannot re-enter fulfillment, and a voided \xD7 non-cancelled pair cannot be materialized from nothing (complements no-leave-voided, which only freezes the payment axis). Moves INTO voided from a live payment state are the void flow\u2019s concern, not this rule\u2019s",
1201
+ check: (from, to) => (from === null || from.payment === "voided") && to.payment === "voided" && to.fulfillment !== "cancelled"
1202
+ },
1203
+ {
1204
+ id: "no-reopen-refunded-fulfillment",
1205
+ description: "A fully refunded order cannot re-enter fulfillment \u2014 it may only relabel between its canonical landings, returned and cancelled (complements no-leave-refunded, which only freezes the payment axis)",
1206
+ check: (from, to) => from?.payment === "refunded" && !REFUNDED_FULFILLMENT_LANDINGS.has(to.fulfillment)
1185
1207
  }
1186
1208
  ];
1187
1209
  function getFinancialInvariantViolations(from, to) {
@@ -1269,24 +1291,17 @@ var DISPLAY_LABEL_OVERRIDES = {
1269
1291
  "paid|on_hold": "Parked - Paid",
1270
1292
  "paid|fulfilled": "Completed",
1271
1293
  "partially_refunded|fulfilled": "Partially Refunded",
1272
- "partially_refunded|partially_returned": "Partially Refunded",
1273
- "refunded|fulfilled": "Refunded",
1274
- "refunded|returned": "Refunded",
1275
- // Full refund of a never-fulfilled order (kaching refund op lands on
1276
- // cancelled when nothing was delivered). Terminal payment states emit no
1277
- // generic rows, so without this the pair fell through to the raw
1278
- // "refunded / cancelled" engine fallback.
1279
- "refunded|cancelled": "Refunded",
1280
- "voided|cancelled": "Cancelled"
1294
+ "partially_refunded|partially_returned": "Partially Refunded"
1295
+ };
1296
+ var TERMINAL_LABEL_OVERRIDES = {
1297
+ voided: "Cancelled"
1281
1298
  };
1282
1299
  function buildDefaultDisplayStateMap() {
1283
1300
  const rules = [];
1284
1301
  for (const p of PAYMENT_STATES) {
1302
+ const terminalLabel = TERMINAL_PAYMENT_STATES.has(p) ? TERMINAL_LABEL_OVERRIDES[p] ?? PAYMENT_LABELS.get(p) : void 0;
1285
1303
  for (const f of FULFILLMENT_STATES) {
1286
- const key = `${p}|${f}`;
1287
- const override = DISPLAY_LABEL_OVERRIDES[key];
1288
- if (TERMINAL_PAYMENT_STATES.has(p) && !override) continue;
1289
- const label = override ?? `${PAYMENT_LABELS.get(p)} / ${FULFILLMENT_LABELS.get(f)}`;
1304
+ const label = DISPLAY_LABEL_OVERRIDES[`${p}|${f}`] ?? terminalLabel ?? `${PAYMENT_LABELS.get(p)} / ${FULFILLMENT_LABELS.get(f)}`;
1290
1305
  rules.push({ paymentState: p, fulfillmentState: f, label });
1291
1306
  }
1292
1307
  }
@@ -1409,6 +1424,9 @@ function buildOrderContext(order, extras) {
1409
1424
  }
1410
1425
 
1411
1426
  // order-state-machine/display-state.ts
1427
+ function atomLabel(atom, defs) {
1428
+ return defs.find((d) => d.id === atom)?.label ?? atom;
1429
+ }
1412
1430
  function matchesOptional(actual, rule) {
1413
1431
  if (rule === void 0) return true;
1414
1432
  return Array.isArray(rule) ? rule.includes(actual) : rule === actual;
@@ -1419,7 +1437,9 @@ function deriveDisplayState(payment, fulfillment, config) {
1419
1437
  return { label: rule.label, color: rule.color, icon: rule.icon };
1420
1438
  }
1421
1439
  }
1422
- return { label: `${payment} / ${fulfillment}` };
1440
+ return {
1441
+ label: `${atomLabel(payment, STATE_SCHEMA.paymentStates)} / ${atomLabel(fulfillment, STATE_SCHEMA.fulfillmentStates)}`
1442
+ };
1423
1443
  }
1424
1444
  function displayRuleMatchesPair(rule, payment, fulfillment) {
1425
1445
  return matchesOptional(payment, rule.paymentState) && matchesOptional(fulfillment, rule.fulfillmentState);
@@ -1655,13 +1675,11 @@ function labelForFulfillment(id, config) {
1655
1675
  return config.fulfillmentStates.find((s) => s.id === id)?.label ?? id;
1656
1676
  }
1657
1677
  function transitionDisplayLabel(to, config) {
1658
- const pay = labelForPayment(to.payment, config);
1659
- const ful = labelForFulfillment(to.fulfillment, config);
1660
1678
  const derived = deriveDisplayState(to.payment, to.fulfillment, config).label;
1661
- if (derived && derived !== `${to.payment} / ${to.fulfillment}`) {
1679
+ if (derived) {
1662
1680
  return derived;
1663
1681
  }
1664
- return `${pay} \xB7 ${ful}`;
1682
+ return `${labelForPayment(to.payment, config)} \xB7 ${labelForFulfillment(to.fulfillment, config)}`;
1665
1683
  }
1666
1684
  function conditionMetLines(config, from, to, context) {
1667
1685
  const lines = [];
@@ -1690,20 +1708,12 @@ function conditionMetLines(config, from, to, context) {
1690
1708
  }
1691
1709
  function getAvailableTransitions(current, config, context) {
1692
1710
  const candidates = [];
1693
- for (const s of config.paymentStates) {
1694
- if (s.id === current.payment) continue;
1695
- candidates.push({ payment: s.id, fulfillment: current.fulfillment });
1696
- }
1697
1711
  for (const s of config.fulfillmentStates) {
1698
1712
  if (s.id === current.fulfillment) continue;
1699
1713
  candidates.push({ payment: current.payment, fulfillment: s.id });
1700
1714
  }
1701
1715
  const out = [];
1702
- const seen = /* @__PURE__ */ new Set();
1703
1716
  for (const to of candidates) {
1704
- const key = `${to.payment}:${to.fulfillment}`;
1705
- if (seen.has(key)) continue;
1706
- seen.add(key);
1707
1717
  const result = canTransition({
1708
1718
  from: current,
1709
1719
  to,