@plasius/economy 0.3.0 → 0.3.2

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.js CHANGED
@@ -81,6 +81,14 @@ function sumTokenSubunits(values) {
81
81
  return total;
82
82
  }
83
83
 
84
+ // src/canonical-order.ts
85
+ function compareUnicodeCodeUnits(left, right) {
86
+ if (left === right) {
87
+ return 0;
88
+ }
89
+ return left < right ? -1 : 1;
90
+ }
91
+
84
92
  // src/contracts.ts
85
93
  var ECONOMY_CONTRACT_VERSION = "1";
86
94
  var IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
@@ -117,7 +125,9 @@ function parseIsoTimestamp(value) {
117
125
  }
118
126
  function sortStringRecord(value) {
119
127
  return Object.fromEntries(
120
- Object.entries(value).sort(([left], [right]) => left.localeCompare(right))
128
+ Object.entries(value).sort(
129
+ ([left], [right]) => compareUnicodeCodeUnits(left, right)
130
+ )
121
131
  );
122
132
  }
123
133
 
@@ -1047,7 +1057,9 @@ function assertEconomicJournalTransaction(transaction) {
1047
1057
  }
1048
1058
  function canonicalTransactionPayload(transaction) {
1049
1059
  assertBalancedTransaction(transaction);
1050
- const postings = [...transaction.postings].sort((left, right) => left.postingId.localeCompare(right.postingId)).map((posting) => ({
1060
+ const postings = [...transaction.postings].sort(
1061
+ (left, right) => compareUnicodeCodeUnits(left.postingId, right.postingId)
1062
+ ).map((posting) => ({
1051
1063
  schemaVersion: posting.schemaVersion,
1052
1064
  postingId: posting.postingId,
1053
1065
  transactionId: posting.transactionId,
@@ -1580,6 +1592,420 @@ function assertDistinctAdjustmentApproval(request) {
1580
1592
  }
1581
1593
  }
1582
1594
 
1595
+ // src/paid-lots.ts
1596
+ var ZERO_ARITHMETIC = Object.freeze({
1597
+ retainedDelta: serializeTokenSubunits(0n),
1598
+ heldDelta: serializeTokenSubunits(0n),
1599
+ reversedDelta: serializeTokenSubunits(0n),
1600
+ refundedDelta: serializeTokenSubunits(0n),
1601
+ chargebackDelta: serializeTokenSubunits(0n),
1602
+ oneTimeReversalDelta: serializeTokenSubunits(0n)
1603
+ });
1604
+ var EVENT_TYPES = [
1605
+ "refund",
1606
+ "dispute-hold",
1607
+ "dispute-won",
1608
+ "dispute-lost",
1609
+ "chargeback",
1610
+ "reversal"
1611
+ ];
1612
+ var EVENT_PRECEDENCE = {
1613
+ "dispute-hold": 0,
1614
+ "dispute-won": 1,
1615
+ "dispute-lost": 1,
1616
+ refund: 2,
1617
+ chargeback: 3,
1618
+ reversal: 4
1619
+ };
1620
+ function assertPaidLotPurchaseProvenance(provenance) {
1621
+ economyAssert(
1622
+ provenance.schemaVersion === ECONOMY_CONTRACT_VERSION,
1623
+ "INVALID_CONTRACT",
1624
+ "Unsupported paid-lot provenance contract version"
1625
+ );
1626
+ assertEconomyIdentifier(provenance.purchaseId, "purchaseId");
1627
+ assertEconomyIdentifier(provenance.catalogVersion, "catalogVersion");
1628
+ parseIsoTimestamp(provenance.purchasedAt);
1629
+ }
1630
+ function assertPaidLotLifecycleEvent(event) {
1631
+ economyAssert(
1632
+ event.schemaVersion === ECONOMY_CONTRACT_VERSION,
1633
+ "INVALID_CONTRACT",
1634
+ "Unsupported paid-lot lifecycle event contract version"
1635
+ );
1636
+ assertEconomyIdentifier(event.eventId, "eventId");
1637
+ assertEconomyIdentifier(event.lotId, "lotId");
1638
+ economyAssert(
1639
+ EVENT_TYPES.includes(event.eventType),
1640
+ "INVALID_CONTRACT",
1641
+ "Paid-lot lifecycle event type is unsupported"
1642
+ );
1643
+ economyAssert(
1644
+ parseTokenSubunits(event.amount) > 0n,
1645
+ "INVALID_AMOUNT",
1646
+ "Paid-lot lifecycle event amount must be positive"
1647
+ );
1648
+ parseIsoTimestamp(event.occurredAt);
1649
+ }
1650
+ function sameEvent(left, right) {
1651
+ 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;
1652
+ }
1653
+ function compareEvents(left, right) {
1654
+ const timeDifference = parseIsoTimestamp(left.occurredAt) - parseIsoTimestamp(right.occurredAt);
1655
+ if (timeDifference !== 0) {
1656
+ return timeDifference;
1657
+ }
1658
+ const precedenceDifference = EVENT_PRECEDENCE[left.eventType] - EVENT_PRECEDENCE[right.eventType];
1659
+ return precedenceDifference === 0 ? compareUnicodeCodeUnits(left.eventId, right.eventId) : precedenceDifference;
1660
+ }
1661
+ function deriveStatus(retained, held, reversed, chargeback) {
1662
+ if (held > 0n) {
1663
+ return "disputed";
1664
+ }
1665
+ if (chargeback > 0n) {
1666
+ return "chargeback-lost";
1667
+ }
1668
+ if (retained === 0n) {
1669
+ return "fully-reversed";
1670
+ }
1671
+ return reversed > 0n ? "partially-reversed" : "clear";
1672
+ }
1673
+ function immutableFactsFromLifecycle(lifecycle) {
1674
+ return {
1675
+ schemaVersion: lifecycle.schemaVersion,
1676
+ lotId: lifecycle.lotId,
1677
+ payerAccountId: lifecycle.payerAccountId,
1678
+ receivingHouseholdId: lifecycle.receivingHouseholdId,
1679
+ purchaseId: lifecycle.purchaseId,
1680
+ catalogVersion: lifecycle.catalogVersion,
1681
+ purchasedAt: lifecycle.purchasedAt,
1682
+ settledAt: lifecycle.settledAt,
1683
+ creditedAt: lifecycle.creditedAt,
1684
+ originalAmount: lifecycle.originalAmount
1685
+ };
1686
+ }
1687
+ function assertImmutableFacts(facts) {
1688
+ economyAssert(
1689
+ facts.schemaVersion === ECONOMY_CONTRACT_VERSION,
1690
+ "INVALID_CONTRACT",
1691
+ "Unsupported paid-lot lifecycle contract version"
1692
+ );
1693
+ assertEconomyIdentifier(facts.lotId, "lotId");
1694
+ assertEconomyIdentifier(facts.payerAccountId, "payerAccountId");
1695
+ assertEconomyIdentifier(
1696
+ facts.receivingHouseholdId,
1697
+ "receivingHouseholdId"
1698
+ );
1699
+ assertEconomyIdentifier(facts.purchaseId, "purchaseId");
1700
+ assertEconomyIdentifier(facts.catalogVersion, "catalogVersion");
1701
+ const purchasedAt = parseIsoTimestamp(facts.purchasedAt);
1702
+ const settledAt = parseIsoTimestamp(facts.settledAt);
1703
+ const creditedAt = parseIsoTimestamp(facts.creditedAt);
1704
+ economyAssert(
1705
+ purchasedAt <= settledAt && settledAt <= creditedAt,
1706
+ "INVALID_TIME_WINDOW",
1707
+ "Paid-lot timestamps must follow purchase, settlement, then credit order"
1708
+ );
1709
+ economyAssert(
1710
+ parseTokenSubunits(facts.originalAmount) > 0n,
1711
+ "INVALID_AMOUNT",
1712
+ "Paid-lot original amount must be positive"
1713
+ );
1714
+ }
1715
+ function projectPaidLot(facts, receipts) {
1716
+ assertImmutableFacts(facts);
1717
+ const eventIds = /* @__PURE__ */ new Set();
1718
+ let reversalReceipts = 0;
1719
+ for (const receipt of receipts) {
1720
+ economyAssert(
1721
+ receipt.schemaVersion === ECONOMY_CONTRACT_VERSION,
1722
+ "INVALID_CONTRACT",
1723
+ "Unsupported paid-lot receipt contract version"
1724
+ );
1725
+ assertPaidLotLifecycleEvent(receipt.event);
1726
+ economyAssert(
1727
+ receipt.event.lotId === facts.lotId && !eventIds.has(receipt.event.eventId),
1728
+ "DUPLICATE_IDENTIFIER",
1729
+ "Paid-lot receipts must be unique and belong to the lifecycle lot"
1730
+ );
1731
+ eventIds.add(receipt.event.eventId);
1732
+ if (receipt.event.eventType === "reversal") {
1733
+ reversalReceipts += 1;
1734
+ }
1735
+ }
1736
+ economyAssert(
1737
+ reversalReceipts <= 1,
1738
+ "REVERSAL_ALREADY_EXISTS",
1739
+ "Paid-lot lifecycle permits only one distinct reversal event"
1740
+ );
1741
+ let retained = parseTokenSubunits(facts.originalAmount);
1742
+ let held = 0n;
1743
+ let reversed = 0n;
1744
+ let refunded = 0n;
1745
+ let chargeback = 0n;
1746
+ let oneTimeReversal = 0n;
1747
+ const effectiveEventIds = [];
1748
+ for (const receipt of [...receipts].sort(
1749
+ (left, right) => compareEvents(left.event, right.event)
1750
+ )) {
1751
+ const event = receipt.event;
1752
+ economyAssert(
1753
+ parseIsoTimestamp(event.occurredAt) >= parseIsoTimestamp(facts.creditedAt),
1754
+ "INVALID_TIME_WINDOW",
1755
+ "Paid-lot lifecycle event cannot precede credit"
1756
+ );
1757
+ const amount = parseTokenSubunits(event.amount);
1758
+ if (event.eventType === "dispute-hold") {
1759
+ economyAssert(
1760
+ amount <= retained - held,
1761
+ "INSUFFICIENT_ELIGIBLE_LOTS",
1762
+ "Dispute hold exceeds unheld retained paid-lot basis"
1763
+ );
1764
+ held += amount;
1765
+ } else if (event.eventType === "dispute-won") {
1766
+ economyAssert(
1767
+ amount <= held,
1768
+ "INVALID_AMOUNT",
1769
+ "Dispute-win release exceeds held paid-lot basis"
1770
+ );
1771
+ held -= amount;
1772
+ } else if (event.eventType === "dispute-lost") {
1773
+ economyAssert(
1774
+ amount <= held,
1775
+ "INVALID_AMOUNT",
1776
+ "Dispute-loss amount exceeds held paid-lot basis"
1777
+ );
1778
+ held -= amount;
1779
+ retained -= amount;
1780
+ reversed += amount;
1781
+ chargeback += amount;
1782
+ } else {
1783
+ economyAssert(
1784
+ amount <= retained - held,
1785
+ "INSUFFICIENT_ELIGIBLE_LOTS",
1786
+ "Paid-lot reversal exceeds unheld retained basis"
1787
+ );
1788
+ if (event.eventType === "reversal") {
1789
+ oneTimeReversal += amount;
1790
+ } else if (event.eventType === "refund") {
1791
+ refunded += amount;
1792
+ } else {
1793
+ chargeback += amount;
1794
+ }
1795
+ retained -= amount;
1796
+ reversed += amount;
1797
+ }
1798
+ effectiveEventIds.push(event.eventId);
1799
+ }
1800
+ return {
1801
+ retained,
1802
+ held,
1803
+ reversed,
1804
+ refunded,
1805
+ chargeback,
1806
+ oneTimeReversal,
1807
+ status: deriveStatus(retained, held, reversed, chargeback),
1808
+ effectiveEventIds
1809
+ };
1810
+ }
1811
+ function projectionMatchesLifecycle(projection, lifecycle) {
1812
+ 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(
1813
+ (eventId, index) => eventId === lifecycle.effectiveEventIds[index]
1814
+ );
1815
+ }
1816
+ function assertPaidLotLifecycle(lifecycle) {
1817
+ economyAssert(
1818
+ Number.isSafeInteger(lifecycle.version) && lifecycle.version === lifecycle.receipts.length + 1,
1819
+ "INVALID_CONTRACT",
1820
+ "Paid-lot lifecycle version must match its receipt count"
1821
+ );
1822
+ const projection = projectPaidLot(
1823
+ immutableFactsFromLifecycle(lifecycle),
1824
+ lifecycle.receipts
1825
+ );
1826
+ economyAssert(
1827
+ projectionMatchesLifecycle(projection, lifecycle),
1828
+ "INVALID_CONTRACT",
1829
+ "Paid-lot lifecycle does not match its immutable event rebuild"
1830
+ );
1831
+ }
1832
+ function createPaidLotLifecycle(lot, provenance) {
1833
+ assertSourceLot(lot);
1834
+ assertPaidLotPurchaseProvenance(provenance);
1835
+ economyAssert(
1836
+ lot.payerAccountId !== void 0 && lot.householdId !== void 0,
1837
+ "INVALID_CONTRACT",
1838
+ "Paid-lot lifecycle requires payer and receiving-household provenance"
1839
+ );
1840
+ economyAssert(
1841
+ lot.refundState === "none" && parseTokenSubunits(lot.heldAmount) === 0n && parseTokenSubunits(lot.reversedAmount) === 0n,
1842
+ "INVALID_CONTRACT",
1843
+ "Paid-lot lifecycle must be created before any refund or dispute movement"
1844
+ );
1845
+ economyAssert(
1846
+ parseIsoTimestamp(provenance.purchasedAt) <= parseIsoTimestamp(lot.settledAt),
1847
+ "INVALID_TIME_WINDOW",
1848
+ "Paid purchase time cannot follow settlement"
1849
+ );
1850
+ const facts = {
1851
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
1852
+ lotId: lot.lotId,
1853
+ payerAccountId: lot.payerAccountId,
1854
+ receivingHouseholdId: lot.householdId,
1855
+ purchaseId: provenance.purchaseId,
1856
+ catalogVersion: provenance.catalogVersion,
1857
+ purchasedAt: provenance.purchasedAt,
1858
+ settledAt: lot.settledAt,
1859
+ creditedAt: lot.creditedAt,
1860
+ originalAmount: lot.originalAmount
1861
+ };
1862
+ const projection = projectPaidLot(facts, []);
1863
+ const lifecycle = {
1864
+ ...facts,
1865
+ retainedAmount: serializeTokenSubunits(projection.retained),
1866
+ heldAmount: serializeTokenSubunits(projection.held),
1867
+ reversedAmount: serializeTokenSubunits(projection.reversed),
1868
+ refundedAmount: serializeTokenSubunits(projection.refunded),
1869
+ chargebackAmount: serializeTokenSubunits(projection.chargeback),
1870
+ oneTimeReversalAmount: serializeTokenSubunits(
1871
+ projection.oneTimeReversal
1872
+ ),
1873
+ status: projection.status,
1874
+ version: 1,
1875
+ receipts: [],
1876
+ effectiveEventIds: []
1877
+ };
1878
+ assertPaidLotLifecycle(lifecycle);
1879
+ return lifecycle;
1880
+ }
1881
+ function applyPaidLotLifecycleEvent(lifecycle, event, expectedVersion) {
1882
+ assertPaidLotLifecycle(lifecycle);
1883
+ assertPaidLotLifecycleEvent(event);
1884
+ economyAssert(
1885
+ event.lotId === lifecycle.lotId,
1886
+ "INVALID_CONTRACT",
1887
+ "Paid-lot event belongs to another source lot"
1888
+ );
1889
+ const replay = lifecycle.receipts.find(
1890
+ (receipt) => receipt.event.eventId === event.eventId
1891
+ );
1892
+ if (replay !== void 0) {
1893
+ economyAssert(
1894
+ sameEvent(replay.event, event),
1895
+ "DUPLICATE_IDENTIFIER",
1896
+ "Paid-lot event ID was reused with different facts"
1897
+ );
1898
+ return {
1899
+ lifecycle,
1900
+ applied: false,
1901
+ stateChanged: false,
1902
+ arithmetic: ZERO_ARITHMETIC
1903
+ };
1904
+ }
1905
+ economyAssert(
1906
+ expectedVersion === lifecycle.version,
1907
+ "INVALID_CONTRACT",
1908
+ "Paid-lot lifecycle event has a stale expected version"
1909
+ );
1910
+ const receipts = [
1911
+ ...lifecycle.receipts,
1912
+ { schemaVersion: ECONOMY_CONTRACT_VERSION, event }
1913
+ ];
1914
+ const projection = projectPaidLot(
1915
+ immutableFactsFromLifecycle(lifecycle),
1916
+ receipts
1917
+ );
1918
+ const before = {
1919
+ retained: parseTokenSubunits(lifecycle.retainedAmount),
1920
+ held: parseTokenSubunits(lifecycle.heldAmount),
1921
+ reversed: parseTokenSubunits(lifecycle.reversedAmount),
1922
+ refunded: parseTokenSubunits(lifecycle.refundedAmount),
1923
+ chargeback: parseTokenSubunits(lifecycle.chargebackAmount),
1924
+ oneTimeReversal: parseTokenSubunits(lifecycle.oneTimeReversalAmount)
1925
+ };
1926
+ const next = {
1927
+ ...lifecycle,
1928
+ retainedAmount: serializeTokenSubunits(projection.retained),
1929
+ heldAmount: serializeTokenSubunits(projection.held),
1930
+ reversedAmount: serializeTokenSubunits(projection.reversed),
1931
+ refundedAmount: serializeTokenSubunits(projection.refunded),
1932
+ chargebackAmount: serializeTokenSubunits(projection.chargeback),
1933
+ oneTimeReversalAmount: serializeTokenSubunits(
1934
+ projection.oneTimeReversal
1935
+ ),
1936
+ status: projection.status,
1937
+ version: lifecycle.version + 1,
1938
+ receipts,
1939
+ effectiveEventIds: projection.effectiveEventIds
1940
+ };
1941
+ assertPaidLotLifecycle(next);
1942
+ const arithmetic = {
1943
+ retainedDelta: serializeTokenSubunits(
1944
+ projection.retained - before.retained
1945
+ ),
1946
+ heldDelta: serializeTokenSubunits(projection.held - before.held),
1947
+ reversedDelta: serializeTokenSubunits(
1948
+ projection.reversed - before.reversed
1949
+ ),
1950
+ refundedDelta: serializeTokenSubunits(
1951
+ projection.refunded - before.refunded
1952
+ ),
1953
+ chargebackDelta: serializeTokenSubunits(
1954
+ projection.chargeback - before.chargeback
1955
+ ),
1956
+ oneTimeReversalDelta: serializeTokenSubunits(
1957
+ projection.oneTimeReversal - before.oneTimeReversal
1958
+ )
1959
+ };
1960
+ return {
1961
+ lifecycle: next,
1962
+ applied: true,
1963
+ stateChanged: Object.values(arithmetic).some((delta) => delta !== "0"),
1964
+ arithmetic
1965
+ };
1966
+ }
1967
+ function reducePaidLotLifecycleEvents(initial, events) {
1968
+ assertPaidLotLifecycle(initial);
1969
+ const unique = /* @__PURE__ */ new Map();
1970
+ for (const event of events) {
1971
+ assertPaidLotLifecycleEvent(event);
1972
+ const existing = unique.get(event.eventId);
1973
+ if (existing !== void 0) {
1974
+ economyAssert(
1975
+ sameEvent(existing, event),
1976
+ "DUPLICATE_IDENTIFIER",
1977
+ "Paid-lot event ID was reused with different facts"
1978
+ );
1979
+ } else {
1980
+ unique.set(event.eventId, event);
1981
+ }
1982
+ }
1983
+ let lifecycle = initial;
1984
+ for (const event of [...unique.values()].sort(compareEvents)) {
1985
+ lifecycle = applyPaidLotLifecycleEvent(
1986
+ lifecycle,
1987
+ event,
1988
+ lifecycle.version
1989
+ ).lifecycle;
1990
+ }
1991
+ return lifecycle;
1992
+ }
1993
+ function createEarlyBackerRetentionFromPaidLot(lifecycle) {
1994
+ assertPaidLotLifecycle(lifecycle);
1995
+ return {
1996
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
1997
+ lotId: lifecycle.lotId,
1998
+ payerAccountId: lifecycle.payerAccountId,
1999
+ receivingHouseholdId: lifecycle.receivingHouseholdId,
2000
+ purchaseId: lifecycle.purchaseId,
2001
+ catalogVersion: lifecycle.catalogVersion,
2002
+ purchasedAt: lifecycle.purchasedAt,
2003
+ settledAt: lifecycle.settledAt,
2004
+ creditedAt: lifecycle.creditedAt,
2005
+ retainedAmount: lifecycle.retainedAmount
2006
+ };
2007
+ }
2008
+
1583
2009
  // src/ports/persistence.ts
1584
2010
  function assertScopedPersistedIdempotencyResult(result) {
1585
2011
  economyAssert(
@@ -2647,6 +3073,899 @@ function assertWalletActivityPageForPortfolio(page, scope) {
2647
3073
  );
2648
3074
  }
2649
3075
 
3076
+ // src/purchase-caps.ts
3077
+ var MAX_SIGNED_BIGINT = 2n ** 63n - 1n;
3078
+ var CANONICAL_NON_NEGATIVE_INTEGER = /^(?:0|[1-9][0-9]*)$/u;
3079
+ function parseGbpMinorUnits(value) {
3080
+ economyAssert(
3081
+ typeof value === "string" && CANONICAL_NON_NEGATIVE_INTEGER.test(value),
3082
+ "INVALID_AMOUNT",
3083
+ "GBP minor units must be a canonical non-negative integer string"
3084
+ );
3085
+ try {
3086
+ const amount = BigInt(value);
3087
+ economyAssert(
3088
+ amount <= MAX_SIGNED_BIGINT,
3089
+ "AMOUNT_OUT_OF_RANGE",
3090
+ "GBP minor units are outside the signed 64-bit range"
3091
+ );
3092
+ return amount;
3093
+ } catch (error) {
3094
+ if (error instanceof EconomyError) {
3095
+ throw error;
3096
+ }
3097
+ throw new EconomyError("INVALID_AMOUNT", "GBP minor units are invalid");
3098
+ }
3099
+ }
3100
+ function serializeGbpMinorUnits(amount) {
3101
+ economyAssert(
3102
+ amount >= 0n && amount <= MAX_SIGNED_BIGINT,
3103
+ "AMOUNT_OUT_OF_RANGE",
3104
+ "GBP minor units are outside the signed 64-bit range"
3105
+ );
3106
+ return amount.toString(10);
3107
+ }
3108
+ var RESERVATION_STATUSES = [
3109
+ "reserved",
3110
+ "settled",
3111
+ "released",
3112
+ "expired"
3113
+ ];
3114
+ var TRANSITION_PRECEDENCE = {
3115
+ settle: 0,
3116
+ release: 1,
3117
+ expire: 2
3118
+ };
3119
+ function assertPurchaseCapReservationTransition(transition) {
3120
+ economyAssert(
3121
+ transition.schemaVersion === ECONOMY_CONTRACT_VERSION,
3122
+ "INVALID_CONTRACT",
3123
+ "Unsupported purchase-cap transition contract version"
3124
+ );
3125
+ assertEconomyIdentifier(transition.transitionId, "transitionId");
3126
+ assertEconomyIdentifier(transition.reservationId, "reservationId");
3127
+ economyAssert(
3128
+ ["settle", "release", "expire"].includes(transition.transitionType),
3129
+ "INVALID_CONTRACT",
3130
+ "Purchase-cap transition type is unsupported"
3131
+ );
3132
+ parseIsoTimestamp(transition.occurredAt);
3133
+ }
3134
+ function sameTransition(left, right) {
3135
+ return left.schemaVersion === right.schemaVersion && left.transitionId === right.transitionId && left.reservationId === right.reservationId && left.transitionType === right.transitionType && left.occurredAt === right.occurredAt;
3136
+ }
3137
+ function compareTransitions(left, right) {
3138
+ const timeDifference = parseIsoTimestamp(left.occurredAt) - parseIsoTimestamp(right.occurredAt);
3139
+ if (timeDifference !== 0) {
3140
+ return timeDifference;
3141
+ }
3142
+ const precedenceDifference = TRANSITION_PRECEDENCE[left.transitionType] - TRANSITION_PRECEDENCE[right.transitionType];
3143
+ return precedenceDifference === 0 ? compareUnicodeCodeUnits(left.transitionId, right.transitionId) : precedenceDifference;
3144
+ }
3145
+ function assertBinding(binding) {
3146
+ economyAssert(
3147
+ binding.schemaVersion === ECONOMY_CONTRACT_VERSION,
3148
+ "INVALID_CONTRACT",
3149
+ "Unsupported purchase-cap reservation contract version"
3150
+ );
3151
+ assertEconomyIdentifier(binding.reservationId, "reservationId");
3152
+ assertEconomyIdentifier(binding.payerAccountId, "payerAccountId");
3153
+ assertEconomyIdentifier(binding.householdId, "householdId");
3154
+ economyAssert(
3155
+ parseGbpMinorUnits(binding.priceMinorUnits) > 0n,
3156
+ "INVALID_AMOUNT",
3157
+ "Purchase-cap reservation amount must be positive"
3158
+ );
3159
+ economyAssert(
3160
+ parseIsoTimestamp(binding.expiresAt) > parseIsoTimestamp(binding.reservedAt),
3161
+ "INVALID_TIME_WINDOW",
3162
+ "Purchase-cap reservation expiry must follow its creation"
3163
+ );
3164
+ }
3165
+ function bindingFromReservation(reservation) {
3166
+ return {
3167
+ schemaVersion: reservation.schemaVersion,
3168
+ reservationId: reservation.reservationId,
3169
+ payerAccountId: reservation.payerAccountId,
3170
+ householdId: reservation.householdId,
3171
+ priceMinorUnits: reservation.priceMinorUnits,
3172
+ reservedAt: reservation.reservedAt,
3173
+ expiresAt: reservation.expiresAt
3174
+ };
3175
+ }
3176
+ function projectReservation(binding, receipts) {
3177
+ assertBinding(binding);
3178
+ const reservedAt = parseIsoTimestamp(binding.reservedAt);
3179
+ const expiresAt = parseIsoTimestamp(binding.expiresAt);
3180
+ const transitionIds = /* @__PURE__ */ new Set();
3181
+ for (const receipt of receipts) {
3182
+ economyAssert(
3183
+ receipt.schemaVersion === ECONOMY_CONTRACT_VERSION,
3184
+ "INVALID_CONTRACT",
3185
+ "Unsupported purchase-cap transition receipt version"
3186
+ );
3187
+ assertPurchaseCapReservationTransition(receipt.transition);
3188
+ economyAssert(
3189
+ receipt.transition.reservationId === binding.reservationId && !transitionIds.has(receipt.transition.transitionId),
3190
+ "DUPLICATE_IDENTIFIER",
3191
+ "Purchase-cap receipts must be unique and belong to the reservation"
3192
+ );
3193
+ transitionIds.add(receipt.transition.transitionId);
3194
+ }
3195
+ let status = "reserved";
3196
+ let finalTransitionId;
3197
+ let finalizedAt;
3198
+ for (const receipt of [...receipts].sort(
3199
+ (left, right) => compareTransitions(left.transition, right.transition)
3200
+ )) {
3201
+ const transition = receipt.transition;
3202
+ const occurredAt = parseIsoTimestamp(transition.occurredAt);
3203
+ economyAssert(
3204
+ occurredAt >= reservedAt,
3205
+ "INVALID_TIME_WINDOW",
3206
+ "Purchase-cap transition cannot precede reservation"
3207
+ );
3208
+ if (transition.transitionType === "settle") {
3209
+ economyAssert(
3210
+ occurredAt < expiresAt,
3211
+ "INVALID_TIME_WINDOW",
3212
+ "Purchase-cap settlement must use payment time before expiry"
3213
+ );
3214
+ } else if (transition.transitionType === "release") {
3215
+ economyAssert(
3216
+ occurredAt < expiresAt,
3217
+ "INVALID_TIME_WINDOW",
3218
+ "Purchase-cap release must precede reservation expiry"
3219
+ );
3220
+ } else {
3221
+ economyAssert(
3222
+ occurredAt >= expiresAt,
3223
+ "INVALID_TIME_WINDOW",
3224
+ "Purchase-cap reservation cannot expire before its deadline"
3225
+ );
3226
+ }
3227
+ if (status === "reserved") {
3228
+ status = transition.transitionType === "settle" ? "settled" : transition.transitionType === "release" ? "released" : "expired";
3229
+ finalTransitionId = transition.transitionId;
3230
+ finalizedAt = transition.occurredAt;
3231
+ }
3232
+ }
3233
+ return {
3234
+ ...binding,
3235
+ status,
3236
+ ...finalTransitionId === void 0 ? {} : { finalTransitionId, finalizedAt },
3237
+ transitionReceipts: receipts
3238
+ };
3239
+ }
3240
+ function sameReservationFacts(left, right) {
3241
+ 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) => {
3242
+ const other = right.transitionReceipts[index];
3243
+ return other !== void 0 && receipt.schemaVersion === other.schemaVersion && sameTransition(receipt.transition, other.transition);
3244
+ });
3245
+ }
3246
+ function sameReservationBinding(reservation, proposed) {
3247
+ 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;
3248
+ }
3249
+ function assertPurchaseCapReservation(reservation) {
3250
+ economyAssert(
3251
+ RESERVATION_STATUSES.includes(reservation.status),
3252
+ "INVALID_CONTRACT",
3253
+ "Purchase-cap reservation status is unsupported"
3254
+ );
3255
+ const projected = projectReservation(
3256
+ bindingFromReservation(reservation),
3257
+ reservation.transitionReceipts
3258
+ );
3259
+ economyAssert(
3260
+ sameReservationFacts(projected, reservation),
3261
+ "INVALID_CONTRACT",
3262
+ "Purchase-cap reservation does not match its immutable event rebuild"
3263
+ );
3264
+ }
3265
+ function assertRollingPurchaseCapState(state) {
3266
+ economyAssert(
3267
+ state.schemaVersion === ECONOMY_CONTRACT_VERSION,
3268
+ "INVALID_CONTRACT",
3269
+ "Unsupported rolling purchase-cap contract version"
3270
+ );
3271
+ economyAssert(
3272
+ state.scopeType === "payer" || state.scopeType === "household",
3273
+ "INVALID_CONTRACT",
3274
+ "Purchase-cap scope type is unsupported"
3275
+ );
3276
+ assertEconomyIdentifier(state.scopeId, "scopeId");
3277
+ const reservationIds = /* @__PURE__ */ new Set();
3278
+ const transitionIds = /* @__PURE__ */ new Set();
3279
+ let receiptCount = 0;
3280
+ for (const reservation of state.reservations) {
3281
+ assertPurchaseCapReservation(reservation);
3282
+ economyAssert(
3283
+ !reservationIds.has(reservation.reservationId),
3284
+ "DUPLICATE_IDENTIFIER",
3285
+ "Purchase-cap state cannot repeat a reservation"
3286
+ );
3287
+ reservationIds.add(reservation.reservationId);
3288
+ economyAssert(
3289
+ state.scopeType === "payer" ? reservation.payerAccountId === state.scopeId : reservation.householdId === state.scopeId,
3290
+ "INVALID_CONTRACT",
3291
+ "Purchase-cap reservation belongs to another aggregate scope"
3292
+ );
3293
+ for (const receipt of reservation.transitionReceipts) {
3294
+ economyAssert(
3295
+ !transitionIds.has(receipt.transition.transitionId),
3296
+ "DUPLICATE_IDENTIFIER",
3297
+ "Purchase-cap state cannot repeat a transition ID"
3298
+ );
3299
+ transitionIds.add(receipt.transition.transitionId);
3300
+ receiptCount += 1;
3301
+ }
3302
+ }
3303
+ economyAssert(
3304
+ Number.isSafeInteger(state.version) && state.version === 1 + state.reservations.length + receiptCount,
3305
+ "INVALID_CONTRACT",
3306
+ "Purchase-cap state version must match reservation and receipt counts"
3307
+ );
3308
+ }
3309
+ function assertAggregatePair(payerState, householdState, payerAccountId, householdId) {
3310
+ assertRollingPurchaseCapState(payerState);
3311
+ assertRollingPurchaseCapState(householdState);
3312
+ economyAssert(
3313
+ payerState.scopeType === "payer" && payerState.scopeId === payerAccountId && householdState.scopeType === "household" && householdState.scopeId === householdId,
3314
+ "INVALID_CONTRACT",
3315
+ "Purchase-cap aggregate pair does not match payer and household"
3316
+ );
3317
+ }
3318
+ function findMirroredReservation(payerState, householdState, reservationId) {
3319
+ const payerReservation = payerState.reservations.find(
3320
+ (reservation) => reservation.reservationId === reservationId
3321
+ );
3322
+ const householdReservation = householdState.reservations.find(
3323
+ (reservation) => reservation.reservationId === reservationId
3324
+ );
3325
+ economyAssert(
3326
+ payerReservation === void 0 === (householdReservation === void 0),
3327
+ "INVALID_CONTRACT",
3328
+ "Purchase-cap reservation must exist in both aggregate scopes atomically"
3329
+ );
3330
+ if (payerReservation === void 0 || householdReservation === void 0) {
3331
+ return void 0;
3332
+ }
3333
+ economyAssert(
3334
+ sameReservationFacts(payerReservation, householdReservation),
3335
+ "INVALID_CONTRACT",
3336
+ "Payer and household purchase-cap reservations have diverged"
3337
+ );
3338
+ return payerReservation;
3339
+ }
3340
+ function windowStart(checkedAt, rollingDays) {
3341
+ return checkedAt - rollingDays * 24 * 60 * 60 * 1e3;
3342
+ }
3343
+ function calculateRollingPurchaseCapUsage(state, checkedAt, policy) {
3344
+ assertRollingPurchaseCapState(state);
3345
+ assertPurchaseLimitPolicy(policy);
3346
+ const checkedAtValue = parseIsoTimestamp(checkedAt);
3347
+ const start = windowStart(checkedAtValue, policy.rollingDays);
3348
+ let total = 0n;
3349
+ for (const reservation of state.reservations) {
3350
+ if (reservation.status === "reserved" && parseIsoTimestamp(reservation.reservedAt) <= checkedAtValue && checkedAtValue < parseIsoTimestamp(reservation.expiresAt)) {
3351
+ total += parseGbpMinorUnits(reservation.priceMinorUnits);
3352
+ } else if (reservation.status === "settled" && reservation.finalizedAt !== void 0) {
3353
+ const settledAt = parseIsoTimestamp(reservation.finalizedAt);
3354
+ if (settledAt >= start && settledAt <= checkedAtValue) {
3355
+ total += parseGbpMinorUnits(reservation.priceMinorUnits);
3356
+ }
3357
+ }
3358
+ economyAssert(
3359
+ total <= MAX_SIGNED_BIGINT,
3360
+ "AMOUNT_OUT_OF_RANGE",
3361
+ "Rolling purchase-cap usage exceeds signed 64-bit range"
3362
+ );
3363
+ }
3364
+ return serializeGbpMinorUnits(total);
3365
+ }
3366
+ function createRollingPurchaseCapState(scopeType, scopeId) {
3367
+ economyAssert(
3368
+ scopeType === "payer" || scopeType === "household",
3369
+ "INVALID_CONTRACT",
3370
+ "Purchase-cap scope type is unsupported"
3371
+ );
3372
+ assertEconomyIdentifier(scopeId, "scopeId");
3373
+ return {
3374
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
3375
+ scopeType,
3376
+ scopeId,
3377
+ version: 1,
3378
+ reservations: []
3379
+ };
3380
+ }
3381
+ function sortedWithReservation(state, reservation) {
3382
+ return [...state.reservations, reservation].sort(
3383
+ (left, right) => compareUnicodeCodeUnits(left.reservationId, right.reservationId)
3384
+ );
3385
+ }
3386
+ function resultWithUsage(payerState, householdState, reservation, applied, stateChanged, checkedAt, policy) {
3387
+ return {
3388
+ payerState,
3389
+ householdState,
3390
+ reservation,
3391
+ applied,
3392
+ stateChanged,
3393
+ payerUsageMinorUnits: calculateRollingPurchaseCapUsage(
3394
+ payerState,
3395
+ checkedAt,
3396
+ policy
3397
+ ),
3398
+ householdUsageMinorUnits: calculateRollingPurchaseCapUsage(
3399
+ householdState,
3400
+ checkedAt,
3401
+ policy
3402
+ )
3403
+ };
3404
+ }
3405
+ function reserveRollingPurchaseCaps(payerState, householdState, command, policy) {
3406
+ economyAssert(
3407
+ command.schemaVersion === ECONOMY_CONTRACT_VERSION,
3408
+ "INVALID_CONTRACT",
3409
+ "Unsupported purchase-cap reservation command version"
3410
+ );
3411
+ const binding = {
3412
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
3413
+ reservationId: command.reservationId,
3414
+ payerAccountId: command.payerAccountId,
3415
+ householdId: command.householdId,
3416
+ priceMinorUnits: command.priceMinorUnits,
3417
+ reservedAt: command.reservedAt,
3418
+ expiresAt: command.expiresAt
3419
+ };
3420
+ assertBinding(binding);
3421
+ assertAggregatePair(
3422
+ payerState,
3423
+ householdState,
3424
+ command.payerAccountId,
3425
+ command.householdId
3426
+ );
3427
+ assertPurchaseLimitPolicy(policy);
3428
+ const proposed = projectReservation(binding, []);
3429
+ const replay = findMirroredReservation(
3430
+ payerState,
3431
+ householdState,
3432
+ command.reservationId
3433
+ );
3434
+ if (replay !== void 0) {
3435
+ economyAssert(
3436
+ sameReservationBinding(replay, proposed),
3437
+ "DUPLICATE_IDENTIFIER",
3438
+ "Purchase-cap reservation ID was reused with different facts"
3439
+ );
3440
+ return resultWithUsage(
3441
+ payerState,
3442
+ householdState,
3443
+ replay,
3444
+ false,
3445
+ false,
3446
+ replay.finalizedAt ?? command.reservedAt,
3447
+ policy
3448
+ );
3449
+ }
3450
+ economyAssert(
3451
+ command.expectedPayerVersion === payerState.version && command.expectedHouseholdVersion === householdState.version,
3452
+ "INVALID_CONTRACT",
3453
+ "Purchase-cap reservation has a stale aggregate version"
3454
+ );
3455
+ const amount = parseGbpMinorUnits(command.priceMinorUnits);
3456
+ economyAssert(
3457
+ amount <= parseGbpMinorUnits(policy.maxOrderPriceMinorUnits),
3458
+ "INSUFFICIENT_BALANCE",
3459
+ "Purchase amount exceeds the per-order ceiling"
3460
+ );
3461
+ const payerUsage = parseGbpMinorUnits(
3462
+ calculateRollingPurchaseCapUsage(payerState, command.reservedAt, policy)
3463
+ );
3464
+ const householdUsage = parseGbpMinorUnits(
3465
+ calculateRollingPurchaseCapUsage(
3466
+ householdState,
3467
+ command.reservedAt,
3468
+ policy
3469
+ )
3470
+ );
3471
+ economyAssert(
3472
+ payerUsage + amount <= parseGbpMinorUnits(policy.rollingPayerPriceMinorUnits),
3473
+ "INSUFFICIENT_BALANCE",
3474
+ "Purchase amount exceeds the rolling payer ceiling"
3475
+ );
3476
+ economyAssert(
3477
+ householdUsage + amount <= parseGbpMinorUnits(policy.rollingHouseholdPriceMinorUnits),
3478
+ "INSUFFICIENT_BALANCE",
3479
+ "Purchase amount exceeds the rolling household ceiling"
3480
+ );
3481
+ const nextPayer = {
3482
+ ...payerState,
3483
+ version: payerState.version + 1,
3484
+ reservations: sortedWithReservation(payerState, proposed)
3485
+ };
3486
+ const nextHousehold = {
3487
+ ...householdState,
3488
+ version: householdState.version + 1,
3489
+ reservations: sortedWithReservation(householdState, proposed)
3490
+ };
3491
+ return resultWithUsage(
3492
+ nextPayer,
3493
+ nextHousehold,
3494
+ proposed,
3495
+ true,
3496
+ true,
3497
+ command.reservedAt,
3498
+ policy
3499
+ );
3500
+ }
3501
+ function replaceReservation(state, replacement) {
3502
+ return {
3503
+ ...state,
3504
+ version: state.version + 1,
3505
+ reservations: state.reservations.map(
3506
+ (reservation) => reservation.reservationId === replacement.reservationId ? replacement : reservation
3507
+ )
3508
+ };
3509
+ }
3510
+ function findTransitionById(state, transitionId) {
3511
+ for (const reservation of state.reservations) {
3512
+ const receipt = reservation.transitionReceipts.find(
3513
+ (candidate) => candidate.transition.transitionId === transitionId
3514
+ );
3515
+ if (receipt !== void 0) {
3516
+ return { reservationId: reservation.reservationId, transition: receipt.transition };
3517
+ }
3518
+ }
3519
+ return void 0;
3520
+ }
3521
+ function transitionRollingPurchaseCapReservation(payerState, householdState, transition, expectedPayerVersion, expectedHouseholdVersion, policy) {
3522
+ assertPurchaseCapReservationTransition(transition);
3523
+ assertRollingPurchaseCapState(payerState);
3524
+ assertRollingPurchaseCapState(householdState);
3525
+ const current = findMirroredReservation(
3526
+ payerState,
3527
+ householdState,
3528
+ transition.reservationId
3529
+ );
3530
+ economyAssert(
3531
+ current !== void 0,
3532
+ "INVALID_CONTRACT",
3533
+ "Purchase-cap reservation does not exist in both aggregate scopes"
3534
+ );
3535
+ assertAggregatePair(
3536
+ payerState,
3537
+ householdState,
3538
+ current.payerAccountId,
3539
+ current.householdId
3540
+ );
3541
+ assertPurchaseLimitPolicy(policy);
3542
+ const payerExisting = findTransitionById(
3543
+ payerState,
3544
+ transition.transitionId
3545
+ );
3546
+ const householdExisting = findTransitionById(
3547
+ householdState,
3548
+ transition.transitionId
3549
+ );
3550
+ economyAssert(
3551
+ payerExisting === void 0 === (householdExisting === void 0),
3552
+ "INVALID_CONTRACT",
3553
+ "Purchase-cap transition must exist in both scopes atomically"
3554
+ );
3555
+ if (payerExisting !== void 0 && householdExisting !== void 0) {
3556
+ economyAssert(
3557
+ payerExisting.reservationId === transition.reservationId && householdExisting.reservationId === transition.reservationId && sameTransition(payerExisting.transition, transition) && sameTransition(householdExisting.transition, transition),
3558
+ "DUPLICATE_IDENTIFIER",
3559
+ "Purchase-cap transition ID was reused with different facts"
3560
+ );
3561
+ return resultWithUsage(
3562
+ payerState,
3563
+ householdState,
3564
+ current,
3565
+ false,
3566
+ false,
3567
+ transition.occurredAt,
3568
+ policy
3569
+ );
3570
+ }
3571
+ economyAssert(
3572
+ expectedPayerVersion === payerState.version && expectedHouseholdVersion === householdState.version,
3573
+ "INVALID_CONTRACT",
3574
+ "Purchase-cap transition has a stale aggregate version"
3575
+ );
3576
+ const replacement = projectReservation(bindingFromReservation(current), [
3577
+ ...current.transitionReceipts,
3578
+ { schemaVersion: ECONOMY_CONTRACT_VERSION, transition }
3579
+ ]);
3580
+ const stateChanged = replacement.status !== current.status || replacement.finalTransitionId !== current.finalTransitionId || replacement.finalizedAt !== current.finalizedAt;
3581
+ const nextPayer = replaceReservation(payerState, replacement);
3582
+ const nextHousehold = replaceReservation(householdState, replacement);
3583
+ return resultWithUsage(
3584
+ nextPayer,
3585
+ nextHousehold,
3586
+ replacement,
3587
+ true,
3588
+ stateChanged,
3589
+ transition.occurredAt,
3590
+ policy
3591
+ );
3592
+ }
3593
+
3594
+ // src/purchase-intents.ts
3595
+ var TRANSITION_TYPES = [
3596
+ "checkout-bound",
3597
+ "payment-observed",
3598
+ "credit-recorded",
3599
+ "expire",
3600
+ "cancel",
3601
+ "dispute-opened",
3602
+ "dispute-won",
3603
+ "dispute-lost"
3604
+ ];
3605
+ var TRANSITION_PRECEDENCE2 = {
3606
+ "checkout-bound": 0,
3607
+ "payment-observed": 1,
3608
+ "credit-recorded": 2,
3609
+ "dispute-opened": 3,
3610
+ "dispute-won": 4,
3611
+ "dispute-lost": 4,
3612
+ cancel: 5,
3613
+ expire: 6
3614
+ };
3615
+ function assertCheckoutHash(value) {
3616
+ economyAssert(
3617
+ /^sha256:[a-f0-9]{64}$/u.test(value),
3618
+ "INVALID_CONTRACT",
3619
+ "Checkout references must be stored as sanitized SHA-256 hashes"
3620
+ );
3621
+ }
3622
+ function assertPurchaseIntentTransition(transition) {
3623
+ economyAssert(
3624
+ transition.schemaVersion === ECONOMY_CONTRACT_VERSION,
3625
+ "INVALID_CONTRACT",
3626
+ "Unsupported purchase-intent transition contract version"
3627
+ );
3628
+ assertEconomyIdentifier(transition.transitionId, "transitionId");
3629
+ assertEconomyIdentifier(transition.intentId, "intentId");
3630
+ economyAssert(
3631
+ TRANSITION_TYPES.includes(transition.transitionType),
3632
+ "INVALID_CONTRACT",
3633
+ "Purchase-intent transition type is unsupported"
3634
+ );
3635
+ parseIsoTimestamp(transition.occurredAt);
3636
+ if (transition.providerCheckoutReferenceHash !== void 0) {
3637
+ assertCheckoutHash(transition.providerCheckoutReferenceHash);
3638
+ }
3639
+ const requiresCheckoutHash = transition.transitionType === "checkout-bound" || transition.transitionType === "payment-observed";
3640
+ economyAssert(
3641
+ requiresCheckoutHash === (transition.providerCheckoutReferenceHash !== void 0),
3642
+ "INVALID_CONTRACT",
3643
+ "Only checkout and payment transitions must carry a checkout binding"
3644
+ );
3645
+ }
3646
+ function sameTransition2(left, right) {
3647
+ 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;
3648
+ }
3649
+ function sameIntent(left, right) {
3650
+ 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;
3651
+ }
3652
+ function compareTransitions2(left, right) {
3653
+ const timeDifference = parseIsoTimestamp(left.occurredAt) - parseIsoTimestamp(right.occurredAt);
3654
+ if (timeDifference !== 0) {
3655
+ return timeDifference;
3656
+ }
3657
+ const precedenceDifference = TRANSITION_PRECEDENCE2[left.transitionType] - TRANSITION_PRECEDENCE2[right.transitionType];
3658
+ return precedenceDifference === 0 ? compareUnicodeCodeUnits(left.transitionId, right.transitionId) : precedenceDifference;
3659
+ }
3660
+ function assertMatchingCheckoutBinding(intent, checkoutHash) {
3661
+ economyAssert(
3662
+ intent.providerCheckoutReferenceHash === void 0 || intent.providerCheckoutReferenceHash === checkoutHash,
3663
+ "INVALID_CONTRACT",
3664
+ "Purchase-intent transition conflicts with its checkout binding"
3665
+ );
3666
+ }
3667
+ function projectPurchaseIntent(initialIntent, transitions, pack) {
3668
+ assertPurchaseIntentBinding(initialIntent, pack);
3669
+ economyAssert(
3670
+ initialIntent.status === "created" && initialIntent.providerCheckoutReferenceHash === void 0,
3671
+ "INVALID_CONTRACT",
3672
+ "Purchase-intent lifecycle requires immutable created intent facts"
3673
+ );
3674
+ const createdAt = parseIsoTimestamp(initialIntent.createdAt);
3675
+ const expiresAt = parseIsoTimestamp(initialIntent.expiresAt);
3676
+ let intent = initialIntent;
3677
+ let disputeDisposition = "none";
3678
+ let creditTransitionId;
3679
+ const effectiveTransitionIds = [];
3680
+ for (const transition of [...transitions].sort(compareTransitions2)) {
3681
+ assertPurchaseIntentTransition(transition);
3682
+ economyAssert(
3683
+ transition.intentId === initialIntent.intentId,
3684
+ "INVALID_CONTRACT",
3685
+ "Purchase-intent transition belongs to another intent"
3686
+ );
3687
+ const eventTime = parseIsoTimestamp(transition.occurredAt);
3688
+ economyAssert(
3689
+ eventTime >= createdAt,
3690
+ "INVALID_TIME_WINDOW",
3691
+ "Purchase-intent transition cannot precede intent creation"
3692
+ );
3693
+ let effective = false;
3694
+ if (transition.transitionType === "checkout-bound") {
3695
+ const checkoutHash = transition.providerCheckoutReferenceHash;
3696
+ assertMatchingCheckoutBinding(intent, checkoutHash);
3697
+ if (intent.status === "created") {
3698
+ economyAssert(
3699
+ eventTime < expiresAt,
3700
+ "INVALID_TIME_WINDOW",
3701
+ "Checkout cannot be bound after purchase-intent expiry"
3702
+ );
3703
+ intent = {
3704
+ ...intent,
3705
+ status: "checkout-created",
3706
+ providerCheckoutReferenceHash: checkoutHash
3707
+ };
3708
+ effective = true;
3709
+ } else {
3710
+ economyAssert(
3711
+ !["expired", "cancelled"].includes(intent.status),
3712
+ "INVALID_CONTRACT",
3713
+ "A terminal purchase intent cannot bind a checkout"
3714
+ );
3715
+ }
3716
+ } else if (transition.transitionType === "payment-observed") {
3717
+ const checkoutHash = transition.providerCheckoutReferenceHash;
3718
+ assertMatchingCheckoutBinding(intent, checkoutHash);
3719
+ economyAssert(
3720
+ eventTime < expiresAt,
3721
+ "INVALID_TIME_WINDOW",
3722
+ "Authoritative payment time must precede purchase-intent expiry"
3723
+ );
3724
+ if (intent.status === "created") {
3725
+ intent = {
3726
+ ...intent,
3727
+ status: "paid-unreconciled",
3728
+ providerCheckoutReferenceHash: checkoutHash
3729
+ };
3730
+ effective = true;
3731
+ } else if (intent.status === "checkout-created") {
3732
+ intent = { ...intent, status: "paid-unreconciled" };
3733
+ effective = true;
3734
+ } else {
3735
+ economyAssert(
3736
+ !["expired", "cancelled"].includes(intent.status),
3737
+ "INVALID_CONTRACT",
3738
+ "A terminal purchase intent cannot observe payment"
3739
+ );
3740
+ }
3741
+ } else if (transition.transitionType === "credit-recorded") {
3742
+ if (intent.status === "paid-unreconciled") {
3743
+ intent = { ...intent, status: "credited" };
3744
+ disputeDisposition = "none";
3745
+ creditTransitionId = transition.transitionId;
3746
+ effective = true;
3747
+ } else {
3748
+ economyAssert(
3749
+ intent.status === "credited" || intent.status === "disputed",
3750
+ "INVALID_CONTRACT",
3751
+ "Purchase intent cannot be credited before authoritative payment"
3752
+ );
3753
+ }
3754
+ } else if (transition.transitionType === "dispute-opened") {
3755
+ if (intent.status === "credited" && disputeDisposition === "none") {
3756
+ intent = { ...intent, status: "disputed" };
3757
+ disputeDisposition = "open";
3758
+ effective = true;
3759
+ } else {
3760
+ economyAssert(
3761
+ intent.status === "disputed" && disputeDisposition === "open",
3762
+ "INVALID_CONTRACT",
3763
+ "Dispute can open only once after purchase credit"
3764
+ );
3765
+ }
3766
+ } else if (transition.transitionType === "dispute-won") {
3767
+ if (intent.status === "disputed" && disputeDisposition === "open") {
3768
+ intent = { ...intent, status: "credited" };
3769
+ disputeDisposition = "won";
3770
+ effective = true;
3771
+ } else {
3772
+ economyAssert(
3773
+ intent.status === "credited" && disputeDisposition === "won",
3774
+ "INVALID_CONTRACT",
3775
+ "Dispute-win transition requires one open dispute"
3776
+ );
3777
+ }
3778
+ } else if (transition.transitionType === "dispute-lost") {
3779
+ if (intent.status === "disputed" && disputeDisposition === "open") {
3780
+ disputeDisposition = "lost";
3781
+ effective = true;
3782
+ } else {
3783
+ economyAssert(
3784
+ intent.status === "disputed" && disputeDisposition === "lost",
3785
+ "INVALID_CONTRACT",
3786
+ "Dispute-loss transition requires one open dispute"
3787
+ );
3788
+ }
3789
+ } else if (transition.transitionType === "cancel") {
3790
+ if (intent.status === "created" || intent.status === "checkout-created") {
3791
+ intent = { ...intent, status: "cancelled" };
3792
+ effective = true;
3793
+ }
3794
+ } else {
3795
+ economyAssert(
3796
+ eventTime >= expiresAt,
3797
+ "INVALID_TIME_WINDOW",
3798
+ "Expiry transition cannot precede purchase-intent expiry"
3799
+ );
3800
+ if (intent.status === "created" || intent.status === "checkout-created") {
3801
+ intent = { ...intent, status: "expired" };
3802
+ effective = true;
3803
+ }
3804
+ }
3805
+ if (effective) {
3806
+ effectiveTransitionIds.push(transition.transitionId);
3807
+ }
3808
+ }
3809
+ return {
3810
+ intent,
3811
+ disputeDisposition,
3812
+ effectiveTransitionIds,
3813
+ ...creditTransitionId === void 0 ? {} : { creditTransitionId }
3814
+ };
3815
+ }
3816
+ function assertPurchaseIntentLifecycle(lifecycle, pack) {
3817
+ economyAssert(
3818
+ lifecycle.schemaVersion === ECONOMY_CONTRACT_VERSION,
3819
+ "INVALID_CONTRACT",
3820
+ "Unsupported purchase-intent lifecycle contract version"
3821
+ );
3822
+ economyAssert(
3823
+ Number.isSafeInteger(lifecycle.version) && lifecycle.version === lifecycle.receipts.length + 1,
3824
+ "INVALID_CONTRACT",
3825
+ "Purchase-intent lifecycle version must match its receipt count"
3826
+ );
3827
+ const transitionIds = /* @__PURE__ */ new Set();
3828
+ for (const receipt of lifecycle.receipts) {
3829
+ economyAssert(
3830
+ receipt.schemaVersion === ECONOMY_CONTRACT_VERSION,
3831
+ "INVALID_CONTRACT",
3832
+ "Purchase-intent receipt is invalid"
3833
+ );
3834
+ assertPurchaseIntentTransition(receipt.transition);
3835
+ economyAssert(
3836
+ receipt.transition.intentId === lifecycle.initialIntent.intentId && !transitionIds.has(receipt.transition.transitionId),
3837
+ "DUPLICATE_IDENTIFIER",
3838
+ "Purchase-intent receipts must be unique and belong to the intent"
3839
+ );
3840
+ transitionIds.add(receipt.transition.transitionId);
3841
+ }
3842
+ const projection = projectPurchaseIntent(
3843
+ lifecycle.initialIntent,
3844
+ lifecycle.receipts.map((receipt) => receipt.transition),
3845
+ pack
3846
+ );
3847
+ economyAssert(
3848
+ 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(
3849
+ (transitionId, index) => transitionId === projection.effectiveTransitionIds[index]
3850
+ ),
3851
+ "INVALID_CONTRACT",
3852
+ "Purchase-intent lifecycle does not match its immutable event rebuild"
3853
+ );
3854
+ }
3855
+ function createPurchaseIntentLifecycle(initialIntent, pack) {
3856
+ const projection = projectPurchaseIntent(initialIntent, [], pack);
3857
+ return {
3858
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
3859
+ initialIntent,
3860
+ intent: projection.intent,
3861
+ version: 1,
3862
+ creditRecorded: false,
3863
+ disputeDisposition: projection.disputeDisposition,
3864
+ receipts: [],
3865
+ effectiveTransitionIds: []
3866
+ };
3867
+ }
3868
+ function sameProjection(lifecycle, projection) {
3869
+ return sameIntent(lifecycle.intent, projection.intent) && lifecycle.disputeDisposition === projection.disputeDisposition && lifecycle.effectiveTransitionIds.length === projection.effectiveTransitionIds.length && lifecycle.effectiveTransitionIds.every(
3870
+ (transitionId, index) => transitionId === projection.effectiveTransitionIds[index]
3871
+ );
3872
+ }
3873
+ function applyPurchaseIntentTransition(lifecycle, transition, expectedVersion, pack) {
3874
+ assertPurchaseIntentLifecycle(lifecycle, pack);
3875
+ assertPurchaseIntentTransition(transition);
3876
+ economyAssert(
3877
+ transition.intentId === lifecycle.initialIntent.intentId,
3878
+ "INVALID_CONTRACT",
3879
+ "Purchase-intent transition belongs to another intent"
3880
+ );
3881
+ const replay = lifecycle.receipts.find(
3882
+ (receipt) => receipt.transition.transitionId === transition.transitionId
3883
+ );
3884
+ if (replay !== void 0) {
3885
+ economyAssert(
3886
+ sameTransition2(replay.transition, transition),
3887
+ "DUPLICATE_IDENTIFIER",
3888
+ "Purchase-intent transition ID was reused with different facts"
3889
+ );
3890
+ return { lifecycle, recorded: false, stateChanged: false };
3891
+ }
3892
+ economyAssert(
3893
+ expectedVersion === lifecycle.version,
3894
+ "INVALID_CONTRACT",
3895
+ "Purchase-intent transition has a stale expected version"
3896
+ );
3897
+ const receipts = [
3898
+ ...lifecycle.receipts,
3899
+ { schemaVersion: ECONOMY_CONTRACT_VERSION, transition }
3900
+ ];
3901
+ const projection = projectPurchaseIntent(
3902
+ lifecycle.initialIntent,
3903
+ receipts.map((receipt) => receipt.transition),
3904
+ pack
3905
+ );
3906
+ const stateChanged = !sameProjection(lifecycle, projection);
3907
+ const nextLifecycle = {
3908
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
3909
+ initialIntent: lifecycle.initialIntent,
3910
+ intent: projection.intent,
3911
+ version: lifecycle.version + 1,
3912
+ creditRecorded: projection.intent.status === "credited" || projection.intent.status === "disputed",
3913
+ disputeDisposition: projection.disputeDisposition,
3914
+ receipts,
3915
+ effectiveTransitionIds: projection.effectiveTransitionIds
3916
+ };
3917
+ assertPurchaseIntentLifecycle(nextLifecycle, pack);
3918
+ const producesCredit = projection.creditTransitionId === transition.transitionId && !lifecycle.creditRecorded && nextLifecycle.creditRecorded;
3919
+ const creditInstruction = producesCredit ? {
3920
+ schemaVersion: ECONOMY_CONTRACT_VERSION,
3921
+ intentId: nextLifecycle.intent.intentId,
3922
+ transitionId: transition.transitionId,
3923
+ idempotencyKey: nextLifecycle.intent.intentId
3924
+ } : void 0;
3925
+ return {
3926
+ lifecycle: nextLifecycle,
3927
+ recorded: true,
3928
+ stateChanged,
3929
+ ...creditInstruction === void 0 ? {} : { creditInstruction }
3930
+ };
3931
+ }
3932
+ function reducePurchaseIntentTransitions(initialIntent, transitions, pack) {
3933
+ const unique = /* @__PURE__ */ new Map();
3934
+ for (const transition of transitions) {
3935
+ assertPurchaseIntentTransition(transition);
3936
+ const existing = unique.get(transition.transitionId);
3937
+ if (existing !== void 0) {
3938
+ economyAssert(
3939
+ sameTransition2(existing, transition),
3940
+ "DUPLICATE_IDENTIFIER",
3941
+ "Purchase-intent transition ID was reused with different facts"
3942
+ );
3943
+ } else {
3944
+ unique.set(transition.transitionId, transition);
3945
+ }
3946
+ }
3947
+ let lifecycle = createPurchaseIntentLifecycle(initialIntent, pack);
3948
+ const creditInstructions = [];
3949
+ for (const transition of [...unique.values()].sort(compareTransitions2)) {
3950
+ const result = applyPurchaseIntentTransition(
3951
+ lifecycle,
3952
+ transition,
3953
+ lifecycle.version,
3954
+ pack
3955
+ );
3956
+ lifecycle = result.lifecycle;
3957
+ if (result.creditInstruction !== void 0) {
3958
+ creditInstructions.push(result.creditInstruction);
3959
+ }
3960
+ }
3961
+ economyAssert(
3962
+ creditInstructions.length <= 1,
3963
+ "DUPLICATE_TRANSACTION",
3964
+ "Purchase-intent reduction produced more than one credit"
3965
+ );
3966
+ return { lifecycle, creditInstructions };
3967
+ }
3968
+
2650
3969
  // src/recurrence.ts
2651
3970
  var BASELINE_MONTHLY_SUBSCRIPTION_PLAN = Object.freeze({
2652
3971
  schemaVersion: ECONOMY_CONTRACT_VERSION,
@@ -2784,6 +4103,8 @@ export {
2784
4103
  TOKEN_SUBUNITS_MIN,
2785
4104
  TOKEN_SUBUNITS_PER_TOKEN,
2786
4105
  advanceJournalChainHead,
4106
+ applyPaidLotLifecycleEvent,
4107
+ applyPurchaseIntentTransition,
2787
4108
  applySourceLotMovement,
2788
4109
  applyTransactionToProjection,
2789
4110
  applyWalletBalanceDelta,
@@ -2803,11 +4124,19 @@ export {
2803
4124
  assertJournalChainHead,
2804
4125
  assertNonNegativeAccounts,
2805
4126
  assertOpenPurchaseIntent,
4127
+ assertPaidLotLifecycle,
4128
+ assertPaidLotLifecycleEvent,
4129
+ assertPaidLotPurchaseProvenance,
4130
+ assertPurchaseCapReservation,
4131
+ assertPurchaseCapReservationTransition,
2806
4132
  assertPurchaseIntentBinding,
4133
+ assertPurchaseIntentLifecycle,
4134
+ assertPurchaseIntentTransition,
2807
4135
  assertPurchaseLimitPolicy,
2808
4136
  assertReversalAvailable,
2809
4137
  assertRewardConversion,
2810
4138
  assertRewardRate,
4139
+ assertRollingPurchaseCapState,
2811
4140
  assertScopedPersistedIdempotencyResult,
2812
4141
  assertSourceLot,
2813
4142
  assertSourceLotMovement,
@@ -2833,13 +4162,18 @@ export {
2833
4162
  assertWalletPortfolioSummary,
2834
4163
  availableSourceLotAmount,
2835
4164
  boostGameplayAllocation,
4165
+ calculateRollingPurchaseCapUsage,
2836
4166
  canUseSourceLot,
2837
4167
  canonicalTransactionPayload,
2838
4168
  convertRewardPayout,
4169
+ createEarlyBackerRetentionFromPaidLot,
2839
4170
  createGameplayAllocation,
2840
4171
  createInitialSourceLotSnapshot,
4172
+ createPaidLotLifecycle,
2841
4173
  createProjectionSnapshot,
4174
+ createPurchaseIntentLifecycle,
2842
4175
  createReversalTransaction,
4176
+ createRollingPurchaseCapState,
2843
4177
  createWalletBalanceSummary,
2844
4178
  createWalletPortfolioLifetime,
2845
4179
  createWalletPortfolioSummary,
@@ -2850,16 +4184,22 @@ export {
2850
4184
  evaluateEarlyBackerBySettlementV2,
2851
4185
  isWholeTokenAmount,
2852
4186
  mapBitLabsConversionState,
4187
+ parseGbpMinorUnits,
2853
4188
  parseIsoTimestamp,
2854
4189
  parseTokenSubunits,
2855
4190
  rebuildBalanceProjection,
2856
4191
  reclaimGameplayAllocation,
4192
+ reducePaidLotLifecycleEvents,
4193
+ reducePurchaseIntentTransitions,
4194
+ reserveRollingPurchaseCaps,
2857
4195
  selectSourceLots,
4196
+ serializeGbpMinorUnits,
2858
4197
  serializeTokenSubunits,
2859
4198
  sortStringRecord,
2860
4199
  subunitsToWholeTokens,
2861
4200
  sumTokenSubunits,
2862
4201
  toEconomyErrorEnvelope,
4202
+ transitionRollingPurchaseCapReservation,
2863
4203
  wholeTokensToSubunits
2864
4204
  };
2865
4205
  //# sourceMappingURL=index.js.map