@plasius/economy 0.3.1 → 0.4.0

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