@plasius/economy 0.3.1 → 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/README.md +39 -0
- package/dist/index.cjs +1349 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +240 -1
- package/dist/index.d.ts +240 -1
- package/dist/index.js +1328 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -33,6 +33,8 @@ __export(index_exports, {
|
|
|
33
33
|
TOKEN_SUBUNITS_MIN: () => TOKEN_SUBUNITS_MIN,
|
|
34
34
|
TOKEN_SUBUNITS_PER_TOKEN: () => TOKEN_SUBUNITS_PER_TOKEN,
|
|
35
35
|
advanceJournalChainHead: () => advanceJournalChainHead,
|
|
36
|
+
applyPaidLotLifecycleEvent: () => applyPaidLotLifecycleEvent,
|
|
37
|
+
applyPurchaseIntentTransition: () => applyPurchaseIntentTransition,
|
|
36
38
|
applySourceLotMovement: () => applySourceLotMovement,
|
|
37
39
|
applyTransactionToProjection: () => applyTransactionToProjection,
|
|
38
40
|
applyWalletBalanceDelta: () => applyWalletBalanceDelta,
|
|
@@ -52,11 +54,19 @@ __export(index_exports, {
|
|
|
52
54
|
assertJournalChainHead: () => assertJournalChainHead,
|
|
53
55
|
assertNonNegativeAccounts: () => assertNonNegativeAccounts,
|
|
54
56
|
assertOpenPurchaseIntent: () => assertOpenPurchaseIntent,
|
|
57
|
+
assertPaidLotLifecycle: () => assertPaidLotLifecycle,
|
|
58
|
+
assertPaidLotLifecycleEvent: () => assertPaidLotLifecycleEvent,
|
|
59
|
+
assertPaidLotPurchaseProvenance: () => assertPaidLotPurchaseProvenance,
|
|
60
|
+
assertPurchaseCapReservation: () => assertPurchaseCapReservation,
|
|
61
|
+
assertPurchaseCapReservationTransition: () => assertPurchaseCapReservationTransition,
|
|
55
62
|
assertPurchaseIntentBinding: () => assertPurchaseIntentBinding,
|
|
63
|
+
assertPurchaseIntentLifecycle: () => assertPurchaseIntentLifecycle,
|
|
64
|
+
assertPurchaseIntentTransition: () => assertPurchaseIntentTransition,
|
|
56
65
|
assertPurchaseLimitPolicy: () => assertPurchaseLimitPolicy,
|
|
57
66
|
assertReversalAvailable: () => assertReversalAvailable,
|
|
58
67
|
assertRewardConversion: () => assertRewardConversion,
|
|
59
68
|
assertRewardRate: () => assertRewardRate,
|
|
69
|
+
assertRollingPurchaseCapState: () => assertRollingPurchaseCapState,
|
|
60
70
|
assertScopedPersistedIdempotencyResult: () => assertScopedPersistedIdempotencyResult,
|
|
61
71
|
assertSourceLot: () => assertSourceLot,
|
|
62
72
|
assertSourceLotMovement: () => assertSourceLotMovement,
|
|
@@ -82,13 +92,18 @@ __export(index_exports, {
|
|
|
82
92
|
assertWalletPortfolioSummary: () => assertWalletPortfolioSummary,
|
|
83
93
|
availableSourceLotAmount: () => availableSourceLotAmount,
|
|
84
94
|
boostGameplayAllocation: () => boostGameplayAllocation,
|
|
95
|
+
calculateRollingPurchaseCapUsage: () => calculateRollingPurchaseCapUsage,
|
|
85
96
|
canUseSourceLot: () => canUseSourceLot,
|
|
86
97
|
canonicalTransactionPayload: () => canonicalTransactionPayload,
|
|
87
98
|
convertRewardPayout: () => convertRewardPayout,
|
|
99
|
+
createEarlyBackerRetentionFromPaidLot: () => createEarlyBackerRetentionFromPaidLot,
|
|
88
100
|
createGameplayAllocation: () => createGameplayAllocation,
|
|
89
101
|
createInitialSourceLotSnapshot: () => createInitialSourceLotSnapshot,
|
|
102
|
+
createPaidLotLifecycle: () => createPaidLotLifecycle,
|
|
90
103
|
createProjectionSnapshot: () => createProjectionSnapshot,
|
|
104
|
+
createPurchaseIntentLifecycle: () => createPurchaseIntentLifecycle,
|
|
91
105
|
createReversalTransaction: () => createReversalTransaction,
|
|
106
|
+
createRollingPurchaseCapState: () => createRollingPurchaseCapState,
|
|
92
107
|
createWalletBalanceSummary: () => createWalletBalanceSummary,
|
|
93
108
|
createWalletPortfolioLifetime: () => createWalletPortfolioLifetime,
|
|
94
109
|
createWalletPortfolioSummary: () => createWalletPortfolioSummary,
|
|
@@ -99,16 +114,22 @@ __export(index_exports, {
|
|
|
99
114
|
evaluateEarlyBackerBySettlementV2: () => evaluateEarlyBackerBySettlementV2,
|
|
100
115
|
isWholeTokenAmount: () => isWholeTokenAmount,
|
|
101
116
|
mapBitLabsConversionState: () => mapBitLabsConversionState,
|
|
117
|
+
parseGbpMinorUnits: () => parseGbpMinorUnits,
|
|
102
118
|
parseIsoTimestamp: () => parseIsoTimestamp,
|
|
103
119
|
parseTokenSubunits: () => parseTokenSubunits,
|
|
104
120
|
rebuildBalanceProjection: () => rebuildBalanceProjection,
|
|
105
121
|
reclaimGameplayAllocation: () => reclaimGameplayAllocation,
|
|
122
|
+
reducePaidLotLifecycleEvents: () => reducePaidLotLifecycleEvents,
|
|
123
|
+
reducePurchaseIntentTransitions: () => reducePurchaseIntentTransitions,
|
|
124
|
+
reserveRollingPurchaseCaps: () => reserveRollingPurchaseCaps,
|
|
106
125
|
selectSourceLots: () => selectSourceLots,
|
|
126
|
+
serializeGbpMinorUnits: () => serializeGbpMinorUnits,
|
|
107
127
|
serializeTokenSubunits: () => serializeTokenSubunits,
|
|
108
128
|
sortStringRecord: () => sortStringRecord,
|
|
109
129
|
subunitsToWholeTokens: () => subunitsToWholeTokens,
|
|
110
130
|
sumTokenSubunits: () => sumTokenSubunits,
|
|
111
131
|
toEconomyErrorEnvelope: () => toEconomyErrorEnvelope,
|
|
132
|
+
transitionRollingPurchaseCapReservation: () => transitionRollingPurchaseCapReservation,
|
|
112
133
|
wholeTokensToSubunits: () => wholeTokensToSubunits
|
|
113
134
|
});
|
|
114
135
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -1707,6 +1728,420 @@ function assertDistinctAdjustmentApproval(request) {
|
|
|
1707
1728
|
}
|
|
1708
1729
|
}
|
|
1709
1730
|
|
|
1731
|
+
// src/paid-lots.ts
|
|
1732
|
+
var ZERO_ARITHMETIC = Object.freeze({
|
|
1733
|
+
retainedDelta: serializeTokenSubunits(0n),
|
|
1734
|
+
heldDelta: serializeTokenSubunits(0n),
|
|
1735
|
+
reversedDelta: serializeTokenSubunits(0n),
|
|
1736
|
+
refundedDelta: serializeTokenSubunits(0n),
|
|
1737
|
+
chargebackDelta: serializeTokenSubunits(0n),
|
|
1738
|
+
oneTimeReversalDelta: serializeTokenSubunits(0n)
|
|
1739
|
+
});
|
|
1740
|
+
var EVENT_TYPES = [
|
|
1741
|
+
"refund",
|
|
1742
|
+
"dispute-hold",
|
|
1743
|
+
"dispute-won",
|
|
1744
|
+
"dispute-lost",
|
|
1745
|
+
"chargeback",
|
|
1746
|
+
"reversal"
|
|
1747
|
+
];
|
|
1748
|
+
var EVENT_PRECEDENCE = {
|
|
1749
|
+
"dispute-hold": 0,
|
|
1750
|
+
"dispute-won": 1,
|
|
1751
|
+
"dispute-lost": 1,
|
|
1752
|
+
refund: 2,
|
|
1753
|
+
chargeback: 3,
|
|
1754
|
+
reversal: 4
|
|
1755
|
+
};
|
|
1756
|
+
function assertPaidLotPurchaseProvenance(provenance) {
|
|
1757
|
+
economyAssert(
|
|
1758
|
+
provenance.schemaVersion === ECONOMY_CONTRACT_VERSION,
|
|
1759
|
+
"INVALID_CONTRACT",
|
|
1760
|
+
"Unsupported paid-lot provenance contract version"
|
|
1761
|
+
);
|
|
1762
|
+
assertEconomyIdentifier(provenance.purchaseId, "purchaseId");
|
|
1763
|
+
assertEconomyIdentifier(provenance.catalogVersion, "catalogVersion");
|
|
1764
|
+
parseIsoTimestamp(provenance.purchasedAt);
|
|
1765
|
+
}
|
|
1766
|
+
function assertPaidLotLifecycleEvent(event) {
|
|
1767
|
+
economyAssert(
|
|
1768
|
+
event.schemaVersion === ECONOMY_CONTRACT_VERSION,
|
|
1769
|
+
"INVALID_CONTRACT",
|
|
1770
|
+
"Unsupported paid-lot lifecycle event contract version"
|
|
1771
|
+
);
|
|
1772
|
+
assertEconomyIdentifier(event.eventId, "eventId");
|
|
1773
|
+
assertEconomyIdentifier(event.lotId, "lotId");
|
|
1774
|
+
economyAssert(
|
|
1775
|
+
EVENT_TYPES.includes(event.eventType),
|
|
1776
|
+
"INVALID_CONTRACT",
|
|
1777
|
+
"Paid-lot lifecycle event type is unsupported"
|
|
1778
|
+
);
|
|
1779
|
+
economyAssert(
|
|
1780
|
+
parseTokenSubunits(event.amount) > 0n,
|
|
1781
|
+
"INVALID_AMOUNT",
|
|
1782
|
+
"Paid-lot lifecycle event amount must be positive"
|
|
1783
|
+
);
|
|
1784
|
+
parseIsoTimestamp(event.occurredAt);
|
|
1785
|
+
}
|
|
1786
|
+
function sameEvent(left, right) {
|
|
1787
|
+
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;
|
|
1788
|
+
}
|
|
1789
|
+
function compareEvents(left, right) {
|
|
1790
|
+
const timeDifference = parseIsoTimestamp(left.occurredAt) - parseIsoTimestamp(right.occurredAt);
|
|
1791
|
+
if (timeDifference !== 0) {
|
|
1792
|
+
return timeDifference;
|
|
1793
|
+
}
|
|
1794
|
+
const precedenceDifference = EVENT_PRECEDENCE[left.eventType] - EVENT_PRECEDENCE[right.eventType];
|
|
1795
|
+
return precedenceDifference === 0 ? compareUnicodeCodeUnits(left.eventId, right.eventId) : precedenceDifference;
|
|
1796
|
+
}
|
|
1797
|
+
function deriveStatus(retained, held, reversed, chargeback) {
|
|
1798
|
+
if (held > 0n) {
|
|
1799
|
+
return "disputed";
|
|
1800
|
+
}
|
|
1801
|
+
if (chargeback > 0n) {
|
|
1802
|
+
return "chargeback-lost";
|
|
1803
|
+
}
|
|
1804
|
+
if (retained === 0n) {
|
|
1805
|
+
return "fully-reversed";
|
|
1806
|
+
}
|
|
1807
|
+
return reversed > 0n ? "partially-reversed" : "clear";
|
|
1808
|
+
}
|
|
1809
|
+
function immutableFactsFromLifecycle(lifecycle) {
|
|
1810
|
+
return {
|
|
1811
|
+
schemaVersion: lifecycle.schemaVersion,
|
|
1812
|
+
lotId: lifecycle.lotId,
|
|
1813
|
+
payerAccountId: lifecycle.payerAccountId,
|
|
1814
|
+
receivingHouseholdId: lifecycle.receivingHouseholdId,
|
|
1815
|
+
purchaseId: lifecycle.purchaseId,
|
|
1816
|
+
catalogVersion: lifecycle.catalogVersion,
|
|
1817
|
+
purchasedAt: lifecycle.purchasedAt,
|
|
1818
|
+
settledAt: lifecycle.settledAt,
|
|
1819
|
+
creditedAt: lifecycle.creditedAt,
|
|
1820
|
+
originalAmount: lifecycle.originalAmount
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
function assertImmutableFacts(facts) {
|
|
1824
|
+
economyAssert(
|
|
1825
|
+
facts.schemaVersion === ECONOMY_CONTRACT_VERSION,
|
|
1826
|
+
"INVALID_CONTRACT",
|
|
1827
|
+
"Unsupported paid-lot lifecycle contract version"
|
|
1828
|
+
);
|
|
1829
|
+
assertEconomyIdentifier(facts.lotId, "lotId");
|
|
1830
|
+
assertEconomyIdentifier(facts.payerAccountId, "payerAccountId");
|
|
1831
|
+
assertEconomyIdentifier(
|
|
1832
|
+
facts.receivingHouseholdId,
|
|
1833
|
+
"receivingHouseholdId"
|
|
1834
|
+
);
|
|
1835
|
+
assertEconomyIdentifier(facts.purchaseId, "purchaseId");
|
|
1836
|
+
assertEconomyIdentifier(facts.catalogVersion, "catalogVersion");
|
|
1837
|
+
const purchasedAt = parseIsoTimestamp(facts.purchasedAt);
|
|
1838
|
+
const settledAt = parseIsoTimestamp(facts.settledAt);
|
|
1839
|
+
const creditedAt = parseIsoTimestamp(facts.creditedAt);
|
|
1840
|
+
economyAssert(
|
|
1841
|
+
purchasedAt <= settledAt && settledAt <= creditedAt,
|
|
1842
|
+
"INVALID_TIME_WINDOW",
|
|
1843
|
+
"Paid-lot timestamps must follow purchase, settlement, then credit order"
|
|
1844
|
+
);
|
|
1845
|
+
economyAssert(
|
|
1846
|
+
parseTokenSubunits(facts.originalAmount) > 0n,
|
|
1847
|
+
"INVALID_AMOUNT",
|
|
1848
|
+
"Paid-lot original amount must be positive"
|
|
1849
|
+
);
|
|
1850
|
+
}
|
|
1851
|
+
function projectPaidLot(facts, receipts) {
|
|
1852
|
+
assertImmutableFacts(facts);
|
|
1853
|
+
const eventIds = /* @__PURE__ */ new Set();
|
|
1854
|
+
let reversalReceipts = 0;
|
|
1855
|
+
for (const receipt of receipts) {
|
|
1856
|
+
economyAssert(
|
|
1857
|
+
receipt.schemaVersion === ECONOMY_CONTRACT_VERSION,
|
|
1858
|
+
"INVALID_CONTRACT",
|
|
1859
|
+
"Unsupported paid-lot receipt contract version"
|
|
1860
|
+
);
|
|
1861
|
+
assertPaidLotLifecycleEvent(receipt.event);
|
|
1862
|
+
economyAssert(
|
|
1863
|
+
receipt.event.lotId === facts.lotId && !eventIds.has(receipt.event.eventId),
|
|
1864
|
+
"DUPLICATE_IDENTIFIER",
|
|
1865
|
+
"Paid-lot receipts must be unique and belong to the lifecycle lot"
|
|
1866
|
+
);
|
|
1867
|
+
eventIds.add(receipt.event.eventId);
|
|
1868
|
+
if (receipt.event.eventType === "reversal") {
|
|
1869
|
+
reversalReceipts += 1;
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
economyAssert(
|
|
1873
|
+
reversalReceipts <= 1,
|
|
1874
|
+
"REVERSAL_ALREADY_EXISTS",
|
|
1875
|
+
"Paid-lot lifecycle permits only one distinct reversal event"
|
|
1876
|
+
);
|
|
1877
|
+
let retained = parseTokenSubunits(facts.originalAmount);
|
|
1878
|
+
let held = 0n;
|
|
1879
|
+
let reversed = 0n;
|
|
1880
|
+
let refunded = 0n;
|
|
1881
|
+
let chargeback = 0n;
|
|
1882
|
+
let oneTimeReversal = 0n;
|
|
1883
|
+
const effectiveEventIds = [];
|
|
1884
|
+
for (const receipt of [...receipts].sort(
|
|
1885
|
+
(left, right) => compareEvents(left.event, right.event)
|
|
1886
|
+
)) {
|
|
1887
|
+
const event = receipt.event;
|
|
1888
|
+
economyAssert(
|
|
1889
|
+
parseIsoTimestamp(event.occurredAt) >= parseIsoTimestamp(facts.creditedAt),
|
|
1890
|
+
"INVALID_TIME_WINDOW",
|
|
1891
|
+
"Paid-lot lifecycle event cannot precede credit"
|
|
1892
|
+
);
|
|
1893
|
+
const amount = parseTokenSubunits(event.amount);
|
|
1894
|
+
if (event.eventType === "dispute-hold") {
|
|
1895
|
+
economyAssert(
|
|
1896
|
+
amount <= retained - held,
|
|
1897
|
+
"INSUFFICIENT_ELIGIBLE_LOTS",
|
|
1898
|
+
"Dispute hold exceeds unheld retained paid-lot basis"
|
|
1899
|
+
);
|
|
1900
|
+
held += amount;
|
|
1901
|
+
} else if (event.eventType === "dispute-won") {
|
|
1902
|
+
economyAssert(
|
|
1903
|
+
amount <= held,
|
|
1904
|
+
"INVALID_AMOUNT",
|
|
1905
|
+
"Dispute-win release exceeds held paid-lot basis"
|
|
1906
|
+
);
|
|
1907
|
+
held -= amount;
|
|
1908
|
+
} else if (event.eventType === "dispute-lost") {
|
|
1909
|
+
economyAssert(
|
|
1910
|
+
amount <= held,
|
|
1911
|
+
"INVALID_AMOUNT",
|
|
1912
|
+
"Dispute-loss amount exceeds held paid-lot basis"
|
|
1913
|
+
);
|
|
1914
|
+
held -= amount;
|
|
1915
|
+
retained -= amount;
|
|
1916
|
+
reversed += amount;
|
|
1917
|
+
chargeback += amount;
|
|
1918
|
+
} else {
|
|
1919
|
+
economyAssert(
|
|
1920
|
+
amount <= retained - held,
|
|
1921
|
+
"INSUFFICIENT_ELIGIBLE_LOTS",
|
|
1922
|
+
"Paid-lot reversal exceeds unheld retained basis"
|
|
1923
|
+
);
|
|
1924
|
+
if (event.eventType === "reversal") {
|
|
1925
|
+
oneTimeReversal += amount;
|
|
1926
|
+
} else if (event.eventType === "refund") {
|
|
1927
|
+
refunded += amount;
|
|
1928
|
+
} else {
|
|
1929
|
+
chargeback += amount;
|
|
1930
|
+
}
|
|
1931
|
+
retained -= amount;
|
|
1932
|
+
reversed += amount;
|
|
1933
|
+
}
|
|
1934
|
+
effectiveEventIds.push(event.eventId);
|
|
1935
|
+
}
|
|
1936
|
+
return {
|
|
1937
|
+
retained,
|
|
1938
|
+
held,
|
|
1939
|
+
reversed,
|
|
1940
|
+
refunded,
|
|
1941
|
+
chargeback,
|
|
1942
|
+
oneTimeReversal,
|
|
1943
|
+
status: deriveStatus(retained, held, reversed, chargeback),
|
|
1944
|
+
effectiveEventIds
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
function projectionMatchesLifecycle(projection, lifecycle) {
|
|
1948
|
+
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(
|
|
1949
|
+
(eventId, index) => eventId === lifecycle.effectiveEventIds[index]
|
|
1950
|
+
);
|
|
1951
|
+
}
|
|
1952
|
+
function assertPaidLotLifecycle(lifecycle) {
|
|
1953
|
+
economyAssert(
|
|
1954
|
+
Number.isSafeInteger(lifecycle.version) && lifecycle.version === lifecycle.receipts.length + 1,
|
|
1955
|
+
"INVALID_CONTRACT",
|
|
1956
|
+
"Paid-lot lifecycle version must match its receipt count"
|
|
1957
|
+
);
|
|
1958
|
+
const projection = projectPaidLot(
|
|
1959
|
+
immutableFactsFromLifecycle(lifecycle),
|
|
1960
|
+
lifecycle.receipts
|
|
1961
|
+
);
|
|
1962
|
+
economyAssert(
|
|
1963
|
+
projectionMatchesLifecycle(projection, lifecycle),
|
|
1964
|
+
"INVALID_CONTRACT",
|
|
1965
|
+
"Paid-lot lifecycle does not match its immutable event rebuild"
|
|
1966
|
+
);
|
|
1967
|
+
}
|
|
1968
|
+
function createPaidLotLifecycle(lot, provenance) {
|
|
1969
|
+
assertSourceLot(lot);
|
|
1970
|
+
assertPaidLotPurchaseProvenance(provenance);
|
|
1971
|
+
economyAssert(
|
|
1972
|
+
lot.payerAccountId !== void 0 && lot.householdId !== void 0,
|
|
1973
|
+
"INVALID_CONTRACT",
|
|
1974
|
+
"Paid-lot lifecycle requires payer and receiving-household provenance"
|
|
1975
|
+
);
|
|
1976
|
+
economyAssert(
|
|
1977
|
+
lot.refundState === "none" && parseTokenSubunits(lot.heldAmount) === 0n && parseTokenSubunits(lot.reversedAmount) === 0n,
|
|
1978
|
+
"INVALID_CONTRACT",
|
|
1979
|
+
"Paid-lot lifecycle must be created before any refund or dispute movement"
|
|
1980
|
+
);
|
|
1981
|
+
economyAssert(
|
|
1982
|
+
parseIsoTimestamp(provenance.purchasedAt) <= parseIsoTimestamp(lot.settledAt),
|
|
1983
|
+
"INVALID_TIME_WINDOW",
|
|
1984
|
+
"Paid purchase time cannot follow settlement"
|
|
1985
|
+
);
|
|
1986
|
+
const facts = {
|
|
1987
|
+
schemaVersion: ECONOMY_CONTRACT_VERSION,
|
|
1988
|
+
lotId: lot.lotId,
|
|
1989
|
+
payerAccountId: lot.payerAccountId,
|
|
1990
|
+
receivingHouseholdId: lot.householdId,
|
|
1991
|
+
purchaseId: provenance.purchaseId,
|
|
1992
|
+
catalogVersion: provenance.catalogVersion,
|
|
1993
|
+
purchasedAt: provenance.purchasedAt,
|
|
1994
|
+
settledAt: lot.settledAt,
|
|
1995
|
+
creditedAt: lot.creditedAt,
|
|
1996
|
+
originalAmount: lot.originalAmount
|
|
1997
|
+
};
|
|
1998
|
+
const projection = projectPaidLot(facts, []);
|
|
1999
|
+
const lifecycle = {
|
|
2000
|
+
...facts,
|
|
2001
|
+
retainedAmount: serializeTokenSubunits(projection.retained),
|
|
2002
|
+
heldAmount: serializeTokenSubunits(projection.held),
|
|
2003
|
+
reversedAmount: serializeTokenSubunits(projection.reversed),
|
|
2004
|
+
refundedAmount: serializeTokenSubunits(projection.refunded),
|
|
2005
|
+
chargebackAmount: serializeTokenSubunits(projection.chargeback),
|
|
2006
|
+
oneTimeReversalAmount: serializeTokenSubunits(
|
|
2007
|
+
projection.oneTimeReversal
|
|
2008
|
+
),
|
|
2009
|
+
status: projection.status,
|
|
2010
|
+
version: 1,
|
|
2011
|
+
receipts: [],
|
|
2012
|
+
effectiveEventIds: []
|
|
2013
|
+
};
|
|
2014
|
+
assertPaidLotLifecycle(lifecycle);
|
|
2015
|
+
return lifecycle;
|
|
2016
|
+
}
|
|
2017
|
+
function applyPaidLotLifecycleEvent(lifecycle, event, expectedVersion) {
|
|
2018
|
+
assertPaidLotLifecycle(lifecycle);
|
|
2019
|
+
assertPaidLotLifecycleEvent(event);
|
|
2020
|
+
economyAssert(
|
|
2021
|
+
event.lotId === lifecycle.lotId,
|
|
2022
|
+
"INVALID_CONTRACT",
|
|
2023
|
+
"Paid-lot event belongs to another source lot"
|
|
2024
|
+
);
|
|
2025
|
+
const replay = lifecycle.receipts.find(
|
|
2026
|
+
(receipt) => receipt.event.eventId === event.eventId
|
|
2027
|
+
);
|
|
2028
|
+
if (replay !== void 0) {
|
|
2029
|
+
economyAssert(
|
|
2030
|
+
sameEvent(replay.event, event),
|
|
2031
|
+
"DUPLICATE_IDENTIFIER",
|
|
2032
|
+
"Paid-lot event ID was reused with different facts"
|
|
2033
|
+
);
|
|
2034
|
+
return {
|
|
2035
|
+
lifecycle,
|
|
2036
|
+
applied: false,
|
|
2037
|
+
stateChanged: false,
|
|
2038
|
+
arithmetic: ZERO_ARITHMETIC
|
|
2039
|
+
};
|
|
2040
|
+
}
|
|
2041
|
+
economyAssert(
|
|
2042
|
+
expectedVersion === lifecycle.version,
|
|
2043
|
+
"INVALID_CONTRACT",
|
|
2044
|
+
"Paid-lot lifecycle event has a stale expected version"
|
|
2045
|
+
);
|
|
2046
|
+
const receipts = [
|
|
2047
|
+
...lifecycle.receipts,
|
|
2048
|
+
{ schemaVersion: ECONOMY_CONTRACT_VERSION, event }
|
|
2049
|
+
];
|
|
2050
|
+
const projection = projectPaidLot(
|
|
2051
|
+
immutableFactsFromLifecycle(lifecycle),
|
|
2052
|
+
receipts
|
|
2053
|
+
);
|
|
2054
|
+
const before = {
|
|
2055
|
+
retained: parseTokenSubunits(lifecycle.retainedAmount),
|
|
2056
|
+
held: parseTokenSubunits(lifecycle.heldAmount),
|
|
2057
|
+
reversed: parseTokenSubunits(lifecycle.reversedAmount),
|
|
2058
|
+
refunded: parseTokenSubunits(lifecycle.refundedAmount),
|
|
2059
|
+
chargeback: parseTokenSubunits(lifecycle.chargebackAmount),
|
|
2060
|
+
oneTimeReversal: parseTokenSubunits(lifecycle.oneTimeReversalAmount)
|
|
2061
|
+
};
|
|
2062
|
+
const next = {
|
|
2063
|
+
...lifecycle,
|
|
2064
|
+
retainedAmount: serializeTokenSubunits(projection.retained),
|
|
2065
|
+
heldAmount: serializeTokenSubunits(projection.held),
|
|
2066
|
+
reversedAmount: serializeTokenSubunits(projection.reversed),
|
|
2067
|
+
refundedAmount: serializeTokenSubunits(projection.refunded),
|
|
2068
|
+
chargebackAmount: serializeTokenSubunits(projection.chargeback),
|
|
2069
|
+
oneTimeReversalAmount: serializeTokenSubunits(
|
|
2070
|
+
projection.oneTimeReversal
|
|
2071
|
+
),
|
|
2072
|
+
status: projection.status,
|
|
2073
|
+
version: lifecycle.version + 1,
|
|
2074
|
+
receipts,
|
|
2075
|
+
effectiveEventIds: projection.effectiveEventIds
|
|
2076
|
+
};
|
|
2077
|
+
assertPaidLotLifecycle(next);
|
|
2078
|
+
const arithmetic = {
|
|
2079
|
+
retainedDelta: serializeTokenSubunits(
|
|
2080
|
+
projection.retained - before.retained
|
|
2081
|
+
),
|
|
2082
|
+
heldDelta: serializeTokenSubunits(projection.held - before.held),
|
|
2083
|
+
reversedDelta: serializeTokenSubunits(
|
|
2084
|
+
projection.reversed - before.reversed
|
|
2085
|
+
),
|
|
2086
|
+
refundedDelta: serializeTokenSubunits(
|
|
2087
|
+
projection.refunded - before.refunded
|
|
2088
|
+
),
|
|
2089
|
+
chargebackDelta: serializeTokenSubunits(
|
|
2090
|
+
projection.chargeback - before.chargeback
|
|
2091
|
+
),
|
|
2092
|
+
oneTimeReversalDelta: serializeTokenSubunits(
|
|
2093
|
+
projection.oneTimeReversal - before.oneTimeReversal
|
|
2094
|
+
)
|
|
2095
|
+
};
|
|
2096
|
+
return {
|
|
2097
|
+
lifecycle: next,
|
|
2098
|
+
applied: true,
|
|
2099
|
+
stateChanged: Object.values(arithmetic).some((delta) => delta !== "0"),
|
|
2100
|
+
arithmetic
|
|
2101
|
+
};
|
|
2102
|
+
}
|
|
2103
|
+
function reducePaidLotLifecycleEvents(initial, events) {
|
|
2104
|
+
assertPaidLotLifecycle(initial);
|
|
2105
|
+
const unique = /* @__PURE__ */ new Map();
|
|
2106
|
+
for (const event of events) {
|
|
2107
|
+
assertPaidLotLifecycleEvent(event);
|
|
2108
|
+
const existing = unique.get(event.eventId);
|
|
2109
|
+
if (existing !== void 0) {
|
|
2110
|
+
economyAssert(
|
|
2111
|
+
sameEvent(existing, event),
|
|
2112
|
+
"DUPLICATE_IDENTIFIER",
|
|
2113
|
+
"Paid-lot event ID was reused with different facts"
|
|
2114
|
+
);
|
|
2115
|
+
} else {
|
|
2116
|
+
unique.set(event.eventId, event);
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
let lifecycle = initial;
|
|
2120
|
+
for (const event of [...unique.values()].sort(compareEvents)) {
|
|
2121
|
+
lifecycle = applyPaidLotLifecycleEvent(
|
|
2122
|
+
lifecycle,
|
|
2123
|
+
event,
|
|
2124
|
+
lifecycle.version
|
|
2125
|
+
).lifecycle;
|
|
2126
|
+
}
|
|
2127
|
+
return lifecycle;
|
|
2128
|
+
}
|
|
2129
|
+
function createEarlyBackerRetentionFromPaidLot(lifecycle) {
|
|
2130
|
+
assertPaidLotLifecycle(lifecycle);
|
|
2131
|
+
return {
|
|
2132
|
+
schemaVersion: ECONOMY_CONTRACT_VERSION,
|
|
2133
|
+
lotId: lifecycle.lotId,
|
|
2134
|
+
payerAccountId: lifecycle.payerAccountId,
|
|
2135
|
+
receivingHouseholdId: lifecycle.receivingHouseholdId,
|
|
2136
|
+
purchaseId: lifecycle.purchaseId,
|
|
2137
|
+
catalogVersion: lifecycle.catalogVersion,
|
|
2138
|
+
purchasedAt: lifecycle.purchasedAt,
|
|
2139
|
+
settledAt: lifecycle.settledAt,
|
|
2140
|
+
creditedAt: lifecycle.creditedAt,
|
|
2141
|
+
retainedAmount: lifecycle.retainedAmount
|
|
2142
|
+
};
|
|
2143
|
+
}
|
|
2144
|
+
|
|
1710
2145
|
// src/ports/persistence.ts
|
|
1711
2146
|
function assertScopedPersistedIdempotencyResult(result) {
|
|
1712
2147
|
economyAssert(
|
|
@@ -2774,6 +3209,899 @@ function assertWalletActivityPageForPortfolio(page, scope) {
|
|
|
2774
3209
|
);
|
|
2775
3210
|
}
|
|
2776
3211
|
|
|
3212
|
+
// src/purchase-caps.ts
|
|
3213
|
+
var MAX_SIGNED_BIGINT = 2n ** 63n - 1n;
|
|
3214
|
+
var CANONICAL_NON_NEGATIVE_INTEGER = /^(?:0|[1-9][0-9]*)$/u;
|
|
3215
|
+
function parseGbpMinorUnits(value) {
|
|
3216
|
+
economyAssert(
|
|
3217
|
+
typeof value === "string" && CANONICAL_NON_NEGATIVE_INTEGER.test(value),
|
|
3218
|
+
"INVALID_AMOUNT",
|
|
3219
|
+
"GBP minor units must be a canonical non-negative integer string"
|
|
3220
|
+
);
|
|
3221
|
+
try {
|
|
3222
|
+
const amount = BigInt(value);
|
|
3223
|
+
economyAssert(
|
|
3224
|
+
amount <= MAX_SIGNED_BIGINT,
|
|
3225
|
+
"AMOUNT_OUT_OF_RANGE",
|
|
3226
|
+
"GBP minor units are outside the signed 64-bit range"
|
|
3227
|
+
);
|
|
3228
|
+
return amount;
|
|
3229
|
+
} catch (error) {
|
|
3230
|
+
if (error instanceof EconomyError) {
|
|
3231
|
+
throw error;
|
|
3232
|
+
}
|
|
3233
|
+
throw new EconomyError("INVALID_AMOUNT", "GBP minor units are invalid");
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
function serializeGbpMinorUnits(amount) {
|
|
3237
|
+
economyAssert(
|
|
3238
|
+
amount >= 0n && amount <= MAX_SIGNED_BIGINT,
|
|
3239
|
+
"AMOUNT_OUT_OF_RANGE",
|
|
3240
|
+
"GBP minor units are outside the signed 64-bit range"
|
|
3241
|
+
);
|
|
3242
|
+
return amount.toString(10);
|
|
3243
|
+
}
|
|
3244
|
+
var RESERVATION_STATUSES = [
|
|
3245
|
+
"reserved",
|
|
3246
|
+
"settled",
|
|
3247
|
+
"released",
|
|
3248
|
+
"expired"
|
|
3249
|
+
];
|
|
3250
|
+
var TRANSITION_PRECEDENCE = {
|
|
3251
|
+
settle: 0,
|
|
3252
|
+
release: 1,
|
|
3253
|
+
expire: 2
|
|
3254
|
+
};
|
|
3255
|
+
function assertPurchaseCapReservationTransition(transition) {
|
|
3256
|
+
economyAssert(
|
|
3257
|
+
transition.schemaVersion === ECONOMY_CONTRACT_VERSION,
|
|
3258
|
+
"INVALID_CONTRACT",
|
|
3259
|
+
"Unsupported purchase-cap transition contract version"
|
|
3260
|
+
);
|
|
3261
|
+
assertEconomyIdentifier(transition.transitionId, "transitionId");
|
|
3262
|
+
assertEconomyIdentifier(transition.reservationId, "reservationId");
|
|
3263
|
+
economyAssert(
|
|
3264
|
+
["settle", "release", "expire"].includes(transition.transitionType),
|
|
3265
|
+
"INVALID_CONTRACT",
|
|
3266
|
+
"Purchase-cap transition type is unsupported"
|
|
3267
|
+
);
|
|
3268
|
+
parseIsoTimestamp(transition.occurredAt);
|
|
3269
|
+
}
|
|
3270
|
+
function sameTransition(left, right) {
|
|
3271
|
+
return left.schemaVersion === right.schemaVersion && left.transitionId === right.transitionId && left.reservationId === right.reservationId && left.transitionType === right.transitionType && left.occurredAt === right.occurredAt;
|
|
3272
|
+
}
|
|
3273
|
+
function compareTransitions(left, right) {
|
|
3274
|
+
const timeDifference = parseIsoTimestamp(left.occurredAt) - parseIsoTimestamp(right.occurredAt);
|
|
3275
|
+
if (timeDifference !== 0) {
|
|
3276
|
+
return timeDifference;
|
|
3277
|
+
}
|
|
3278
|
+
const precedenceDifference = TRANSITION_PRECEDENCE[left.transitionType] - TRANSITION_PRECEDENCE[right.transitionType];
|
|
3279
|
+
return precedenceDifference === 0 ? compareUnicodeCodeUnits(left.transitionId, right.transitionId) : precedenceDifference;
|
|
3280
|
+
}
|
|
3281
|
+
function assertBinding(binding) {
|
|
3282
|
+
economyAssert(
|
|
3283
|
+
binding.schemaVersion === ECONOMY_CONTRACT_VERSION,
|
|
3284
|
+
"INVALID_CONTRACT",
|
|
3285
|
+
"Unsupported purchase-cap reservation contract version"
|
|
3286
|
+
);
|
|
3287
|
+
assertEconomyIdentifier(binding.reservationId, "reservationId");
|
|
3288
|
+
assertEconomyIdentifier(binding.payerAccountId, "payerAccountId");
|
|
3289
|
+
assertEconomyIdentifier(binding.householdId, "householdId");
|
|
3290
|
+
economyAssert(
|
|
3291
|
+
parseGbpMinorUnits(binding.priceMinorUnits) > 0n,
|
|
3292
|
+
"INVALID_AMOUNT",
|
|
3293
|
+
"Purchase-cap reservation amount must be positive"
|
|
3294
|
+
);
|
|
3295
|
+
economyAssert(
|
|
3296
|
+
parseIsoTimestamp(binding.expiresAt) > parseIsoTimestamp(binding.reservedAt),
|
|
3297
|
+
"INVALID_TIME_WINDOW",
|
|
3298
|
+
"Purchase-cap reservation expiry must follow its creation"
|
|
3299
|
+
);
|
|
3300
|
+
}
|
|
3301
|
+
function bindingFromReservation(reservation) {
|
|
3302
|
+
return {
|
|
3303
|
+
schemaVersion: reservation.schemaVersion,
|
|
3304
|
+
reservationId: reservation.reservationId,
|
|
3305
|
+
payerAccountId: reservation.payerAccountId,
|
|
3306
|
+
householdId: reservation.householdId,
|
|
3307
|
+
priceMinorUnits: reservation.priceMinorUnits,
|
|
3308
|
+
reservedAt: reservation.reservedAt,
|
|
3309
|
+
expiresAt: reservation.expiresAt
|
|
3310
|
+
};
|
|
3311
|
+
}
|
|
3312
|
+
function projectReservation(binding, receipts) {
|
|
3313
|
+
assertBinding(binding);
|
|
3314
|
+
const reservedAt = parseIsoTimestamp(binding.reservedAt);
|
|
3315
|
+
const expiresAt = parseIsoTimestamp(binding.expiresAt);
|
|
3316
|
+
const transitionIds = /* @__PURE__ */ new Set();
|
|
3317
|
+
for (const receipt of receipts) {
|
|
3318
|
+
economyAssert(
|
|
3319
|
+
receipt.schemaVersion === ECONOMY_CONTRACT_VERSION,
|
|
3320
|
+
"INVALID_CONTRACT",
|
|
3321
|
+
"Unsupported purchase-cap transition receipt version"
|
|
3322
|
+
);
|
|
3323
|
+
assertPurchaseCapReservationTransition(receipt.transition);
|
|
3324
|
+
economyAssert(
|
|
3325
|
+
receipt.transition.reservationId === binding.reservationId && !transitionIds.has(receipt.transition.transitionId),
|
|
3326
|
+
"DUPLICATE_IDENTIFIER",
|
|
3327
|
+
"Purchase-cap receipts must be unique and belong to the reservation"
|
|
3328
|
+
);
|
|
3329
|
+
transitionIds.add(receipt.transition.transitionId);
|
|
3330
|
+
}
|
|
3331
|
+
let status = "reserved";
|
|
3332
|
+
let finalTransitionId;
|
|
3333
|
+
let finalizedAt;
|
|
3334
|
+
for (const receipt of [...receipts].sort(
|
|
3335
|
+
(left, right) => compareTransitions(left.transition, right.transition)
|
|
3336
|
+
)) {
|
|
3337
|
+
const transition = receipt.transition;
|
|
3338
|
+
const occurredAt = parseIsoTimestamp(transition.occurredAt);
|
|
3339
|
+
economyAssert(
|
|
3340
|
+
occurredAt >= reservedAt,
|
|
3341
|
+
"INVALID_TIME_WINDOW",
|
|
3342
|
+
"Purchase-cap transition cannot precede reservation"
|
|
3343
|
+
);
|
|
3344
|
+
if (transition.transitionType === "settle") {
|
|
3345
|
+
economyAssert(
|
|
3346
|
+
occurredAt < expiresAt,
|
|
3347
|
+
"INVALID_TIME_WINDOW",
|
|
3348
|
+
"Purchase-cap settlement must use payment time before expiry"
|
|
3349
|
+
);
|
|
3350
|
+
} else if (transition.transitionType === "release") {
|
|
3351
|
+
economyAssert(
|
|
3352
|
+
occurredAt < expiresAt,
|
|
3353
|
+
"INVALID_TIME_WINDOW",
|
|
3354
|
+
"Purchase-cap release must precede reservation expiry"
|
|
3355
|
+
);
|
|
3356
|
+
} else {
|
|
3357
|
+
economyAssert(
|
|
3358
|
+
occurredAt >= expiresAt,
|
|
3359
|
+
"INVALID_TIME_WINDOW",
|
|
3360
|
+
"Purchase-cap reservation cannot expire before its deadline"
|
|
3361
|
+
);
|
|
3362
|
+
}
|
|
3363
|
+
if (status === "reserved") {
|
|
3364
|
+
status = transition.transitionType === "settle" ? "settled" : transition.transitionType === "release" ? "released" : "expired";
|
|
3365
|
+
finalTransitionId = transition.transitionId;
|
|
3366
|
+
finalizedAt = transition.occurredAt;
|
|
3367
|
+
}
|
|
3368
|
+
}
|
|
3369
|
+
return {
|
|
3370
|
+
...binding,
|
|
3371
|
+
status,
|
|
3372
|
+
...finalTransitionId === void 0 ? {} : { finalTransitionId, finalizedAt },
|
|
3373
|
+
transitionReceipts: receipts
|
|
3374
|
+
};
|
|
3375
|
+
}
|
|
3376
|
+
function sameReservationFacts(left, right) {
|
|
3377
|
+
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) => {
|
|
3378
|
+
const other = right.transitionReceipts[index];
|
|
3379
|
+
return other !== void 0 && receipt.schemaVersion === other.schemaVersion && sameTransition(receipt.transition, other.transition);
|
|
3380
|
+
});
|
|
3381
|
+
}
|
|
3382
|
+
function sameReservationBinding(reservation, proposed) {
|
|
3383
|
+
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;
|
|
3384
|
+
}
|
|
3385
|
+
function assertPurchaseCapReservation(reservation) {
|
|
3386
|
+
economyAssert(
|
|
3387
|
+
RESERVATION_STATUSES.includes(reservation.status),
|
|
3388
|
+
"INVALID_CONTRACT",
|
|
3389
|
+
"Purchase-cap reservation status is unsupported"
|
|
3390
|
+
);
|
|
3391
|
+
const projected = projectReservation(
|
|
3392
|
+
bindingFromReservation(reservation),
|
|
3393
|
+
reservation.transitionReceipts
|
|
3394
|
+
);
|
|
3395
|
+
economyAssert(
|
|
3396
|
+
sameReservationFacts(projected, reservation),
|
|
3397
|
+
"INVALID_CONTRACT",
|
|
3398
|
+
"Purchase-cap reservation does not match its immutable event rebuild"
|
|
3399
|
+
);
|
|
3400
|
+
}
|
|
3401
|
+
function assertRollingPurchaseCapState(state) {
|
|
3402
|
+
economyAssert(
|
|
3403
|
+
state.schemaVersion === ECONOMY_CONTRACT_VERSION,
|
|
3404
|
+
"INVALID_CONTRACT",
|
|
3405
|
+
"Unsupported rolling purchase-cap contract version"
|
|
3406
|
+
);
|
|
3407
|
+
economyAssert(
|
|
3408
|
+
state.scopeType === "payer" || state.scopeType === "household",
|
|
3409
|
+
"INVALID_CONTRACT",
|
|
3410
|
+
"Purchase-cap scope type is unsupported"
|
|
3411
|
+
);
|
|
3412
|
+
assertEconomyIdentifier(state.scopeId, "scopeId");
|
|
3413
|
+
const reservationIds = /* @__PURE__ */ new Set();
|
|
3414
|
+
const transitionIds = /* @__PURE__ */ new Set();
|
|
3415
|
+
let receiptCount = 0;
|
|
3416
|
+
for (const reservation of state.reservations) {
|
|
3417
|
+
assertPurchaseCapReservation(reservation);
|
|
3418
|
+
economyAssert(
|
|
3419
|
+
!reservationIds.has(reservation.reservationId),
|
|
3420
|
+
"DUPLICATE_IDENTIFIER",
|
|
3421
|
+
"Purchase-cap state cannot repeat a reservation"
|
|
3422
|
+
);
|
|
3423
|
+
reservationIds.add(reservation.reservationId);
|
|
3424
|
+
economyAssert(
|
|
3425
|
+
state.scopeType === "payer" ? reservation.payerAccountId === state.scopeId : reservation.householdId === state.scopeId,
|
|
3426
|
+
"INVALID_CONTRACT",
|
|
3427
|
+
"Purchase-cap reservation belongs to another aggregate scope"
|
|
3428
|
+
);
|
|
3429
|
+
for (const receipt of reservation.transitionReceipts) {
|
|
3430
|
+
economyAssert(
|
|
3431
|
+
!transitionIds.has(receipt.transition.transitionId),
|
|
3432
|
+
"DUPLICATE_IDENTIFIER",
|
|
3433
|
+
"Purchase-cap state cannot repeat a transition ID"
|
|
3434
|
+
);
|
|
3435
|
+
transitionIds.add(receipt.transition.transitionId);
|
|
3436
|
+
receiptCount += 1;
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3439
|
+
economyAssert(
|
|
3440
|
+
Number.isSafeInteger(state.version) && state.version === 1 + state.reservations.length + receiptCount,
|
|
3441
|
+
"INVALID_CONTRACT",
|
|
3442
|
+
"Purchase-cap state version must match reservation and receipt counts"
|
|
3443
|
+
);
|
|
3444
|
+
}
|
|
3445
|
+
function assertAggregatePair(payerState, householdState, payerAccountId, householdId) {
|
|
3446
|
+
assertRollingPurchaseCapState(payerState);
|
|
3447
|
+
assertRollingPurchaseCapState(householdState);
|
|
3448
|
+
economyAssert(
|
|
3449
|
+
payerState.scopeType === "payer" && payerState.scopeId === payerAccountId && householdState.scopeType === "household" && householdState.scopeId === householdId,
|
|
3450
|
+
"INVALID_CONTRACT",
|
|
3451
|
+
"Purchase-cap aggregate pair does not match payer and household"
|
|
3452
|
+
);
|
|
3453
|
+
}
|
|
3454
|
+
function findMirroredReservation(payerState, householdState, reservationId) {
|
|
3455
|
+
const payerReservation = payerState.reservations.find(
|
|
3456
|
+
(reservation) => reservation.reservationId === reservationId
|
|
3457
|
+
);
|
|
3458
|
+
const householdReservation = householdState.reservations.find(
|
|
3459
|
+
(reservation) => reservation.reservationId === reservationId
|
|
3460
|
+
);
|
|
3461
|
+
economyAssert(
|
|
3462
|
+
payerReservation === void 0 === (householdReservation === void 0),
|
|
3463
|
+
"INVALID_CONTRACT",
|
|
3464
|
+
"Purchase-cap reservation must exist in both aggregate scopes atomically"
|
|
3465
|
+
);
|
|
3466
|
+
if (payerReservation === void 0 || householdReservation === void 0) {
|
|
3467
|
+
return void 0;
|
|
3468
|
+
}
|
|
3469
|
+
economyAssert(
|
|
3470
|
+
sameReservationFacts(payerReservation, householdReservation),
|
|
3471
|
+
"INVALID_CONTRACT",
|
|
3472
|
+
"Payer and household purchase-cap reservations have diverged"
|
|
3473
|
+
);
|
|
3474
|
+
return payerReservation;
|
|
3475
|
+
}
|
|
3476
|
+
function windowStart(checkedAt, rollingDays) {
|
|
3477
|
+
return checkedAt - rollingDays * 24 * 60 * 60 * 1e3;
|
|
3478
|
+
}
|
|
3479
|
+
function calculateRollingPurchaseCapUsage(state, checkedAt, policy) {
|
|
3480
|
+
assertRollingPurchaseCapState(state);
|
|
3481
|
+
assertPurchaseLimitPolicy(policy);
|
|
3482
|
+
const checkedAtValue = parseIsoTimestamp(checkedAt);
|
|
3483
|
+
const start = windowStart(checkedAtValue, policy.rollingDays);
|
|
3484
|
+
let total = 0n;
|
|
3485
|
+
for (const reservation of state.reservations) {
|
|
3486
|
+
if (reservation.status === "reserved" && parseIsoTimestamp(reservation.reservedAt) <= checkedAtValue && checkedAtValue < parseIsoTimestamp(reservation.expiresAt)) {
|
|
3487
|
+
total += parseGbpMinorUnits(reservation.priceMinorUnits);
|
|
3488
|
+
} else if (reservation.status === "settled" && reservation.finalizedAt !== void 0) {
|
|
3489
|
+
const settledAt = parseIsoTimestamp(reservation.finalizedAt);
|
|
3490
|
+
if (settledAt >= start && settledAt <= checkedAtValue) {
|
|
3491
|
+
total += parseGbpMinorUnits(reservation.priceMinorUnits);
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
economyAssert(
|
|
3495
|
+
total <= MAX_SIGNED_BIGINT,
|
|
3496
|
+
"AMOUNT_OUT_OF_RANGE",
|
|
3497
|
+
"Rolling purchase-cap usage exceeds signed 64-bit range"
|
|
3498
|
+
);
|
|
3499
|
+
}
|
|
3500
|
+
return serializeGbpMinorUnits(total);
|
|
3501
|
+
}
|
|
3502
|
+
function createRollingPurchaseCapState(scopeType, scopeId) {
|
|
3503
|
+
economyAssert(
|
|
3504
|
+
scopeType === "payer" || scopeType === "household",
|
|
3505
|
+
"INVALID_CONTRACT",
|
|
3506
|
+
"Purchase-cap scope type is unsupported"
|
|
3507
|
+
);
|
|
3508
|
+
assertEconomyIdentifier(scopeId, "scopeId");
|
|
3509
|
+
return {
|
|
3510
|
+
schemaVersion: ECONOMY_CONTRACT_VERSION,
|
|
3511
|
+
scopeType,
|
|
3512
|
+
scopeId,
|
|
3513
|
+
version: 1,
|
|
3514
|
+
reservations: []
|
|
3515
|
+
};
|
|
3516
|
+
}
|
|
3517
|
+
function sortedWithReservation(state, reservation) {
|
|
3518
|
+
return [...state.reservations, reservation].sort(
|
|
3519
|
+
(left, right) => compareUnicodeCodeUnits(left.reservationId, right.reservationId)
|
|
3520
|
+
);
|
|
3521
|
+
}
|
|
3522
|
+
function resultWithUsage(payerState, householdState, reservation, applied, stateChanged, checkedAt, policy) {
|
|
3523
|
+
return {
|
|
3524
|
+
payerState,
|
|
3525
|
+
householdState,
|
|
3526
|
+
reservation,
|
|
3527
|
+
applied,
|
|
3528
|
+
stateChanged,
|
|
3529
|
+
payerUsageMinorUnits: calculateRollingPurchaseCapUsage(
|
|
3530
|
+
payerState,
|
|
3531
|
+
checkedAt,
|
|
3532
|
+
policy
|
|
3533
|
+
),
|
|
3534
|
+
householdUsageMinorUnits: calculateRollingPurchaseCapUsage(
|
|
3535
|
+
householdState,
|
|
3536
|
+
checkedAt,
|
|
3537
|
+
policy
|
|
3538
|
+
)
|
|
3539
|
+
};
|
|
3540
|
+
}
|
|
3541
|
+
function reserveRollingPurchaseCaps(payerState, householdState, command, policy) {
|
|
3542
|
+
economyAssert(
|
|
3543
|
+
command.schemaVersion === ECONOMY_CONTRACT_VERSION,
|
|
3544
|
+
"INVALID_CONTRACT",
|
|
3545
|
+
"Unsupported purchase-cap reservation command version"
|
|
3546
|
+
);
|
|
3547
|
+
const binding = {
|
|
3548
|
+
schemaVersion: ECONOMY_CONTRACT_VERSION,
|
|
3549
|
+
reservationId: command.reservationId,
|
|
3550
|
+
payerAccountId: command.payerAccountId,
|
|
3551
|
+
householdId: command.householdId,
|
|
3552
|
+
priceMinorUnits: command.priceMinorUnits,
|
|
3553
|
+
reservedAt: command.reservedAt,
|
|
3554
|
+
expiresAt: command.expiresAt
|
|
3555
|
+
};
|
|
3556
|
+
assertBinding(binding);
|
|
3557
|
+
assertAggregatePair(
|
|
3558
|
+
payerState,
|
|
3559
|
+
householdState,
|
|
3560
|
+
command.payerAccountId,
|
|
3561
|
+
command.householdId
|
|
3562
|
+
);
|
|
3563
|
+
assertPurchaseLimitPolicy(policy);
|
|
3564
|
+
const proposed = projectReservation(binding, []);
|
|
3565
|
+
const replay = findMirroredReservation(
|
|
3566
|
+
payerState,
|
|
3567
|
+
householdState,
|
|
3568
|
+
command.reservationId
|
|
3569
|
+
);
|
|
3570
|
+
if (replay !== void 0) {
|
|
3571
|
+
economyAssert(
|
|
3572
|
+
sameReservationBinding(replay, proposed),
|
|
3573
|
+
"DUPLICATE_IDENTIFIER",
|
|
3574
|
+
"Purchase-cap reservation ID was reused with different facts"
|
|
3575
|
+
);
|
|
3576
|
+
return resultWithUsage(
|
|
3577
|
+
payerState,
|
|
3578
|
+
householdState,
|
|
3579
|
+
replay,
|
|
3580
|
+
false,
|
|
3581
|
+
false,
|
|
3582
|
+
replay.finalizedAt ?? command.reservedAt,
|
|
3583
|
+
policy
|
|
3584
|
+
);
|
|
3585
|
+
}
|
|
3586
|
+
economyAssert(
|
|
3587
|
+
command.expectedPayerVersion === payerState.version && command.expectedHouseholdVersion === householdState.version,
|
|
3588
|
+
"INVALID_CONTRACT",
|
|
3589
|
+
"Purchase-cap reservation has a stale aggregate version"
|
|
3590
|
+
);
|
|
3591
|
+
const amount = parseGbpMinorUnits(command.priceMinorUnits);
|
|
3592
|
+
economyAssert(
|
|
3593
|
+
amount <= parseGbpMinorUnits(policy.maxOrderPriceMinorUnits),
|
|
3594
|
+
"INSUFFICIENT_BALANCE",
|
|
3595
|
+
"Purchase amount exceeds the per-order ceiling"
|
|
3596
|
+
);
|
|
3597
|
+
const payerUsage = parseGbpMinorUnits(
|
|
3598
|
+
calculateRollingPurchaseCapUsage(payerState, command.reservedAt, policy)
|
|
3599
|
+
);
|
|
3600
|
+
const householdUsage = parseGbpMinorUnits(
|
|
3601
|
+
calculateRollingPurchaseCapUsage(
|
|
3602
|
+
householdState,
|
|
3603
|
+
command.reservedAt,
|
|
3604
|
+
policy
|
|
3605
|
+
)
|
|
3606
|
+
);
|
|
3607
|
+
economyAssert(
|
|
3608
|
+
payerUsage + amount <= parseGbpMinorUnits(policy.rollingPayerPriceMinorUnits),
|
|
3609
|
+
"INSUFFICIENT_BALANCE",
|
|
3610
|
+
"Purchase amount exceeds the rolling payer ceiling"
|
|
3611
|
+
);
|
|
3612
|
+
economyAssert(
|
|
3613
|
+
householdUsage + amount <= parseGbpMinorUnits(policy.rollingHouseholdPriceMinorUnits),
|
|
3614
|
+
"INSUFFICIENT_BALANCE",
|
|
3615
|
+
"Purchase amount exceeds the rolling household ceiling"
|
|
3616
|
+
);
|
|
3617
|
+
const nextPayer = {
|
|
3618
|
+
...payerState,
|
|
3619
|
+
version: payerState.version + 1,
|
|
3620
|
+
reservations: sortedWithReservation(payerState, proposed)
|
|
3621
|
+
};
|
|
3622
|
+
const nextHousehold = {
|
|
3623
|
+
...householdState,
|
|
3624
|
+
version: householdState.version + 1,
|
|
3625
|
+
reservations: sortedWithReservation(householdState, proposed)
|
|
3626
|
+
};
|
|
3627
|
+
return resultWithUsage(
|
|
3628
|
+
nextPayer,
|
|
3629
|
+
nextHousehold,
|
|
3630
|
+
proposed,
|
|
3631
|
+
true,
|
|
3632
|
+
true,
|
|
3633
|
+
command.reservedAt,
|
|
3634
|
+
policy
|
|
3635
|
+
);
|
|
3636
|
+
}
|
|
3637
|
+
function replaceReservation(state, replacement) {
|
|
3638
|
+
return {
|
|
3639
|
+
...state,
|
|
3640
|
+
version: state.version + 1,
|
|
3641
|
+
reservations: state.reservations.map(
|
|
3642
|
+
(reservation) => reservation.reservationId === replacement.reservationId ? replacement : reservation
|
|
3643
|
+
)
|
|
3644
|
+
};
|
|
3645
|
+
}
|
|
3646
|
+
function findTransitionById(state, transitionId) {
|
|
3647
|
+
for (const reservation of state.reservations) {
|
|
3648
|
+
const receipt = reservation.transitionReceipts.find(
|
|
3649
|
+
(candidate) => candidate.transition.transitionId === transitionId
|
|
3650
|
+
);
|
|
3651
|
+
if (receipt !== void 0) {
|
|
3652
|
+
return { reservationId: reservation.reservationId, transition: receipt.transition };
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3655
|
+
return void 0;
|
|
3656
|
+
}
|
|
3657
|
+
function transitionRollingPurchaseCapReservation(payerState, householdState, transition, expectedPayerVersion, expectedHouseholdVersion, policy) {
|
|
3658
|
+
assertPurchaseCapReservationTransition(transition);
|
|
3659
|
+
assertRollingPurchaseCapState(payerState);
|
|
3660
|
+
assertRollingPurchaseCapState(householdState);
|
|
3661
|
+
const current = findMirroredReservation(
|
|
3662
|
+
payerState,
|
|
3663
|
+
householdState,
|
|
3664
|
+
transition.reservationId
|
|
3665
|
+
);
|
|
3666
|
+
economyAssert(
|
|
3667
|
+
current !== void 0,
|
|
3668
|
+
"INVALID_CONTRACT",
|
|
3669
|
+
"Purchase-cap reservation does not exist in both aggregate scopes"
|
|
3670
|
+
);
|
|
3671
|
+
assertAggregatePair(
|
|
3672
|
+
payerState,
|
|
3673
|
+
householdState,
|
|
3674
|
+
current.payerAccountId,
|
|
3675
|
+
current.householdId
|
|
3676
|
+
);
|
|
3677
|
+
assertPurchaseLimitPolicy(policy);
|
|
3678
|
+
const payerExisting = findTransitionById(
|
|
3679
|
+
payerState,
|
|
3680
|
+
transition.transitionId
|
|
3681
|
+
);
|
|
3682
|
+
const householdExisting = findTransitionById(
|
|
3683
|
+
householdState,
|
|
3684
|
+
transition.transitionId
|
|
3685
|
+
);
|
|
3686
|
+
economyAssert(
|
|
3687
|
+
payerExisting === void 0 === (householdExisting === void 0),
|
|
3688
|
+
"INVALID_CONTRACT",
|
|
3689
|
+
"Purchase-cap transition must exist in both scopes atomically"
|
|
3690
|
+
);
|
|
3691
|
+
if (payerExisting !== void 0 && householdExisting !== void 0) {
|
|
3692
|
+
economyAssert(
|
|
3693
|
+
payerExisting.reservationId === transition.reservationId && householdExisting.reservationId === transition.reservationId && sameTransition(payerExisting.transition, transition) && sameTransition(householdExisting.transition, transition),
|
|
3694
|
+
"DUPLICATE_IDENTIFIER",
|
|
3695
|
+
"Purchase-cap transition ID was reused with different facts"
|
|
3696
|
+
);
|
|
3697
|
+
return resultWithUsage(
|
|
3698
|
+
payerState,
|
|
3699
|
+
householdState,
|
|
3700
|
+
current,
|
|
3701
|
+
false,
|
|
3702
|
+
false,
|
|
3703
|
+
transition.occurredAt,
|
|
3704
|
+
policy
|
|
3705
|
+
);
|
|
3706
|
+
}
|
|
3707
|
+
economyAssert(
|
|
3708
|
+
expectedPayerVersion === payerState.version && expectedHouseholdVersion === householdState.version,
|
|
3709
|
+
"INVALID_CONTRACT",
|
|
3710
|
+
"Purchase-cap transition has a stale aggregate version"
|
|
3711
|
+
);
|
|
3712
|
+
const replacement = projectReservation(bindingFromReservation(current), [
|
|
3713
|
+
...current.transitionReceipts,
|
|
3714
|
+
{ schemaVersion: ECONOMY_CONTRACT_VERSION, transition }
|
|
3715
|
+
]);
|
|
3716
|
+
const stateChanged = replacement.status !== current.status || replacement.finalTransitionId !== current.finalTransitionId || replacement.finalizedAt !== current.finalizedAt;
|
|
3717
|
+
const nextPayer = replaceReservation(payerState, replacement);
|
|
3718
|
+
const nextHousehold = replaceReservation(householdState, replacement);
|
|
3719
|
+
return resultWithUsage(
|
|
3720
|
+
nextPayer,
|
|
3721
|
+
nextHousehold,
|
|
3722
|
+
replacement,
|
|
3723
|
+
true,
|
|
3724
|
+
stateChanged,
|
|
3725
|
+
transition.occurredAt,
|
|
3726
|
+
policy
|
|
3727
|
+
);
|
|
3728
|
+
}
|
|
3729
|
+
|
|
3730
|
+
// src/purchase-intents.ts
|
|
3731
|
+
var TRANSITION_TYPES = [
|
|
3732
|
+
"checkout-bound",
|
|
3733
|
+
"payment-observed",
|
|
3734
|
+
"credit-recorded",
|
|
3735
|
+
"expire",
|
|
3736
|
+
"cancel",
|
|
3737
|
+
"dispute-opened",
|
|
3738
|
+
"dispute-won",
|
|
3739
|
+
"dispute-lost"
|
|
3740
|
+
];
|
|
3741
|
+
var TRANSITION_PRECEDENCE2 = {
|
|
3742
|
+
"checkout-bound": 0,
|
|
3743
|
+
"payment-observed": 1,
|
|
3744
|
+
"credit-recorded": 2,
|
|
3745
|
+
"dispute-opened": 3,
|
|
3746
|
+
"dispute-won": 4,
|
|
3747
|
+
"dispute-lost": 4,
|
|
3748
|
+
cancel: 5,
|
|
3749
|
+
expire: 6
|
|
3750
|
+
};
|
|
3751
|
+
function assertCheckoutHash(value) {
|
|
3752
|
+
economyAssert(
|
|
3753
|
+
/^sha256:[a-f0-9]{64}$/u.test(value),
|
|
3754
|
+
"INVALID_CONTRACT",
|
|
3755
|
+
"Checkout references must be stored as sanitized SHA-256 hashes"
|
|
3756
|
+
);
|
|
3757
|
+
}
|
|
3758
|
+
function assertPurchaseIntentTransition(transition) {
|
|
3759
|
+
economyAssert(
|
|
3760
|
+
transition.schemaVersion === ECONOMY_CONTRACT_VERSION,
|
|
3761
|
+
"INVALID_CONTRACT",
|
|
3762
|
+
"Unsupported purchase-intent transition contract version"
|
|
3763
|
+
);
|
|
3764
|
+
assertEconomyIdentifier(transition.transitionId, "transitionId");
|
|
3765
|
+
assertEconomyIdentifier(transition.intentId, "intentId");
|
|
3766
|
+
economyAssert(
|
|
3767
|
+
TRANSITION_TYPES.includes(transition.transitionType),
|
|
3768
|
+
"INVALID_CONTRACT",
|
|
3769
|
+
"Purchase-intent transition type is unsupported"
|
|
3770
|
+
);
|
|
3771
|
+
parseIsoTimestamp(transition.occurredAt);
|
|
3772
|
+
if (transition.providerCheckoutReferenceHash !== void 0) {
|
|
3773
|
+
assertCheckoutHash(transition.providerCheckoutReferenceHash);
|
|
3774
|
+
}
|
|
3775
|
+
const requiresCheckoutHash = transition.transitionType === "checkout-bound" || transition.transitionType === "payment-observed";
|
|
3776
|
+
economyAssert(
|
|
3777
|
+
requiresCheckoutHash === (transition.providerCheckoutReferenceHash !== void 0),
|
|
3778
|
+
"INVALID_CONTRACT",
|
|
3779
|
+
"Only checkout and payment transitions must carry a checkout binding"
|
|
3780
|
+
);
|
|
3781
|
+
}
|
|
3782
|
+
function sameTransition2(left, right) {
|
|
3783
|
+
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;
|
|
3784
|
+
}
|
|
3785
|
+
function sameIntent(left, right) {
|
|
3786
|
+
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;
|
|
3787
|
+
}
|
|
3788
|
+
function compareTransitions2(left, right) {
|
|
3789
|
+
const timeDifference = parseIsoTimestamp(left.occurredAt) - parseIsoTimestamp(right.occurredAt);
|
|
3790
|
+
if (timeDifference !== 0) {
|
|
3791
|
+
return timeDifference;
|
|
3792
|
+
}
|
|
3793
|
+
const precedenceDifference = TRANSITION_PRECEDENCE2[left.transitionType] - TRANSITION_PRECEDENCE2[right.transitionType];
|
|
3794
|
+
return precedenceDifference === 0 ? compareUnicodeCodeUnits(left.transitionId, right.transitionId) : precedenceDifference;
|
|
3795
|
+
}
|
|
3796
|
+
function assertMatchingCheckoutBinding(intent, checkoutHash) {
|
|
3797
|
+
economyAssert(
|
|
3798
|
+
intent.providerCheckoutReferenceHash === void 0 || intent.providerCheckoutReferenceHash === checkoutHash,
|
|
3799
|
+
"INVALID_CONTRACT",
|
|
3800
|
+
"Purchase-intent transition conflicts with its checkout binding"
|
|
3801
|
+
);
|
|
3802
|
+
}
|
|
3803
|
+
function projectPurchaseIntent(initialIntent, transitions, pack) {
|
|
3804
|
+
assertPurchaseIntentBinding(initialIntent, pack);
|
|
3805
|
+
economyAssert(
|
|
3806
|
+
initialIntent.status === "created" && initialIntent.providerCheckoutReferenceHash === void 0,
|
|
3807
|
+
"INVALID_CONTRACT",
|
|
3808
|
+
"Purchase-intent lifecycle requires immutable created intent facts"
|
|
3809
|
+
);
|
|
3810
|
+
const createdAt = parseIsoTimestamp(initialIntent.createdAt);
|
|
3811
|
+
const expiresAt = parseIsoTimestamp(initialIntent.expiresAt);
|
|
3812
|
+
let intent = initialIntent;
|
|
3813
|
+
let disputeDisposition = "none";
|
|
3814
|
+
let creditTransitionId;
|
|
3815
|
+
const effectiveTransitionIds = [];
|
|
3816
|
+
for (const transition of [...transitions].sort(compareTransitions2)) {
|
|
3817
|
+
assertPurchaseIntentTransition(transition);
|
|
3818
|
+
economyAssert(
|
|
3819
|
+
transition.intentId === initialIntent.intentId,
|
|
3820
|
+
"INVALID_CONTRACT",
|
|
3821
|
+
"Purchase-intent transition belongs to another intent"
|
|
3822
|
+
);
|
|
3823
|
+
const eventTime = parseIsoTimestamp(transition.occurredAt);
|
|
3824
|
+
economyAssert(
|
|
3825
|
+
eventTime >= createdAt,
|
|
3826
|
+
"INVALID_TIME_WINDOW",
|
|
3827
|
+
"Purchase-intent transition cannot precede intent creation"
|
|
3828
|
+
);
|
|
3829
|
+
let effective = false;
|
|
3830
|
+
if (transition.transitionType === "checkout-bound") {
|
|
3831
|
+
const checkoutHash = transition.providerCheckoutReferenceHash;
|
|
3832
|
+
assertMatchingCheckoutBinding(intent, checkoutHash);
|
|
3833
|
+
if (intent.status === "created") {
|
|
3834
|
+
economyAssert(
|
|
3835
|
+
eventTime < expiresAt,
|
|
3836
|
+
"INVALID_TIME_WINDOW",
|
|
3837
|
+
"Checkout cannot be bound after purchase-intent expiry"
|
|
3838
|
+
);
|
|
3839
|
+
intent = {
|
|
3840
|
+
...intent,
|
|
3841
|
+
status: "checkout-created",
|
|
3842
|
+
providerCheckoutReferenceHash: checkoutHash
|
|
3843
|
+
};
|
|
3844
|
+
effective = true;
|
|
3845
|
+
} else {
|
|
3846
|
+
economyAssert(
|
|
3847
|
+
!["expired", "cancelled"].includes(intent.status),
|
|
3848
|
+
"INVALID_CONTRACT",
|
|
3849
|
+
"A terminal purchase intent cannot bind a checkout"
|
|
3850
|
+
);
|
|
3851
|
+
}
|
|
3852
|
+
} else if (transition.transitionType === "payment-observed") {
|
|
3853
|
+
const checkoutHash = transition.providerCheckoutReferenceHash;
|
|
3854
|
+
assertMatchingCheckoutBinding(intent, checkoutHash);
|
|
3855
|
+
economyAssert(
|
|
3856
|
+
eventTime < expiresAt,
|
|
3857
|
+
"INVALID_TIME_WINDOW",
|
|
3858
|
+
"Authoritative payment time must precede purchase-intent expiry"
|
|
3859
|
+
);
|
|
3860
|
+
if (intent.status === "created") {
|
|
3861
|
+
intent = {
|
|
3862
|
+
...intent,
|
|
3863
|
+
status: "paid-unreconciled",
|
|
3864
|
+
providerCheckoutReferenceHash: checkoutHash
|
|
3865
|
+
};
|
|
3866
|
+
effective = true;
|
|
3867
|
+
} else if (intent.status === "checkout-created") {
|
|
3868
|
+
intent = { ...intent, status: "paid-unreconciled" };
|
|
3869
|
+
effective = true;
|
|
3870
|
+
} else {
|
|
3871
|
+
economyAssert(
|
|
3872
|
+
!["expired", "cancelled"].includes(intent.status),
|
|
3873
|
+
"INVALID_CONTRACT",
|
|
3874
|
+
"A terminal purchase intent cannot observe payment"
|
|
3875
|
+
);
|
|
3876
|
+
}
|
|
3877
|
+
} else if (transition.transitionType === "credit-recorded") {
|
|
3878
|
+
if (intent.status === "paid-unreconciled") {
|
|
3879
|
+
intent = { ...intent, status: "credited" };
|
|
3880
|
+
disputeDisposition = "none";
|
|
3881
|
+
creditTransitionId = transition.transitionId;
|
|
3882
|
+
effective = true;
|
|
3883
|
+
} else {
|
|
3884
|
+
economyAssert(
|
|
3885
|
+
intent.status === "credited" || intent.status === "disputed",
|
|
3886
|
+
"INVALID_CONTRACT",
|
|
3887
|
+
"Purchase intent cannot be credited before authoritative payment"
|
|
3888
|
+
);
|
|
3889
|
+
}
|
|
3890
|
+
} else if (transition.transitionType === "dispute-opened") {
|
|
3891
|
+
if (intent.status === "credited" && disputeDisposition === "none") {
|
|
3892
|
+
intent = { ...intent, status: "disputed" };
|
|
3893
|
+
disputeDisposition = "open";
|
|
3894
|
+
effective = true;
|
|
3895
|
+
} else {
|
|
3896
|
+
economyAssert(
|
|
3897
|
+
intent.status === "disputed" && disputeDisposition === "open",
|
|
3898
|
+
"INVALID_CONTRACT",
|
|
3899
|
+
"Dispute can open only once after purchase credit"
|
|
3900
|
+
);
|
|
3901
|
+
}
|
|
3902
|
+
} else if (transition.transitionType === "dispute-won") {
|
|
3903
|
+
if (intent.status === "disputed" && disputeDisposition === "open") {
|
|
3904
|
+
intent = { ...intent, status: "credited" };
|
|
3905
|
+
disputeDisposition = "won";
|
|
3906
|
+
effective = true;
|
|
3907
|
+
} else {
|
|
3908
|
+
economyAssert(
|
|
3909
|
+
intent.status === "credited" && disputeDisposition === "won",
|
|
3910
|
+
"INVALID_CONTRACT",
|
|
3911
|
+
"Dispute-win transition requires one open dispute"
|
|
3912
|
+
);
|
|
3913
|
+
}
|
|
3914
|
+
} else if (transition.transitionType === "dispute-lost") {
|
|
3915
|
+
if (intent.status === "disputed" && disputeDisposition === "open") {
|
|
3916
|
+
disputeDisposition = "lost";
|
|
3917
|
+
effective = true;
|
|
3918
|
+
} else {
|
|
3919
|
+
economyAssert(
|
|
3920
|
+
intent.status === "disputed" && disputeDisposition === "lost",
|
|
3921
|
+
"INVALID_CONTRACT",
|
|
3922
|
+
"Dispute-loss transition requires one open dispute"
|
|
3923
|
+
);
|
|
3924
|
+
}
|
|
3925
|
+
} else if (transition.transitionType === "cancel") {
|
|
3926
|
+
if (intent.status === "created" || intent.status === "checkout-created") {
|
|
3927
|
+
intent = { ...intent, status: "cancelled" };
|
|
3928
|
+
effective = true;
|
|
3929
|
+
}
|
|
3930
|
+
} else {
|
|
3931
|
+
economyAssert(
|
|
3932
|
+
eventTime >= expiresAt,
|
|
3933
|
+
"INVALID_TIME_WINDOW",
|
|
3934
|
+
"Expiry transition cannot precede purchase-intent expiry"
|
|
3935
|
+
);
|
|
3936
|
+
if (intent.status === "created" || intent.status === "checkout-created") {
|
|
3937
|
+
intent = { ...intent, status: "expired" };
|
|
3938
|
+
effective = true;
|
|
3939
|
+
}
|
|
3940
|
+
}
|
|
3941
|
+
if (effective) {
|
|
3942
|
+
effectiveTransitionIds.push(transition.transitionId);
|
|
3943
|
+
}
|
|
3944
|
+
}
|
|
3945
|
+
return {
|
|
3946
|
+
intent,
|
|
3947
|
+
disputeDisposition,
|
|
3948
|
+
effectiveTransitionIds,
|
|
3949
|
+
...creditTransitionId === void 0 ? {} : { creditTransitionId }
|
|
3950
|
+
};
|
|
3951
|
+
}
|
|
3952
|
+
function assertPurchaseIntentLifecycle(lifecycle, pack) {
|
|
3953
|
+
economyAssert(
|
|
3954
|
+
lifecycle.schemaVersion === ECONOMY_CONTRACT_VERSION,
|
|
3955
|
+
"INVALID_CONTRACT",
|
|
3956
|
+
"Unsupported purchase-intent lifecycle contract version"
|
|
3957
|
+
);
|
|
3958
|
+
economyAssert(
|
|
3959
|
+
Number.isSafeInteger(lifecycle.version) && lifecycle.version === lifecycle.receipts.length + 1,
|
|
3960
|
+
"INVALID_CONTRACT",
|
|
3961
|
+
"Purchase-intent lifecycle version must match its receipt count"
|
|
3962
|
+
);
|
|
3963
|
+
const transitionIds = /* @__PURE__ */ new Set();
|
|
3964
|
+
for (const receipt of lifecycle.receipts) {
|
|
3965
|
+
economyAssert(
|
|
3966
|
+
receipt.schemaVersion === ECONOMY_CONTRACT_VERSION,
|
|
3967
|
+
"INVALID_CONTRACT",
|
|
3968
|
+
"Purchase-intent receipt is invalid"
|
|
3969
|
+
);
|
|
3970
|
+
assertPurchaseIntentTransition(receipt.transition);
|
|
3971
|
+
economyAssert(
|
|
3972
|
+
receipt.transition.intentId === lifecycle.initialIntent.intentId && !transitionIds.has(receipt.transition.transitionId),
|
|
3973
|
+
"DUPLICATE_IDENTIFIER",
|
|
3974
|
+
"Purchase-intent receipts must be unique and belong to the intent"
|
|
3975
|
+
);
|
|
3976
|
+
transitionIds.add(receipt.transition.transitionId);
|
|
3977
|
+
}
|
|
3978
|
+
const projection = projectPurchaseIntent(
|
|
3979
|
+
lifecycle.initialIntent,
|
|
3980
|
+
lifecycle.receipts.map((receipt) => receipt.transition),
|
|
3981
|
+
pack
|
|
3982
|
+
);
|
|
3983
|
+
economyAssert(
|
|
3984
|
+
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(
|
|
3985
|
+
(transitionId, index) => transitionId === projection.effectiveTransitionIds[index]
|
|
3986
|
+
),
|
|
3987
|
+
"INVALID_CONTRACT",
|
|
3988
|
+
"Purchase-intent lifecycle does not match its immutable event rebuild"
|
|
3989
|
+
);
|
|
3990
|
+
}
|
|
3991
|
+
function createPurchaseIntentLifecycle(initialIntent, pack) {
|
|
3992
|
+
const projection = projectPurchaseIntent(initialIntent, [], pack);
|
|
3993
|
+
return {
|
|
3994
|
+
schemaVersion: ECONOMY_CONTRACT_VERSION,
|
|
3995
|
+
initialIntent,
|
|
3996
|
+
intent: projection.intent,
|
|
3997
|
+
version: 1,
|
|
3998
|
+
creditRecorded: false,
|
|
3999
|
+
disputeDisposition: projection.disputeDisposition,
|
|
4000
|
+
receipts: [],
|
|
4001
|
+
effectiveTransitionIds: []
|
|
4002
|
+
};
|
|
4003
|
+
}
|
|
4004
|
+
function sameProjection(lifecycle, projection) {
|
|
4005
|
+
return sameIntent(lifecycle.intent, projection.intent) && lifecycle.disputeDisposition === projection.disputeDisposition && lifecycle.effectiveTransitionIds.length === projection.effectiveTransitionIds.length && lifecycle.effectiveTransitionIds.every(
|
|
4006
|
+
(transitionId, index) => transitionId === projection.effectiveTransitionIds[index]
|
|
4007
|
+
);
|
|
4008
|
+
}
|
|
4009
|
+
function applyPurchaseIntentTransition(lifecycle, transition, expectedVersion, pack) {
|
|
4010
|
+
assertPurchaseIntentLifecycle(lifecycle, pack);
|
|
4011
|
+
assertPurchaseIntentTransition(transition);
|
|
4012
|
+
economyAssert(
|
|
4013
|
+
transition.intentId === lifecycle.initialIntent.intentId,
|
|
4014
|
+
"INVALID_CONTRACT",
|
|
4015
|
+
"Purchase-intent transition belongs to another intent"
|
|
4016
|
+
);
|
|
4017
|
+
const replay = lifecycle.receipts.find(
|
|
4018
|
+
(receipt) => receipt.transition.transitionId === transition.transitionId
|
|
4019
|
+
);
|
|
4020
|
+
if (replay !== void 0) {
|
|
4021
|
+
economyAssert(
|
|
4022
|
+
sameTransition2(replay.transition, transition),
|
|
4023
|
+
"DUPLICATE_IDENTIFIER",
|
|
4024
|
+
"Purchase-intent transition ID was reused with different facts"
|
|
4025
|
+
);
|
|
4026
|
+
return { lifecycle, recorded: false, stateChanged: false };
|
|
4027
|
+
}
|
|
4028
|
+
economyAssert(
|
|
4029
|
+
expectedVersion === lifecycle.version,
|
|
4030
|
+
"INVALID_CONTRACT",
|
|
4031
|
+
"Purchase-intent transition has a stale expected version"
|
|
4032
|
+
);
|
|
4033
|
+
const receipts = [
|
|
4034
|
+
...lifecycle.receipts,
|
|
4035
|
+
{ schemaVersion: ECONOMY_CONTRACT_VERSION, transition }
|
|
4036
|
+
];
|
|
4037
|
+
const projection = projectPurchaseIntent(
|
|
4038
|
+
lifecycle.initialIntent,
|
|
4039
|
+
receipts.map((receipt) => receipt.transition),
|
|
4040
|
+
pack
|
|
4041
|
+
);
|
|
4042
|
+
const stateChanged = !sameProjection(lifecycle, projection);
|
|
4043
|
+
const nextLifecycle = {
|
|
4044
|
+
schemaVersion: ECONOMY_CONTRACT_VERSION,
|
|
4045
|
+
initialIntent: lifecycle.initialIntent,
|
|
4046
|
+
intent: projection.intent,
|
|
4047
|
+
version: lifecycle.version + 1,
|
|
4048
|
+
creditRecorded: projection.intent.status === "credited" || projection.intent.status === "disputed",
|
|
4049
|
+
disputeDisposition: projection.disputeDisposition,
|
|
4050
|
+
receipts,
|
|
4051
|
+
effectiveTransitionIds: projection.effectiveTransitionIds
|
|
4052
|
+
};
|
|
4053
|
+
assertPurchaseIntentLifecycle(nextLifecycle, pack);
|
|
4054
|
+
const producesCredit = projection.creditTransitionId === transition.transitionId && !lifecycle.creditRecorded && nextLifecycle.creditRecorded;
|
|
4055
|
+
const creditInstruction = producesCredit ? {
|
|
4056
|
+
schemaVersion: ECONOMY_CONTRACT_VERSION,
|
|
4057
|
+
intentId: nextLifecycle.intent.intentId,
|
|
4058
|
+
transitionId: transition.transitionId,
|
|
4059
|
+
idempotencyKey: nextLifecycle.intent.intentId
|
|
4060
|
+
} : void 0;
|
|
4061
|
+
return {
|
|
4062
|
+
lifecycle: nextLifecycle,
|
|
4063
|
+
recorded: true,
|
|
4064
|
+
stateChanged,
|
|
4065
|
+
...creditInstruction === void 0 ? {} : { creditInstruction }
|
|
4066
|
+
};
|
|
4067
|
+
}
|
|
4068
|
+
function reducePurchaseIntentTransitions(initialIntent, transitions, pack) {
|
|
4069
|
+
const unique = /* @__PURE__ */ new Map();
|
|
4070
|
+
for (const transition of transitions) {
|
|
4071
|
+
assertPurchaseIntentTransition(transition);
|
|
4072
|
+
const existing = unique.get(transition.transitionId);
|
|
4073
|
+
if (existing !== void 0) {
|
|
4074
|
+
economyAssert(
|
|
4075
|
+
sameTransition2(existing, transition),
|
|
4076
|
+
"DUPLICATE_IDENTIFIER",
|
|
4077
|
+
"Purchase-intent transition ID was reused with different facts"
|
|
4078
|
+
);
|
|
4079
|
+
} else {
|
|
4080
|
+
unique.set(transition.transitionId, transition);
|
|
4081
|
+
}
|
|
4082
|
+
}
|
|
4083
|
+
let lifecycle = createPurchaseIntentLifecycle(initialIntent, pack);
|
|
4084
|
+
const creditInstructions = [];
|
|
4085
|
+
for (const transition of [...unique.values()].sort(compareTransitions2)) {
|
|
4086
|
+
const result = applyPurchaseIntentTransition(
|
|
4087
|
+
lifecycle,
|
|
4088
|
+
transition,
|
|
4089
|
+
lifecycle.version,
|
|
4090
|
+
pack
|
|
4091
|
+
);
|
|
4092
|
+
lifecycle = result.lifecycle;
|
|
4093
|
+
if (result.creditInstruction !== void 0) {
|
|
4094
|
+
creditInstructions.push(result.creditInstruction);
|
|
4095
|
+
}
|
|
4096
|
+
}
|
|
4097
|
+
economyAssert(
|
|
4098
|
+
creditInstructions.length <= 1,
|
|
4099
|
+
"DUPLICATE_TRANSACTION",
|
|
4100
|
+
"Purchase-intent reduction produced more than one credit"
|
|
4101
|
+
);
|
|
4102
|
+
return { lifecycle, creditInstructions };
|
|
4103
|
+
}
|
|
4104
|
+
|
|
2777
4105
|
// src/recurrence.ts
|
|
2778
4106
|
var BASELINE_MONTHLY_SUBSCRIPTION_PLAN = Object.freeze({
|
|
2779
4107
|
schemaVersion: ECONOMY_CONTRACT_VERSION,
|
|
@@ -2912,6 +4240,8 @@ function assertSpendRequest(request) {
|
|
|
2912
4240
|
TOKEN_SUBUNITS_MIN,
|
|
2913
4241
|
TOKEN_SUBUNITS_PER_TOKEN,
|
|
2914
4242
|
advanceJournalChainHead,
|
|
4243
|
+
applyPaidLotLifecycleEvent,
|
|
4244
|
+
applyPurchaseIntentTransition,
|
|
2915
4245
|
applySourceLotMovement,
|
|
2916
4246
|
applyTransactionToProjection,
|
|
2917
4247
|
applyWalletBalanceDelta,
|
|
@@ -2931,11 +4261,19 @@ function assertSpendRequest(request) {
|
|
|
2931
4261
|
assertJournalChainHead,
|
|
2932
4262
|
assertNonNegativeAccounts,
|
|
2933
4263
|
assertOpenPurchaseIntent,
|
|
4264
|
+
assertPaidLotLifecycle,
|
|
4265
|
+
assertPaidLotLifecycleEvent,
|
|
4266
|
+
assertPaidLotPurchaseProvenance,
|
|
4267
|
+
assertPurchaseCapReservation,
|
|
4268
|
+
assertPurchaseCapReservationTransition,
|
|
2934
4269
|
assertPurchaseIntentBinding,
|
|
4270
|
+
assertPurchaseIntentLifecycle,
|
|
4271
|
+
assertPurchaseIntentTransition,
|
|
2935
4272
|
assertPurchaseLimitPolicy,
|
|
2936
4273
|
assertReversalAvailable,
|
|
2937
4274
|
assertRewardConversion,
|
|
2938
4275
|
assertRewardRate,
|
|
4276
|
+
assertRollingPurchaseCapState,
|
|
2939
4277
|
assertScopedPersistedIdempotencyResult,
|
|
2940
4278
|
assertSourceLot,
|
|
2941
4279
|
assertSourceLotMovement,
|
|
@@ -2961,13 +4299,18 @@ function assertSpendRequest(request) {
|
|
|
2961
4299
|
assertWalletPortfolioSummary,
|
|
2962
4300
|
availableSourceLotAmount,
|
|
2963
4301
|
boostGameplayAllocation,
|
|
4302
|
+
calculateRollingPurchaseCapUsage,
|
|
2964
4303
|
canUseSourceLot,
|
|
2965
4304
|
canonicalTransactionPayload,
|
|
2966
4305
|
convertRewardPayout,
|
|
4306
|
+
createEarlyBackerRetentionFromPaidLot,
|
|
2967
4307
|
createGameplayAllocation,
|
|
2968
4308
|
createInitialSourceLotSnapshot,
|
|
4309
|
+
createPaidLotLifecycle,
|
|
2969
4310
|
createProjectionSnapshot,
|
|
4311
|
+
createPurchaseIntentLifecycle,
|
|
2970
4312
|
createReversalTransaction,
|
|
4313
|
+
createRollingPurchaseCapState,
|
|
2971
4314
|
createWalletBalanceSummary,
|
|
2972
4315
|
createWalletPortfolioLifetime,
|
|
2973
4316
|
createWalletPortfolioSummary,
|
|
@@ -2978,16 +4321,22 @@ function assertSpendRequest(request) {
|
|
|
2978
4321
|
evaluateEarlyBackerBySettlementV2,
|
|
2979
4322
|
isWholeTokenAmount,
|
|
2980
4323
|
mapBitLabsConversionState,
|
|
4324
|
+
parseGbpMinorUnits,
|
|
2981
4325
|
parseIsoTimestamp,
|
|
2982
4326
|
parseTokenSubunits,
|
|
2983
4327
|
rebuildBalanceProjection,
|
|
2984
4328
|
reclaimGameplayAllocation,
|
|
4329
|
+
reducePaidLotLifecycleEvents,
|
|
4330
|
+
reducePurchaseIntentTransitions,
|
|
4331
|
+
reserveRollingPurchaseCaps,
|
|
2985
4332
|
selectSourceLots,
|
|
4333
|
+
serializeGbpMinorUnits,
|
|
2986
4334
|
serializeTokenSubunits,
|
|
2987
4335
|
sortStringRecord,
|
|
2988
4336
|
subunitsToWholeTokens,
|
|
2989
4337
|
sumTokenSubunits,
|
|
2990
4338
|
toEconomyErrorEnvelope,
|
|
4339
|
+
transitionRollingPurchaseCapReservation,
|
|
2991
4340
|
wholeTokensToSubunits
|
|
2992
4341
|
});
|
|
2993
4342
|
//# sourceMappingURL=index.cjs.map
|