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