@plasius/economy 0.3.1 → 0.4.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/index.cjs CHANGED
@@ -28,11 +28,14 @@ __export(index_exports, {
28
28
  EARLY_BACKER_SETTLEMENT_POLICY_V2: () => EARLY_BACKER_SETTLEMENT_POLICY_V2,
29
29
  ECONOMY_CONTRACT_VERSION: () => ECONOMY_CONTRACT_VERSION,
30
30
  EconomyError: () => EconomyError,
31
+ MODULE_ALLOWANCE_PURPOSE: () => MODULE_ALLOWANCE_PURPOSE,
31
32
  PRE_UTILITY_BACKER_COHORT: () => PRE_UTILITY_BACKER_COHORT,
32
33
  TOKEN_SUBUNITS_MAX: () => TOKEN_SUBUNITS_MAX,
33
34
  TOKEN_SUBUNITS_MIN: () => TOKEN_SUBUNITS_MIN,
34
35
  TOKEN_SUBUNITS_PER_TOKEN: () => TOKEN_SUBUNITS_PER_TOKEN,
35
36
  advanceJournalChainHead: () => advanceJournalChainHead,
37
+ applyPaidLotLifecycleEvent: () => applyPaidLotLifecycleEvent,
38
+ applyPurchaseIntentTransition: () => applyPurchaseIntentTransition,
36
39
  applySourceLotMovement: () => applySourceLotMovement,
37
40
  applyTransactionToProjection: () => applyTransactionToProjection,
38
41
  applyWalletBalanceDelta: () => applyWalletBalanceDelta,
@@ -50,13 +53,26 @@ __export(index_exports, {
50
53
  assertFxSnapshot: () => assertFxSnapshot,
51
54
  assertGameplayAllocation: () => assertGameplayAllocation,
52
55
  assertJournalChainHead: () => assertJournalChainHead,
56
+ assertModuleAllowance: () => assertModuleAllowance,
57
+ assertModulePurchaseReceipt: () => assertModulePurchaseReceipt,
58
+ assertModulePurchaseReconciliationObservation: () => assertModulePurchaseReconciliationObservation,
59
+ assertModuleSpendHold: () => assertModuleSpendHold,
60
+ assertModuleSpendQuote: () => assertModuleSpendQuote,
53
61
  assertNonNegativeAccounts: () => assertNonNegativeAccounts,
54
62
  assertOpenPurchaseIntent: () => assertOpenPurchaseIntent,
63
+ assertPaidLotLifecycle: () => assertPaidLotLifecycle,
64
+ assertPaidLotLifecycleEvent: () => assertPaidLotLifecycleEvent,
65
+ assertPaidLotPurchaseProvenance: () => assertPaidLotPurchaseProvenance,
66
+ assertPurchaseCapReservation: () => assertPurchaseCapReservation,
67
+ assertPurchaseCapReservationTransition: () => assertPurchaseCapReservationTransition,
55
68
  assertPurchaseIntentBinding: () => assertPurchaseIntentBinding,
69
+ assertPurchaseIntentLifecycle: () => assertPurchaseIntentLifecycle,
70
+ assertPurchaseIntentTransition: () => assertPurchaseIntentTransition,
56
71
  assertPurchaseLimitPolicy: () => assertPurchaseLimitPolicy,
57
72
  assertReversalAvailable: () => assertReversalAvailable,
58
73
  assertRewardConversion: () => assertRewardConversion,
59
74
  assertRewardRate: () => assertRewardRate,
75
+ assertRollingPurchaseCapState: () => assertRollingPurchaseCapState,
60
76
  assertScopedPersistedIdempotencyResult: () => assertScopedPersistedIdempotencyResult,
61
77
  assertSourceLot: () => assertSourceLot,
62
78
  assertSourceLotMovement: () => assertSourceLotMovement,
@@ -82,13 +98,23 @@ __export(index_exports, {
82
98
  assertWalletPortfolioSummary: () => assertWalletPortfolioSummary,
83
99
  availableSourceLotAmount: () => availableSourceLotAmount,
84
100
  boostGameplayAllocation: () => boostGameplayAllocation,
101
+ calculateRollingPurchaseCapUsage: () => calculateRollingPurchaseCapUsage,
85
102
  canUseSourceLot: () => canUseSourceLot,
86
103
  canonicalTransactionPayload: () => canonicalTransactionPayload,
104
+ closeModuleAllowance: () => closeModuleAllowance,
87
105
  convertRewardPayout: () => convertRewardPayout,
106
+ createEarlyBackerRetentionFromPaidLot: () => createEarlyBackerRetentionFromPaidLot,
88
107
  createGameplayAllocation: () => createGameplayAllocation,
89
108
  createInitialSourceLotSnapshot: () => createInitialSourceLotSnapshot,
109
+ createModuleAllowance: () => createModuleAllowance,
110
+ createModulePurchaseReceipt: () => createModulePurchaseReceipt,
111
+ createModuleSpendHold: () => createModuleSpendHold,
112
+ createModuleSpendQuote: () => createModuleSpendQuote,
113
+ createPaidLotLifecycle: () => createPaidLotLifecycle,
90
114
  createProjectionSnapshot: () => createProjectionSnapshot,
115
+ createPurchaseIntentLifecycle: () => createPurchaseIntentLifecycle,
91
116
  createReversalTransaction: () => createReversalTransaction,
117
+ createRollingPurchaseCapState: () => createRollingPurchaseCapState,
92
118
  createWalletBalanceSummary: () => createWalletBalanceSummary,
93
119
  createWalletPortfolioLifetime: () => createWalletPortfolioLifetime,
94
120
  createWalletPortfolioSummary: () => createWalletPortfolioSummary,
@@ -97,18 +123,29 @@ __export(index_exports, {
97
123
  economyAssert: () => economyAssert,
98
124
  evaluateEarlyBacker: () => evaluateEarlyBacker,
99
125
  evaluateEarlyBackerBySettlementV2: () => evaluateEarlyBackerBySettlementV2,
126
+ fundModuleAllowance: () => fundModuleAllowance,
100
127
  isWholeTokenAmount: () => isWholeTokenAmount,
101
128
  mapBitLabsConversionState: () => mapBitLabsConversionState,
129
+ parseGbpMinorUnits: () => parseGbpMinorUnits,
102
130
  parseIsoTimestamp: () => parseIsoTimestamp,
103
131
  parseTokenSubunits: () => parseTokenSubunits,
104
132
  rebuildBalanceProjection: () => rebuildBalanceProjection,
105
133
  reclaimGameplayAllocation: () => reclaimGameplayAllocation,
134
+ reclaimModuleAllowance: () => reclaimModuleAllowance,
135
+ reconcileModulePurchase: () => reconcileModulePurchase,
136
+ reducePaidLotLifecycleEvents: () => reducePaidLotLifecycleEvents,
137
+ reducePurchaseIntentTransitions: () => reducePurchaseIntentTransitions,
138
+ releaseModuleSpendHold: () => releaseModuleSpendHold,
139
+ reserveRollingPurchaseCaps: () => reserveRollingPurchaseCaps,
106
140
  selectSourceLots: () => selectSourceLots,
141
+ serializeGbpMinorUnits: () => serializeGbpMinorUnits,
107
142
  serializeTokenSubunits: () => serializeTokenSubunits,
143
+ settleModuleSpendHold: () => settleModuleSpendHold,
108
144
  sortStringRecord: () => sortStringRecord,
109
145
  subunitsToWholeTokens: () => subunitsToWholeTokens,
110
146
  sumTokenSubunits: () => sumTokenSubunits,
111
147
  toEconomyErrorEnvelope: () => toEconomyErrorEnvelope,
148
+ transitionRollingPurchaseCapReservation: () => transitionRollingPurchaseCapReservation,
112
149
  wholeTokensToSubunits: () => wholeTokensToSubunits
113
150
  });
114
151
  module.exports = __toCommonJS(index_exports);
@@ -1661,1117 +1698,3100 @@ function selectSourceLots(lots, requestedAmount, context) {
1661
1698
  );
1662
1699
  }
1663
1700
 
1664
- // src/operations.ts
1665
- function assertDistinctAdjustmentApproval(request) {
1701
+ // src/module-allowances.ts
1702
+ var MODULE_ALLOWANCE_PURPOSE = "junior-coder-module-entitlement";
1703
+ function assertWholePositive2(amount, label) {
1704
+ const parsed = parseTokenSubunits(amount);
1705
+ economyAssert(parsed > 0n, "INVALID_AMOUNT", `${label} must be positive`);
1666
1706
  economyAssert(
1667
- request.schemaVersion === ECONOMY_CONTRACT_VERSION,
1707
+ isWholeTokenAmount(parsed),
1708
+ "AMOUNT_NOT_WHOLE_TOKEN",
1709
+ `${label} must contain whole Tokens`
1710
+ );
1711
+ return parsed;
1712
+ }
1713
+ function assertPositiveSlices(slices, expectedAmount, label) {
1714
+ economyAssert(slices.length > 0, "INVALID_CONTRACT", `${label} are required`);
1715
+ const lotIds = /* @__PURE__ */ new Set();
1716
+ for (const slice of slices) {
1717
+ assertEconomyIdentifier(slice.lotId, "lotId");
1718
+ economyAssert(
1719
+ !lotIds.has(slice.lotId),
1720
+ "DUPLICATE_IDENTIFIER",
1721
+ `${label} must not repeat a lot`
1722
+ );
1723
+ lotIds.add(slice.lotId);
1724
+ economyAssert(
1725
+ parseTokenSubunits(slice.amount) > 0n,
1726
+ "INVALID_AMOUNT",
1727
+ `${label} amounts must be positive`
1728
+ );
1729
+ }
1730
+ economyAssert(
1731
+ sumTokenSubunits(slices.map((slice) => slice.amount)) === expectedAmount,
1668
1732
  "INVALID_CONTRACT",
1669
- "Unsupported adjustment contract version"
1733
+ `${label} must sum to the operation amount`
1670
1734
  );
1671
- assertEconomyIdentifier(request.adjustmentId, "adjustmentId");
1672
- assertEconomyIdentifier(request.walletId, "walletId");
1673
- assertEconomyIdentifier(request.initiatedByAccountId, "initiatedByAccountId");
1735
+ }
1736
+ function sumSlicesByLot(slices) {
1737
+ const totals = /* @__PURE__ */ new Map();
1738
+ for (const slice of slices) {
1739
+ assertEconomyIdentifier(slice.lotId, "lotId");
1740
+ const amount = parseTokenSubunits(slice.amount);
1741
+ economyAssert(amount > 0n, "INVALID_AMOUNT", "Lot slices must be positive");
1742
+ totals.set(slice.lotId, (totals.get(slice.lotId) ?? 0n) + amount);
1743
+ }
1744
+ return totals;
1745
+ }
1746
+ function assertSha256Reference(value, label) {
1674
1747
  economyAssert(
1675
- parseTokenSubunits(request.amount) !== 0n,
1676
- "INVALID_AMOUNT",
1677
- "Adjustment amount cannot be zero"
1748
+ typeof value === "string" && /^sha256:[a-f0-9]{64}$/u.test(value),
1749
+ "INVALID_CONTRACT",
1750
+ `${label} must be a canonical SHA-256 reference`
1678
1751
  );
1752
+ }
1753
+ function assertUpdatedAt(current, occurredAt, label) {
1679
1754
  economyAssert(
1680
- request.reason.trim().length > 0 && request.reason.length <= 512,
1755
+ parseIsoTimestamp(occurredAt) >= parseIsoTimestamp(current),
1756
+ "INVALID_TIME_WINDOW",
1757
+ `${label} cannot precede the current state`
1758
+ );
1759
+ }
1760
+ function assertModuleAllowance(allowance) {
1761
+ economyAssert(
1762
+ allowance.schemaVersion === ECONOMY_CONTRACT_VERSION && allowance.purpose === MODULE_ALLOWANCE_PURPOSE,
1681
1763
  "INVALID_CONTRACT",
1682
- "Adjustment reason is required and must be bounded"
1764
+ "Unsupported Module Allowance contract"
1683
1765
  );
1684
- assertEconomyIdentifier(request.ticketReference, "ticketReference");
1685
- const initiatedAt = parseIsoTimestamp(request.initiatedAt);
1766
+ assertEconomyIdentifier(allowance.allowanceId, "allowanceId");
1767
+ assertEconomyIdentifier(allowance.householdId, "householdId");
1768
+ assertEconomyIdentifier(allowance.hostWalletId, "hostWalletId");
1769
+ assertEconomyIdentifier(allowance.allowanceWalletId, "allowanceWalletId");
1770
+ assertEconomyIdentifier(allowance.childAccountId, "childAccountId");
1686
1771
  economyAssert(
1687
- request.approvedByAccountId !== void 0 && request.approvedByAccountId !== request.initiatedByAccountId,
1772
+ allowance.hostWalletId !== allowance.allowanceWalletId,
1688
1773
  "INVALID_CONTRACT",
1689
- "Adjustment approval requires a distinct authorized operator"
1774
+ "Host and Module Allowance wallets must be distinct"
1690
1775
  );
1691
- assertEconomyIdentifier(request.approvedByAccountId, "approvedByAccountId");
1692
1776
  economyAssert(
1693
- request.approvedAt !== void 0 && parseIsoTimestamp(request.approvedAt) >= initiatedAt,
1777
+ allowance.status === "active" || allowance.status === "closed",
1778
+ "INVALID_CONTRACT",
1779
+ "Module Allowance status is unsupported"
1780
+ );
1781
+ const createdAt = parseIsoTimestamp(allowance.createdAt);
1782
+ const updatedAt = parseIsoTimestamp(allowance.updatedAt);
1783
+ economyAssert(
1784
+ updatedAt >= createdAt,
1694
1785
  "INVALID_TIME_WINDOW",
1695
- "Adjustment approval time must follow initiation"
1786
+ "Module Allowance update cannot precede creation"
1696
1787
  );
1697
1788
  economyAssert(
1698
- request.status === "approved" || request.status === "executed",
1789
+ Number.isSafeInteger(allowance.version) && allowance.version >= 1,
1699
1790
  "INVALID_CONTRACT",
1700
- "Only approved adjustments satisfy dual-control approval"
1791
+ "Module Allowance version must be a positive safe integer"
1701
1792
  );
1702
- if (request.resultingTransactionId !== void 0) {
1703
- assertEconomyIdentifier(
1704
- request.resultingTransactionId,
1705
- "resultingTransactionId"
1706
- );
1707
- }
1708
- }
1709
-
1710
- // src/ports/persistence.ts
1711
- function assertScopedPersistedIdempotencyResult(result) {
1793
+ const allocated = parseTokenSubunits(allowance.allocatedAmount);
1794
+ const available = parseTokenSubunits(allowance.availableAmount);
1795
+ const held = parseTokenSubunits(allowance.heldAmount);
1796
+ const spent = parseTokenSubunits(allowance.spentAmount);
1797
+ const reclaimed = parseTokenSubunits(allowance.reclaimedAmount);
1712
1798
  economyAssert(
1713
- result.schemaVersion === ECONOMY_CONTRACT_VERSION,
1714
- "INVALID_CONTRACT",
1715
- "Unsupported scoped idempotency-result contract version"
1799
+ allocated > 0n && [allocated, available, held, spent, reclaimed].every(
1800
+ (amount) => amount >= 0n && isWholeTokenAmount(amount)
1801
+ ),
1802
+ "AMOUNT_NOT_WHOLE_TOKEN",
1803
+ "Module Allowance amounts must be non-negative whole Tokens"
1716
1804
  );
1717
- assertEconomyIdentifier(result.commandId, "commandId");
1718
- assertEconomyIdentifier(result.transactionId, "transactionId");
1719
1805
  economyAssert(
1720
- /^sha256:[a-f0-9]{64}$/u.test(result.responseHash),
1806
+ allocated === available + held + spent + reclaimed,
1721
1807
  "INVALID_CONTRACT",
1722
- "Idempotency response hash must be a canonical SHA-256 reference"
1808
+ "Module Allowance buckets must equal its lifetime allocation"
1723
1809
  );
1724
- parseIsoTimestamp(result.recordedAt);
1725
- }
1726
- var ECONOMY_COMMAND_TYPES = /* @__PURE__ */ new Set([
1727
- "credit-purchase",
1728
- "credit-subscription",
1729
- "credit-reward",
1730
- "credit-event",
1731
- "credit-competition",
1732
- "allocate",
1733
- "boost",
1734
- "reclaim",
1735
- "spend",
1736
- "hold",
1737
- "release-hold",
1738
- "refund",
1739
- "chargeback",
1740
- "reverse",
1741
- "adjust"
1742
- ]);
1743
- function assertEconomyIdempotencyScope(scope) {
1744
1810
  economyAssert(
1745
- scope.schemaVersion === ECONOMY_CONTRACT_VERSION && ECONOMY_COMMAND_TYPES.has(scope.commandType),
1811
+ allowance.status !== "closed" || available === 0n && held === 0n,
1746
1812
  "INVALID_CONTRACT",
1747
- "Unsupported economy idempotency scope"
1813
+ "A closed Module Allowance cannot retain available or held value"
1748
1814
  );
1749
- assertEconomyIdentifier(scope.idempotencyKey, "idempotencyKey");
1750
- assertEconomyIdentifier(scope.actorAccountId, "actorAccountId");
1751
- assertEconomyIdentifier(scope.subjectAccountId, "subjectAccountId");
1752
- }
1753
- function assertEconomyCommandEnvelope(envelope) {
1815
+ const fundedByLot = sumSlicesByLot(allowance.fundingSlices);
1816
+ const reclaimedByLot = sumSlicesByLot(allowance.reclaimedSlices);
1754
1817
  economyAssert(
1755
- envelope.schemaVersion === ECONOMY_CONTRACT_VERSION,
1818
+ sumTokenSubunits(allowance.fundingSlices.map((slice) => slice.amount)) === allocated,
1756
1819
  "INVALID_CONTRACT",
1757
- "Unsupported economy-command-envelope contract version"
1820
+ "Module Allowance funding provenance must equal allocated value"
1758
1821
  );
1759
- assertEconomyIdentifier(envelope.commandId, "commandId");
1760
1822
  economyAssert(
1761
- ECONOMY_COMMAND_TYPES.has(envelope.commandType),
1823
+ sumTokenSubunits(allowance.reclaimedSlices.map((slice) => slice.amount)) === reclaimed,
1762
1824
  "INVALID_CONTRACT",
1763
- "Economy command has an unsupported type"
1825
+ "Module Allowance reclaim provenance must equal reclaimed value"
1764
1826
  );
1765
- assertEconomyIdentifier(envelope.idempotencyKey, "idempotencyKey");
1766
- assertEconomyIdentifier(envelope.actorAccountId, "actorAccountId");
1767
- assertEconomyIdentifier(envelope.subjectAccountId, "subjectAccountId");
1768
- if (envelope.relationshipId !== void 0) {
1769
- assertEconomyIdentifier(envelope.relationshipId, "relationshipId");
1770
- }
1771
- if (envelope.authorizationVersion !== void 0) {
1827
+ for (const [lotId, amount] of reclaimedByLot) {
1772
1828
  economyAssert(
1773
- Number.isSafeInteger(envelope.authorizationVersion) && envelope.authorizationVersion >= 1,
1774
- "INVALID_CONTRACT",
1775
- "Command authorization version must be a positive safe integer"
1829
+ amount <= (fundedByLot.get(lotId) ?? 0n),
1830
+ "SOURCE_LOT_RESTRICTED",
1831
+ "Reclaimed provenance cannot exceed Module Allowance funding"
1776
1832
  );
1777
1833
  }
1834
+ }
1835
+ function createModuleAllowance(input) {
1836
+ const amount = assertWholePositive2(input.amount, "Module Allowance amount");
1837
+ assertPositiveSlices(input.fundingSlices, amount, "Funding slices");
1838
+ const allowance = {
1839
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
1840
+ purpose: MODULE_ALLOWANCE_PURPOSE,
1841
+ allowanceId: input.allowanceId,
1842
+ householdId: input.householdId,
1843
+ hostWalletId: input.hostWalletId,
1844
+ allowanceWalletId: input.allowanceWalletId,
1845
+ childAccountId: input.childAccountId,
1846
+ status: "active",
1847
+ allocatedAmount: input.amount,
1848
+ availableAmount: input.amount,
1849
+ heldAmount: serializeTokenSubunits(0n),
1850
+ spentAmount: serializeTokenSubunits(0n),
1851
+ reclaimedAmount: serializeTokenSubunits(0n),
1852
+ fundingSlices: input.fundingSlices,
1853
+ reclaimedSlices: [],
1854
+ version: 1,
1855
+ createdAt: input.occurredAt,
1856
+ updatedAt: input.occurredAt
1857
+ };
1858
+ assertModuleAllowance(allowance);
1859
+ return allowance;
1860
+ }
1861
+ function fundModuleAllowance(allowance, input) {
1862
+ assertModuleAllowance(allowance);
1778
1863
  economyAssert(
1779
- envelope.relationshipId === void 0 === (envelope.authorizationVersion === void 0),
1864
+ allowance.status === "active" && allowance.version === input.expectedVersion,
1780
1865
  "INVALID_CONTRACT",
1781
- "Relationship identity and authorization version must be supplied together"
1866
+ "Module Allowance is closed or its optimistic version is stale"
1782
1867
  );
1868
+ assertUpdatedAt(allowance.updatedAt, input.occurredAt, "Funding time");
1869
+ const amount = assertWholePositive2(input.amount, "Module Allowance funding");
1870
+ assertPositiveSlices(input.fundingSlices, amount, "Funding slices");
1871
+ const next = {
1872
+ ...allowance,
1873
+ allocatedAmount: serializeTokenSubunits(
1874
+ parseTokenSubunits(allowance.allocatedAmount) + amount
1875
+ ),
1876
+ availableAmount: serializeTokenSubunits(
1877
+ parseTokenSubunits(allowance.availableAmount) + amount
1878
+ ),
1879
+ fundingSlices: [...allowance.fundingSlices, ...input.fundingSlices],
1880
+ version: allowance.version + 1,
1881
+ updatedAt: input.occurredAt
1882
+ };
1883
+ assertModuleAllowance(next);
1884
+ return next;
1885
+ }
1886
+ function reclaimModuleAllowance(allowance, input) {
1887
+ assertModuleAllowance(allowance);
1783
1888
  economyAssert(
1784
- /^sha256:[a-f0-9]{64}$/u.test(envelope.payloadHash),
1889
+ allowance.status === "active" && allowance.version === input.expectedVersion,
1785
1890
  "INVALID_CONTRACT",
1786
- "Command payload hash must be a canonical SHA-256 reference"
1891
+ "Module Allowance is closed or its optimistic version is stale"
1787
1892
  );
1788
- parseIsoTimestamp(envelope.acceptedAt);
1789
- assertEconomyIdentifier(envelope.acceptedRegion, "acceptedRegion");
1790
- assertEconomyIdentifier(
1791
- envelope.writerFencingToken,
1792
- "writerFencingToken"
1893
+ assertUpdatedAt(allowance.updatedAt, input.occurredAt, "Reclaim time");
1894
+ const amount = assertWholePositive2(input.amount, "Module Allowance reclaim");
1895
+ economyAssert(
1896
+ amount <= parseTokenSubunits(allowance.availableAmount),
1897
+ "INSUFFICIENT_BALANCE",
1898
+ "Cannot reclaim held, spent, or unavailable Module Allowance value"
1899
+ );
1900
+ assertPositiveSlices(input.sourceSlices, amount, "Reclaim slices");
1901
+ const fundedLotIds = new Set(
1902
+ allowance.fundingSlices.map((slice) => slice.lotId)
1903
+ );
1904
+ economyAssert(
1905
+ input.sourceSlices.every((slice) => fundedLotIds.has(slice.lotId)),
1906
+ "SOURCE_LOT_RESTRICTED",
1907
+ "Reclaim slices must originate from the Module Allowance"
1793
1908
  );
1909
+ const next = {
1910
+ ...allowance,
1911
+ availableAmount: serializeTokenSubunits(
1912
+ parseTokenSubunits(allowance.availableAmount) - amount
1913
+ ),
1914
+ reclaimedAmount: serializeTokenSubunits(
1915
+ parseTokenSubunits(allowance.reclaimedAmount) + amount
1916
+ ),
1917
+ reclaimedSlices: [...allowance.reclaimedSlices, ...input.sourceSlices],
1918
+ version: allowance.version + 1,
1919
+ updatedAt: input.occurredAt
1920
+ };
1921
+ assertModuleAllowance(next);
1922
+ return next;
1794
1923
  }
1795
- function assertEconomyCommandWorkflowEvent(event) {
1924
+ function closeModuleAllowance(allowance, input) {
1925
+ assertModuleAllowance(allowance);
1796
1926
  economyAssert(
1797
- event.schemaVersion === ECONOMY_CONTRACT_VERSION,
1927
+ allowance.status === "active" && allowance.version === input.expectedVersion,
1798
1928
  "INVALID_CONTRACT",
1799
- "Unsupported command-workflow-event contract version"
1929
+ "Module Allowance is closed or its optimistic version is stale"
1800
1930
  );
1801
- assertEconomyIdentifier(event.workflowEventId, "workflowEventId");
1802
- assertEconomyIdentifier(event.commandId, "commandId");
1931
+ assertUpdatedAt(allowance.updatedAt, input.occurredAt, "Closure time");
1803
1932
  economyAssert(
1804
- ["accepted", "processing", "failed", "completed"].includes(event.state),
1933
+ parseTokenSubunits(allowance.availableAmount) === 0n && parseTokenSubunits(allowance.heldAmount) === 0n,
1805
1934
  "INVALID_CONTRACT",
1806
- "Command workflow has an unsupported state"
1935
+ "Available value must be reclaimed and holds resolved before closure"
1807
1936
  );
1937
+ const next = {
1938
+ ...allowance,
1939
+ status: "closed",
1940
+ version: allowance.version + 1,
1941
+ updatedAt: input.occurredAt
1942
+ };
1943
+ assertModuleAllowance(next);
1944
+ return next;
1945
+ }
1946
+ function assertModuleSpendQuote(quote) {
1808
1947
  economyAssert(
1809
- event.state === "failed" === (event.failureCode !== void 0),
1948
+ quote.schemaVersion === ECONOMY_CONTRACT_VERSION,
1810
1949
  "INVALID_CONTRACT",
1811
- "Only failed command workflows require a failure code"
1950
+ "Unsupported module-spend quote contract version"
1951
+ );
1952
+ assertEconomyIdentifier(quote.quoteId, "quoteId");
1953
+ assertEconomyIdentifier(quote.allowanceId, "allowanceId");
1954
+ assertEconomyIdentifier(quote.householdId, "householdId");
1955
+ assertEconomyIdentifier(quote.guardianAccountId, "guardianAccountId");
1956
+ assertEconomyIdentifier(quote.childAccountId, "childAccountId");
1957
+ assertEconomyIdentifier(quote.moduleVersionId, "moduleVersionId");
1958
+ assertEconomyIdentifier(quote.catalogVersion, "catalogVersion");
1959
+ assertEconomyIdentifier(
1960
+ quote.requirementsManifestVersion,
1961
+ "requirementsManifestVersion"
1962
+ );
1963
+ assertSha256Reference(
1964
+ quote.requirementsManifestHash,
1965
+ "Requirements manifest hash"
1966
+ );
1967
+ assertWholePositive2(quote.amount, "Module price");
1968
+ const acknowledgedAt = parseIsoTimestamp(quote.requirementsAcknowledgedAt);
1969
+ const issuedAt = parseIsoTimestamp(quote.issuedAt);
1970
+ const expiresAt = parseIsoTimestamp(quote.expiresAt);
1971
+ economyAssert(
1972
+ acknowledgedAt <= issuedAt && expiresAt > issuedAt,
1973
+ "INVALID_TIME_WINDOW",
1974
+ "Requirements must be acknowledged before a bounded quote is issued"
1812
1975
  );
1813
- if (event.failureCode !== void 0) {
1814
- assertEconomyIdentifier(event.failureCode, "failureCode");
1815
- }
1816
- parseIsoTimestamp(event.occurredAt);
1817
1976
  }
1818
-
1819
- // src/wallets.ts
1820
- function assertWalletOwnerReference(owner) {
1977
+ function createModuleSpendQuote(input) {
1978
+ const quote = {
1979
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
1980
+ ...input
1981
+ };
1982
+ assertModuleSpendQuote(quote);
1983
+ return quote;
1984
+ }
1985
+ function assertModuleSpendHold(hold) {
1821
1986
  economyAssert(
1822
- ["account", "household", "system"].includes(owner.ownerType),
1987
+ hold.schemaVersion === ECONOMY_CONTRACT_VERSION,
1823
1988
  "INVALID_CONTRACT",
1824
- "Wallet owner reference has an unsupported type"
1989
+ "Unsupported module-spend hold contract version"
1990
+ );
1991
+ assertEconomyIdentifier(hold.holdId, "holdId");
1992
+ assertEconomyIdentifier(hold.quoteId, "quoteId");
1993
+ assertEconomyIdentifier(hold.allowanceId, "allowanceId");
1994
+ assertEconomyIdentifier(hold.householdId, "householdId");
1995
+ assertEconomyIdentifier(hold.guardianAccountId, "guardianAccountId");
1996
+ assertEconomyIdentifier(hold.childAccountId, "childAccountId");
1997
+ assertEconomyIdentifier(hold.moduleVersionId, "moduleVersionId");
1998
+ assertEconomyIdentifier(
1999
+ hold.requirementsManifestVersion,
2000
+ "requirementsManifestVersion"
1825
2001
  );
1826
- if (owner.ownerType === "system") {
2002
+ assertSha256Reference(
2003
+ hold.requirementsManifestHash,
2004
+ "Requirements manifest hash"
2005
+ );
2006
+ assertEconomyIdentifier(hold.holdIdempotencyKey, "holdIdempotencyKey");
2007
+ const amount = assertWholePositive2(hold.amount, "Module hold amount");
2008
+ assertPositiveSlices(hold.sourceSlices, amount, "Hold source slices");
2009
+ economyAssert(
2010
+ ["held", "settled", "released"].includes(hold.status),
2011
+ "INVALID_CONTRACT",
2012
+ "Module hold status is unsupported"
2013
+ );
2014
+ economyAssert(
2015
+ Number.isSafeInteger(hold.version) && hold.version >= 1,
2016
+ "INVALID_CONTRACT",
2017
+ "Module hold version must be a positive safe integer"
2018
+ );
2019
+ const createdAt = parseIsoTimestamp(hold.createdAt);
2020
+ const expiresAt = parseIsoTimestamp(hold.expiresAt);
2021
+ const updatedAt = parseIsoTimestamp(hold.updatedAt);
2022
+ economyAssert(
2023
+ expiresAt > createdAt && updatedAt >= createdAt,
2024
+ "INVALID_TIME_WINDOW",
2025
+ "Module hold requires a bounded lifetime and ordered updates"
2026
+ );
2027
+ if (hold.status === "held") {
1827
2028
  economyAssert(
1828
- owner.ownerId === "system",
2029
+ hold.entitlementId === void 0 && hold.settlementTransactionId === void 0 && hold.releaseTransactionId === void 0 && hold.settlementIdempotencyKey === void 0 && hold.releaseIdempotencyKey === void 0,
1829
2030
  "INVALID_CONTRACT",
1830
- "System wallet lookups must use the system owner"
2031
+ "An unsettled hold cannot claim an entitlement or final transaction"
2032
+ );
2033
+ } else if (hold.status === "settled") {
2034
+ economyAssert(
2035
+ hold.entitlementId !== void 0 && hold.settlementTransactionId !== void 0 && hold.settlementIdempotencyKey !== void 0 && hold.releaseTransactionId === void 0 && hold.releaseIdempotencyKey === void 0,
2036
+ "INVALID_CONTRACT",
2037
+ "A settled hold requires entitlement and transaction evidence"
2038
+ );
2039
+ assertEconomyIdentifier(hold.entitlementId, "entitlementId");
2040
+ assertEconomyIdentifier(
2041
+ hold.settlementTransactionId,
2042
+ "settlementTransactionId"
2043
+ );
2044
+ assertEconomyIdentifier(
2045
+ hold.settlementIdempotencyKey,
2046
+ "settlementIdempotencyKey"
1831
2047
  );
1832
2048
  } else {
1833
- assertEconomyIdentifier(owner.ownerId, "ownerId");
1834
2049
  economyAssert(
1835
- owner.ownerId !== "system",
2050
+ hold.entitlementId === void 0 && hold.settlementTransactionId === void 0 && hold.settlementIdempotencyKey === void 0 && hold.releaseTransactionId !== void 0 && hold.releaseIdempotencyKey !== void 0,
1836
2051
  "INVALID_CONTRACT",
1837
- "Account and household wallets cannot use the system owner"
2052
+ "A released hold requires release evidence only"
1838
2053
  );
2054
+ assertEconomyIdentifier(hold.releaseTransactionId, "releaseTransactionId");
2055
+ assertEconomyIdentifier(hold.releaseIdempotencyKey, "releaseIdempotencyKey");
1839
2056
  }
1840
2057
  }
1841
- function assertWallet(wallet) {
2058
+ function assertAllowanceMatchesQuote(allowance, quote) {
1842
2059
  economyAssert(
1843
- wallet.schemaVersion === ECONOMY_CONTRACT_VERSION,
2060
+ allowance.status === "active" && allowance.allowanceId === quote.allowanceId && allowance.householdId === quote.householdId && allowance.childAccountId === quote.childAccountId,
1844
2061
  "INVALID_CONTRACT",
1845
- "Unsupported wallet contract version"
2062
+ "Module quote does not belong to this active child allowance"
1846
2063
  );
1847
- assertEconomyIdentifier(wallet.walletId, "walletId");
1848
- assertEconomyIdentifier(wallet.accountId, "accountId");
2064
+ }
2065
+ function assertAllowanceMatchesHold(allowance, hold) {
1849
2066
  economyAssert(
1850
- [
1851
- "household-treasury",
1852
- "personal",
1853
- "gameplay-allocation",
1854
- "hold",
1855
- "purchase-clearing",
1856
- "provider-clearing",
1857
- "system"
1858
- ].includes(wallet.kind),
2067
+ allowance.status === "active" && allowance.allowanceId === hold.allowanceId && allowance.householdId === hold.householdId && allowance.childAccountId === hold.childAccountId,
1859
2068
  "INVALID_CONTRACT",
1860
- "Wallet has an unsupported kind"
2069
+ "Module hold does not belong to this active child allowance"
1861
2070
  );
2071
+ }
2072
+ function createModuleSpendHold(allowance, quote, input) {
2073
+ assertModuleAllowance(allowance);
2074
+ assertModuleSpendQuote(quote);
2075
+ assertAllowanceMatchesQuote(allowance, quote);
1862
2076
  economyAssert(
1863
- ["account", "household", "system"].includes(wallet.ownerType),
2077
+ allowance.version === input.expectedAllowanceVersion,
1864
2078
  "INVALID_CONTRACT",
1865
- "Wallet has an unsupported owner type"
2079
+ "Module Allowance optimistic version is stale"
1866
2080
  );
2081
+ const occurredAt = parseIsoTimestamp(input.occurredAt);
1867
2082
  economyAssert(
1868
- ["active", "restricted", "closed"].includes(wallet.status),
1869
- "INVALID_CONTRACT",
1870
- "Wallet has an unsupported status"
2083
+ occurredAt >= parseIsoTimestamp(allowance.updatedAt) && occurredAt >= parseIsoTimestamp(quote.issuedAt) && occurredAt < parseIsoTimestamp(quote.expiresAt),
2084
+ "INVALID_TIME_WINDOW",
2085
+ "Module quote is not valid at hold time"
1871
2086
  );
1872
- if (wallet.ownerType === "system") {
1873
- economyAssert(
1874
- wallet.ownerId === "system",
1875
- "INVALID_CONTRACT",
1876
- "System wallets must be owned by the system"
1877
- );
1878
- } else {
1879
- assertEconomyIdentifier(wallet.ownerId, "ownerId");
1880
- }
1881
- if (wallet.householdId !== void 0) {
1882
- assertEconomyIdentifier(wallet.householdId, "householdId");
1883
- }
1884
- if (wallet.ownerType === "household") {
1885
- economyAssert(
1886
- wallet.householdId === wallet.ownerId,
1887
- "INVALID_CONTRACT",
1888
- "Household wallets must identify the same household owner"
1889
- );
1890
- }
1891
- if (wallet.kind === "household-treasury") {
1892
- economyAssert(
1893
- wallet.ownerType === "household" && wallet.householdId !== void 0,
1894
- "INVALID_CONTRACT",
1895
- "Household treasuries must be owned by their household"
1896
- );
1897
- }
1898
- if (wallet.kind === "personal" || wallet.kind === "gameplay-allocation") {
1899
- economyAssert(
1900
- wallet.ownerType === "account",
1901
- "INVALID_CONTRACT",
1902
- "Personal and gameplay-allocation wallets must be account-owned"
1903
- );
1904
- }
1905
- if (wallet.kind === "gameplay-allocation") {
1906
- economyAssert(
1907
- wallet.householdId !== void 0,
1908
- "INVALID_CONTRACT",
1909
- "Gameplay-allocation wallets must retain household provenance"
1910
- );
1911
- }
1912
- if (wallet.kind === "purchase-clearing" || wallet.kind === "provider-clearing" || wallet.kind === "system") {
1913
- economyAssert(
1914
- wallet.ownerType === "system" && wallet.householdId === void 0,
1915
- "INVALID_CONTRACT",
1916
- "Clearing and system wallets must be system-owned"
1917
- );
1918
- }
2087
+ const amount = parseTokenSubunits(quote.amount);
1919
2088
  economyAssert(
1920
- Number.isSafeInteger(wallet.version) && wallet.version >= 1,
2089
+ amount <= parseTokenSubunits(allowance.availableAmount),
2090
+ "INSUFFICIENT_BALANCE",
2091
+ "Module Allowance has insufficient available value"
2092
+ );
2093
+ assertEconomyIdentifier(input.holdId, "holdId");
2094
+ assertEconomyIdentifier(input.idempotencyKey, "idempotencyKey");
2095
+ assertPositiveSlices(input.sourceSlices, amount, "Hold source slices");
2096
+ const fundedLotIds = new Set(
2097
+ allowance.fundingSlices.map((slice) => slice.lotId)
2098
+ );
2099
+ economyAssert(
2100
+ input.sourceSlices.every((slice) => fundedLotIds.has(slice.lotId)),
2101
+ "SOURCE_LOT_RESTRICTED",
2102
+ "Hold slices must originate from the Module Allowance"
2103
+ );
2104
+ const nextAllowance = {
2105
+ ...allowance,
2106
+ availableAmount: serializeTokenSubunits(
2107
+ parseTokenSubunits(allowance.availableAmount) - amount
2108
+ ),
2109
+ heldAmount: serializeTokenSubunits(
2110
+ parseTokenSubunits(allowance.heldAmount) + amount
2111
+ ),
2112
+ version: allowance.version + 1,
2113
+ updatedAt: input.occurredAt
2114
+ };
2115
+ const hold = {
2116
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
2117
+ holdId: input.holdId,
2118
+ quoteId: quote.quoteId,
2119
+ allowanceId: quote.allowanceId,
2120
+ householdId: quote.householdId,
2121
+ guardianAccountId: quote.guardianAccountId,
2122
+ childAccountId: quote.childAccountId,
2123
+ moduleVersionId: quote.moduleVersionId,
2124
+ amount: quote.amount,
2125
+ requirementsManifestVersion: quote.requirementsManifestVersion,
2126
+ requirementsManifestHash: quote.requirementsManifestHash,
2127
+ sourceSlices: input.sourceSlices,
2128
+ status: "held",
2129
+ holdIdempotencyKey: input.idempotencyKey,
2130
+ version: 1,
2131
+ createdAt: input.occurredAt,
2132
+ expiresAt: quote.expiresAt,
2133
+ updatedAt: input.occurredAt
2134
+ };
2135
+ assertModuleAllowance(nextAllowance);
2136
+ assertModuleSpendHold(hold);
2137
+ return { allowance: nextAllowance, hold };
2138
+ }
2139
+ function settleModuleSpendHold(allowance, hold, input) {
2140
+ assertModuleAllowance(allowance);
2141
+ assertModuleSpendHold(hold);
2142
+ assertAllowanceMatchesHold(allowance, hold);
2143
+ economyAssert(
2144
+ hold.status === "held" && allowance.version === input.expectedAllowanceVersion && hold.version === input.expectedHoldVersion,
1921
2145
  "INVALID_CONTRACT",
1922
- "Wallet version must be a positive safe integer"
2146
+ "Module hold is final or an optimistic version is stale"
1923
2147
  );
1924
- const createdAt = parseIsoTimestamp(wallet.createdAt);
1925
- if (wallet.closedAt !== void 0) {
1926
- economyAssert(
1927
- parseIsoTimestamp(wallet.closedAt) >= createdAt && wallet.status === "closed",
1928
- "INVALID_TIME_WINDOW",
1929
- "Only closed wallets may have a valid closure time"
1930
- );
1931
- } else {
1932
- economyAssert(
1933
- wallet.status !== "closed",
1934
- "INVALID_CONTRACT",
1935
- "Closed wallets require a closure time"
1936
- );
1937
- }
2148
+ assertEconomyIdentifier(input.entitlementId, "entitlementId");
2149
+ assertEconomyIdentifier(
2150
+ input.settlementTransactionId,
2151
+ "settlementTransactionId"
2152
+ );
2153
+ assertEconomyIdentifier(input.idempotencyKey, "idempotencyKey");
2154
+ const occurredAt = parseIsoTimestamp(input.occurredAt);
2155
+ economyAssert(
2156
+ occurredAt >= parseIsoTimestamp(allowance.updatedAt) && occurredAt >= parseIsoTimestamp(hold.updatedAt) && occurredAt < parseIsoTimestamp(hold.expiresAt),
2157
+ "INVALID_TIME_WINDOW",
2158
+ "Module hold cannot settle outside its valid lifetime"
2159
+ );
2160
+ const amount = parseTokenSubunits(hold.amount);
2161
+ economyAssert(
2162
+ amount <= parseTokenSubunits(allowance.heldAmount),
2163
+ "INSUFFICIENT_BALANCE",
2164
+ "Module Allowance does not contain the quoted held value"
2165
+ );
2166
+ const nextAllowance = {
2167
+ ...allowance,
2168
+ heldAmount: serializeTokenSubunits(
2169
+ parseTokenSubunits(allowance.heldAmount) - amount
2170
+ ),
2171
+ spentAmount: serializeTokenSubunits(
2172
+ parseTokenSubunits(allowance.spentAmount) + amount
2173
+ ),
2174
+ version: allowance.version + 1,
2175
+ updatedAt: input.occurredAt
2176
+ };
2177
+ const nextHold = {
2178
+ ...hold,
2179
+ status: "settled",
2180
+ settlementIdempotencyKey: input.idempotencyKey,
2181
+ entitlementId: input.entitlementId,
2182
+ settlementTransactionId: input.settlementTransactionId,
2183
+ version: hold.version + 1,
2184
+ updatedAt: input.occurredAt
2185
+ };
2186
+ assertModuleAllowance(nextAllowance);
2187
+ assertModuleSpendHold(nextHold);
2188
+ return { allowance: nextAllowance, hold: nextHold };
1938
2189
  }
1939
- function assertWalletBalanceSummary(summary) {
2190
+ function releaseModuleSpendHold(allowance, hold, input) {
2191
+ assertModuleAllowance(allowance);
2192
+ assertModuleSpendHold(hold);
2193
+ assertAllowanceMatchesHold(allowance, hold);
1940
2194
  economyAssert(
1941
- summary.schemaVersion === ECONOMY_CONTRACT_VERSION,
2195
+ hold.status === "held" && allowance.version === input.expectedAllowanceVersion && hold.version === input.expectedHoldVersion,
1942
2196
  "INVALID_CONTRACT",
1943
- "Unsupported wallet summary contract version"
2197
+ "Module hold is final or an optimistic version is stale"
1944
2198
  );
1945
- assertEconomyIdentifier(summary.walletId, "walletId");
1946
- for (const amount of [
1947
- summary.available,
1948
- summary.reserved,
1949
- summary.held,
1950
- summary.rewardProgress
1951
- ]) {
1952
- economyAssert(
1953
- parseTokenSubunits(amount) >= 0n,
1954
- "INVALID_AMOUNT",
1955
- "Wallet summary categories cannot be negative"
1956
- );
1957
- }
2199
+ assertEconomyIdentifier(input.releaseTransactionId, "releaseTransactionId");
2200
+ assertEconomyIdentifier(input.idempotencyKey, "idempotencyKey");
2201
+ const occurredAt = parseIsoTimestamp(input.occurredAt);
1958
2202
  economyAssert(
1959
- Number.isSafeInteger(summary.version) && summary.version >= 1,
2203
+ occurredAt >= parseIsoTimestamp(allowance.updatedAt) && occurredAt >= parseIsoTimestamp(hold.updatedAt),
2204
+ "INVALID_TIME_WINDOW",
2205
+ "Module hold release cannot precede its current state"
2206
+ );
2207
+ const amount = parseTokenSubunits(hold.amount);
2208
+ economyAssert(
2209
+ amount <= parseTokenSubunits(allowance.heldAmount),
2210
+ "INSUFFICIENT_BALANCE",
2211
+ "Module Allowance does not contain the quoted held value"
2212
+ );
2213
+ const nextAllowance = {
2214
+ ...allowance,
2215
+ availableAmount: serializeTokenSubunits(
2216
+ parseTokenSubunits(allowance.availableAmount) + amount
2217
+ ),
2218
+ heldAmount: serializeTokenSubunits(
2219
+ parseTokenSubunits(allowance.heldAmount) - amount
2220
+ ),
2221
+ version: allowance.version + 1,
2222
+ updatedAt: input.occurredAt
2223
+ };
2224
+ const nextHold = {
2225
+ ...hold,
2226
+ status: "released",
2227
+ releaseIdempotencyKey: input.idempotencyKey,
2228
+ releaseTransactionId: input.releaseTransactionId,
2229
+ version: hold.version + 1,
2230
+ updatedAt: input.occurredAt
2231
+ };
2232
+ assertModuleAllowance(nextAllowance);
2233
+ assertModuleSpendHold(nextHold);
2234
+ return { allowance: nextAllowance, hold: nextHold };
2235
+ }
2236
+ function assertModulePurchaseReceipt(receipt) {
2237
+ economyAssert(
2238
+ receipt.schemaVersion === ECONOMY_CONTRACT_VERSION,
1960
2239
  "INVALID_CONTRACT",
1961
- "Wallet summary version must be a positive safe integer"
2240
+ "Unsupported module-purchase receipt contract version"
2241
+ );
2242
+ assertEconomyIdentifier(receipt.receiptId, "receiptId");
2243
+ assertEconomyIdentifier(receipt.quoteId, "quoteId");
2244
+ assertEconomyIdentifier(receipt.holdId, "holdId");
2245
+ assertEconomyIdentifier(receipt.allowanceId, "allowanceId");
2246
+ assertEconomyIdentifier(receipt.householdId, "householdId");
2247
+ assertEconomyIdentifier(receipt.guardianAccountId, "guardianAccountId");
2248
+ assertEconomyIdentifier(receipt.childAccountId, "childAccountId");
2249
+ assertEconomyIdentifier(receipt.moduleVersionId, "moduleVersionId");
2250
+ assertEconomyIdentifier(receipt.entitlementId, "entitlementId");
2251
+ assertEconomyIdentifier(
2252
+ receipt.requirementsManifestVersion,
2253
+ "requirementsManifestVersion"
1962
2254
  );
1963
- parseIsoTimestamp(summary.asOf);
2255
+ assertSha256Reference(
2256
+ receipt.requirementsManifestHash,
2257
+ "Requirements manifest hash"
2258
+ );
2259
+ assertEconomyIdentifier(
2260
+ receipt.settlementTransactionId,
2261
+ "settlementTransactionId"
2262
+ );
2263
+ assertWholePositive2(receipt.amount, "Receipt amount");
2264
+ parseIsoTimestamp(receipt.issuedAt);
1964
2265
  }
1965
- function assertWalletLifetimeTotals(totals) {
2266
+ function createModulePurchaseReceipt(quote, hold, input) {
2267
+ assertModuleSpendQuote(quote);
2268
+ assertModuleSpendHold(hold);
1966
2269
  economyAssert(
1967
- totals.schemaVersion === ECONOMY_CONTRACT_VERSION,
2270
+ hold.status === "settled" && hold.quoteId === quote.quoteId && hold.allowanceId === quote.allowanceId && hold.householdId === quote.householdId && hold.guardianAccountId === quote.guardianAccountId && hold.childAccountId === quote.childAccountId && hold.moduleVersionId === quote.moduleVersionId && hold.amount === quote.amount && hold.requirementsManifestVersion === quote.requirementsManifestVersion && hold.requirementsManifestHash === quote.requirementsManifestHash,
1968
2271
  "INVALID_CONTRACT",
1969
- "Unsupported wallet lifetime-total contract version"
2272
+ "Receipt quote and settled hold evidence do not match"
1970
2273
  );
1971
- for (const amount of [
1972
- totals.bought,
1973
- totals.earned,
1974
- totals.allocated,
1975
- totals.reclaimed,
1976
- totals.spent,
1977
- totals.reversed
1978
- ]) {
1979
- economyAssert(
1980
- parseTokenSubunits(amount) >= 0n,
1981
- "INVALID_AMOUNT",
1982
- "Wallet lifetime totals cannot be negative"
1983
- );
1984
- }
2274
+ economyAssert(
2275
+ parseIsoTimestamp(input.issuedAt) >= parseIsoTimestamp(hold.updatedAt),
2276
+ "INVALID_TIME_WINDOW",
2277
+ "Receipt cannot precede settlement"
2278
+ );
2279
+ const receipt = {
2280
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
2281
+ receiptId: input.receiptId,
2282
+ quoteId: quote.quoteId,
2283
+ holdId: hold.holdId,
2284
+ allowanceId: quote.allowanceId,
2285
+ householdId: quote.householdId,
2286
+ guardianAccountId: quote.guardianAccountId,
2287
+ childAccountId: quote.childAccountId,
2288
+ moduleVersionId: quote.moduleVersionId,
2289
+ entitlementId: hold.entitlementId,
2290
+ amount: quote.amount,
2291
+ requirementsManifestVersion: quote.requirementsManifestVersion,
2292
+ requirementsManifestHash: quote.requirementsManifestHash,
2293
+ settlementTransactionId: hold.settlementTransactionId,
2294
+ issuedAt: input.issuedAt
2295
+ };
2296
+ assertModulePurchaseReceipt(receipt);
2297
+ return receipt;
1985
2298
  }
1986
-
1987
- // src/projection.ts
1988
- function assertWalletBalanceProjection(projection) {
2299
+ function assertModulePurchaseReconciliationObservation(observation) {
1989
2300
  economyAssert(
1990
- projection.schemaVersion === ECONOMY_CONTRACT_VERSION,
2301
+ observation.schemaVersion === ECONOMY_CONTRACT_VERSION,
1991
2302
  "INVALID_CONTRACT",
1992
- "Unsupported wallet-balance-projection contract version"
2303
+ "Unsupported module-purchase reconciliation contract version"
1993
2304
  );
1994
- assertEconomyIdentifier(projection.walletId, "walletId");
1995
- for (const amount of [
1996
- projection.spendable,
1997
- projection.reserved,
1998
- projection.held
1999
- ]) {
2000
- economyAssert(
2001
- parseTokenSubunits(amount) >= 0n,
2002
- "NEGATIVE_PROJECTION",
2003
- "Wallet projection buckets cannot be negative"
2004
- );
2005
- }
2305
+ assertEconomyIdentifier(observation.quoteId, "quoteId");
2306
+ assertEconomyIdentifier(observation.holdId, "holdId");
2307
+ assertEconomyIdentifier(observation.childAccountId, "childAccountId");
2308
+ assertEconomyIdentifier(observation.moduleVersionId, "moduleVersionId");
2006
2309
  economyAssert(
2007
- Number.isSafeInteger(projection.version) && projection.version >= 1,
2310
+ ["missing", "held", "settled", "released"].includes(
2311
+ observation.financialState
2312
+ ),
2008
2313
  "INVALID_CONTRACT",
2009
- "Wallet projection version must be a positive safe integer"
2314
+ "Reconciliation financial state is unsupported"
2010
2315
  );
2011
- if (projection.lastTransactionId !== void 0) {
2012
- assertEconomyIdentifier(
2013
- projection.lastTransactionId,
2014
- "lastTransactionId"
2316
+ economyAssert(
2317
+ ["missing", "pending", "active", "cancelled"].includes(
2318
+ observation.entitlementState
2319
+ ),
2320
+ "INVALID_CONTRACT",
2321
+ "Reconciliation entitlement state is unsupported"
2322
+ );
2323
+ economyAssert(
2324
+ typeof observation.receiptPresent === "boolean",
2325
+ "INVALID_CONTRACT",
2326
+ "Receipt presence must be Boolean"
2327
+ );
2328
+ parseIsoTimestamp(observation.observedAt);
2329
+ }
2330
+ function reconciliationResult(action, reasonCode, blocking = false, consistent = false) {
2331
+ return {
2332
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
2333
+ action,
2334
+ blocking,
2335
+ consistent,
2336
+ reasonCode
2337
+ };
2338
+ }
2339
+ function reconcileModulePurchase(observation) {
2340
+ assertModulePurchaseReconciliationObservation(observation);
2341
+ const { financialState, entitlementState, receiptPresent } = observation;
2342
+ if (receiptPresent && !(financialState === "settled" && entitlementState === "active")) {
2343
+ return reconciliationResult(
2344
+ "manual-review",
2345
+ "receipt-conflicts-with-authoritative-state",
2346
+ true
2015
2347
  );
2016
2348
  }
2017
- parseIsoTimestamp(projection.asOf);
2349
+ if (financialState === "settled" && entitlementState === "active") {
2350
+ return receiptPresent ? reconciliationResult("none", "purchase-consistent", false, true) : reconciliationResult("issue-receipt", "settled-active-receipt-missing");
2351
+ }
2352
+ if (financialState === "held" && entitlementState === "pending") {
2353
+ return reconciliationResult("resume-settlement", "held-pending");
2354
+ }
2355
+ if (financialState === "held" && (entitlementState === "missing" || entitlementState === "cancelled")) {
2356
+ return reconciliationResult("release-hold", "held-without-usable-entitlement");
2357
+ }
2358
+ if (financialState === "settled" && entitlementState === "pending") {
2359
+ return reconciliationResult("activate-entitlement", "settled-pending");
2360
+ }
2361
+ if ((financialState === "missing" || financialState === "released") && entitlementState === "pending") {
2362
+ return reconciliationResult(
2363
+ "cancel-pending-entitlement",
2364
+ "pending-without-settlement-path"
2365
+ );
2366
+ }
2367
+ if ((financialState === "missing" || financialState === "released") && (entitlementState === "missing" || entitlementState === "cancelled")) {
2368
+ return reconciliationResult("none", "no-active-purchase", false, true);
2369
+ }
2370
+ return reconciliationResult(
2371
+ "manual-review",
2372
+ financialState === "settled" ? "settled-debit-without-entitlement" : "active-entitlement-without-settled-debit",
2373
+ true
2374
+ );
2018
2375
  }
2019
- function assertWalletBalanceDelta(delta) {
2376
+
2377
+ // src/operations.ts
2378
+ function assertDistinctAdjustmentApproval(request) {
2020
2379
  economyAssert(
2021
- delta.schemaVersion === ECONOMY_CONTRACT_VERSION,
2380
+ request.schemaVersion === ECONOMY_CONTRACT_VERSION,
2022
2381
  "INVALID_CONTRACT",
2023
- "Unsupported wallet-balance-delta contract version"
2382
+ "Unsupported adjustment contract version"
2024
2383
  );
2025
- assertEconomyIdentifier(delta.walletId, "walletId");
2026
- const values = [
2027
- parseTokenSubunits(delta.spendableDelta),
2028
- parseTokenSubunits(delta.reservedDelta),
2029
- parseTokenSubunits(delta.heldDelta)
2030
- ];
2384
+ assertEconomyIdentifier(request.adjustmentId, "adjustmentId");
2385
+ assertEconomyIdentifier(request.walletId, "walletId");
2386
+ assertEconomyIdentifier(request.initiatedByAccountId, "initiatedByAccountId");
2031
2387
  economyAssert(
2032
- values.some((value) => value !== 0n),
2388
+ parseTokenSubunits(request.amount) !== 0n,
2033
2389
  "INVALID_AMOUNT",
2034
- "Wallet balance delta must change at least one bucket"
2390
+ "Adjustment amount cannot be zero"
2035
2391
  );
2036
- }
2037
- function assertWalletBalanceDeltaBatch(transactionId, deltas) {
2038
- assertEconomyIdentifier(transactionId, "transactionId");
2039
2392
  economyAssert(
2040
- deltas.length > 0,
2393
+ request.reason.trim().length > 0 && request.reason.length <= 512,
2041
2394
  "INVALID_CONTRACT",
2042
- "An economic transaction must update at least one wallet projection"
2395
+ "Adjustment reason is required and must be bounded"
2043
2396
  );
2044
- const walletIds = /* @__PURE__ */ new Set();
2045
- for (const delta of deltas) {
2046
- assertWalletBalanceDelta(delta);
2047
- economyAssert(
2048
- !walletIds.has(delta.walletId),
2049
- "DUPLICATE_IDENTIFIER",
2050
- "A balance-delta batch must update each wallet once"
2051
- );
2052
- walletIds.add(delta.walletId);
2053
- }
2054
- }
2055
- function applyWalletBalanceDelta(projection, delta, transactionId, asOf) {
2056
- assertWalletBalanceProjection(projection);
2057
- assertWalletBalanceDelta(delta);
2058
- assertEconomyIdentifier(transactionId, "transactionId");
2397
+ assertEconomyIdentifier(request.ticketReference, "ticketReference");
2398
+ const initiatedAt = parseIsoTimestamp(request.initiatedAt);
2059
2399
  economyAssert(
2060
- projection.walletId === delta.walletId,
2400
+ request.approvedByAccountId !== void 0 && request.approvedByAccountId !== request.initiatedByAccountId,
2061
2401
  "INVALID_CONTRACT",
2062
- "Wallet balance delta must target its projection wallet"
2402
+ "Adjustment approval requires a distinct authorized operator"
2063
2403
  );
2404
+ assertEconomyIdentifier(request.approvedByAccountId, "approvedByAccountId");
2064
2405
  economyAssert(
2065
- parseIsoTimestamp(asOf) >= parseIsoTimestamp(projection.asOf),
2406
+ request.approvedAt !== void 0 && parseIsoTimestamp(request.approvedAt) >= initiatedAt,
2066
2407
  "INVALID_TIME_WINDOW",
2067
- "Wallet balance update cannot precede its projection"
2408
+ "Adjustment approval time must follow initiation"
2068
2409
  );
2069
- const next = {
2070
- ...projection,
2071
- spendable: serializeTokenSubunits(
2072
- parseTokenSubunits(projection.spendable) + parseTokenSubunits(delta.spendableDelta)
2073
- ),
2074
- reserved: serializeTokenSubunits(
2075
- parseTokenSubunits(projection.reserved) + parseTokenSubunits(delta.reservedDelta)
2076
- ),
2077
- held: serializeTokenSubunits(
2078
- parseTokenSubunits(projection.held) + parseTokenSubunits(delta.heldDelta)
2079
- ),
2080
- version: projection.version + 1,
2081
- lastTransactionId: transactionId,
2082
- asOf
2083
- };
2084
- assertWalletBalanceProjection(next);
2085
- return next;
2410
+ economyAssert(
2411
+ request.status === "approved" || request.status === "executed",
2412
+ "INVALID_CONTRACT",
2413
+ "Only approved adjustments satisfy dual-control approval"
2414
+ );
2415
+ if (request.resultingTransactionId !== void 0) {
2416
+ assertEconomyIdentifier(
2417
+ request.resultingTransactionId,
2418
+ "resultingTransactionId"
2419
+ );
2420
+ }
2086
2421
  }
2087
- function createWalletBalanceSummary(projection) {
2088
- assertWalletBalanceProjection(projection);
2089
- const spendable = parseTokenSubunits(projection.spendable);
2090
- const rewardProgress = spendable % 1000n;
2091
- const summary = {
2092
- schemaVersion: ECONOMY_CONTRACT_VERSION,
2093
- walletId: projection.walletId,
2094
- available: serializeTokenSubunits(spendable - rewardProgress),
2095
- reserved: projection.reserved,
2096
- held: projection.held,
2097
- rewardProgress: serializeTokenSubunits(rewardProgress),
2098
- version: projection.version,
2099
- asOf: projection.asOf
2422
+
2423
+ // src/paid-lots.ts
2424
+ var ZERO_ARITHMETIC = Object.freeze({
2425
+ retainedDelta: serializeTokenSubunits(0n),
2426
+ heldDelta: serializeTokenSubunits(0n),
2427
+ reversedDelta: serializeTokenSubunits(0n),
2428
+ refundedDelta: serializeTokenSubunits(0n),
2429
+ chargebackDelta: serializeTokenSubunits(0n),
2430
+ oneTimeReversalDelta: serializeTokenSubunits(0n)
2431
+ });
2432
+ var EVENT_TYPES = [
2433
+ "refund",
2434
+ "dispute-hold",
2435
+ "dispute-won",
2436
+ "dispute-lost",
2437
+ "chargeback",
2438
+ "reversal"
2439
+ ];
2440
+ var EVENT_PRECEDENCE = {
2441
+ "dispute-hold": 0,
2442
+ "dispute-won": 1,
2443
+ "dispute-lost": 1,
2444
+ refund: 2,
2445
+ chargeback: 3,
2446
+ reversal: 4
2447
+ };
2448
+ function assertPaidLotPurchaseProvenance(provenance) {
2449
+ economyAssert(
2450
+ provenance.schemaVersion === ECONOMY_CONTRACT_VERSION,
2451
+ "INVALID_CONTRACT",
2452
+ "Unsupported paid-lot provenance contract version"
2453
+ );
2454
+ assertEconomyIdentifier(provenance.purchaseId, "purchaseId");
2455
+ assertEconomyIdentifier(provenance.catalogVersion, "catalogVersion");
2456
+ parseIsoTimestamp(provenance.purchasedAt);
2457
+ }
2458
+ function assertPaidLotLifecycleEvent(event) {
2459
+ economyAssert(
2460
+ event.schemaVersion === ECONOMY_CONTRACT_VERSION,
2461
+ "INVALID_CONTRACT",
2462
+ "Unsupported paid-lot lifecycle event contract version"
2463
+ );
2464
+ assertEconomyIdentifier(event.eventId, "eventId");
2465
+ assertEconomyIdentifier(event.lotId, "lotId");
2466
+ economyAssert(
2467
+ EVENT_TYPES.includes(event.eventType),
2468
+ "INVALID_CONTRACT",
2469
+ "Paid-lot lifecycle event type is unsupported"
2470
+ );
2471
+ economyAssert(
2472
+ parseTokenSubunits(event.amount) > 0n,
2473
+ "INVALID_AMOUNT",
2474
+ "Paid-lot lifecycle event amount must be positive"
2475
+ );
2476
+ parseIsoTimestamp(event.occurredAt);
2477
+ }
2478
+ function sameEvent(left, right) {
2479
+ return left.schemaVersion === right.schemaVersion && left.eventId === right.eventId && left.lotId === right.lotId && left.eventType === right.eventType && left.amount === right.amount && left.occurredAt === right.occurredAt;
2480
+ }
2481
+ function compareEvents(left, right) {
2482
+ const timeDifference = parseIsoTimestamp(left.occurredAt) - parseIsoTimestamp(right.occurredAt);
2483
+ if (timeDifference !== 0) {
2484
+ return timeDifference;
2485
+ }
2486
+ const precedenceDifference = EVENT_PRECEDENCE[left.eventType] - EVENT_PRECEDENCE[right.eventType];
2487
+ return precedenceDifference === 0 ? compareUnicodeCodeUnits(left.eventId, right.eventId) : precedenceDifference;
2488
+ }
2489
+ function deriveStatus(retained, held, reversed, chargeback) {
2490
+ if (held > 0n) {
2491
+ return "disputed";
2492
+ }
2493
+ if (chargeback > 0n) {
2494
+ return "chargeback-lost";
2495
+ }
2496
+ if (retained === 0n) {
2497
+ return "fully-reversed";
2498
+ }
2499
+ return reversed > 0n ? "partially-reversed" : "clear";
2500
+ }
2501
+ function immutableFactsFromLifecycle(lifecycle) {
2502
+ return {
2503
+ schemaVersion: lifecycle.schemaVersion,
2504
+ lotId: lifecycle.lotId,
2505
+ payerAccountId: lifecycle.payerAccountId,
2506
+ receivingHouseholdId: lifecycle.receivingHouseholdId,
2507
+ purchaseId: lifecycle.purchaseId,
2508
+ catalogVersion: lifecycle.catalogVersion,
2509
+ purchasedAt: lifecycle.purchasedAt,
2510
+ settledAt: lifecycle.settledAt,
2511
+ creditedAt: lifecycle.creditedAt,
2512
+ originalAmount: lifecycle.originalAmount
2100
2513
  };
2101
- assertDeterministicWalletBalanceSummary(summary);
2102
- return summary;
2103
2514
  }
2104
- function assertDeterministicWalletBalanceSummary(summary) {
2105
- assertWalletBalanceSummary(summary);
2515
+ function assertImmutableFacts(facts) {
2106
2516
  economyAssert(
2107
- parseTokenSubunits(summary.available) % 1000n === 0n && parseTokenSubunits(summary.rewardProgress) < 1000n,
2517
+ facts.schemaVersion === ECONOMY_CONTRACT_VERSION,
2108
2518
  "INVALID_CONTRACT",
2109
- "Per-wallet availability must be whole Tokens with sub-Token progress below one Token"
2519
+ "Unsupported paid-lot lifecycle contract version"
2520
+ );
2521
+ assertEconomyIdentifier(facts.lotId, "lotId");
2522
+ assertEconomyIdentifier(facts.payerAccountId, "payerAccountId");
2523
+ assertEconomyIdentifier(
2524
+ facts.receivingHouseholdId,
2525
+ "receivingHouseholdId"
2526
+ );
2527
+ assertEconomyIdentifier(facts.purchaseId, "purchaseId");
2528
+ assertEconomyIdentifier(facts.catalogVersion, "catalogVersion");
2529
+ const purchasedAt = parseIsoTimestamp(facts.purchasedAt);
2530
+ const settledAt = parseIsoTimestamp(facts.settledAt);
2531
+ const creditedAt = parseIsoTimestamp(facts.creditedAt);
2532
+ economyAssert(
2533
+ purchasedAt <= settledAt && settledAt <= creditedAt,
2534
+ "INVALID_TIME_WINDOW",
2535
+ "Paid-lot timestamps must follow purchase, settlement, then credit order"
2536
+ );
2537
+ economyAssert(
2538
+ parseTokenSubunits(facts.originalAmount) > 0n,
2539
+ "INVALID_AMOUNT",
2540
+ "Paid-lot original amount must be positive"
2110
2541
  );
2111
2542
  }
2112
- function deriveWalletBalanceDeltas(transaction, bindings) {
2113
- assertEconomicJournalTransaction(transaction);
2114
- const byAccount = /* @__PURE__ */ new Map();
2115
- for (const binding of bindings) {
2116
- assertEconomyIdentifier(binding.accountId, "accountId");
2117
- assertEconomyIdentifier(binding.walletId, "walletId");
2543
+ function projectPaidLot(facts, receipts) {
2544
+ assertImmutableFacts(facts);
2545
+ const eventIds = /* @__PURE__ */ new Set();
2546
+ let reversalReceipts = 0;
2547
+ for (const receipt of receipts) {
2118
2548
  economyAssert(
2119
- ["spendable", "reserved", "held"].includes(binding.bucket),
2549
+ receipt.schemaVersion === ECONOMY_CONTRACT_VERSION,
2120
2550
  "INVALID_CONTRACT",
2121
- "Wallet account binding has an unsupported bucket"
2551
+ "Unsupported paid-lot receipt contract version"
2122
2552
  );
2553
+ assertPaidLotLifecycleEvent(receipt.event);
2123
2554
  economyAssert(
2124
- !byAccount.has(binding.accountId),
2555
+ receipt.event.lotId === facts.lotId && !eventIds.has(receipt.event.eventId),
2125
2556
  "DUPLICATE_IDENTIFIER",
2126
- "A ledger account may bind to only one wallet bucket"
2557
+ "Paid-lot receipts must be unique and belong to the lifecycle lot"
2127
2558
  );
2128
- byAccount.set(binding.accountId, binding);
2129
- }
2130
- const byWallet = /* @__PURE__ */ new Map();
2131
- for (const posting of transaction.postings) {
2132
- if (posting.walletId === void 0) {
2133
- continue;
2559
+ eventIds.add(receipt.event.eventId);
2560
+ if (receipt.event.eventType === "reversal") {
2561
+ reversalReceipts += 1;
2134
2562
  }
2135
- const binding = byAccount.get(posting.accountId);
2563
+ }
2564
+ economyAssert(
2565
+ reversalReceipts <= 1,
2566
+ "REVERSAL_ALREADY_EXISTS",
2567
+ "Paid-lot lifecycle permits only one distinct reversal event"
2568
+ );
2569
+ let retained = parseTokenSubunits(facts.originalAmount);
2570
+ let held = 0n;
2571
+ let reversed = 0n;
2572
+ let refunded = 0n;
2573
+ let chargeback = 0n;
2574
+ let oneTimeReversal = 0n;
2575
+ const effectiveEventIds = [];
2576
+ for (const receipt of [...receipts].sort(
2577
+ (left, right) => compareEvents(left.event, right.event)
2578
+ )) {
2579
+ const event = receipt.event;
2136
2580
  economyAssert(
2137
- binding !== void 0 && binding.walletId === posting.walletId,
2138
- "INVALID_CONTRACT",
2139
- "Every wallet-linked posting must match an authoritative account binding"
2581
+ parseIsoTimestamp(event.occurredAt) >= parseIsoTimestamp(facts.creditedAt),
2582
+ "INVALID_TIME_WINDOW",
2583
+ "Paid-lot lifecycle event cannot precede credit"
2140
2584
  );
2141
- const values = byWallet.get(binding.walletId) ?? [0n, 0n, 0n];
2142
- const amount = parseTokenSubunits(posting.amount);
2143
- if (binding.bucket === "spendable") {
2144
- values[0] += amount;
2145
- } else if (binding.bucket === "reserved") {
2146
- values[1] += amount;
2585
+ const amount = parseTokenSubunits(event.amount);
2586
+ if (event.eventType === "dispute-hold") {
2587
+ economyAssert(
2588
+ amount <= retained - held,
2589
+ "INSUFFICIENT_ELIGIBLE_LOTS",
2590
+ "Dispute hold exceeds unheld retained paid-lot basis"
2591
+ );
2592
+ held += amount;
2593
+ } else if (event.eventType === "dispute-won") {
2594
+ economyAssert(
2595
+ amount <= held,
2596
+ "INVALID_AMOUNT",
2597
+ "Dispute-win release exceeds held paid-lot basis"
2598
+ );
2599
+ held -= amount;
2600
+ } else if (event.eventType === "dispute-lost") {
2601
+ economyAssert(
2602
+ amount <= held,
2603
+ "INVALID_AMOUNT",
2604
+ "Dispute-loss amount exceeds held paid-lot basis"
2605
+ );
2606
+ held -= amount;
2607
+ retained -= amount;
2608
+ reversed += amount;
2609
+ chargeback += amount;
2147
2610
  } else {
2148
- values[2] += amount;
2611
+ economyAssert(
2612
+ amount <= retained - held,
2613
+ "INSUFFICIENT_ELIGIBLE_LOTS",
2614
+ "Paid-lot reversal exceeds unheld retained basis"
2615
+ );
2616
+ if (event.eventType === "reversal") {
2617
+ oneTimeReversal += amount;
2618
+ } else if (event.eventType === "refund") {
2619
+ refunded += amount;
2620
+ } else {
2621
+ chargeback += amount;
2622
+ }
2623
+ retained -= amount;
2624
+ reversed += amount;
2149
2625
  }
2150
- byWallet.set(binding.walletId, values);
2626
+ effectiveEventIds.push(event.eventId);
2151
2627
  }
2152
- const deltas = [...byWallet.entries()].map(([walletId, values]) => ({
2628
+ return {
2629
+ retained,
2630
+ held,
2631
+ reversed,
2632
+ refunded,
2633
+ chargeback,
2634
+ oneTimeReversal,
2635
+ status: deriveStatus(retained, held, reversed, chargeback),
2636
+ effectiveEventIds
2637
+ };
2638
+ }
2639
+ function projectionMatchesLifecycle(projection, lifecycle) {
2640
+ return serializeTokenSubunits(projection.retained) === lifecycle.retainedAmount && serializeTokenSubunits(projection.held) === lifecycle.heldAmount && serializeTokenSubunits(projection.reversed) === lifecycle.reversedAmount && serializeTokenSubunits(projection.refunded) === lifecycle.refundedAmount && serializeTokenSubunits(projection.chargeback) === lifecycle.chargebackAmount && serializeTokenSubunits(projection.oneTimeReversal) === lifecycle.oneTimeReversalAmount && projection.status === lifecycle.status && projection.effectiveEventIds.length === lifecycle.effectiveEventIds.length && projection.effectiveEventIds.every(
2641
+ (eventId, index) => eventId === lifecycle.effectiveEventIds[index]
2642
+ );
2643
+ }
2644
+ function assertPaidLotLifecycle(lifecycle) {
2645
+ economyAssert(
2646
+ Number.isSafeInteger(lifecycle.version) && lifecycle.version === lifecycle.receipts.length + 1,
2647
+ "INVALID_CONTRACT",
2648
+ "Paid-lot lifecycle version must match its receipt count"
2649
+ );
2650
+ const projection = projectPaidLot(
2651
+ immutableFactsFromLifecycle(lifecycle),
2652
+ lifecycle.receipts
2653
+ );
2654
+ economyAssert(
2655
+ projectionMatchesLifecycle(projection, lifecycle),
2656
+ "INVALID_CONTRACT",
2657
+ "Paid-lot lifecycle does not match its immutable event rebuild"
2658
+ );
2659
+ }
2660
+ function createPaidLotLifecycle(lot, provenance) {
2661
+ assertSourceLot(lot);
2662
+ assertPaidLotPurchaseProvenance(provenance);
2663
+ economyAssert(
2664
+ lot.payerAccountId !== void 0 && lot.householdId !== void 0,
2665
+ "INVALID_CONTRACT",
2666
+ "Paid-lot lifecycle requires payer and receiving-household provenance"
2667
+ );
2668
+ economyAssert(
2669
+ lot.refundState === "none" && parseTokenSubunits(lot.heldAmount) === 0n && parseTokenSubunits(lot.reversedAmount) === 0n,
2670
+ "INVALID_CONTRACT",
2671
+ "Paid-lot lifecycle must be created before any refund or dispute movement"
2672
+ );
2673
+ economyAssert(
2674
+ parseIsoTimestamp(provenance.purchasedAt) <= parseIsoTimestamp(lot.settledAt),
2675
+ "INVALID_TIME_WINDOW",
2676
+ "Paid purchase time cannot follow settlement"
2677
+ );
2678
+ const facts = {
2153
2679
  schemaVersion: ECONOMY_CONTRACT_VERSION,
2154
- walletId,
2155
- spendableDelta: serializeTokenSubunits(values[0]),
2156
- reservedDelta: serializeTokenSubunits(values[1]),
2157
- heldDelta: serializeTokenSubunits(values[2])
2158
- })).filter(
2159
- (delta) => delta.spendableDelta !== "0" || delta.reservedDelta !== "0" || delta.heldDelta !== "0"
2160
- ).sort((left, right) => left.walletId.localeCompare(right.walletId));
2161
- assertWalletBalanceDeltaBatch(transaction.transactionId, deltas);
2162
- return deltas;
2680
+ lotId: lot.lotId,
2681
+ payerAccountId: lot.payerAccountId,
2682
+ receivingHouseholdId: lot.householdId,
2683
+ purchaseId: provenance.purchaseId,
2684
+ catalogVersion: provenance.catalogVersion,
2685
+ purchasedAt: provenance.purchasedAt,
2686
+ settledAt: lot.settledAt,
2687
+ creditedAt: lot.creditedAt,
2688
+ originalAmount: lot.originalAmount
2689
+ };
2690
+ const projection = projectPaidLot(facts, []);
2691
+ const lifecycle = {
2692
+ ...facts,
2693
+ retainedAmount: serializeTokenSubunits(projection.retained),
2694
+ heldAmount: serializeTokenSubunits(projection.held),
2695
+ reversedAmount: serializeTokenSubunits(projection.reversed),
2696
+ refundedAmount: serializeTokenSubunits(projection.refunded),
2697
+ chargebackAmount: serializeTokenSubunits(projection.chargeback),
2698
+ oneTimeReversalAmount: serializeTokenSubunits(
2699
+ projection.oneTimeReversal
2700
+ ),
2701
+ status: projection.status,
2702
+ version: 1,
2703
+ receipts: [],
2704
+ effectiveEventIds: []
2705
+ };
2706
+ assertPaidLotLifecycle(lifecycle);
2707
+ return lifecycle;
2163
2708
  }
2164
- var EARNED_ACTIVITY_TYPES = /* @__PURE__ */ new Set([
2165
- "rewarded-ad",
2166
- "offerwall",
2167
- "event",
2168
- "competition"
2169
- ]);
2170
- function deriveWalletLifetimeDeltas(transaction) {
2171
- assertEconomicJournalTransaction(transaction);
2172
- if (transaction.status !== "settled") {
2173
- return [];
2709
+ function applyPaidLotLifecycleEvent(lifecycle, event, expectedVersion) {
2710
+ assertPaidLotLifecycle(lifecycle);
2711
+ assertPaidLotLifecycleEvent(event);
2712
+ economyAssert(
2713
+ event.lotId === lifecycle.lotId,
2714
+ "INVALID_CONTRACT",
2715
+ "Paid-lot event belongs to another source lot"
2716
+ );
2717
+ const replay = lifecycle.receipts.find(
2718
+ (receipt) => receipt.event.eventId === event.eventId
2719
+ );
2720
+ if (replay !== void 0) {
2721
+ economyAssert(
2722
+ sameEvent(replay.event, event),
2723
+ "DUPLICATE_IDENTIFIER",
2724
+ "Paid-lot event ID was reused with different facts"
2725
+ );
2726
+ return {
2727
+ lifecycle,
2728
+ applied: false,
2729
+ stateChanged: false,
2730
+ arithmetic: ZERO_ARITHMETIC
2731
+ };
2174
2732
  }
2175
- const netByWallet = /* @__PURE__ */ new Map();
2176
- for (const posting of transaction.postings) {
2177
- if (posting.walletId !== void 0) {
2178
- netByWallet.set(
2179
- posting.walletId,
2180
- (netByWallet.get(posting.walletId) ?? 0n) + parseTokenSubunits(posting.amount)
2733
+ economyAssert(
2734
+ expectedVersion === lifecycle.version,
2735
+ "INVALID_CONTRACT",
2736
+ "Paid-lot lifecycle event has a stale expected version"
2737
+ );
2738
+ const receipts = [
2739
+ ...lifecycle.receipts,
2740
+ { schemaVersion: ECONOMY_CONTRACT_VERSION, event }
2741
+ ];
2742
+ const projection = projectPaidLot(
2743
+ immutableFactsFromLifecycle(lifecycle),
2744
+ receipts
2745
+ );
2746
+ const before = {
2747
+ retained: parseTokenSubunits(lifecycle.retainedAmount),
2748
+ held: parseTokenSubunits(lifecycle.heldAmount),
2749
+ reversed: parseTokenSubunits(lifecycle.reversedAmount),
2750
+ refunded: parseTokenSubunits(lifecycle.refundedAmount),
2751
+ chargeback: parseTokenSubunits(lifecycle.chargebackAmount),
2752
+ oneTimeReversal: parseTokenSubunits(lifecycle.oneTimeReversalAmount)
2753
+ };
2754
+ const next = {
2755
+ ...lifecycle,
2756
+ retainedAmount: serializeTokenSubunits(projection.retained),
2757
+ heldAmount: serializeTokenSubunits(projection.held),
2758
+ reversedAmount: serializeTokenSubunits(projection.reversed),
2759
+ refundedAmount: serializeTokenSubunits(projection.refunded),
2760
+ chargebackAmount: serializeTokenSubunits(projection.chargeback),
2761
+ oneTimeReversalAmount: serializeTokenSubunits(
2762
+ projection.oneTimeReversal
2763
+ ),
2764
+ status: projection.status,
2765
+ version: lifecycle.version + 1,
2766
+ receipts,
2767
+ effectiveEventIds: projection.effectiveEventIds
2768
+ };
2769
+ assertPaidLotLifecycle(next);
2770
+ const arithmetic = {
2771
+ retainedDelta: serializeTokenSubunits(
2772
+ projection.retained - before.retained
2773
+ ),
2774
+ heldDelta: serializeTokenSubunits(projection.held - before.held),
2775
+ reversedDelta: serializeTokenSubunits(
2776
+ projection.reversed - before.reversed
2777
+ ),
2778
+ refundedDelta: serializeTokenSubunits(
2779
+ projection.refunded - before.refunded
2780
+ ),
2781
+ chargebackDelta: serializeTokenSubunits(
2782
+ projection.chargeback - before.chargeback
2783
+ ),
2784
+ oneTimeReversalDelta: serializeTokenSubunits(
2785
+ projection.oneTimeReversal - before.oneTimeReversal
2786
+ )
2787
+ };
2788
+ return {
2789
+ lifecycle: next,
2790
+ applied: true,
2791
+ stateChanged: Object.values(arithmetic).some((delta) => delta !== "0"),
2792
+ arithmetic
2793
+ };
2794
+ }
2795
+ function reducePaidLotLifecycleEvents(initial, events) {
2796
+ assertPaidLotLifecycle(initial);
2797
+ const unique = /* @__PURE__ */ new Map();
2798
+ for (const event of events) {
2799
+ assertPaidLotLifecycleEvent(event);
2800
+ const existing = unique.get(event.eventId);
2801
+ if (existing !== void 0) {
2802
+ economyAssert(
2803
+ sameEvent(existing, event),
2804
+ "DUPLICATE_IDENTIFIER",
2805
+ "Paid-lot event ID was reused with different facts"
2181
2806
  );
2807
+ } else {
2808
+ unique.set(event.eventId, event);
2182
2809
  }
2183
2810
  }
2184
- const deltas = [];
2185
- for (const [walletId, net] of [...netByWallet.entries()].sort(
2186
- ([left], [right]) => left.localeCompare(right)
2187
- )) {
2188
- let bucket;
2189
- let amount = 0n;
2190
- if (net > 0n && (transaction.activityType === "purchase" || transaction.activityType === "subscription")) {
2191
- bucket = "bought";
2192
- amount = net;
2193
- } else if (net > 0n && EARNED_ACTIVITY_TYPES.has(transaction.activityType)) {
2194
- bucket = "earned";
2195
- amount = net;
2196
- } else if (net < 0n && (transaction.activityType === "allocation" || transaction.activityType === "boost")) {
2197
- bucket = "allocated";
2198
- amount = -net;
2199
- } else if (net > 0n && transaction.activityType === "reclaim") {
2200
- bucket = "reclaimed";
2201
- amount = net;
2202
- } else if (net < 0n && transaction.activityType === "spend") {
2203
- bucket = "spent";
2204
- amount = -net;
2205
- } else if (net < 0n && (transaction.activityType === "refund" || transaction.activityType === "chargeback" || transaction.activityType === "reversal")) {
2206
- bucket = "reversed";
2207
- amount = -net;
2208
- }
2209
- if (bucket !== void 0) {
2210
- deltas.push({
2211
- schemaVersion: ECONOMY_CONTRACT_VERSION,
2212
- walletId,
2213
- bucket,
2214
- amount: serializeTokenSubunits(amount)
2215
- });
2216
- }
2811
+ let lifecycle = initial;
2812
+ for (const event of [...unique.values()].sort(compareEvents)) {
2813
+ lifecycle = applyPaidLotLifecycleEvent(
2814
+ lifecycle,
2815
+ event,
2816
+ lifecycle.version
2817
+ ).lifecycle;
2217
2818
  }
2218
- return deltas;
2819
+ return lifecycle;
2219
2820
  }
2220
- function applyWalletLifetimeDeltas(current, walletId, deltas) {
2221
- assertWalletLifetimeTotals(current);
2222
- assertEconomyIdentifier(walletId, "walletId");
2223
- const next = {
2224
- bought: parseTokenSubunits(current.bought),
2225
- earned: parseTokenSubunits(current.earned),
2226
- allocated: parseTokenSubunits(current.allocated),
2227
- reclaimed: parseTokenSubunits(current.reclaimed),
2228
- spent: parseTokenSubunits(current.spent),
2229
- reversed: parseTokenSubunits(current.reversed)
2821
+ function createEarlyBackerRetentionFromPaidLot(lifecycle) {
2822
+ assertPaidLotLifecycle(lifecycle);
2823
+ return {
2824
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
2825
+ lotId: lifecycle.lotId,
2826
+ payerAccountId: lifecycle.payerAccountId,
2827
+ receivingHouseholdId: lifecycle.receivingHouseholdId,
2828
+ purchaseId: lifecycle.purchaseId,
2829
+ catalogVersion: lifecycle.catalogVersion,
2830
+ purchasedAt: lifecycle.purchasedAt,
2831
+ settledAt: lifecycle.settledAt,
2832
+ creditedAt: lifecycle.creditedAt,
2833
+ retainedAmount: lifecycle.retainedAmount
2230
2834
  };
2231
- for (const delta of deltas) {
2232
- economyAssert(
2233
- delta.schemaVersion === ECONOMY_CONTRACT_VERSION && delta.walletId === walletId && [
2234
- "bought",
2235
- "earned",
2236
- "allocated",
2237
- "reclaimed",
2238
- "spent",
2239
- "reversed"
2240
- ].includes(delta.bucket),
2835
+ }
2836
+
2837
+ // src/ports/persistence.ts
2838
+ function assertScopedPersistedIdempotencyResult(result) {
2839
+ economyAssert(
2840
+ result.schemaVersion === ECONOMY_CONTRACT_VERSION,
2841
+ "INVALID_CONTRACT",
2842
+ "Unsupported scoped idempotency-result contract version"
2843
+ );
2844
+ assertEconomyIdentifier(result.commandId, "commandId");
2845
+ assertEconomyIdentifier(result.transactionId, "transactionId");
2846
+ economyAssert(
2847
+ /^sha256:[a-f0-9]{64}$/u.test(result.responseHash),
2848
+ "INVALID_CONTRACT",
2849
+ "Idempotency response hash must be a canonical SHA-256 reference"
2850
+ );
2851
+ parseIsoTimestamp(result.recordedAt);
2852
+ }
2853
+ var ECONOMY_COMMAND_TYPES = /* @__PURE__ */ new Set([
2854
+ "credit-purchase",
2855
+ "credit-subscription",
2856
+ "credit-reward",
2857
+ "credit-event",
2858
+ "credit-competition",
2859
+ "allocate",
2860
+ "boost",
2861
+ "reclaim",
2862
+ "spend",
2863
+ "hold",
2864
+ "release-hold",
2865
+ "refund",
2866
+ "chargeback",
2867
+ "reverse",
2868
+ "adjust"
2869
+ ]);
2870
+ function assertEconomyIdempotencyScope(scope) {
2871
+ economyAssert(
2872
+ scope.schemaVersion === ECONOMY_CONTRACT_VERSION && ECONOMY_COMMAND_TYPES.has(scope.commandType),
2873
+ "INVALID_CONTRACT",
2874
+ "Unsupported economy idempotency scope"
2875
+ );
2876
+ assertEconomyIdentifier(scope.idempotencyKey, "idempotencyKey");
2877
+ assertEconomyIdentifier(scope.actorAccountId, "actorAccountId");
2878
+ assertEconomyIdentifier(scope.subjectAccountId, "subjectAccountId");
2879
+ }
2880
+ function assertEconomyCommandEnvelope(envelope) {
2881
+ economyAssert(
2882
+ envelope.schemaVersion === ECONOMY_CONTRACT_VERSION,
2883
+ "INVALID_CONTRACT",
2884
+ "Unsupported economy-command-envelope contract version"
2885
+ );
2886
+ assertEconomyIdentifier(envelope.commandId, "commandId");
2887
+ economyAssert(
2888
+ ECONOMY_COMMAND_TYPES.has(envelope.commandType),
2889
+ "INVALID_CONTRACT",
2890
+ "Economy command has an unsupported type"
2891
+ );
2892
+ assertEconomyIdentifier(envelope.idempotencyKey, "idempotencyKey");
2893
+ assertEconomyIdentifier(envelope.actorAccountId, "actorAccountId");
2894
+ assertEconomyIdentifier(envelope.subjectAccountId, "subjectAccountId");
2895
+ if (envelope.relationshipId !== void 0) {
2896
+ assertEconomyIdentifier(envelope.relationshipId, "relationshipId");
2897
+ }
2898
+ if (envelope.authorizationVersion !== void 0) {
2899
+ economyAssert(
2900
+ Number.isSafeInteger(envelope.authorizationVersion) && envelope.authorizationVersion >= 1,
2241
2901
  "INVALID_CONTRACT",
2242
- "Lifetime delta must target the requested wallet and a known bucket"
2902
+ "Command authorization version must be a positive safe integer"
2243
2903
  );
2244
- const amount = parseTokenSubunits(delta.amount);
2904
+ }
2905
+ economyAssert(
2906
+ envelope.relationshipId === void 0 === (envelope.authorizationVersion === void 0),
2907
+ "INVALID_CONTRACT",
2908
+ "Relationship identity and authorization version must be supplied together"
2909
+ );
2910
+ economyAssert(
2911
+ /^sha256:[a-f0-9]{64}$/u.test(envelope.payloadHash),
2912
+ "INVALID_CONTRACT",
2913
+ "Command payload hash must be a canonical SHA-256 reference"
2914
+ );
2915
+ parseIsoTimestamp(envelope.acceptedAt);
2916
+ assertEconomyIdentifier(envelope.acceptedRegion, "acceptedRegion");
2917
+ assertEconomyIdentifier(
2918
+ envelope.writerFencingToken,
2919
+ "writerFencingToken"
2920
+ );
2921
+ }
2922
+ function assertEconomyCommandWorkflowEvent(event) {
2923
+ economyAssert(
2924
+ event.schemaVersion === ECONOMY_CONTRACT_VERSION,
2925
+ "INVALID_CONTRACT",
2926
+ "Unsupported command-workflow-event contract version"
2927
+ );
2928
+ assertEconomyIdentifier(event.workflowEventId, "workflowEventId");
2929
+ assertEconomyIdentifier(event.commandId, "commandId");
2930
+ economyAssert(
2931
+ ["accepted", "processing", "failed", "completed"].includes(event.state),
2932
+ "INVALID_CONTRACT",
2933
+ "Command workflow has an unsupported state"
2934
+ );
2935
+ economyAssert(
2936
+ event.state === "failed" === (event.failureCode !== void 0),
2937
+ "INVALID_CONTRACT",
2938
+ "Only failed command workflows require a failure code"
2939
+ );
2940
+ if (event.failureCode !== void 0) {
2941
+ assertEconomyIdentifier(event.failureCode, "failureCode");
2942
+ }
2943
+ parseIsoTimestamp(event.occurredAt);
2944
+ }
2945
+
2946
+ // src/wallets.ts
2947
+ function assertWalletOwnerReference(owner) {
2948
+ economyAssert(
2949
+ ["account", "household", "system"].includes(owner.ownerType),
2950
+ "INVALID_CONTRACT",
2951
+ "Wallet owner reference has an unsupported type"
2952
+ );
2953
+ if (owner.ownerType === "system") {
2245
2954
  economyAssert(
2246
- amount > 0n,
2955
+ owner.ownerId === "system",
2956
+ "INVALID_CONTRACT",
2957
+ "System wallet lookups must use the system owner"
2958
+ );
2959
+ } else {
2960
+ assertEconomyIdentifier(owner.ownerId, "ownerId");
2961
+ economyAssert(
2962
+ owner.ownerId !== "system",
2963
+ "INVALID_CONTRACT",
2964
+ "Account and household wallets cannot use the system owner"
2965
+ );
2966
+ }
2967
+ }
2968
+ function assertWallet(wallet) {
2969
+ economyAssert(
2970
+ wallet.schemaVersion === ECONOMY_CONTRACT_VERSION,
2971
+ "INVALID_CONTRACT",
2972
+ "Unsupported wallet contract version"
2973
+ );
2974
+ assertEconomyIdentifier(wallet.walletId, "walletId");
2975
+ assertEconomyIdentifier(wallet.accountId, "accountId");
2976
+ economyAssert(
2977
+ [
2978
+ "household-treasury",
2979
+ "personal",
2980
+ "gameplay-allocation",
2981
+ "hold",
2982
+ "purchase-clearing",
2983
+ "provider-clearing",
2984
+ "system"
2985
+ ].includes(wallet.kind),
2986
+ "INVALID_CONTRACT",
2987
+ "Wallet has an unsupported kind"
2988
+ );
2989
+ economyAssert(
2990
+ ["account", "household", "system"].includes(wallet.ownerType),
2991
+ "INVALID_CONTRACT",
2992
+ "Wallet has an unsupported owner type"
2993
+ );
2994
+ economyAssert(
2995
+ ["active", "restricted", "closed"].includes(wallet.status),
2996
+ "INVALID_CONTRACT",
2997
+ "Wallet has an unsupported status"
2998
+ );
2999
+ if (wallet.ownerType === "system") {
3000
+ economyAssert(
3001
+ wallet.ownerId === "system",
3002
+ "INVALID_CONTRACT",
3003
+ "System wallets must be owned by the system"
3004
+ );
3005
+ } else {
3006
+ assertEconomyIdentifier(wallet.ownerId, "ownerId");
3007
+ }
3008
+ if (wallet.householdId !== void 0) {
3009
+ assertEconomyIdentifier(wallet.householdId, "householdId");
3010
+ }
3011
+ if (wallet.ownerType === "household") {
3012
+ economyAssert(
3013
+ wallet.householdId === wallet.ownerId,
3014
+ "INVALID_CONTRACT",
3015
+ "Household wallets must identify the same household owner"
3016
+ );
3017
+ }
3018
+ if (wallet.kind === "household-treasury") {
3019
+ economyAssert(
3020
+ wallet.ownerType === "household" && wallet.householdId !== void 0,
3021
+ "INVALID_CONTRACT",
3022
+ "Household treasuries must be owned by their household"
3023
+ );
3024
+ }
3025
+ if (wallet.kind === "personal" || wallet.kind === "gameplay-allocation") {
3026
+ economyAssert(
3027
+ wallet.ownerType === "account",
3028
+ "INVALID_CONTRACT",
3029
+ "Personal and gameplay-allocation wallets must be account-owned"
3030
+ );
3031
+ }
3032
+ if (wallet.kind === "gameplay-allocation") {
3033
+ economyAssert(
3034
+ wallet.householdId !== void 0,
3035
+ "INVALID_CONTRACT",
3036
+ "Gameplay-allocation wallets must retain household provenance"
3037
+ );
3038
+ }
3039
+ if (wallet.kind === "purchase-clearing" || wallet.kind === "provider-clearing" || wallet.kind === "system") {
3040
+ economyAssert(
3041
+ wallet.ownerType === "system" && wallet.householdId === void 0,
3042
+ "INVALID_CONTRACT",
3043
+ "Clearing and system wallets must be system-owned"
3044
+ );
3045
+ }
3046
+ economyAssert(
3047
+ Number.isSafeInteger(wallet.version) && wallet.version >= 1,
3048
+ "INVALID_CONTRACT",
3049
+ "Wallet version must be a positive safe integer"
3050
+ );
3051
+ const createdAt = parseIsoTimestamp(wallet.createdAt);
3052
+ if (wallet.closedAt !== void 0) {
3053
+ economyAssert(
3054
+ parseIsoTimestamp(wallet.closedAt) >= createdAt && wallet.status === "closed",
3055
+ "INVALID_TIME_WINDOW",
3056
+ "Only closed wallets may have a valid closure time"
3057
+ );
3058
+ } else {
3059
+ economyAssert(
3060
+ wallet.status !== "closed",
3061
+ "INVALID_CONTRACT",
3062
+ "Closed wallets require a closure time"
3063
+ );
3064
+ }
3065
+ }
3066
+ function assertWalletBalanceSummary(summary) {
3067
+ economyAssert(
3068
+ summary.schemaVersion === ECONOMY_CONTRACT_VERSION,
3069
+ "INVALID_CONTRACT",
3070
+ "Unsupported wallet summary contract version"
3071
+ );
3072
+ assertEconomyIdentifier(summary.walletId, "walletId");
3073
+ for (const amount of [
3074
+ summary.available,
3075
+ summary.reserved,
3076
+ summary.held,
3077
+ summary.rewardProgress
3078
+ ]) {
3079
+ economyAssert(
3080
+ parseTokenSubunits(amount) >= 0n,
2247
3081
  "INVALID_AMOUNT",
2248
- "Lifetime deltas must be positive"
3082
+ "Wallet summary categories cannot be negative"
3083
+ );
3084
+ }
3085
+ economyAssert(
3086
+ Number.isSafeInteger(summary.version) && summary.version >= 1,
3087
+ "INVALID_CONTRACT",
3088
+ "Wallet summary version must be a positive safe integer"
3089
+ );
3090
+ parseIsoTimestamp(summary.asOf);
3091
+ }
3092
+ function assertWalletLifetimeTotals(totals) {
3093
+ economyAssert(
3094
+ totals.schemaVersion === ECONOMY_CONTRACT_VERSION,
3095
+ "INVALID_CONTRACT",
3096
+ "Unsupported wallet lifetime-total contract version"
3097
+ );
3098
+ for (const amount of [
3099
+ totals.bought,
3100
+ totals.earned,
3101
+ totals.allocated,
3102
+ totals.reclaimed,
3103
+ totals.spent,
3104
+ totals.reversed
3105
+ ]) {
3106
+ economyAssert(
3107
+ parseTokenSubunits(amount) >= 0n,
3108
+ "INVALID_AMOUNT",
3109
+ "Wallet lifetime totals cannot be negative"
3110
+ );
3111
+ }
3112
+ }
3113
+
3114
+ // src/projection.ts
3115
+ function assertWalletBalanceProjection(projection) {
3116
+ economyAssert(
3117
+ projection.schemaVersion === ECONOMY_CONTRACT_VERSION,
3118
+ "INVALID_CONTRACT",
3119
+ "Unsupported wallet-balance-projection contract version"
3120
+ );
3121
+ assertEconomyIdentifier(projection.walletId, "walletId");
3122
+ for (const amount of [
3123
+ projection.spendable,
3124
+ projection.reserved,
3125
+ projection.held
3126
+ ]) {
3127
+ economyAssert(
3128
+ parseTokenSubunits(amount) >= 0n,
3129
+ "NEGATIVE_PROJECTION",
3130
+ "Wallet projection buckets cannot be negative"
3131
+ );
3132
+ }
3133
+ economyAssert(
3134
+ Number.isSafeInteger(projection.version) && projection.version >= 1,
3135
+ "INVALID_CONTRACT",
3136
+ "Wallet projection version must be a positive safe integer"
3137
+ );
3138
+ if (projection.lastTransactionId !== void 0) {
3139
+ assertEconomyIdentifier(
3140
+ projection.lastTransactionId,
3141
+ "lastTransactionId"
3142
+ );
3143
+ }
3144
+ parseIsoTimestamp(projection.asOf);
3145
+ }
3146
+ function assertWalletBalanceDelta(delta) {
3147
+ economyAssert(
3148
+ delta.schemaVersion === ECONOMY_CONTRACT_VERSION,
3149
+ "INVALID_CONTRACT",
3150
+ "Unsupported wallet-balance-delta contract version"
3151
+ );
3152
+ assertEconomyIdentifier(delta.walletId, "walletId");
3153
+ const values = [
3154
+ parseTokenSubunits(delta.spendableDelta),
3155
+ parseTokenSubunits(delta.reservedDelta),
3156
+ parseTokenSubunits(delta.heldDelta)
3157
+ ];
3158
+ economyAssert(
3159
+ values.some((value) => value !== 0n),
3160
+ "INVALID_AMOUNT",
3161
+ "Wallet balance delta must change at least one bucket"
3162
+ );
3163
+ }
3164
+ function assertWalletBalanceDeltaBatch(transactionId, deltas) {
3165
+ assertEconomyIdentifier(transactionId, "transactionId");
3166
+ economyAssert(
3167
+ deltas.length > 0,
3168
+ "INVALID_CONTRACT",
3169
+ "An economic transaction must update at least one wallet projection"
3170
+ );
3171
+ const walletIds = /* @__PURE__ */ new Set();
3172
+ for (const delta of deltas) {
3173
+ assertWalletBalanceDelta(delta);
3174
+ economyAssert(
3175
+ !walletIds.has(delta.walletId),
3176
+ "DUPLICATE_IDENTIFIER",
3177
+ "A balance-delta batch must update each wallet once"
3178
+ );
3179
+ walletIds.add(delta.walletId);
3180
+ }
3181
+ }
3182
+ function applyWalletBalanceDelta(projection, delta, transactionId, asOf) {
3183
+ assertWalletBalanceProjection(projection);
3184
+ assertWalletBalanceDelta(delta);
3185
+ assertEconomyIdentifier(transactionId, "transactionId");
3186
+ economyAssert(
3187
+ projection.walletId === delta.walletId,
3188
+ "INVALID_CONTRACT",
3189
+ "Wallet balance delta must target its projection wallet"
3190
+ );
3191
+ economyAssert(
3192
+ parseIsoTimestamp(asOf) >= parseIsoTimestamp(projection.asOf),
3193
+ "INVALID_TIME_WINDOW",
3194
+ "Wallet balance update cannot precede its projection"
3195
+ );
3196
+ const next = {
3197
+ ...projection,
3198
+ spendable: serializeTokenSubunits(
3199
+ parseTokenSubunits(projection.spendable) + parseTokenSubunits(delta.spendableDelta)
3200
+ ),
3201
+ reserved: serializeTokenSubunits(
3202
+ parseTokenSubunits(projection.reserved) + parseTokenSubunits(delta.reservedDelta)
3203
+ ),
3204
+ held: serializeTokenSubunits(
3205
+ parseTokenSubunits(projection.held) + parseTokenSubunits(delta.heldDelta)
3206
+ ),
3207
+ version: projection.version + 1,
3208
+ lastTransactionId: transactionId,
3209
+ asOf
3210
+ };
3211
+ assertWalletBalanceProjection(next);
3212
+ return next;
3213
+ }
3214
+ function createWalletBalanceSummary(projection) {
3215
+ assertWalletBalanceProjection(projection);
3216
+ const spendable = parseTokenSubunits(projection.spendable);
3217
+ const rewardProgress = spendable % 1000n;
3218
+ const summary = {
3219
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
3220
+ walletId: projection.walletId,
3221
+ available: serializeTokenSubunits(spendable - rewardProgress),
3222
+ reserved: projection.reserved,
3223
+ held: projection.held,
3224
+ rewardProgress: serializeTokenSubunits(rewardProgress),
3225
+ version: projection.version,
3226
+ asOf: projection.asOf
3227
+ };
3228
+ assertDeterministicWalletBalanceSummary(summary);
3229
+ return summary;
3230
+ }
3231
+ function assertDeterministicWalletBalanceSummary(summary) {
3232
+ assertWalletBalanceSummary(summary);
3233
+ economyAssert(
3234
+ parseTokenSubunits(summary.available) % 1000n === 0n && parseTokenSubunits(summary.rewardProgress) < 1000n,
3235
+ "INVALID_CONTRACT",
3236
+ "Per-wallet availability must be whole Tokens with sub-Token progress below one Token"
3237
+ );
3238
+ }
3239
+ function deriveWalletBalanceDeltas(transaction, bindings) {
3240
+ assertEconomicJournalTransaction(transaction);
3241
+ const byAccount = /* @__PURE__ */ new Map();
3242
+ for (const binding of bindings) {
3243
+ assertEconomyIdentifier(binding.accountId, "accountId");
3244
+ assertEconomyIdentifier(binding.walletId, "walletId");
3245
+ economyAssert(
3246
+ ["spendable", "reserved", "held"].includes(binding.bucket),
3247
+ "INVALID_CONTRACT",
3248
+ "Wallet account binding has an unsupported bucket"
3249
+ );
3250
+ economyAssert(
3251
+ !byAccount.has(binding.accountId),
3252
+ "DUPLICATE_IDENTIFIER",
3253
+ "A ledger account may bind to only one wallet bucket"
3254
+ );
3255
+ byAccount.set(binding.accountId, binding);
3256
+ }
3257
+ const byWallet = /* @__PURE__ */ new Map();
3258
+ for (const posting of transaction.postings) {
3259
+ if (posting.walletId === void 0) {
3260
+ continue;
3261
+ }
3262
+ const binding = byAccount.get(posting.accountId);
3263
+ economyAssert(
3264
+ binding !== void 0 && binding.walletId === posting.walletId,
3265
+ "INVALID_CONTRACT",
3266
+ "Every wallet-linked posting must match an authoritative account binding"
3267
+ );
3268
+ const values = byWallet.get(binding.walletId) ?? [0n, 0n, 0n];
3269
+ const amount = parseTokenSubunits(posting.amount);
3270
+ if (binding.bucket === "spendable") {
3271
+ values[0] += amount;
3272
+ } else if (binding.bucket === "reserved") {
3273
+ values[1] += amount;
3274
+ } else {
3275
+ values[2] += amount;
3276
+ }
3277
+ byWallet.set(binding.walletId, values);
3278
+ }
3279
+ const deltas = [...byWallet.entries()].map(([walletId, values]) => ({
3280
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
3281
+ walletId,
3282
+ spendableDelta: serializeTokenSubunits(values[0]),
3283
+ reservedDelta: serializeTokenSubunits(values[1]),
3284
+ heldDelta: serializeTokenSubunits(values[2])
3285
+ })).filter(
3286
+ (delta) => delta.spendableDelta !== "0" || delta.reservedDelta !== "0" || delta.heldDelta !== "0"
3287
+ ).sort((left, right) => left.walletId.localeCompare(right.walletId));
3288
+ assertWalletBalanceDeltaBatch(transaction.transactionId, deltas);
3289
+ return deltas;
3290
+ }
3291
+ var EARNED_ACTIVITY_TYPES = /* @__PURE__ */ new Set([
3292
+ "rewarded-ad",
3293
+ "offerwall",
3294
+ "event",
3295
+ "competition"
3296
+ ]);
3297
+ function deriveWalletLifetimeDeltas(transaction) {
3298
+ assertEconomicJournalTransaction(transaction);
3299
+ if (transaction.status !== "settled") {
3300
+ return [];
3301
+ }
3302
+ const netByWallet = /* @__PURE__ */ new Map();
3303
+ for (const posting of transaction.postings) {
3304
+ if (posting.walletId !== void 0) {
3305
+ netByWallet.set(
3306
+ posting.walletId,
3307
+ (netByWallet.get(posting.walletId) ?? 0n) + parseTokenSubunits(posting.amount)
3308
+ );
3309
+ }
3310
+ }
3311
+ const deltas = [];
3312
+ for (const [walletId, net] of [...netByWallet.entries()].sort(
3313
+ ([left], [right]) => left.localeCompare(right)
3314
+ )) {
3315
+ let bucket;
3316
+ let amount = 0n;
3317
+ if (net > 0n && (transaction.activityType === "purchase" || transaction.activityType === "subscription")) {
3318
+ bucket = "bought";
3319
+ amount = net;
3320
+ } else if (net > 0n && EARNED_ACTIVITY_TYPES.has(transaction.activityType)) {
3321
+ bucket = "earned";
3322
+ amount = net;
3323
+ } else if (net < 0n && (transaction.activityType === "allocation" || transaction.activityType === "boost")) {
3324
+ bucket = "allocated";
3325
+ amount = -net;
3326
+ } else if (net > 0n && transaction.activityType === "reclaim") {
3327
+ bucket = "reclaimed";
3328
+ amount = net;
3329
+ } else if (net < 0n && transaction.activityType === "spend") {
3330
+ bucket = "spent";
3331
+ amount = -net;
3332
+ } else if (net < 0n && (transaction.activityType === "refund" || transaction.activityType === "chargeback" || transaction.activityType === "reversal")) {
3333
+ bucket = "reversed";
3334
+ amount = -net;
3335
+ }
3336
+ if (bucket !== void 0) {
3337
+ deltas.push({
3338
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
3339
+ walletId,
3340
+ bucket,
3341
+ amount: serializeTokenSubunits(amount)
3342
+ });
3343
+ }
3344
+ }
3345
+ return deltas;
3346
+ }
3347
+ function applyWalletLifetimeDeltas(current, walletId, deltas) {
3348
+ assertWalletLifetimeTotals(current);
3349
+ assertEconomyIdentifier(walletId, "walletId");
3350
+ const next = {
3351
+ bought: parseTokenSubunits(current.bought),
3352
+ earned: parseTokenSubunits(current.earned),
3353
+ allocated: parseTokenSubunits(current.allocated),
3354
+ reclaimed: parseTokenSubunits(current.reclaimed),
3355
+ spent: parseTokenSubunits(current.spent),
3356
+ reversed: parseTokenSubunits(current.reversed)
3357
+ };
3358
+ for (const delta of deltas) {
3359
+ economyAssert(
3360
+ delta.schemaVersion === ECONOMY_CONTRACT_VERSION && delta.walletId === walletId && [
3361
+ "bought",
3362
+ "earned",
3363
+ "allocated",
3364
+ "reclaimed",
3365
+ "spent",
3366
+ "reversed"
3367
+ ].includes(delta.bucket),
3368
+ "INVALID_CONTRACT",
3369
+ "Lifetime delta must target the requested wallet and a known bucket"
3370
+ );
3371
+ const amount = parseTokenSubunits(delta.amount);
3372
+ economyAssert(
3373
+ amount > 0n,
3374
+ "INVALID_AMOUNT",
3375
+ "Lifetime deltas must be positive"
3376
+ );
3377
+ next[delta.bucket] += amount;
3378
+ }
3379
+ const totals = {
3380
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
3381
+ bought: serializeTokenSubunits(next.bought),
3382
+ earned: serializeTokenSubunits(next.earned),
3383
+ allocated: serializeTokenSubunits(next.allocated),
3384
+ reclaimed: serializeTokenSubunits(next.reclaimed),
3385
+ spent: serializeTokenSubunits(next.spent),
3386
+ reversed: serializeTokenSubunits(next.reversed)
3387
+ };
3388
+ assertWalletLifetimeTotals(totals);
3389
+ return totals;
3390
+ }
3391
+ function applyTransactionToProjection(current, transaction) {
3392
+ assertBalancedTransaction(transaction);
3393
+ const next = { ...current };
3394
+ for (const posting of transaction.postings) {
3395
+ const prior = parseTokenSubunits(next[posting.accountId] ?? "0");
3396
+ next[posting.accountId] = serializeTokenSubunits(
3397
+ prior + parseTokenSubunits(posting.amount)
3398
+ );
3399
+ }
3400
+ return next;
3401
+ }
3402
+ function rebuildBalanceProjection(transactions) {
3403
+ let projection = {};
3404
+ const transactionIds = /* @__PURE__ */ new Set();
3405
+ const idempotencyKeys = /* @__PURE__ */ new Set();
3406
+ const providerEvents = /* @__PURE__ */ new Set();
3407
+ for (const transaction of transactions) {
3408
+ economyAssert(
3409
+ !transactionIds.has(transaction.transactionId),
3410
+ "DUPLICATE_TRANSACTION",
3411
+ "Transaction identifiers must be unique"
3412
+ );
3413
+ economyAssert(
3414
+ !idempotencyKeys.has(transaction.idempotencyKey),
3415
+ "DUPLICATE_TRANSACTION",
3416
+ "Idempotency keys must be unique"
3417
+ );
3418
+ if (transaction.providerEventId !== void 0) {
3419
+ economyAssert(
3420
+ !providerEvents.has(transaction.providerEventId),
3421
+ "DUPLICATE_TRANSACTION",
3422
+ "Provider event identifiers must be unique"
3423
+ );
3424
+ providerEvents.add(transaction.providerEventId);
3425
+ }
3426
+ transactionIds.add(transaction.transactionId);
3427
+ idempotencyKeys.add(transaction.idempotencyKey);
3428
+ projection = applyTransactionToProjection(projection, transaction);
3429
+ }
3430
+ return projection;
3431
+ }
3432
+ function assertNonNegativeAccounts(projection, protectedAccountIds) {
3433
+ for (const accountId of protectedAccountIds) {
3434
+ economyAssert(
3435
+ parseTokenSubunits(projection[accountId] ?? "0") >= 0n,
3436
+ "NEGATIVE_PROJECTION",
3437
+ "A protected economy account would become negative"
3438
+ );
3439
+ }
3440
+ }
3441
+ function createProjectionSnapshot(transactions, rebuiltAt) {
3442
+ parseIsoTimestamp(rebuiltAt);
3443
+ const last = transactions.at(-1);
3444
+ return {
3445
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
3446
+ balances: rebuildBalanceProjection(transactions),
3447
+ ...last === void 0 ? {} : { lastTransactionId: last.transactionId },
3448
+ rebuiltAt
3449
+ };
3450
+ }
3451
+
3452
+ // src/ports/query.ts
3453
+ var ACTIVITY_TYPES2 = /* @__PURE__ */ new Set([
3454
+ "purchase",
3455
+ "subscription",
3456
+ "rewarded-ad",
3457
+ "offerwall",
3458
+ "allocation",
3459
+ "boost",
3460
+ "reclaim",
3461
+ "spend",
3462
+ "hold",
3463
+ "refund",
3464
+ "chargeback",
3465
+ "adjustment",
3466
+ "reversal",
3467
+ "event",
3468
+ "competition"
3469
+ ]);
3470
+ var ACTIVITY_STATUSES2 = /* @__PURE__ */ new Set([
3471
+ "pending",
3472
+ "held",
3473
+ "settled",
3474
+ "reversed",
3475
+ "failed"
3476
+ ]);
3477
+ var ACTIVITY_SOURCES2 = /* @__PURE__ */ new Set([
3478
+ "shopify",
3479
+ "ayet",
3480
+ "bitlabs",
3481
+ "subscription",
3482
+ "event",
3483
+ "competition",
3484
+ "adjustment"
3485
+ ]);
3486
+ var PORTFOLIO_ROLES = /* @__PURE__ */ new Set([
3487
+ "household-treasury",
3488
+ "personal",
3489
+ "gameplay-allocation",
3490
+ "hold"
3491
+ ]);
3492
+ function hasControlCharacters2(value) {
3493
+ return [...value].some((character) => {
3494
+ const code = character.charCodeAt(0);
3495
+ return code <= 31 || code === 127;
3496
+ });
3497
+ }
3498
+ function assertSafeDisplayString(value, label) {
3499
+ economyAssert(
3500
+ typeof value === "string" && value.trim().length > 0 && value.length <= 128 && !hasControlCharacters2(value),
3501
+ "INVALID_CONTRACT",
3502
+ `${label} must be bounded and safe for display`
3503
+ );
3504
+ }
3505
+ function assertOpaqueCursor(value) {
3506
+ economyAssert(
3507
+ typeof value === "string" && value.length > 0 && value.length <= 512 && !hasControlCharacters2(value),
3508
+ "INVALID_CONTRACT",
3509
+ "Activity cursor must be a bounded opaque value"
3510
+ );
3511
+ }
3512
+ function assertUniqueValues(values, allowed, label) {
3513
+ const unique = new Set(values);
3514
+ economyAssert(
3515
+ values.length > 0 && values.length === unique.size && values.every((value) => allowed.has(value)),
3516
+ "INVALID_CONTRACT",
3517
+ `${label} must contain unique supported values`
3518
+ );
3519
+ }
3520
+ function assertWalletPortfolioReadScope(scope) {
3521
+ economyAssert(
3522
+ scope.schemaVersion === ECONOMY_CONTRACT_VERSION,
3523
+ "INVALID_CONTRACT",
3524
+ "Unsupported wallet-portfolio scope contract version"
3525
+ );
3526
+ assertEconomyIdentifier(scope.portfolioId, "portfolioId");
3527
+ assertEconomyIdentifier(scope.subjectAccountId, "subjectAccountId");
3528
+ economyAssert(
3529
+ scope.components.length > 0 && scope.components.length <= 100,
3530
+ "INVALID_CONTRACT",
3531
+ "Wallet portfolio must contain between one and 100 components"
3532
+ );
3533
+ const walletIds = /* @__PURE__ */ new Set();
3534
+ for (const component of scope.components) {
3535
+ assertEconomyIdentifier(component.walletId, "walletId");
3536
+ economyAssert(
3537
+ PORTFOLIO_ROLES.has(component.role),
3538
+ "INVALID_CONTRACT",
3539
+ "Wallet portfolio component has an unsupported role"
3540
+ );
3541
+ economyAssert(
3542
+ !walletIds.has(component.walletId),
3543
+ "DUPLICATE_IDENTIFIER",
3544
+ "Wallet portfolio cannot repeat a wallet"
3545
+ );
3546
+ walletIds.add(component.walletId);
3547
+ if (component.beneficiaryAccountId !== void 0) {
3548
+ assertEconomyIdentifier(
3549
+ component.beneficiaryAccountId,
3550
+ "beneficiaryAccountId"
3551
+ );
3552
+ }
3553
+ economyAssert(
3554
+ component.role !== "gameplay-allocation" || component.beneficiaryAccountId !== void 0,
3555
+ "INVALID_CONTRACT",
3556
+ "Gameplay-allocation portfolio components require a beneficiary"
3557
+ );
3558
+ }
3559
+ }
3560
+ function assertWalletLifetimeSnapshot(snapshot) {
3561
+ economyAssert(
3562
+ snapshot.schemaVersion === ECONOMY_CONTRACT_VERSION,
3563
+ "INVALID_CONTRACT",
3564
+ "Unsupported wallet-lifetime-snapshot contract version"
3565
+ );
3566
+ assertEconomyIdentifier(snapshot.walletId, "walletId");
3567
+ assertWalletLifetimeTotals(snapshot.totals);
3568
+ economyAssert(
3569
+ Number.isSafeInteger(snapshot.version) && snapshot.version >= 1,
3570
+ "INVALID_CONTRACT",
3571
+ "Wallet lifetime version must be a positive safe integer"
3572
+ );
3573
+ parseIsoTimestamp(snapshot.asOf);
3574
+ }
3575
+ function assertPortfolioComponentMatch(expected, actual) {
3576
+ economyAssert(
3577
+ expected.walletId === actual.walletId && expected.role === actual.role && expected.beneficiaryAccountId === actual.beneficiaryAccountId,
3578
+ "INVALID_CONTRACT",
3579
+ "Portfolio result component does not match its authorized read scope"
3580
+ );
3581
+ }
3582
+ function createWalletPortfolioSummary(scope, components, asOf) {
3583
+ assertWalletPortfolioReadScope(scope);
3584
+ parseIsoTimestamp(asOf);
3585
+ economyAssert(
3586
+ components.length === scope.components.length,
3587
+ "INVALID_CONTRACT",
3588
+ "Portfolio balance must include every authorized component exactly once"
3589
+ );
3590
+ const byWallet = new Map(components.map((item) => [item.walletId, item]));
3591
+ economyAssert(
3592
+ byWallet.size === components.length,
3593
+ "DUPLICATE_IDENTIFIER",
3594
+ "Portfolio balance cannot repeat a wallet"
3595
+ );
3596
+ const ordered = scope.components.map((expected) => {
3597
+ const actual = byWallet.get(expected.walletId);
3598
+ economyAssert(
3599
+ actual !== void 0,
3600
+ "INVALID_CONTRACT",
3601
+ "Portfolio balance is missing an authorized wallet"
3602
+ );
3603
+ assertPortfolioComponentMatch(expected, actual);
3604
+ assertDeterministicWalletBalanceSummary(actual.summary);
3605
+ economyAssert(
3606
+ actual.summary.walletId === actual.walletId && actual.summary.asOf === asOf,
3607
+ "INVALID_CONTRACT",
3608
+ "Portfolio component summary must identify its wallet and shared snapshot time"
3609
+ );
3610
+ return actual;
3611
+ });
3612
+ const totals = {
3613
+ available: serializeTokenSubunits(
3614
+ ordered.reduce(
3615
+ (sum, item) => sum + parseTokenSubunits(item.summary.available),
3616
+ 0n
3617
+ )
3618
+ ),
3619
+ reserved: serializeTokenSubunits(
3620
+ ordered.reduce(
3621
+ (sum, item) => sum + parseTokenSubunits(item.summary.reserved),
3622
+ 0n
3623
+ )
3624
+ ),
3625
+ held: serializeTokenSubunits(
3626
+ ordered.reduce(
3627
+ (sum, item) => sum + parseTokenSubunits(item.summary.held),
3628
+ 0n
3629
+ )
3630
+ ),
3631
+ rewardProgress: serializeTokenSubunits(
3632
+ ordered.reduce(
3633
+ (sum, item) => sum + parseTokenSubunits(item.summary.rewardProgress),
3634
+ 0n
3635
+ )
3636
+ )
3637
+ };
3638
+ return {
3639
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
3640
+ portfolioId: scope.portfolioId,
3641
+ subjectAccountId: scope.subjectAccountId,
3642
+ components: ordered,
3643
+ totals,
3644
+ asOf
3645
+ };
3646
+ }
3647
+ function assertWalletPortfolioSummary(summary) {
3648
+ const scope = {
3649
+ schemaVersion: summary.schemaVersion,
3650
+ portfolioId: summary.portfolioId,
3651
+ subjectAccountId: summary.subjectAccountId,
3652
+ components: summary.components.map(
3653
+ ({ walletId, role, beneficiaryAccountId }) => ({
3654
+ walletId,
3655
+ role,
3656
+ ...beneficiaryAccountId === void 0 ? {} : { beneficiaryAccountId }
3657
+ })
3658
+ )
3659
+ };
3660
+ const rebuilt = createWalletPortfolioSummary(
3661
+ scope,
3662
+ summary.components,
3663
+ summary.asOf
3664
+ );
3665
+ for (const field of [
3666
+ "available",
3667
+ "reserved",
3668
+ "held",
3669
+ "rewardProgress"
3670
+ ]) {
3671
+ economyAssert(
3672
+ summary.totals[field] === rebuilt.totals[field],
3673
+ "INVALID_CONTRACT",
3674
+ "Portfolio balance totals must equal their component column sums"
3675
+ );
3676
+ }
3677
+ }
3678
+ function createWalletPortfolioLifetime(scope, components, asOf) {
3679
+ assertWalletPortfolioReadScope(scope);
3680
+ parseIsoTimestamp(asOf);
3681
+ economyAssert(
3682
+ components.length === scope.components.length,
3683
+ "INVALID_CONTRACT",
3684
+ "Portfolio lifetime must include every authorized component exactly once"
3685
+ );
3686
+ const byWallet = new Map(components.map((item) => [item.walletId, item]));
3687
+ economyAssert(
3688
+ byWallet.size === components.length,
3689
+ "DUPLICATE_IDENTIFIER",
3690
+ "Portfolio lifetime cannot repeat a wallet"
3691
+ );
3692
+ const ordered = scope.components.map((expected) => {
3693
+ const actual = byWallet.get(expected.walletId);
3694
+ economyAssert(
3695
+ actual !== void 0,
3696
+ "INVALID_CONTRACT",
3697
+ "Portfolio lifetime is missing an authorized wallet"
3698
+ );
3699
+ assertPortfolioComponentMatch(expected, actual);
3700
+ assertWalletLifetimeSnapshot(actual.snapshot);
3701
+ economyAssert(
3702
+ actual.snapshot.walletId === actual.walletId && actual.snapshot.asOf === asOf,
3703
+ "INVALID_CONTRACT",
3704
+ "Portfolio lifetime component must identify its wallet and shared snapshot time"
2249
3705
  );
2250
- next[delta.bucket] += amount;
2251
- }
3706
+ return actual;
3707
+ });
3708
+ const sumField = (field) => serializeTokenSubunits(
3709
+ ordered.reduce(
3710
+ (sum, item) => sum + parseTokenSubunits(item.snapshot.totals[field]),
3711
+ 0n
3712
+ )
3713
+ );
2252
3714
  const totals = {
2253
3715
  schemaVersion: ECONOMY_CONTRACT_VERSION,
2254
- bought: serializeTokenSubunits(next.bought),
2255
- earned: serializeTokenSubunits(next.earned),
2256
- allocated: serializeTokenSubunits(next.allocated),
2257
- reclaimed: serializeTokenSubunits(next.reclaimed),
2258
- spent: serializeTokenSubunits(next.spent),
2259
- reversed: serializeTokenSubunits(next.reversed)
3716
+ bought: sumField("bought"),
3717
+ earned: sumField("earned"),
3718
+ allocated: sumField("allocated"),
3719
+ reclaimed: sumField("reclaimed"),
3720
+ spent: sumField("spent"),
3721
+ reversed: sumField("reversed")
2260
3722
  };
2261
3723
  assertWalletLifetimeTotals(totals);
2262
- return totals;
3724
+ return {
3725
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
3726
+ portfolioId: scope.portfolioId,
3727
+ subjectAccountId: scope.subjectAccountId,
3728
+ components: ordered,
3729
+ totals,
3730
+ asOf
3731
+ };
2263
3732
  }
2264
- function applyTransactionToProjection(current, transaction) {
2265
- assertBalancedTransaction(transaction);
2266
- const next = { ...current };
2267
- for (const posting of transaction.postings) {
2268
- const prior = parseTokenSubunits(next[posting.accountId] ?? "0");
2269
- next[posting.accountId] = serializeTokenSubunits(
2270
- prior + parseTokenSubunits(posting.amount)
3733
+ function assertWalletPortfolioLifetime(lifetime) {
3734
+ const scope = {
3735
+ schemaVersion: lifetime.schemaVersion,
3736
+ portfolioId: lifetime.portfolioId,
3737
+ subjectAccountId: lifetime.subjectAccountId,
3738
+ components: lifetime.components.map(
3739
+ ({ walletId, role, beneficiaryAccountId }) => ({
3740
+ walletId,
3741
+ role,
3742
+ ...beneficiaryAccountId === void 0 ? {} : { beneficiaryAccountId }
3743
+ })
3744
+ )
3745
+ };
3746
+ const rebuilt = createWalletPortfolioLifetime(
3747
+ scope,
3748
+ lifetime.components,
3749
+ lifetime.asOf
3750
+ );
3751
+ for (const field of [
3752
+ "bought",
3753
+ "earned",
3754
+ "allocated",
3755
+ "reclaimed",
3756
+ "spent",
3757
+ "reversed"
3758
+ ]) {
3759
+ economyAssert(
3760
+ lifetime.totals[field] === rebuilt.totals[field],
3761
+ "INVALID_CONTRACT",
3762
+ "Portfolio lifetime totals must equal their component column sums"
2271
3763
  );
2272
3764
  }
2273
- return next;
2274
3765
  }
2275
- function rebuildBalanceProjection(transactions) {
2276
- let projection = {};
2277
- const transactionIds = /* @__PURE__ */ new Set();
2278
- const idempotencyKeys = /* @__PURE__ */ new Set();
2279
- const providerEvents = /* @__PURE__ */ new Set();
2280
- for (const transaction of transactions) {
3766
+ function assertWalletActivityEntry(entry) {
3767
+ economyAssert(
3768
+ entry.schemaVersion === ECONOMY_CONTRACT_VERSION,
3769
+ "INVALID_CONTRACT",
3770
+ "Unsupported wallet-activity contract version"
3771
+ );
3772
+ assertEconomyIdentifier(entry.activityId, "activityId");
3773
+ assertEconomyIdentifier(entry.walletId, "walletId");
3774
+ economyAssert(
3775
+ ACTIVITY_TYPES2.has(entry.activityType) && ACTIVITY_STATUSES2.has(entry.status) && ACTIVITY_SOURCES2.has(entry.source),
3776
+ "INVALID_CONTRACT",
3777
+ "Wallet activity type, status, or source is unsupported"
3778
+ );
3779
+ parseIsoTimestamp(entry.occurredAt);
3780
+ economyAssert(
3781
+ parseTokenSubunits(entry.amount) !== 0n,
3782
+ "INVALID_AMOUNT",
3783
+ "Wallet activity amount cannot be zero"
3784
+ );
3785
+ if (entry.beneficiaryAccountId !== void 0) {
3786
+ assertEconomyIdentifier(
3787
+ entry.beneficiaryAccountId,
3788
+ "beneficiaryAccountId"
3789
+ );
3790
+ }
3791
+ if (entry.maskedReference !== void 0) {
3792
+ assertSafeDisplayString(entry.maskedReference, "Masked reference");
3793
+ }
3794
+ assertSafeDisplayString(entry.sourceLabel, "Source label");
3795
+ economyAssert(
3796
+ entry.entryKind === "economic" || entry.entryKind === "workflow",
3797
+ "INVALID_CONTRACT",
3798
+ "Wallet activity has an unsupported entry kind"
3799
+ );
3800
+ if (entry.entryKind === "economic") {
3801
+ assertEconomyIdentifier(entry.transactionId, "transactionId");
3802
+ economyAssert(
3803
+ entry.status === "held" || entry.status === "settled" || entry.status === "reversed",
3804
+ "INVALID_CONTRACT",
3805
+ "Economic activity cannot use a workflow status"
3806
+ );
3807
+ } else {
3808
+ assertEconomyIdentifier(entry.commandId, "commandId");
3809
+ economyAssert(
3810
+ entry.status === "pending" || entry.status === "failed",
3811
+ "INVALID_CONTRACT",
3812
+ "Workflow activity cannot use an economic status"
3813
+ );
3814
+ }
3815
+ }
3816
+ function assertWalletActivityPageRequest(request) {
3817
+ economyAssert(
3818
+ Number.isSafeInteger(request.limit) && request.limit >= 1 && request.limit <= 100,
3819
+ "INVALID_CONTRACT",
3820
+ "Activity page limit must be between one and 100"
3821
+ );
3822
+ if (request.cursor !== void 0) {
3823
+ assertOpaqueCursor(request.cursor);
3824
+ }
3825
+ const filter = request.filter;
3826
+ if (filter === void 0) {
3827
+ return;
3828
+ }
3829
+ if (filter.activityTypes !== void 0) {
3830
+ assertUniqueValues(filter.activityTypes, ACTIVITY_TYPES2, "Activity types");
3831
+ }
3832
+ if (filter.statuses !== void 0) {
3833
+ assertUniqueValues(filter.statuses, ACTIVITY_STATUSES2, "Activity statuses");
3834
+ }
3835
+ if (filter.sources !== void 0) {
3836
+ assertUniqueValues(filter.sources, ACTIVITY_SOURCES2, "Activity sources");
3837
+ }
3838
+ if (filter.beneficiaryAccountId !== void 0) {
3839
+ assertEconomyIdentifier(
3840
+ filter.beneficiaryAccountId,
3841
+ "beneficiaryAccountId"
3842
+ );
3843
+ }
3844
+ const from = filter.fromInclusive === void 0 ? void 0 : parseIsoTimestamp(filter.fromInclusive);
3845
+ const to = filter.toExclusive === void 0 ? void 0 : parseIsoTimestamp(filter.toExclusive);
3846
+ economyAssert(
3847
+ from === void 0 || to === void 0 || from < to,
3848
+ "INVALID_TIME_WINDOW",
3849
+ "Activity filter start must precede its exclusive end"
3850
+ );
3851
+ }
3852
+ function assertWalletActivityPage(page) {
3853
+ economyAssert(
3854
+ page.schemaVersion === ECONOMY_CONTRACT_VERSION && typeof page.hasMore === "boolean",
3855
+ "INVALID_CONTRACT",
3856
+ "Unsupported wallet-activity-page contract"
3857
+ );
3858
+ economyAssert(
3859
+ page.hasMore === (page.nextCursor !== void 0),
3860
+ "INVALID_CONTRACT",
3861
+ "Activity page cursor must be present exactly when more rows exist"
3862
+ );
3863
+ economyAssert(
3864
+ page.entries.length <= 100 && (!page.hasMore || page.entries.length > 0),
3865
+ "INVALID_CONTRACT",
3866
+ "Activity pages are bounded and cannot advertise more rows from an empty page"
3867
+ );
3868
+ if (page.nextCursor !== void 0) {
3869
+ assertOpaqueCursor(page.nextCursor);
3870
+ }
3871
+ const activityIds = /* @__PURE__ */ new Set();
3872
+ let previous;
3873
+ for (const entry of page.entries) {
3874
+ assertWalletActivityEntry(entry);
3875
+ economyAssert(
3876
+ !activityIds.has(entry.activityId),
3877
+ "DUPLICATE_IDENTIFIER",
3878
+ "Activity page cannot repeat an activity ID"
3879
+ );
3880
+ activityIds.add(entry.activityId);
3881
+ if (previous !== void 0) {
3882
+ const previousTime = parseIsoTimestamp(previous.occurredAt);
3883
+ const currentTime = parseIsoTimestamp(entry.occurredAt);
3884
+ economyAssert(
3885
+ previousTime > currentTime || previousTime === currentTime && previous.activityId.localeCompare(entry.activityId) > 0,
3886
+ "INVALID_CONTRACT",
3887
+ "Activity page must use stable descending time and ID order"
3888
+ );
3889
+ }
3890
+ previous = entry;
3891
+ }
3892
+ }
3893
+ function assertWalletActivityPageForPortfolio(page, scope) {
3894
+ assertWalletActivityPage(page);
3895
+ assertWalletPortfolioReadScope(scope);
3896
+ const walletIds = new Set(scope.components.map((item) => item.walletId));
3897
+ economyAssert(
3898
+ page.entries.every((entry) => walletIds.has(entry.walletId)),
3899
+ "INVALID_CONTRACT",
3900
+ "Portfolio activity contains a wallet outside its authorized read scope"
3901
+ );
3902
+ }
3903
+
3904
+ // src/purchase-caps.ts
3905
+ var MAX_SIGNED_BIGINT = 2n ** 63n - 1n;
3906
+ var CANONICAL_NON_NEGATIVE_INTEGER = /^(?:0|[1-9][0-9]*)$/u;
3907
+ function parseGbpMinorUnits(value) {
3908
+ economyAssert(
3909
+ typeof value === "string" && CANONICAL_NON_NEGATIVE_INTEGER.test(value),
3910
+ "INVALID_AMOUNT",
3911
+ "GBP minor units must be a canonical non-negative integer string"
3912
+ );
3913
+ try {
3914
+ const amount = BigInt(value);
3915
+ economyAssert(
3916
+ amount <= MAX_SIGNED_BIGINT,
3917
+ "AMOUNT_OUT_OF_RANGE",
3918
+ "GBP minor units are outside the signed 64-bit range"
3919
+ );
3920
+ return amount;
3921
+ } catch (error) {
3922
+ if (error instanceof EconomyError) {
3923
+ throw error;
3924
+ }
3925
+ throw new EconomyError("INVALID_AMOUNT", "GBP minor units are invalid");
3926
+ }
3927
+ }
3928
+ function serializeGbpMinorUnits(amount) {
3929
+ economyAssert(
3930
+ amount >= 0n && amount <= MAX_SIGNED_BIGINT,
3931
+ "AMOUNT_OUT_OF_RANGE",
3932
+ "GBP minor units are outside the signed 64-bit range"
3933
+ );
3934
+ return amount.toString(10);
3935
+ }
3936
+ var RESERVATION_STATUSES = [
3937
+ "reserved",
3938
+ "settled",
3939
+ "released",
3940
+ "expired"
3941
+ ];
3942
+ var TRANSITION_PRECEDENCE = {
3943
+ settle: 0,
3944
+ release: 1,
3945
+ expire: 2
3946
+ };
3947
+ function assertPurchaseCapReservationTransition(transition) {
3948
+ economyAssert(
3949
+ transition.schemaVersion === ECONOMY_CONTRACT_VERSION,
3950
+ "INVALID_CONTRACT",
3951
+ "Unsupported purchase-cap transition contract version"
3952
+ );
3953
+ assertEconomyIdentifier(transition.transitionId, "transitionId");
3954
+ assertEconomyIdentifier(transition.reservationId, "reservationId");
3955
+ economyAssert(
3956
+ ["settle", "release", "expire"].includes(transition.transitionType),
3957
+ "INVALID_CONTRACT",
3958
+ "Purchase-cap transition type is unsupported"
3959
+ );
3960
+ parseIsoTimestamp(transition.occurredAt);
3961
+ }
3962
+ function sameTransition(left, right) {
3963
+ return left.schemaVersion === right.schemaVersion && left.transitionId === right.transitionId && left.reservationId === right.reservationId && left.transitionType === right.transitionType && left.occurredAt === right.occurredAt;
3964
+ }
3965
+ function compareTransitions(left, right) {
3966
+ const timeDifference = parseIsoTimestamp(left.occurredAt) - parseIsoTimestamp(right.occurredAt);
3967
+ if (timeDifference !== 0) {
3968
+ return timeDifference;
3969
+ }
3970
+ const precedenceDifference = TRANSITION_PRECEDENCE[left.transitionType] - TRANSITION_PRECEDENCE[right.transitionType];
3971
+ return precedenceDifference === 0 ? compareUnicodeCodeUnits(left.transitionId, right.transitionId) : precedenceDifference;
3972
+ }
3973
+ function assertBinding(binding) {
3974
+ economyAssert(
3975
+ binding.schemaVersion === ECONOMY_CONTRACT_VERSION,
3976
+ "INVALID_CONTRACT",
3977
+ "Unsupported purchase-cap reservation contract version"
3978
+ );
3979
+ assertEconomyIdentifier(binding.reservationId, "reservationId");
3980
+ assertEconomyIdentifier(binding.payerAccountId, "payerAccountId");
3981
+ assertEconomyIdentifier(binding.householdId, "householdId");
3982
+ economyAssert(
3983
+ parseGbpMinorUnits(binding.priceMinorUnits) > 0n,
3984
+ "INVALID_AMOUNT",
3985
+ "Purchase-cap reservation amount must be positive"
3986
+ );
3987
+ economyAssert(
3988
+ parseIsoTimestamp(binding.expiresAt) > parseIsoTimestamp(binding.reservedAt),
3989
+ "INVALID_TIME_WINDOW",
3990
+ "Purchase-cap reservation expiry must follow its creation"
3991
+ );
3992
+ }
3993
+ function bindingFromReservation(reservation) {
3994
+ return {
3995
+ schemaVersion: reservation.schemaVersion,
3996
+ reservationId: reservation.reservationId,
3997
+ payerAccountId: reservation.payerAccountId,
3998
+ householdId: reservation.householdId,
3999
+ priceMinorUnits: reservation.priceMinorUnits,
4000
+ reservedAt: reservation.reservedAt,
4001
+ expiresAt: reservation.expiresAt
4002
+ };
4003
+ }
4004
+ function projectReservation(binding, receipts) {
4005
+ assertBinding(binding);
4006
+ const reservedAt = parseIsoTimestamp(binding.reservedAt);
4007
+ const expiresAt = parseIsoTimestamp(binding.expiresAt);
4008
+ const transitionIds = /* @__PURE__ */ new Set();
4009
+ for (const receipt of receipts) {
2281
4010
  economyAssert(
2282
- !transactionIds.has(transaction.transactionId),
2283
- "DUPLICATE_TRANSACTION",
2284
- "Transaction identifiers must be unique"
4011
+ receipt.schemaVersion === ECONOMY_CONTRACT_VERSION,
4012
+ "INVALID_CONTRACT",
4013
+ "Unsupported purchase-cap transition receipt version"
2285
4014
  );
4015
+ assertPurchaseCapReservationTransition(receipt.transition);
2286
4016
  economyAssert(
2287
- !idempotencyKeys.has(transaction.idempotencyKey),
2288
- "DUPLICATE_TRANSACTION",
2289
- "Idempotency keys must be unique"
4017
+ receipt.transition.reservationId === binding.reservationId && !transitionIds.has(receipt.transition.transitionId),
4018
+ "DUPLICATE_IDENTIFIER",
4019
+ "Purchase-cap receipts must be unique and belong to the reservation"
2290
4020
  );
2291
- if (transaction.providerEventId !== void 0) {
2292
- economyAssert(
2293
- !providerEvents.has(transaction.providerEventId),
2294
- "DUPLICATE_TRANSACTION",
2295
- "Provider event identifiers must be unique"
2296
- );
2297
- providerEvents.add(transaction.providerEventId);
2298
- }
2299
- transactionIds.add(transaction.transactionId);
2300
- idempotencyKeys.add(transaction.idempotencyKey);
2301
- projection = applyTransactionToProjection(projection, transaction);
4021
+ transitionIds.add(receipt.transition.transitionId);
2302
4022
  }
2303
- return projection;
2304
- }
2305
- function assertNonNegativeAccounts(projection, protectedAccountIds) {
2306
- for (const accountId of protectedAccountIds) {
4023
+ let status = "reserved";
4024
+ let finalTransitionId;
4025
+ let finalizedAt;
4026
+ for (const receipt of [...receipts].sort(
4027
+ (left, right) => compareTransitions(left.transition, right.transition)
4028
+ )) {
4029
+ const transition = receipt.transition;
4030
+ const occurredAt = parseIsoTimestamp(transition.occurredAt);
2307
4031
  economyAssert(
2308
- parseTokenSubunits(projection[accountId] ?? "0") >= 0n,
2309
- "NEGATIVE_PROJECTION",
2310
- "A protected economy account would become negative"
4032
+ occurredAt >= reservedAt,
4033
+ "INVALID_TIME_WINDOW",
4034
+ "Purchase-cap transition cannot precede reservation"
2311
4035
  );
4036
+ if (transition.transitionType === "settle") {
4037
+ economyAssert(
4038
+ occurredAt < expiresAt,
4039
+ "INVALID_TIME_WINDOW",
4040
+ "Purchase-cap settlement must use payment time before expiry"
4041
+ );
4042
+ } else if (transition.transitionType === "release") {
4043
+ economyAssert(
4044
+ occurredAt < expiresAt,
4045
+ "INVALID_TIME_WINDOW",
4046
+ "Purchase-cap release must precede reservation expiry"
4047
+ );
4048
+ } else {
4049
+ economyAssert(
4050
+ occurredAt >= expiresAt,
4051
+ "INVALID_TIME_WINDOW",
4052
+ "Purchase-cap reservation cannot expire before its deadline"
4053
+ );
4054
+ }
4055
+ if (status === "reserved") {
4056
+ status = transition.transitionType === "settle" ? "settled" : transition.transitionType === "release" ? "released" : "expired";
4057
+ finalTransitionId = transition.transitionId;
4058
+ finalizedAt = transition.occurredAt;
4059
+ }
2312
4060
  }
2313
- }
2314
- function createProjectionSnapshot(transactions, rebuiltAt) {
2315
- parseIsoTimestamp(rebuiltAt);
2316
- const last = transactions.at(-1);
2317
4061
  return {
2318
- schemaVersion: ECONOMY_CONTRACT_VERSION,
2319
- balances: rebuildBalanceProjection(transactions),
2320
- ...last === void 0 ? {} : { lastTransactionId: last.transactionId },
2321
- rebuiltAt
4062
+ ...binding,
4063
+ status,
4064
+ ...finalTransitionId === void 0 ? {} : { finalTransitionId, finalizedAt },
4065
+ transitionReceipts: receipts
2322
4066
  };
2323
4067
  }
2324
-
2325
- // src/ports/query.ts
2326
- var ACTIVITY_TYPES2 = /* @__PURE__ */ new Set([
2327
- "purchase",
2328
- "subscription",
2329
- "rewarded-ad",
2330
- "offerwall",
2331
- "allocation",
2332
- "boost",
2333
- "reclaim",
2334
- "spend",
2335
- "hold",
2336
- "refund",
2337
- "chargeback",
2338
- "adjustment",
2339
- "reversal",
2340
- "event",
2341
- "competition"
2342
- ]);
2343
- var ACTIVITY_STATUSES2 = /* @__PURE__ */ new Set([
2344
- "pending",
2345
- "held",
2346
- "settled",
2347
- "reversed",
2348
- "failed"
2349
- ]);
2350
- var ACTIVITY_SOURCES2 = /* @__PURE__ */ new Set([
2351
- "shopify",
2352
- "ayet",
2353
- "bitlabs",
2354
- "subscription",
2355
- "event",
2356
- "competition",
2357
- "adjustment"
2358
- ]);
2359
- var PORTFOLIO_ROLES = /* @__PURE__ */ new Set([
2360
- "household-treasury",
2361
- "personal",
2362
- "gameplay-allocation",
2363
- "hold"
2364
- ]);
2365
- function hasControlCharacters2(value) {
2366
- return [...value].some((character) => {
2367
- const code = character.charCodeAt(0);
2368
- return code <= 31 || code === 127;
4068
+ function sameReservationFacts(left, right) {
4069
+ return left.schemaVersion === right.schemaVersion && left.reservationId === right.reservationId && left.payerAccountId === right.payerAccountId && left.householdId === right.householdId && left.priceMinorUnits === right.priceMinorUnits && left.status === right.status && left.reservedAt === right.reservedAt && left.expiresAt === right.expiresAt && left.finalTransitionId === right.finalTransitionId && left.finalizedAt === right.finalizedAt && left.transitionReceipts.length === right.transitionReceipts.length && left.transitionReceipts.every((receipt, index) => {
4070
+ const other = right.transitionReceipts[index];
4071
+ return other !== void 0 && receipt.schemaVersion === other.schemaVersion && sameTransition(receipt.transition, other.transition);
2369
4072
  });
2370
4073
  }
2371
- function assertSafeDisplayString(value, label) {
2372
- economyAssert(
2373
- typeof value === "string" && value.trim().length > 0 && value.length <= 128 && !hasControlCharacters2(value),
2374
- "INVALID_CONTRACT",
2375
- `${label} must be bounded and safe for display`
2376
- );
4074
+ function sameReservationBinding(reservation, proposed) {
4075
+ return reservation.schemaVersion === proposed.schemaVersion && reservation.reservationId === proposed.reservationId && reservation.payerAccountId === proposed.payerAccountId && reservation.householdId === proposed.householdId && reservation.priceMinorUnits === proposed.priceMinorUnits && reservation.reservedAt === proposed.reservedAt && reservation.expiresAt === proposed.expiresAt;
2377
4076
  }
2378
- function assertOpaqueCursor(value) {
4077
+ function assertPurchaseCapReservation(reservation) {
2379
4078
  economyAssert(
2380
- typeof value === "string" && value.length > 0 && value.length <= 512 && !hasControlCharacters2(value),
4079
+ RESERVATION_STATUSES.includes(reservation.status),
2381
4080
  "INVALID_CONTRACT",
2382
- "Activity cursor must be a bounded opaque value"
4081
+ "Purchase-cap reservation status is unsupported"
4082
+ );
4083
+ const projected = projectReservation(
4084
+ bindingFromReservation(reservation),
4085
+ reservation.transitionReceipts
2383
4086
  );
2384
- }
2385
- function assertUniqueValues(values, allowed, label) {
2386
- const unique = new Set(values);
2387
4087
  economyAssert(
2388
- values.length > 0 && values.length === unique.size && values.every((value) => allowed.has(value)),
4088
+ sameReservationFacts(projected, reservation),
2389
4089
  "INVALID_CONTRACT",
2390
- `${label} must contain unique supported values`
4090
+ "Purchase-cap reservation does not match its immutable event rebuild"
2391
4091
  );
2392
4092
  }
2393
- function assertWalletPortfolioReadScope(scope) {
4093
+ function assertRollingPurchaseCapState(state) {
2394
4094
  economyAssert(
2395
- scope.schemaVersion === ECONOMY_CONTRACT_VERSION,
4095
+ state.schemaVersion === ECONOMY_CONTRACT_VERSION,
2396
4096
  "INVALID_CONTRACT",
2397
- "Unsupported wallet-portfolio scope contract version"
4097
+ "Unsupported rolling purchase-cap contract version"
2398
4098
  );
2399
- assertEconomyIdentifier(scope.portfolioId, "portfolioId");
2400
- assertEconomyIdentifier(scope.subjectAccountId, "subjectAccountId");
2401
4099
  economyAssert(
2402
- scope.components.length > 0 && scope.components.length <= 100,
4100
+ state.scopeType === "payer" || state.scopeType === "household",
2403
4101
  "INVALID_CONTRACT",
2404
- "Wallet portfolio must contain between one and 100 components"
2405
- );
2406
- const walletIds = /* @__PURE__ */ new Set();
2407
- for (const component of scope.components) {
2408
- assertEconomyIdentifier(component.walletId, "walletId");
4102
+ "Purchase-cap scope type is unsupported"
4103
+ );
4104
+ assertEconomyIdentifier(state.scopeId, "scopeId");
4105
+ const reservationIds = /* @__PURE__ */ new Set();
4106
+ const transitionIds = /* @__PURE__ */ new Set();
4107
+ let receiptCount = 0;
4108
+ for (const reservation of state.reservations) {
4109
+ assertPurchaseCapReservation(reservation);
2409
4110
  economyAssert(
2410
- PORTFOLIO_ROLES.has(component.role),
2411
- "INVALID_CONTRACT",
2412
- "Wallet portfolio component has an unsupported role"
2413
- );
2414
- economyAssert(
2415
- !walletIds.has(component.walletId),
4111
+ !reservationIds.has(reservation.reservationId),
2416
4112
  "DUPLICATE_IDENTIFIER",
2417
- "Wallet portfolio cannot repeat a wallet"
4113
+ "Purchase-cap state cannot repeat a reservation"
2418
4114
  );
2419
- walletIds.add(component.walletId);
2420
- if (component.beneficiaryAccountId !== void 0) {
2421
- assertEconomyIdentifier(
2422
- component.beneficiaryAccountId,
2423
- "beneficiaryAccountId"
2424
- );
2425
- }
4115
+ reservationIds.add(reservation.reservationId);
2426
4116
  economyAssert(
2427
- component.role !== "gameplay-allocation" || component.beneficiaryAccountId !== void 0,
4117
+ state.scopeType === "payer" ? reservation.payerAccountId === state.scopeId : reservation.householdId === state.scopeId,
2428
4118
  "INVALID_CONTRACT",
2429
- "Gameplay-allocation portfolio components require a beneficiary"
4119
+ "Purchase-cap reservation belongs to another aggregate scope"
2430
4120
  );
4121
+ for (const receipt of reservation.transitionReceipts) {
4122
+ economyAssert(
4123
+ !transitionIds.has(receipt.transition.transitionId),
4124
+ "DUPLICATE_IDENTIFIER",
4125
+ "Purchase-cap state cannot repeat a transition ID"
4126
+ );
4127
+ transitionIds.add(receipt.transition.transitionId);
4128
+ receiptCount += 1;
4129
+ }
2431
4130
  }
2432
- }
2433
- function assertWalletLifetimeSnapshot(snapshot) {
2434
- economyAssert(
2435
- snapshot.schemaVersion === ECONOMY_CONTRACT_VERSION,
2436
- "INVALID_CONTRACT",
2437
- "Unsupported wallet-lifetime-snapshot contract version"
2438
- );
2439
- assertEconomyIdentifier(snapshot.walletId, "walletId");
2440
- assertWalletLifetimeTotals(snapshot.totals);
2441
4131
  economyAssert(
2442
- Number.isSafeInteger(snapshot.version) && snapshot.version >= 1,
4132
+ Number.isSafeInteger(state.version) && state.version === 1 + state.reservations.length + receiptCount,
2443
4133
  "INVALID_CONTRACT",
2444
- "Wallet lifetime version must be a positive safe integer"
4134
+ "Purchase-cap state version must match reservation and receipt counts"
2445
4135
  );
2446
- parseIsoTimestamp(snapshot.asOf);
2447
4136
  }
2448
- function assertPortfolioComponentMatch(expected, actual) {
4137
+ function assertAggregatePair(payerState, householdState, payerAccountId, householdId) {
4138
+ assertRollingPurchaseCapState(payerState);
4139
+ assertRollingPurchaseCapState(householdState);
2449
4140
  economyAssert(
2450
- expected.walletId === actual.walletId && expected.role === actual.role && expected.beneficiaryAccountId === actual.beneficiaryAccountId,
4141
+ payerState.scopeType === "payer" && payerState.scopeId === payerAccountId && householdState.scopeType === "household" && householdState.scopeId === householdId,
2451
4142
  "INVALID_CONTRACT",
2452
- "Portfolio result component does not match its authorized read scope"
4143
+ "Purchase-cap aggregate pair does not match payer and household"
2453
4144
  );
2454
4145
  }
2455
- function createWalletPortfolioSummary(scope, components, asOf) {
2456
- assertWalletPortfolioReadScope(scope);
2457
- parseIsoTimestamp(asOf);
4146
+ function findMirroredReservation(payerState, householdState, reservationId) {
4147
+ const payerReservation = payerState.reservations.find(
4148
+ (reservation) => reservation.reservationId === reservationId
4149
+ );
4150
+ const householdReservation = householdState.reservations.find(
4151
+ (reservation) => reservation.reservationId === reservationId
4152
+ );
2458
4153
  economyAssert(
2459
- components.length === scope.components.length,
4154
+ payerReservation === void 0 === (householdReservation === void 0),
2460
4155
  "INVALID_CONTRACT",
2461
- "Portfolio balance must include every authorized component exactly once"
4156
+ "Purchase-cap reservation must exist in both aggregate scopes atomically"
2462
4157
  );
2463
- const byWallet = new Map(components.map((item) => [item.walletId, item]));
4158
+ if (payerReservation === void 0 || householdReservation === void 0) {
4159
+ return void 0;
4160
+ }
2464
4161
  economyAssert(
2465
- byWallet.size === components.length,
2466
- "DUPLICATE_IDENTIFIER",
2467
- "Portfolio balance cannot repeat a wallet"
4162
+ sameReservationFacts(payerReservation, householdReservation),
4163
+ "INVALID_CONTRACT",
4164
+ "Payer and household purchase-cap reservations have diverged"
2468
4165
  );
2469
- const ordered = scope.components.map((expected) => {
2470
- const actual = byWallet.get(expected.walletId);
2471
- economyAssert(
2472
- actual !== void 0,
2473
- "INVALID_CONTRACT",
2474
- "Portfolio balance is missing an authorized wallet"
2475
- );
2476
- assertPortfolioComponentMatch(expected, actual);
2477
- assertDeterministicWalletBalanceSummary(actual.summary);
4166
+ return payerReservation;
4167
+ }
4168
+ function windowStart(checkedAt, rollingDays) {
4169
+ return checkedAt - rollingDays * 24 * 60 * 60 * 1e3;
4170
+ }
4171
+ function calculateRollingPurchaseCapUsage(state, checkedAt, policy) {
4172
+ assertRollingPurchaseCapState(state);
4173
+ assertPurchaseLimitPolicy(policy);
4174
+ const checkedAtValue = parseIsoTimestamp(checkedAt);
4175
+ const start = windowStart(checkedAtValue, policy.rollingDays);
4176
+ let total = 0n;
4177
+ for (const reservation of state.reservations) {
4178
+ if (reservation.status === "reserved" && parseIsoTimestamp(reservation.reservedAt) <= checkedAtValue && checkedAtValue < parseIsoTimestamp(reservation.expiresAt)) {
4179
+ total += parseGbpMinorUnits(reservation.priceMinorUnits);
4180
+ } else if (reservation.status === "settled" && reservation.finalizedAt !== void 0) {
4181
+ const settledAt = parseIsoTimestamp(reservation.finalizedAt);
4182
+ if (settledAt >= start && settledAt <= checkedAtValue) {
4183
+ total += parseGbpMinorUnits(reservation.priceMinorUnits);
4184
+ }
4185
+ }
2478
4186
  economyAssert(
2479
- actual.summary.walletId === actual.walletId && actual.summary.asOf === asOf,
2480
- "INVALID_CONTRACT",
2481
- "Portfolio component summary must identify its wallet and shared snapshot time"
4187
+ total <= MAX_SIGNED_BIGINT,
4188
+ "AMOUNT_OUT_OF_RANGE",
4189
+ "Rolling purchase-cap usage exceeds signed 64-bit range"
2482
4190
  );
2483
- return actual;
2484
- });
2485
- const totals = {
2486
- available: serializeTokenSubunits(
2487
- ordered.reduce(
2488
- (sum, item) => sum + parseTokenSubunits(item.summary.available),
2489
- 0n
2490
- )
2491
- ),
2492
- reserved: serializeTokenSubunits(
2493
- ordered.reduce(
2494
- (sum, item) => sum + parseTokenSubunits(item.summary.reserved),
2495
- 0n
2496
- )
2497
- ),
2498
- held: serializeTokenSubunits(
2499
- ordered.reduce(
2500
- (sum, item) => sum + parseTokenSubunits(item.summary.held),
2501
- 0n
2502
- )
2503
- ),
2504
- rewardProgress: serializeTokenSubunits(
2505
- ordered.reduce(
2506
- (sum, item) => sum + parseTokenSubunits(item.summary.rewardProgress),
2507
- 0n
2508
- )
2509
- )
2510
- };
4191
+ }
4192
+ return serializeGbpMinorUnits(total);
4193
+ }
4194
+ function createRollingPurchaseCapState(scopeType, scopeId) {
4195
+ economyAssert(
4196
+ scopeType === "payer" || scopeType === "household",
4197
+ "INVALID_CONTRACT",
4198
+ "Purchase-cap scope type is unsupported"
4199
+ );
4200
+ assertEconomyIdentifier(scopeId, "scopeId");
2511
4201
  return {
2512
4202
  schemaVersion: ECONOMY_CONTRACT_VERSION,
2513
- portfolioId: scope.portfolioId,
2514
- subjectAccountId: scope.subjectAccountId,
2515
- components: ordered,
2516
- totals,
2517
- asOf
4203
+ scopeType,
4204
+ scopeId,
4205
+ version: 1,
4206
+ reservations: []
2518
4207
  };
2519
4208
  }
2520
- function assertWalletPortfolioSummary(summary) {
2521
- const scope = {
2522
- schemaVersion: summary.schemaVersion,
2523
- portfolioId: summary.portfolioId,
2524
- subjectAccountId: summary.subjectAccountId,
2525
- components: summary.components.map(
2526
- ({ walletId, role, beneficiaryAccountId }) => ({
2527
- walletId,
2528
- role,
2529
- ...beneficiaryAccountId === void 0 ? {} : { beneficiaryAccountId }
2530
- })
4209
+ function sortedWithReservation(state, reservation) {
4210
+ return [...state.reservations, reservation].sort(
4211
+ (left, right) => compareUnicodeCodeUnits(left.reservationId, right.reservationId)
4212
+ );
4213
+ }
4214
+ function resultWithUsage(payerState, householdState, reservation, applied, stateChanged, checkedAt, policy) {
4215
+ return {
4216
+ payerState,
4217
+ householdState,
4218
+ reservation,
4219
+ applied,
4220
+ stateChanged,
4221
+ payerUsageMinorUnits: calculateRollingPurchaseCapUsage(
4222
+ payerState,
4223
+ checkedAt,
4224
+ policy
4225
+ ),
4226
+ householdUsageMinorUnits: calculateRollingPurchaseCapUsage(
4227
+ householdState,
4228
+ checkedAt,
4229
+ policy
2531
4230
  )
2532
4231
  };
2533
- const rebuilt = createWalletPortfolioSummary(
2534
- scope,
2535
- summary.components,
2536
- summary.asOf
4232
+ }
4233
+ function reserveRollingPurchaseCaps(payerState, householdState, command, policy) {
4234
+ economyAssert(
4235
+ command.schemaVersion === ECONOMY_CONTRACT_VERSION,
4236
+ "INVALID_CONTRACT",
4237
+ "Unsupported purchase-cap reservation command version"
2537
4238
  );
2538
- for (const field of [
2539
- "available",
2540
- "reserved",
2541
- "held",
2542
- "rewardProgress"
2543
- ]) {
4239
+ const binding = {
4240
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
4241
+ reservationId: command.reservationId,
4242
+ payerAccountId: command.payerAccountId,
4243
+ householdId: command.householdId,
4244
+ priceMinorUnits: command.priceMinorUnits,
4245
+ reservedAt: command.reservedAt,
4246
+ expiresAt: command.expiresAt
4247
+ };
4248
+ assertBinding(binding);
4249
+ assertAggregatePair(
4250
+ payerState,
4251
+ householdState,
4252
+ command.payerAccountId,
4253
+ command.householdId
4254
+ );
4255
+ assertPurchaseLimitPolicy(policy);
4256
+ const proposed = projectReservation(binding, []);
4257
+ const replay = findMirroredReservation(
4258
+ payerState,
4259
+ householdState,
4260
+ command.reservationId
4261
+ );
4262
+ if (replay !== void 0) {
2544
4263
  economyAssert(
2545
- summary.totals[field] === rebuilt.totals[field],
2546
- "INVALID_CONTRACT",
2547
- "Portfolio balance totals must equal their component column sums"
4264
+ sameReservationBinding(replay, proposed),
4265
+ "DUPLICATE_IDENTIFIER",
4266
+ "Purchase-cap reservation ID was reused with different facts"
4267
+ );
4268
+ return resultWithUsage(
4269
+ payerState,
4270
+ householdState,
4271
+ replay,
4272
+ false,
4273
+ false,
4274
+ replay.finalizedAt ?? command.reservedAt,
4275
+ policy
2548
4276
  );
2549
4277
  }
2550
- }
2551
- function createWalletPortfolioLifetime(scope, components, asOf) {
2552
- assertWalletPortfolioReadScope(scope);
2553
- parseIsoTimestamp(asOf);
2554
4278
  economyAssert(
2555
- components.length === scope.components.length,
4279
+ command.expectedPayerVersion === payerState.version && command.expectedHouseholdVersion === householdState.version,
2556
4280
  "INVALID_CONTRACT",
2557
- "Portfolio lifetime must include every authorized component exactly once"
4281
+ "Purchase-cap reservation has a stale aggregate version"
2558
4282
  );
2559
- const byWallet = new Map(components.map((item) => [item.walletId, item]));
4283
+ const amount = parseGbpMinorUnits(command.priceMinorUnits);
2560
4284
  economyAssert(
2561
- byWallet.size === components.length,
2562
- "DUPLICATE_IDENTIFIER",
2563
- "Portfolio lifetime cannot repeat a wallet"
4285
+ amount <= parseGbpMinorUnits(policy.maxOrderPriceMinorUnits),
4286
+ "INSUFFICIENT_BALANCE",
4287
+ "Purchase amount exceeds the per-order ceiling"
2564
4288
  );
2565
- const ordered = scope.components.map((expected) => {
2566
- const actual = byWallet.get(expected.walletId);
2567
- economyAssert(
2568
- actual !== void 0,
2569
- "INVALID_CONTRACT",
2570
- "Portfolio lifetime is missing an authorized wallet"
2571
- );
2572
- assertPortfolioComponentMatch(expected, actual);
2573
- assertWalletLifetimeSnapshot(actual.snapshot);
2574
- economyAssert(
2575
- actual.snapshot.walletId === actual.walletId && actual.snapshot.asOf === asOf,
2576
- "INVALID_CONTRACT",
2577
- "Portfolio lifetime component must identify its wallet and shared snapshot time"
2578
- );
2579
- return actual;
2580
- });
2581
- const sumField = (field) => serializeTokenSubunits(
2582
- ordered.reduce(
2583
- (sum, item) => sum + parseTokenSubunits(item.snapshot.totals[field]),
2584
- 0n
4289
+ const payerUsage = parseGbpMinorUnits(
4290
+ calculateRollingPurchaseCapUsage(payerState, command.reservedAt, policy)
4291
+ );
4292
+ const householdUsage = parseGbpMinorUnits(
4293
+ calculateRollingPurchaseCapUsage(
4294
+ householdState,
4295
+ command.reservedAt,
4296
+ policy
2585
4297
  )
2586
4298
  );
2587
- const totals = {
2588
- schemaVersion: ECONOMY_CONTRACT_VERSION,
2589
- bought: sumField("bought"),
2590
- earned: sumField("earned"),
2591
- allocated: sumField("allocated"),
2592
- reclaimed: sumField("reclaimed"),
2593
- spent: sumField("spent"),
2594
- reversed: sumField("reversed")
4299
+ economyAssert(
4300
+ payerUsage + amount <= parseGbpMinorUnits(policy.rollingPayerPriceMinorUnits),
4301
+ "INSUFFICIENT_BALANCE",
4302
+ "Purchase amount exceeds the rolling payer ceiling"
4303
+ );
4304
+ economyAssert(
4305
+ householdUsage + amount <= parseGbpMinorUnits(policy.rollingHouseholdPriceMinorUnits),
4306
+ "INSUFFICIENT_BALANCE",
4307
+ "Purchase amount exceeds the rolling household ceiling"
4308
+ );
4309
+ const nextPayer = {
4310
+ ...payerState,
4311
+ version: payerState.version + 1,
4312
+ reservations: sortedWithReservation(payerState, proposed)
2595
4313
  };
2596
- assertWalletLifetimeTotals(totals);
2597
- return {
2598
- schemaVersion: ECONOMY_CONTRACT_VERSION,
2599
- portfolioId: scope.portfolioId,
2600
- subjectAccountId: scope.subjectAccountId,
2601
- components: ordered,
2602
- totals,
2603
- asOf
4314
+ const nextHousehold = {
4315
+ ...householdState,
4316
+ version: householdState.version + 1,
4317
+ reservations: sortedWithReservation(householdState, proposed)
2604
4318
  };
4319
+ return resultWithUsage(
4320
+ nextPayer,
4321
+ nextHousehold,
4322
+ proposed,
4323
+ true,
4324
+ true,
4325
+ command.reservedAt,
4326
+ policy
4327
+ );
2605
4328
  }
2606
- function assertWalletPortfolioLifetime(lifetime) {
2607
- const scope = {
2608
- schemaVersion: lifetime.schemaVersion,
2609
- portfolioId: lifetime.portfolioId,
2610
- subjectAccountId: lifetime.subjectAccountId,
2611
- components: lifetime.components.map(
2612
- ({ walletId, role, beneficiaryAccountId }) => ({
2613
- walletId,
2614
- role,
2615
- ...beneficiaryAccountId === void 0 ? {} : { beneficiaryAccountId }
2616
- })
4329
+ function replaceReservation(state, replacement) {
4330
+ return {
4331
+ ...state,
4332
+ version: state.version + 1,
4333
+ reservations: state.reservations.map(
4334
+ (reservation) => reservation.reservationId === replacement.reservationId ? replacement : reservation
2617
4335
  )
2618
4336
  };
2619
- const rebuilt = createWalletPortfolioLifetime(
2620
- scope,
2621
- lifetime.components,
2622
- lifetime.asOf
4337
+ }
4338
+ function findTransitionById(state, transitionId) {
4339
+ for (const reservation of state.reservations) {
4340
+ const receipt = reservation.transitionReceipts.find(
4341
+ (candidate) => candidate.transition.transitionId === transitionId
4342
+ );
4343
+ if (receipt !== void 0) {
4344
+ return { reservationId: reservation.reservationId, transition: receipt.transition };
4345
+ }
4346
+ }
4347
+ return void 0;
4348
+ }
4349
+ function transitionRollingPurchaseCapReservation(payerState, householdState, transition, expectedPayerVersion, expectedHouseholdVersion, policy) {
4350
+ assertPurchaseCapReservationTransition(transition);
4351
+ assertRollingPurchaseCapState(payerState);
4352
+ assertRollingPurchaseCapState(householdState);
4353
+ const current = findMirroredReservation(
4354
+ payerState,
4355
+ householdState,
4356
+ transition.reservationId
2623
4357
  );
2624
- for (const field of [
2625
- "bought",
2626
- "earned",
2627
- "allocated",
2628
- "reclaimed",
2629
- "spent",
2630
- "reversed"
2631
- ]) {
4358
+ economyAssert(
4359
+ current !== void 0,
4360
+ "INVALID_CONTRACT",
4361
+ "Purchase-cap reservation does not exist in both aggregate scopes"
4362
+ );
4363
+ assertAggregatePair(
4364
+ payerState,
4365
+ householdState,
4366
+ current.payerAccountId,
4367
+ current.householdId
4368
+ );
4369
+ assertPurchaseLimitPolicy(policy);
4370
+ const payerExisting = findTransitionById(
4371
+ payerState,
4372
+ transition.transitionId
4373
+ );
4374
+ const householdExisting = findTransitionById(
4375
+ householdState,
4376
+ transition.transitionId
4377
+ );
4378
+ economyAssert(
4379
+ payerExisting === void 0 === (householdExisting === void 0),
4380
+ "INVALID_CONTRACT",
4381
+ "Purchase-cap transition must exist in both scopes atomically"
4382
+ );
4383
+ if (payerExisting !== void 0 && householdExisting !== void 0) {
2632
4384
  economyAssert(
2633
- lifetime.totals[field] === rebuilt.totals[field],
2634
- "INVALID_CONTRACT",
2635
- "Portfolio lifetime totals must equal their component column sums"
4385
+ payerExisting.reservationId === transition.reservationId && householdExisting.reservationId === transition.reservationId && sameTransition(payerExisting.transition, transition) && sameTransition(householdExisting.transition, transition),
4386
+ "DUPLICATE_IDENTIFIER",
4387
+ "Purchase-cap transition ID was reused with different facts"
4388
+ );
4389
+ return resultWithUsage(
4390
+ payerState,
4391
+ householdState,
4392
+ current,
4393
+ false,
4394
+ false,
4395
+ transition.occurredAt,
4396
+ policy
2636
4397
  );
2637
4398
  }
4399
+ economyAssert(
4400
+ expectedPayerVersion === payerState.version && expectedHouseholdVersion === householdState.version,
4401
+ "INVALID_CONTRACT",
4402
+ "Purchase-cap transition has a stale aggregate version"
4403
+ );
4404
+ const replacement = projectReservation(bindingFromReservation(current), [
4405
+ ...current.transitionReceipts,
4406
+ { schemaVersion: ECONOMY_CONTRACT_VERSION, transition }
4407
+ ]);
4408
+ const stateChanged = replacement.status !== current.status || replacement.finalTransitionId !== current.finalTransitionId || replacement.finalizedAt !== current.finalizedAt;
4409
+ const nextPayer = replaceReservation(payerState, replacement);
4410
+ const nextHousehold = replaceReservation(householdState, replacement);
4411
+ return resultWithUsage(
4412
+ nextPayer,
4413
+ nextHousehold,
4414
+ replacement,
4415
+ true,
4416
+ stateChanged,
4417
+ transition.occurredAt,
4418
+ policy
4419
+ );
2638
4420
  }
2639
- function assertWalletActivityEntry(entry) {
4421
+
4422
+ // src/purchase-intents.ts
4423
+ var TRANSITION_TYPES = [
4424
+ "checkout-bound",
4425
+ "payment-observed",
4426
+ "credit-recorded",
4427
+ "expire",
4428
+ "cancel",
4429
+ "dispute-opened",
4430
+ "dispute-won",
4431
+ "dispute-lost"
4432
+ ];
4433
+ var TRANSITION_PRECEDENCE2 = {
4434
+ "checkout-bound": 0,
4435
+ "payment-observed": 1,
4436
+ "credit-recorded": 2,
4437
+ "dispute-opened": 3,
4438
+ "dispute-won": 4,
4439
+ "dispute-lost": 4,
4440
+ cancel: 5,
4441
+ expire: 6
4442
+ };
4443
+ function assertCheckoutHash(value) {
2640
4444
  economyAssert(
2641
- entry.schemaVersion === ECONOMY_CONTRACT_VERSION,
4445
+ /^sha256:[a-f0-9]{64}$/u.test(value),
2642
4446
  "INVALID_CONTRACT",
2643
- "Unsupported wallet-activity contract version"
4447
+ "Checkout references must be stored as sanitized SHA-256 hashes"
2644
4448
  );
2645
- assertEconomyIdentifier(entry.activityId, "activityId");
2646
- assertEconomyIdentifier(entry.walletId, "walletId");
4449
+ }
4450
+ function assertPurchaseIntentTransition(transition) {
2647
4451
  economyAssert(
2648
- ACTIVITY_TYPES2.has(entry.activityType) && ACTIVITY_STATUSES2.has(entry.status) && ACTIVITY_SOURCES2.has(entry.source),
4452
+ transition.schemaVersion === ECONOMY_CONTRACT_VERSION,
2649
4453
  "INVALID_CONTRACT",
2650
- "Wallet activity type, status, or source is unsupported"
4454
+ "Unsupported purchase-intent transition contract version"
2651
4455
  );
2652
- parseIsoTimestamp(entry.occurredAt);
4456
+ assertEconomyIdentifier(transition.transitionId, "transitionId");
4457
+ assertEconomyIdentifier(transition.intentId, "intentId");
2653
4458
  economyAssert(
2654
- parseTokenSubunits(entry.amount) !== 0n,
2655
- "INVALID_AMOUNT",
2656
- "Wallet activity amount cannot be zero"
4459
+ TRANSITION_TYPES.includes(transition.transitionType),
4460
+ "INVALID_CONTRACT",
4461
+ "Purchase-intent transition type is unsupported"
2657
4462
  );
2658
- if (entry.beneficiaryAccountId !== void 0) {
2659
- assertEconomyIdentifier(
2660
- entry.beneficiaryAccountId,
2661
- "beneficiaryAccountId"
2662
- );
4463
+ parseIsoTimestamp(transition.occurredAt);
4464
+ if (transition.providerCheckoutReferenceHash !== void 0) {
4465
+ assertCheckoutHash(transition.providerCheckoutReferenceHash);
2663
4466
  }
2664
- if (entry.maskedReference !== void 0) {
2665
- assertSafeDisplayString(entry.maskedReference, "Masked reference");
4467
+ const requiresCheckoutHash = transition.transitionType === "checkout-bound" || transition.transitionType === "payment-observed";
4468
+ economyAssert(
4469
+ requiresCheckoutHash === (transition.providerCheckoutReferenceHash !== void 0),
4470
+ "INVALID_CONTRACT",
4471
+ "Only checkout and payment transitions must carry a checkout binding"
4472
+ );
4473
+ }
4474
+ function sameTransition2(left, right) {
4475
+ return left.schemaVersion === right.schemaVersion && left.transitionId === right.transitionId && left.intentId === right.intentId && left.transitionType === right.transitionType && left.occurredAt === right.occurredAt && left.providerCheckoutReferenceHash === right.providerCheckoutReferenceHash;
4476
+ }
4477
+ function sameIntent(left, right) {
4478
+ return left.schemaVersion === right.schemaVersion && left.intentId === right.intentId && left.payerAccountId === right.payerAccountId && left.receivingHouseholdId === right.receivingHouseholdId && left.receivingWalletId === right.receivingWalletId && left.packId === right.packId && left.catalogVersion === right.catalogVersion && left.expectedCurrency === right.expectedCurrency && left.expectedPriceMinorUnits === right.expectedPriceMinorUnits && left.grantAmount === right.grantAmount && left.status === right.status && left.createdAt === right.createdAt && left.expiresAt === right.expiresAt && left.providerCheckoutReferenceHash === right.providerCheckoutReferenceHash;
4479
+ }
4480
+ function compareTransitions2(left, right) {
4481
+ const timeDifference = parseIsoTimestamp(left.occurredAt) - parseIsoTimestamp(right.occurredAt);
4482
+ if (timeDifference !== 0) {
4483
+ return timeDifference;
2666
4484
  }
2667
- assertSafeDisplayString(entry.sourceLabel, "Source label");
4485
+ const precedenceDifference = TRANSITION_PRECEDENCE2[left.transitionType] - TRANSITION_PRECEDENCE2[right.transitionType];
4486
+ return precedenceDifference === 0 ? compareUnicodeCodeUnits(left.transitionId, right.transitionId) : precedenceDifference;
4487
+ }
4488
+ function assertMatchingCheckoutBinding(intent, checkoutHash) {
2668
4489
  economyAssert(
2669
- entry.entryKind === "economic" || entry.entryKind === "workflow",
4490
+ intent.providerCheckoutReferenceHash === void 0 || intent.providerCheckoutReferenceHash === checkoutHash,
2670
4491
  "INVALID_CONTRACT",
2671
- "Wallet activity has an unsupported entry kind"
4492
+ "Purchase-intent transition conflicts with its checkout binding"
2672
4493
  );
2673
- if (entry.entryKind === "economic") {
2674
- assertEconomyIdentifier(entry.transactionId, "transactionId");
4494
+ }
4495
+ function projectPurchaseIntent(initialIntent, transitions, pack) {
4496
+ assertPurchaseIntentBinding(initialIntent, pack);
4497
+ economyAssert(
4498
+ initialIntent.status === "created" && initialIntent.providerCheckoutReferenceHash === void 0,
4499
+ "INVALID_CONTRACT",
4500
+ "Purchase-intent lifecycle requires immutable created intent facts"
4501
+ );
4502
+ const createdAt = parseIsoTimestamp(initialIntent.createdAt);
4503
+ const expiresAt = parseIsoTimestamp(initialIntent.expiresAt);
4504
+ let intent = initialIntent;
4505
+ let disputeDisposition = "none";
4506
+ let creditTransitionId;
4507
+ const effectiveTransitionIds = [];
4508
+ for (const transition of [...transitions].sort(compareTransitions2)) {
4509
+ assertPurchaseIntentTransition(transition);
2675
4510
  economyAssert(
2676
- entry.status === "held" || entry.status === "settled" || entry.status === "reversed",
4511
+ transition.intentId === initialIntent.intentId,
2677
4512
  "INVALID_CONTRACT",
2678
- "Economic activity cannot use a workflow status"
4513
+ "Purchase-intent transition belongs to another intent"
2679
4514
  );
2680
- } else {
2681
- assertEconomyIdentifier(entry.commandId, "commandId");
4515
+ const eventTime = parseIsoTimestamp(transition.occurredAt);
2682
4516
  economyAssert(
2683
- entry.status === "pending" || entry.status === "failed",
2684
- "INVALID_CONTRACT",
2685
- "Workflow activity cannot use an economic status"
4517
+ eventTime >= createdAt,
4518
+ "INVALID_TIME_WINDOW",
4519
+ "Purchase-intent transition cannot precede intent creation"
2686
4520
  );
4521
+ let effective = false;
4522
+ if (transition.transitionType === "checkout-bound") {
4523
+ const checkoutHash = transition.providerCheckoutReferenceHash;
4524
+ assertMatchingCheckoutBinding(intent, checkoutHash);
4525
+ if (intent.status === "created") {
4526
+ economyAssert(
4527
+ eventTime < expiresAt,
4528
+ "INVALID_TIME_WINDOW",
4529
+ "Checkout cannot be bound after purchase-intent expiry"
4530
+ );
4531
+ intent = {
4532
+ ...intent,
4533
+ status: "checkout-created",
4534
+ providerCheckoutReferenceHash: checkoutHash
4535
+ };
4536
+ effective = true;
4537
+ } else {
4538
+ economyAssert(
4539
+ !["expired", "cancelled"].includes(intent.status),
4540
+ "INVALID_CONTRACT",
4541
+ "A terminal purchase intent cannot bind a checkout"
4542
+ );
4543
+ }
4544
+ } else if (transition.transitionType === "payment-observed") {
4545
+ const checkoutHash = transition.providerCheckoutReferenceHash;
4546
+ assertMatchingCheckoutBinding(intent, checkoutHash);
4547
+ economyAssert(
4548
+ eventTime < expiresAt,
4549
+ "INVALID_TIME_WINDOW",
4550
+ "Authoritative payment time must precede purchase-intent expiry"
4551
+ );
4552
+ if (intent.status === "created") {
4553
+ intent = {
4554
+ ...intent,
4555
+ status: "paid-unreconciled",
4556
+ providerCheckoutReferenceHash: checkoutHash
4557
+ };
4558
+ effective = true;
4559
+ } else if (intent.status === "checkout-created") {
4560
+ intent = { ...intent, status: "paid-unreconciled" };
4561
+ effective = true;
4562
+ } else {
4563
+ economyAssert(
4564
+ !["expired", "cancelled"].includes(intent.status),
4565
+ "INVALID_CONTRACT",
4566
+ "A terminal purchase intent cannot observe payment"
4567
+ );
4568
+ }
4569
+ } else if (transition.transitionType === "credit-recorded") {
4570
+ if (intent.status === "paid-unreconciled") {
4571
+ intent = { ...intent, status: "credited" };
4572
+ disputeDisposition = "none";
4573
+ creditTransitionId = transition.transitionId;
4574
+ effective = true;
4575
+ } else {
4576
+ economyAssert(
4577
+ intent.status === "credited" || intent.status === "disputed",
4578
+ "INVALID_CONTRACT",
4579
+ "Purchase intent cannot be credited before authoritative payment"
4580
+ );
4581
+ }
4582
+ } else if (transition.transitionType === "dispute-opened") {
4583
+ if (intent.status === "credited" && disputeDisposition === "none") {
4584
+ intent = { ...intent, status: "disputed" };
4585
+ disputeDisposition = "open";
4586
+ effective = true;
4587
+ } else {
4588
+ economyAssert(
4589
+ intent.status === "disputed" && disputeDisposition === "open",
4590
+ "INVALID_CONTRACT",
4591
+ "Dispute can open only once after purchase credit"
4592
+ );
4593
+ }
4594
+ } else if (transition.transitionType === "dispute-won") {
4595
+ if (intent.status === "disputed" && disputeDisposition === "open") {
4596
+ intent = { ...intent, status: "credited" };
4597
+ disputeDisposition = "won";
4598
+ effective = true;
4599
+ } else {
4600
+ economyAssert(
4601
+ intent.status === "credited" && disputeDisposition === "won",
4602
+ "INVALID_CONTRACT",
4603
+ "Dispute-win transition requires one open dispute"
4604
+ );
4605
+ }
4606
+ } else if (transition.transitionType === "dispute-lost") {
4607
+ if (intent.status === "disputed" && disputeDisposition === "open") {
4608
+ disputeDisposition = "lost";
4609
+ effective = true;
4610
+ } else {
4611
+ economyAssert(
4612
+ intent.status === "disputed" && disputeDisposition === "lost",
4613
+ "INVALID_CONTRACT",
4614
+ "Dispute-loss transition requires one open dispute"
4615
+ );
4616
+ }
4617
+ } else if (transition.transitionType === "cancel") {
4618
+ if (intent.status === "created" || intent.status === "checkout-created") {
4619
+ intent = { ...intent, status: "cancelled" };
4620
+ effective = true;
4621
+ }
4622
+ } else {
4623
+ economyAssert(
4624
+ eventTime >= expiresAt,
4625
+ "INVALID_TIME_WINDOW",
4626
+ "Expiry transition cannot precede purchase-intent expiry"
4627
+ );
4628
+ if (intent.status === "created" || intent.status === "checkout-created") {
4629
+ intent = { ...intent, status: "expired" };
4630
+ effective = true;
4631
+ }
4632
+ }
4633
+ if (effective) {
4634
+ effectiveTransitionIds.push(transition.transitionId);
4635
+ }
2687
4636
  }
4637
+ return {
4638
+ intent,
4639
+ disputeDisposition,
4640
+ effectiveTransitionIds,
4641
+ ...creditTransitionId === void 0 ? {} : { creditTransitionId }
4642
+ };
2688
4643
  }
2689
- function assertWalletActivityPageRequest(request) {
4644
+ function assertPurchaseIntentLifecycle(lifecycle, pack) {
2690
4645
  economyAssert(
2691
- Number.isSafeInteger(request.limit) && request.limit >= 1 && request.limit <= 100,
4646
+ lifecycle.schemaVersion === ECONOMY_CONTRACT_VERSION,
2692
4647
  "INVALID_CONTRACT",
2693
- "Activity page limit must be between one and 100"
4648
+ "Unsupported purchase-intent lifecycle contract version"
2694
4649
  );
2695
- if (request.cursor !== void 0) {
2696
- assertOpaqueCursor(request.cursor);
2697
- }
2698
- const filter = request.filter;
2699
- if (filter === void 0) {
2700
- return;
2701
- }
2702
- if (filter.activityTypes !== void 0) {
2703
- assertUniqueValues(filter.activityTypes, ACTIVITY_TYPES2, "Activity types");
2704
- }
2705
- if (filter.statuses !== void 0) {
2706
- assertUniqueValues(filter.statuses, ACTIVITY_STATUSES2, "Activity statuses");
2707
- }
2708
- if (filter.sources !== void 0) {
2709
- assertUniqueValues(filter.sources, ACTIVITY_SOURCES2, "Activity sources");
2710
- }
2711
- if (filter.beneficiaryAccountId !== void 0) {
2712
- assertEconomyIdentifier(
2713
- filter.beneficiaryAccountId,
2714
- "beneficiaryAccountId"
4650
+ economyAssert(
4651
+ Number.isSafeInteger(lifecycle.version) && lifecycle.version === lifecycle.receipts.length + 1,
4652
+ "INVALID_CONTRACT",
4653
+ "Purchase-intent lifecycle version must match its receipt count"
4654
+ );
4655
+ const transitionIds = /* @__PURE__ */ new Set();
4656
+ for (const receipt of lifecycle.receipts) {
4657
+ economyAssert(
4658
+ receipt.schemaVersion === ECONOMY_CONTRACT_VERSION,
4659
+ "INVALID_CONTRACT",
4660
+ "Purchase-intent receipt is invalid"
4661
+ );
4662
+ assertPurchaseIntentTransition(receipt.transition);
4663
+ economyAssert(
4664
+ receipt.transition.intentId === lifecycle.initialIntent.intentId && !transitionIds.has(receipt.transition.transitionId),
4665
+ "DUPLICATE_IDENTIFIER",
4666
+ "Purchase-intent receipts must be unique and belong to the intent"
2715
4667
  );
4668
+ transitionIds.add(receipt.transition.transitionId);
2716
4669
  }
2717
- const from = filter.fromInclusive === void 0 ? void 0 : parseIsoTimestamp(filter.fromInclusive);
2718
- const to = filter.toExclusive === void 0 ? void 0 : parseIsoTimestamp(filter.toExclusive);
2719
- economyAssert(
2720
- from === void 0 || to === void 0 || from < to,
2721
- "INVALID_TIME_WINDOW",
2722
- "Activity filter start must precede its exclusive end"
4670
+ const projection = projectPurchaseIntent(
4671
+ lifecycle.initialIntent,
4672
+ lifecycle.receipts.map((receipt) => receipt.transition),
4673
+ pack
2723
4674
  );
2724
- }
2725
- function assertWalletActivityPage(page) {
2726
4675
  economyAssert(
2727
- page.schemaVersion === ECONOMY_CONTRACT_VERSION && typeof page.hasMore === "boolean",
4676
+ sameIntent(projection.intent, lifecycle.intent) && lifecycle.creditRecorded === (projection.intent.status === "credited" || projection.intent.status === "disputed") && lifecycle.disputeDisposition === projection.disputeDisposition && lifecycle.effectiveTransitionIds.length === projection.effectiveTransitionIds.length && lifecycle.effectiveTransitionIds.every(
4677
+ (transitionId, index) => transitionId === projection.effectiveTransitionIds[index]
4678
+ ),
2728
4679
  "INVALID_CONTRACT",
2729
- "Unsupported wallet-activity-page contract"
4680
+ "Purchase-intent lifecycle does not match its immutable event rebuild"
2730
4681
  );
2731
- economyAssert(
2732
- page.hasMore === (page.nextCursor !== void 0),
2733
- "INVALID_CONTRACT",
2734
- "Activity page cursor must be present exactly when more rows exist"
4682
+ }
4683
+ function createPurchaseIntentLifecycle(initialIntent, pack) {
4684
+ const projection = projectPurchaseIntent(initialIntent, [], pack);
4685
+ return {
4686
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
4687
+ initialIntent,
4688
+ intent: projection.intent,
4689
+ version: 1,
4690
+ creditRecorded: false,
4691
+ disputeDisposition: projection.disputeDisposition,
4692
+ receipts: [],
4693
+ effectiveTransitionIds: []
4694
+ };
4695
+ }
4696
+ function sameProjection(lifecycle, projection) {
4697
+ return sameIntent(lifecycle.intent, projection.intent) && lifecycle.disputeDisposition === projection.disputeDisposition && lifecycle.effectiveTransitionIds.length === projection.effectiveTransitionIds.length && lifecycle.effectiveTransitionIds.every(
4698
+ (transitionId, index) => transitionId === projection.effectiveTransitionIds[index]
2735
4699
  );
4700
+ }
4701
+ function applyPurchaseIntentTransition(lifecycle, transition, expectedVersion, pack) {
4702
+ assertPurchaseIntentLifecycle(lifecycle, pack);
4703
+ assertPurchaseIntentTransition(transition);
2736
4704
  economyAssert(
2737
- page.entries.length <= 100 && (!page.hasMore || page.entries.length > 0),
4705
+ transition.intentId === lifecycle.initialIntent.intentId,
2738
4706
  "INVALID_CONTRACT",
2739
- "Activity pages are bounded and cannot advertise more rows from an empty page"
4707
+ "Purchase-intent transition belongs to another intent"
2740
4708
  );
2741
- if (page.nextCursor !== void 0) {
2742
- assertOpaqueCursor(page.nextCursor);
2743
- }
2744
- const activityIds = /* @__PURE__ */ new Set();
2745
- let previous;
2746
- for (const entry of page.entries) {
2747
- assertWalletActivityEntry(entry);
4709
+ const replay = lifecycle.receipts.find(
4710
+ (receipt) => receipt.transition.transitionId === transition.transitionId
4711
+ );
4712
+ if (replay !== void 0) {
2748
4713
  economyAssert(
2749
- !activityIds.has(entry.activityId),
4714
+ sameTransition2(replay.transition, transition),
2750
4715
  "DUPLICATE_IDENTIFIER",
2751
- "Activity page cannot repeat an activity ID"
4716
+ "Purchase-intent transition ID was reused with different facts"
2752
4717
  );
2753
- activityIds.add(entry.activityId);
2754
- if (previous !== void 0) {
2755
- const previousTime = parseIsoTimestamp(previous.occurredAt);
2756
- const currentTime = parseIsoTimestamp(entry.occurredAt);
4718
+ return { lifecycle, recorded: false, stateChanged: false };
4719
+ }
4720
+ economyAssert(
4721
+ expectedVersion === lifecycle.version,
4722
+ "INVALID_CONTRACT",
4723
+ "Purchase-intent transition has a stale expected version"
4724
+ );
4725
+ const receipts = [
4726
+ ...lifecycle.receipts,
4727
+ { schemaVersion: ECONOMY_CONTRACT_VERSION, transition }
4728
+ ];
4729
+ const projection = projectPurchaseIntent(
4730
+ lifecycle.initialIntent,
4731
+ receipts.map((receipt) => receipt.transition),
4732
+ pack
4733
+ );
4734
+ const stateChanged = !sameProjection(lifecycle, projection);
4735
+ const nextLifecycle = {
4736
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
4737
+ initialIntent: lifecycle.initialIntent,
4738
+ intent: projection.intent,
4739
+ version: lifecycle.version + 1,
4740
+ creditRecorded: projection.intent.status === "credited" || projection.intent.status === "disputed",
4741
+ disputeDisposition: projection.disputeDisposition,
4742
+ receipts,
4743
+ effectiveTransitionIds: projection.effectiveTransitionIds
4744
+ };
4745
+ assertPurchaseIntentLifecycle(nextLifecycle, pack);
4746
+ const producesCredit = projection.creditTransitionId === transition.transitionId && !lifecycle.creditRecorded && nextLifecycle.creditRecorded;
4747
+ const creditInstruction = producesCredit ? {
4748
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
4749
+ intentId: nextLifecycle.intent.intentId,
4750
+ transitionId: transition.transitionId,
4751
+ idempotencyKey: nextLifecycle.intent.intentId
4752
+ } : void 0;
4753
+ return {
4754
+ lifecycle: nextLifecycle,
4755
+ recorded: true,
4756
+ stateChanged,
4757
+ ...creditInstruction === void 0 ? {} : { creditInstruction }
4758
+ };
4759
+ }
4760
+ function reducePurchaseIntentTransitions(initialIntent, transitions, pack) {
4761
+ const unique = /* @__PURE__ */ new Map();
4762
+ for (const transition of transitions) {
4763
+ assertPurchaseIntentTransition(transition);
4764
+ const existing = unique.get(transition.transitionId);
4765
+ if (existing !== void 0) {
2757
4766
  economyAssert(
2758
- previousTime > currentTime || previousTime === currentTime && previous.activityId.localeCompare(entry.activityId) > 0,
2759
- "INVALID_CONTRACT",
2760
- "Activity page must use stable descending time and ID order"
4767
+ sameTransition2(existing, transition),
4768
+ "DUPLICATE_IDENTIFIER",
4769
+ "Purchase-intent transition ID was reused with different facts"
2761
4770
  );
4771
+ } else {
4772
+ unique.set(transition.transitionId, transition);
4773
+ }
4774
+ }
4775
+ let lifecycle = createPurchaseIntentLifecycle(initialIntent, pack);
4776
+ const creditInstructions = [];
4777
+ for (const transition of [...unique.values()].sort(compareTransitions2)) {
4778
+ const result = applyPurchaseIntentTransition(
4779
+ lifecycle,
4780
+ transition,
4781
+ lifecycle.version,
4782
+ pack
4783
+ );
4784
+ lifecycle = result.lifecycle;
4785
+ if (result.creditInstruction !== void 0) {
4786
+ creditInstructions.push(result.creditInstruction);
2762
4787
  }
2763
- previous = entry;
2764
4788
  }
2765
- }
2766
- function assertWalletActivityPageForPortfolio(page, scope) {
2767
- assertWalletActivityPage(page);
2768
- assertWalletPortfolioReadScope(scope);
2769
- const walletIds = new Set(scope.components.map((item) => item.walletId));
2770
4789
  economyAssert(
2771
- page.entries.every((entry) => walletIds.has(entry.walletId)),
2772
- "INVALID_CONTRACT",
2773
- "Portfolio activity contains a wallet outside its authorized read scope"
4790
+ creditInstructions.length <= 1,
4791
+ "DUPLICATE_TRANSACTION",
4792
+ "Purchase-intent reduction produced more than one credit"
2774
4793
  );
4794
+ return { lifecycle, creditInstructions };
2775
4795
  }
2776
4796
 
2777
4797
  // src/recurrence.ts
@@ -2907,11 +4927,14 @@ function assertSpendRequest(request) {
2907
4927
  EARLY_BACKER_SETTLEMENT_POLICY_V2,
2908
4928
  ECONOMY_CONTRACT_VERSION,
2909
4929
  EconomyError,
4930
+ MODULE_ALLOWANCE_PURPOSE,
2910
4931
  PRE_UTILITY_BACKER_COHORT,
2911
4932
  TOKEN_SUBUNITS_MAX,
2912
4933
  TOKEN_SUBUNITS_MIN,
2913
4934
  TOKEN_SUBUNITS_PER_TOKEN,
2914
4935
  advanceJournalChainHead,
4936
+ applyPaidLotLifecycleEvent,
4937
+ applyPurchaseIntentTransition,
2915
4938
  applySourceLotMovement,
2916
4939
  applyTransactionToProjection,
2917
4940
  applyWalletBalanceDelta,
@@ -2929,13 +4952,26 @@ function assertSpendRequest(request) {
2929
4952
  assertFxSnapshot,
2930
4953
  assertGameplayAllocation,
2931
4954
  assertJournalChainHead,
4955
+ assertModuleAllowance,
4956
+ assertModulePurchaseReceipt,
4957
+ assertModulePurchaseReconciliationObservation,
4958
+ assertModuleSpendHold,
4959
+ assertModuleSpendQuote,
2932
4960
  assertNonNegativeAccounts,
2933
4961
  assertOpenPurchaseIntent,
4962
+ assertPaidLotLifecycle,
4963
+ assertPaidLotLifecycleEvent,
4964
+ assertPaidLotPurchaseProvenance,
4965
+ assertPurchaseCapReservation,
4966
+ assertPurchaseCapReservationTransition,
2934
4967
  assertPurchaseIntentBinding,
4968
+ assertPurchaseIntentLifecycle,
4969
+ assertPurchaseIntentTransition,
2935
4970
  assertPurchaseLimitPolicy,
2936
4971
  assertReversalAvailable,
2937
4972
  assertRewardConversion,
2938
4973
  assertRewardRate,
4974
+ assertRollingPurchaseCapState,
2939
4975
  assertScopedPersistedIdempotencyResult,
2940
4976
  assertSourceLot,
2941
4977
  assertSourceLotMovement,
@@ -2961,13 +4997,23 @@ function assertSpendRequest(request) {
2961
4997
  assertWalletPortfolioSummary,
2962
4998
  availableSourceLotAmount,
2963
4999
  boostGameplayAllocation,
5000
+ calculateRollingPurchaseCapUsage,
2964
5001
  canUseSourceLot,
2965
5002
  canonicalTransactionPayload,
5003
+ closeModuleAllowance,
2966
5004
  convertRewardPayout,
5005
+ createEarlyBackerRetentionFromPaidLot,
2967
5006
  createGameplayAllocation,
2968
5007
  createInitialSourceLotSnapshot,
5008
+ createModuleAllowance,
5009
+ createModulePurchaseReceipt,
5010
+ createModuleSpendHold,
5011
+ createModuleSpendQuote,
5012
+ createPaidLotLifecycle,
2969
5013
  createProjectionSnapshot,
5014
+ createPurchaseIntentLifecycle,
2970
5015
  createReversalTransaction,
5016
+ createRollingPurchaseCapState,
2971
5017
  createWalletBalanceSummary,
2972
5018
  createWalletPortfolioLifetime,
2973
5019
  createWalletPortfolioSummary,
@@ -2976,18 +5022,29 @@ function assertSpendRequest(request) {
2976
5022
  economyAssert,
2977
5023
  evaluateEarlyBacker,
2978
5024
  evaluateEarlyBackerBySettlementV2,
5025
+ fundModuleAllowance,
2979
5026
  isWholeTokenAmount,
2980
5027
  mapBitLabsConversionState,
5028
+ parseGbpMinorUnits,
2981
5029
  parseIsoTimestamp,
2982
5030
  parseTokenSubunits,
2983
5031
  rebuildBalanceProjection,
2984
5032
  reclaimGameplayAllocation,
5033
+ reclaimModuleAllowance,
5034
+ reconcileModulePurchase,
5035
+ reducePaidLotLifecycleEvents,
5036
+ reducePurchaseIntentTransitions,
5037
+ releaseModuleSpendHold,
5038
+ reserveRollingPurchaseCaps,
2985
5039
  selectSourceLots,
5040
+ serializeGbpMinorUnits,
2986
5041
  serializeTokenSubunits,
5042
+ settleModuleSpendHold,
2987
5043
  sortStringRecord,
2988
5044
  subunitsToWholeTokens,
2989
5045
  sumTokenSubunits,
2990
5046
  toEconomyErrorEnvelope,
5047
+ transitionRollingPurchaseCapReservation,
2991
5048
  wholeTokensToSubunits
2992
5049
  });
2993
5050
  //# sourceMappingURL=index.cjs.map