@voltro/plugin-economy 0.0.0-stage → 0.87.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/CHANGELOG.md +24329 -0
- package/LICENSE +61 -0
- package/README.md +2719 -2
- package/SECURITY.md +56 -0
- package/THIRD-PARTY-NOTICES.md +9189 -0
- package/dist/amount.js +21 -0
- package/dist/appleIap.server.js +263 -0
- package/dist/appleIapNotifications.server.js +163 -0
- package/dist/appleIapTrust.server.js +31 -0
- package/dist/bundleValue.server.js +113 -0
- package/dist/canonicalDataJson.js +11 -0
- package/dist/capture.js +54 -0
- package/dist/codePort.fixture.js +124 -0
- package/dist/codeStore.server.js +440 -0
- package/dist/codeTables.entity.js +44 -0
- package/dist/codes.d.ts +150 -0
- package/dist/codes.js +83 -0
- package/dist/components.d.ts +397 -0
- package/dist/components.js +284 -0
- package/dist/consistency.server.js +116 -0
- package/dist/containerStore.server.js +688 -0
- package/dist/containerStore.testFixtures.js +76 -0
- package/dist/containerTables.entity.js +46 -0
- package/dist/containers.d.ts +311 -0
- package/dist/containers.fixture.js +129 -0
- package/dist/containers.js +215 -0
- package/dist/containers.server.js +40 -0
- package/dist/containersPort.fixture.js +75 -0
- package/dist/currency.js +33 -0
- package/dist/declarationFixtures.js +62 -0
- package/dist/economy-app/events/economy.refundReversed.event.ts +1 -0
- package/dist/economy-app/events/economy.refunded.event.ts +1 -0
- package/dist/economyPort.fixture.js +125 -0
- package/dist/errors.d.ts +14 -0
- package/dist/errors.js +22 -0
- package/dist/events.d.ts +46 -0
- package/dist/events.js +1 -0
- package/dist/googleIap.server.js +392 -0
- package/dist/googleIapNotifications.server.js +131 -0
- package/dist/googleIapSubscription.server.js +184 -0
- package/dist/iap.d.ts +309 -0
- package/dist/iap.js +304 -0
- package/dist/iap.testFixtures.js +145 -0
- package/dist/iapAccountTables.entity.js +40 -0
- package/dist/iapAccounts.server.js +201 -0
- package/dist/iapBinding.server.js +524 -0
- package/dist/iapCheckout.server.js +98 -0
- package/dist/iapCompletion.server.js +56 -0
- package/dist/iapCurrentState.server.js +108 -0
- package/dist/iapCurrentState.testFixtures.js +23 -0
- package/dist/iapCurrentStatePort.fixture.js +64 -0
- package/dist/iapEntitlements.d.ts +79 -0
- package/dist/iapEntitlements.js +58 -0
- package/dist/iapEntitlements.server.js +49 -0
- package/dist/iapEntitlementsBinding.server.js +106 -0
- package/dist/iapEntitlementsPort.fixture.js +81 -0
- package/dist/iapInstalledEntitlements.testFixtures.js +76 -0
- package/dist/iapInstalledNotifications.testFixtures.js +94 -0
- package/dist/iapInventoryRefunds.server.js +262 -0
- package/dist/iapNotificationCustodyPort.fixture.js +68 -0
- package/dist/iapNotificationPort.fixture.js +74 -0
- package/dist/iapNotificationProcessor.server.js +135 -0
- package/dist/iapNotificationProcessor.testFixtures.js +64 -0
- package/dist/iapNotificationProcessorPort.fixture.js +56 -0
- package/dist/iapNotificationSchedule.server.js +70 -0
- package/dist/iapNotificationStore.server.js +353 -0
- package/dist/iapNotificationStore.testFixtures.js +32 -0
- package/dist/iapNotificationTables.entity.js +49 -0
- package/dist/iapNotificationTarget.server.js +37 -0
- package/dist/iapNotificationTarget.testFixtures.js +30 -0
- package/dist/iapNotifications.d.ts +644 -0
- package/dist/iapNotifications.js +6 -0
- package/dist/iapNotifications.server.js +96 -0
- package/dist/iapNotificationsBinding.server.js +233 -0
- package/dist/iapNotificationsSystemPort.fixture.js +51 -0
- package/dist/iapPlugin.testFixtures.js +60 -0
- package/dist/iapPort.fixture.js +75 -0
- package/dist/iapProviderCapture.server.js +18 -0
- package/dist/iapRecovery.d.ts +146 -0
- package/dist/iapRecovery.js +90 -0
- package/dist/iapRefundEventDelivery.server.js +83 -0
- package/dist/iapRefundEvents.js +45 -0
- package/dist/iapRefundEventsPort.fixture.js +60 -0
- package/dist/iapRefundPort.fixture.js +55 -0
- package/dist/iapRefundTables.entity.js +56 -0
- package/dist/iapRefunds.d.ts +755 -0
- package/dist/iapRefunds.js +6 -0
- package/dist/iapRefunds.server.js +554 -0
- package/dist/iapRefunds.testFixtures.js +65 -0
- package/dist/iapSpendAdmission.server.js +99 -0
- package/dist/iapStore.server.js +741 -0
- package/dist/iapStore.testFixtures.js +128 -0
- package/dist/iapTables.entity.js +81 -0
- package/dist/iapVerification.server.js +121 -0
- package/dist/index.d.ts +2984 -0
- package/dist/index.js +15 -0
- package/dist/intentionRecovery.js +134 -0
- package/dist/ledger.js +78 -0
- package/dist/loyalty.d.ts +274 -0
- package/dist/loyalty.fixture.js +93 -0
- package/dist/loyalty.js +312 -0
- package/dist/loyaltyBinding.server.js +204 -0
- package/dist/loyaltyPort.fixture.js +87 -0
- package/dist/loyaltyShopSource.server.js +91 -0
- package/dist/loyaltyStore.server.js +553 -0
- package/dist/loyaltyStore.testFixtures.js +83 -0
- package/dist/loyaltyTables.entity.js +50 -0
- package/dist/offerMerchandising.fixture.js +115 -0
- package/dist/offerMerchandising.js +283 -0
- package/dist/offerPolicies.d.ts +441 -0
- package/dist/offerPolicies.fixture.js +54 -0
- package/dist/offerPolicies.js +268 -0
- package/dist/piggyBanks.d.ts +272 -0
- package/dist/piggyBanks.fixture.js +71 -0
- package/dist/piggyBanks.js +201 -0
- package/dist/piggyBinding.server.js +205 -0
- package/dist/piggyPort.fixture.js +73 -0
- package/dist/piggyStore.server.js +611 -0
- package/dist/piggyStore.testFixtures.js +98 -0
- package/dist/piggyTables.entity.js +41 -0
- package/dist/plugin.js +1048 -0
- package/dist/port.js +13 -0
- package/dist/price.d.ts +99 -0
- package/dist/price.js +144 -0
- package/dist/purchaseRecovery.js +56 -0
- package/dist/reports.server.js +115 -0
- package/dist/rpc.d.ts +3201 -0
- package/dist/rpc.js +1201 -0
- package/dist/sanctionsTradeAuthority.server.js +80 -0
- package/dist/server.d.ts +3856 -0
- package/dist/server.js +43 -0
- package/dist/shop.d.ts +279 -0
- package/dist/shop.js +196 -0
- package/dist/shop.testFixtures.js +114 -0
- package/dist/shopPort.fixture.js +72 -0
- package/dist/shopPresentation.js +68 -0
- package/dist/shopStore.server.js +624 -0
- package/dist/shopTables.entity.js +63 -0
- package/dist/shopUiCopy.js +57 -0
- package/dist/spendLimits.d.ts +140 -0
- package/dist/spendLimits.fixture.js +53 -0
- package/dist/spendLimits.js +155 -0
- package/dist/spendLimitsPort.fixture.js +120 -0
- package/dist/subscriptionBinding.server.js +211 -0
- package/dist/subscriptionStore.server.js +643 -0
- package/dist/subscriptionStore.testFixtures.js +72 -0
- package/dist/subscriptionTables.entity.js +37 -0
- package/dist/subscriptions.d.ts +394 -0
- package/dist/subscriptions.fixture.js +97 -0
- package/dist/subscriptions.js +213 -0
- package/dist/subscriptionsPort.fixture.js +76 -0
- package/dist/systemFixtures.js +89 -0
- package/dist/tables.entity.js +83 -0
- package/dist/testing.d.ts +4615 -0
- package/dist/testing.js +29 -0
- package/dist/tradeAuthority.server.js +172 -0
- package/dist/tradeBinding.server.js +201 -0
- package/dist/tradeComponents.js +342 -0
- package/dist/tradeEscrow.server.js +232 -0
- package/dist/tradeEscrow.testFixtures.js +133 -0
- package/dist/tradeExpiry.server.js +43 -0
- package/dist/tradeRecovery.js +48 -0
- package/dist/tradeStore.server.js +750 -0
- package/dist/tradeStore.testFixtures.js +73 -0
- package/dist/tradeTables.entity.js +66 -0
- package/dist/tradeUiCopy.js +79 -0
- package/dist/trades.d.ts +246 -0
- package/dist/trades.fixture.js +103 -0
- package/dist/trades.js +289 -0
- package/dist/tradesPort.fixture.js +94 -0
- package/dist/walletEscrow.js +106 -0
- package/dist/walletEscrow.server.js +376 -0
- package/dist/walletKeys.server.js +5 -0
- package/dist/walletStore.js +571 -0
- package/dist/web.d.ts +2257 -0
- package/dist/web.js +9 -0
- package/package.json +161 -4
package/README.md
CHANGED
|
@@ -1,3 +1,2720 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @voltro/plugin-economy
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The scoped fixture uses a fresh installed account, accepted SQL period, wallet
|
|
4
|
+
and inventory; provider verification remains an explicit private seam. Scope
|
|
5
|
+
release revokes all four entitlement methods. The policy fixture calls the
|
|
6
|
+
actual default and narrowing path.
|
|
7
|
+
|
|
8
|
+
Partial SQL wallet and exact-price foundation. Runtime and acceptance validation
|
|
9
|
+
are pending. Immediate virtual/free Shop has an explicit server SQL adapter; its
|
|
10
|
+
optional factory binding is implemented, while IAP/ads and complete monetization
|
|
11
|
+
remain unfinished. No capacity claim follows from configured bounds.
|
|
12
|
+
|
|
13
|
+
## Install and configure
|
|
14
|
+
|
|
15
|
+
`economyPlugin()` captures its currencies, service allow-list, work bounds,
|
|
16
|
+
clock, offer price declarations and `priceResolver`. Only this installed instance
|
|
17
|
+
binds to the API app's borrowed `DataStore`. It neither opens nor closes a store.
|
|
18
|
+
The following numbers are example configuration, not deployment pricing.
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { defineCurrency, economyPlugin } from '@voltro/plugin-economy'
|
|
22
|
+
|
|
23
|
+
export const economy = economyPlugin({
|
|
24
|
+
currencies: [defineCurrency({ id: 'gold', kind: 'soft', cap: '1000' })],
|
|
25
|
+
realMoney: false,
|
|
26
|
+
offers: [{
|
|
27
|
+
id: 'starter',
|
|
28
|
+
price: { kind: 'virtual', options: [{ currency: 'gold', amount: '11' }] },
|
|
29
|
+
}],
|
|
30
|
+
})
|
|
31
|
+
// Add this instance to the API app's plugins array.
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`realMoney` defaults to `false`. Both `real` and `mixed` declarations are refused
|
|
35
|
+
when disabled; free and virtual prices need no store, IAP or Stripe credentials.
|
|
36
|
+
This switch currently applies to the implemented wallet and price foundation;
|
|
37
|
+
the complete virtual-only monetization stack remains unfinished.
|
|
38
|
+
|
|
39
|
+
## Exact amounts and prices
|
|
40
|
+
|
|
41
|
+
`economyAmount()` accepts canonical decimal strings, bigint, or safe integer
|
|
42
|
+
numbers in the signed SQL BIGINT range. Unsafe numbers, fractions, negative zero,
|
|
43
|
+
leading zeroes and overflow are refused. Wire amounts and virtual prices remain
|
|
44
|
+
canonical decimal strings. Caps are positive exact amounts.
|
|
45
|
+
|
|
46
|
+
`definePrice(price, currencies, realMoney)` detaches and freezes `virtual`
|
|
47
|
+
alternatives, `real` references, `mixed` alternatives or `free`. Virtual options
|
|
48
|
+
name declared currencies and cannot exceed their balance cap. Real references
|
|
49
|
+
use positive safe integer minor units and app-owned store product identifiers;
|
|
50
|
+
they never verify or fulfill a payment. A mixed price uses
|
|
51
|
+
`{ kind: 'mixed', real: { prices, storeProductIds }, virtual: [...] }`.
|
|
52
|
+
|
|
53
|
+
`priceResolverContext()` captures a price and trusted virtual adjustments:
|
|
54
|
+
integer `discountBps` from zero to 10000 and an exact `minimum` per currency.
|
|
55
|
+
`defaultPriceResolver` is the single exported default: discounts use exact
|
|
56
|
+
integer arithmetic, round upward and respect the floor. `resolvePrice()` admits
|
|
57
|
+
custom output separately: it preserves all payment alternatives, forbids values
|
|
58
|
+
below the trusted floor or above the declaration and keeps real references and
|
|
59
|
+
store products unchanged. Personalized real prices require separately declared
|
|
60
|
+
store variants and verified provider prices; that integration is still open.
|
|
61
|
+
|
|
62
|
+
The installed `economy.economy.price(subject, offerId, adjustments)` facade uses
|
|
63
|
+
the selected resolver. It accepts only a system Subject or an explicitly allowed
|
|
64
|
+
verified service; a price projection grants no value. The `./pricing` subpath is
|
|
65
|
+
browser-safe. `./testing` supplies the actual default resolver and ledger fixtures.
|
|
66
|
+
|
|
67
|
+
## Trusted SQL transfers
|
|
68
|
+
|
|
69
|
+
`economy.economy.service()` returns the installed `EconomyService`; request
|
|
70
|
+
services use the same `EconomyPort` Effect tag. `transfer(subject, input)` accepts
|
|
71
|
+
only system Subjects or verified services explicitly listed in `allowedServices`.
|
|
72
|
+
Users and guests cannot submit balances, credits, debits or transfer values.
|
|
73
|
+
|
|
74
|
+
Every transfer supplies tenant, independent `requestId`, currency, positive
|
|
75
|
+
exact amount, distinct source/destination accounts, a reason and a reference.
|
|
76
|
+
Accounts are `{ kind: 'system', id }` or
|
|
77
|
+
`{ kind: 'wallet', ownerKind: 'player' | 'guild', ownerId }`. At least one side is
|
|
78
|
+
a wallet. System-to-system and same-account transfers are refused.
|
|
79
|
+
|
|
80
|
+
The writer seeds durable admission rows, locks wallet subjects in deterministic
|
|
81
|
+
order, then the currency and request receipt. One transaction updates both
|
|
82
|
+
wallets, inserts exactly two balanced ledger postings and commits a metadata-only
|
|
83
|
+
receipt. The receipt has transaction ID, currency, amount and time; no owners or
|
|
84
|
+
references. A committed identical request returns that original receipt forever;
|
|
85
|
+
a changed intention with the same request conflicts. Aborted work may retain a
|
|
86
|
+
pending admission row but never a committed receipt or monetary posting.
|
|
87
|
+
|
|
88
|
+
Ordinary wallet balances are nonnegative and bounded by the currency cap; the explicit verified hard-currency refund path below may create policy-bound debt. Currency
|
|
89
|
+
configuration is persisted on first admission; later configuration drift refuses
|
|
90
|
+
fresh writes until an explicit migration is designed. Trusted refund debt and Trades are described below; arbitrary GM value writes remain unfinished. Optional Trades is described below. Trusted
|
|
91
|
+
default-wallet Escrow participants are documented below.
|
|
92
|
+
|
|
93
|
+
## Own reads and ledger rebuild
|
|
94
|
+
|
|
95
|
+
`economy.mine` and `useWallets()` (`./web`) read only the authenticated user's or
|
|
96
|
+
guest's player wallets. They take no tenant, owner or value input. The query
|
|
97
|
+
observes wallet, subject-admission and currency tables. Missing configured wallets
|
|
98
|
+
project zero; guild authorization and account merging remain open.
|
|
99
|
+
|
|
100
|
+
`auditWallet()` and `rebuildWallet()` are trusted server operations. Under the
|
|
101
|
+
same subject/currency SQL fences they read the whole bounded wallet ledger,
|
|
102
|
+
validate both postings and the durable receipt state for each transaction, then
|
|
103
|
+
compare or repair the materialized balance. SQL remains the source of truth.
|
|
104
|
+
No timer, process-local mutex, native SQL client or hot cache is introduced.
|
|
105
|
+
|
|
106
|
+
`maxLedgerRows` is a work bound per wallet/currency, not a capacity promise;
|
|
107
|
+
`maxExportRows` bounds complete export/erase selections. Excess fails with
|
|
108
|
+
`limit_exceeded` rather than silently returning or deleting a prefix. A scheduled
|
|
109
|
+
scheduled consistency execution proof and incremental sinks/sources maintenance remain open; the bounded whole-history report is described below.
|
|
110
|
+
|
|
111
|
+
## Privacy and inspection
|
|
112
|
+
|
|
113
|
+
The default SQL wallet declares five table roles: currencies, wallets, ledger, transactions and
|
|
114
|
+
subjects. Export returns the selected player's own wallets and postings. Erase
|
|
115
|
+
removes their wallets and owner links, redacts reference/actor fields across the
|
|
116
|
+
affected posting pairs, and permanently fences recreation and receipt replay.
|
|
117
|
+
Financial debit/credit values remain for conservation and peer rebuilds; they are
|
|
118
|
+
not rewritten by privacy operations. Retained deterministic fence keys are
|
|
119
|
+
pseudonymous, not an assertion of anonymization or a retention-policy decision.
|
|
120
|
+
|
|
121
|
+
The shared, local and Cloud panels observe `/status`: captured currency IDs,
|
|
122
|
+
caps, price kinds, resolver selection, SQL binding and work bounds. They do not
|
|
123
|
+
list wallet owners, balances, ledger values, payment references or deployments.
|
|
124
|
+
Unknown or invalid configuration observations fail closed; stale data is hidden
|
|
125
|
+
on failure. Payment installation remains false; optional Shop configuration is reported separately.
|
|
126
|
+
|
|
127
|
+
## Prepared SQL transfer units
|
|
128
|
+
|
|
129
|
+
The explicit server adapter returned by `dataStoreEconomy` exposes
|
|
130
|
+
`EconomySqlService.prepareTransfer(subject, input)`. It captures trusted transfer
|
|
131
|
+
input, seeds pending admissions and returns an exact-store-bound opaque SQL unit.
|
|
132
|
+
`executeSqlMutations` (`@voltro/database/sql`) can commit it with Inventory units
|
|
133
|
+
and an order writer in one ordinary transaction. A later unit failure rolls back
|
|
134
|
+
accepted wallet values, balanced postings and receipts together. A pending row is
|
|
135
|
+
not payment. Ordinary `transfer` uses this same writer as a single unit.
|
|
136
|
+
|
|
137
|
+
Recreate units from original SQL identities after restart or uncertain reply.
|
|
138
|
+
Accepted retries preflight terminal SQL before clock. Units never authorize
|
|
139
|
+
player values and must not run inside another transaction. The installed
|
|
140
|
+
EconomyPort remains its ordinary four-method surface; prepared units belong to
|
|
141
|
+
the explicit server adapter. Optional Shop binding is described below; complete
|
|
142
|
+
spend/minor/monetization limits remain open. Source composition tests exist for runtime and all six native variants;
|
|
143
|
+
none has run in this build phase. See the Transactions page in the bilingual public docs.
|
|
144
|
+
|
|
145
|
+
## Immediate virtual/free Shop SQL source
|
|
146
|
+
|
|
147
|
+
`defineShopOffer` (`./shop`) captures versioned offers with `Price`, item/currency
|
|
148
|
+
gifts, availability, per-player/per-day/global quantity limits and `oneTime`.
|
|
149
|
+
Every gift is positive and quantities are bounded before multiplication. One-time
|
|
150
|
+
offers have quantity/per-player limit1. Limits use the offer ID across declaration
|
|
151
|
+
versions; a new version does not reset one-time authority. All amounts below are
|
|
152
|
+
illustrative app configuration, not deployment pricing.
|
|
153
|
+
|
|
154
|
+
`dataStoreShop` (`./server`) binds explicit prepared Economy/Inventory participants
|
|
155
|
+
to the same exact selected app store. Item offers require Inventory; currency-only
|
|
156
|
+
or free offers need no Inventory or payment provider. `realMoney:false` rejects
|
|
157
|
+
real and mixed declarations before binding participants. With real money enabled,
|
|
158
|
+
this immediate writer accepts only free or selected virtual alternatives; real
|
|
159
|
+
payments still require the separately unfinished provider/receipt path.
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
import { dataStoreShop } from '@voltro/plugin-economy/server'
|
|
163
|
+
import { Effect } from 'effect'
|
|
164
|
+
|
|
165
|
+
// economySql and inventorySql were constructed from this exact selected store.
|
|
166
|
+
const shop = dataStoreShop(store, {
|
|
167
|
+
currencies: [{ id: 'gold', kind: 'soft' }],
|
|
168
|
+
realMoney: false,
|
|
169
|
+
economy: economySql,
|
|
170
|
+
inventory: inventorySql,
|
|
171
|
+
offers: [{
|
|
172
|
+
id: 'starter', titleKey: 'shop.starter', oneTime: true,
|
|
173
|
+
price: { kind: 'virtual', options: [{ currency: 'gold', amount: '7' }] },
|
|
174
|
+
items: [{ itemId: 'potion', qty: 2 }],
|
|
175
|
+
}],
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
// verifiedSubject comes from the request; no owner/value is supplied by the player.
|
|
179
|
+
const offerView = (await Effect.runPromise(shop.offers(verifiedSubject)))[0]
|
|
180
|
+
if (!offerView) throw new Error('No available offer')
|
|
181
|
+
const receipt = await Effect.runPromise(shop.buy(verifiedSubject, {
|
|
182
|
+
offerId: offerView.offer.id, offerVersion: offerView.offer.version,
|
|
183
|
+
offerDigest: offerView.offerDigest, currency: 'gold', requestId: 'request.1',
|
|
184
|
+
}))
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The player sends only offer ID/version/server declaration digest, request, optional quantity and virtual currency.
|
|
188
|
+
Tenant/owner come from the verified user/guest Subject. The server captures price,
|
|
189
|
+
gifts and trusted eligibility. A `ShopContextSource.read` is required when an offer
|
|
190
|
+
names a segment, flag or countries; its implementation owns all such checks. It
|
|
191
|
+
can supply trusted virtual price adjustments. It runs outside SQL. Nondefault
|
|
192
|
+
price/context rules require an explicit version; missing prerequisites refuse.
|
|
193
|
+
Availability is evaluated at captured server admission time. Per-day limits use
|
|
194
|
+
UTC civil days; no local-time or rolling-window claim is made.
|
|
195
|
+
|
|
196
|
+
Five opt-in SQL roles are exported as `shopTables`: tenant purchase/erase/rebuild
|
|
197
|
+
fence, permanent subject fence, immutable offers, orders and limit counters.
|
|
198
|
+
The tenant fence prevents a purchase or erase from racing complete counter repair.
|
|
199
|
+
One shared transaction checks owner/request/definition/limits, runs the actual
|
|
200
|
+
prepared debit and grants, advances counters and commits the captured order and
|
|
201
|
+
actual child receipts. The completion timestamp is captured after preparation;
|
|
202
|
+
all author/source/clock callbacks remain outside replayable SQL callbacks.
|
|
203
|
+
Failures roll back values, counters and fulfilled order together. Pending
|
|
204
|
+
admissions and zero counters may remain; they are not payment or consumption.
|
|
205
|
+
|
|
206
|
+
Accepted request identity is permanent and intent-bound. Terminal retries return
|
|
207
|
+
the original SQL receipt before clock/source/participants; concurrent accepted
|
|
208
|
+
replay never recharges or grants again. Changed accepted intentions conflict.
|
|
209
|
+
`auditShop` / `rebuildShop` use complete bounded accepted order/definition evidence,
|
|
210
|
+
including retained erased global consumption. They rebuild counters only, never
|
|
211
|
+
repeat transfers/gifts or historical custom callbacks. Whole-history excess
|
|
212
|
+
refuses rather than accepting a prefix. Budgets are not measured capacity.
|
|
213
|
+
|
|
214
|
+
Export returns complete owned pending/fulfilled order intentions and receipts.
|
|
215
|
+
Erase clears owner links, request/price/receipt bodies and owned day/player
|
|
216
|
+
counters, keeps a permanent owner fence and redacted global consumption.
|
|
217
|
+
Shared definitions and financial conservation stay. Keys remain pseudonymous.
|
|
218
|
+
Shop-only erase does not undo accepted destination gifts: run the Economy and
|
|
219
|
+
Inventory privacy hooks as well. Complete account merge/guild lifecycle remains
|
|
220
|
+
open.
|
|
221
|
+
|
|
222
|
+
This explicit adapter and five-method ShopPort are implemented. The installed
|
|
223
|
+
economy factory/routes/inspect panel now have opt-in binding, described below;
|
|
224
|
+
absence of configuration still reports Shop absent. The complete virtual-only
|
|
225
|
+
monetization stack is not claimed.
|
|
226
|
+
|
|
227
|
+
## Install the optional Shop
|
|
228
|
+
|
|
229
|
+
`economyPlugin({ shop: ... })` now mounts the same default SQL ShopPort or a full
|
|
230
|
+
`shop.service` replacement. Without `shop`, no Shop query/action/tag/table role is
|
|
231
|
+
mounted; `economy.economy.shop()` calls refuse unavailable. Configure both plugin
|
|
232
|
+
instances in one API app and import or publish actual matching item content before
|
|
233
|
+
buying. Example amounts remain illustrative app-owned rules:
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
import { economyPlugin } from '@voltro/plugin-economy'
|
|
237
|
+
import { inventoryPlugin, defineItem } from '@voltro/plugin-inventory'
|
|
238
|
+
import { Schema } from 'effect'
|
|
239
|
+
|
|
240
|
+
const potion = defineItem({
|
|
241
|
+
id: 'potion', kind: 'consumable', stackable: true, maxStack: 10,
|
|
242
|
+
tradable: false, rarity: 'common', tags: [],
|
|
243
|
+
attributes: Schema.Struct({}), instanceAttributes: Schema.Struct({}),
|
|
244
|
+
display: { nameKey: 'items.potion' },
|
|
245
|
+
})
|
|
246
|
+
export const inventory = inventoryPlugin({ items: [potion] })
|
|
247
|
+
export const economy = economyPlugin({
|
|
248
|
+
currencies: [{ id: 'gold', kind: 'soft' }], realMoney: false,
|
|
249
|
+
shop: {
|
|
250
|
+
inventory: inventory.inventory,
|
|
251
|
+
offers: [{ id: 'starter', titleKey: 'shop.starter', oneTime: true,
|
|
252
|
+
price: { kind: 'virtual', options: [{ currency: 'gold', amount: '7' }] },
|
|
253
|
+
items: [{ itemId: potion.id, qty: 2 }],
|
|
254
|
+
}],
|
|
255
|
+
},
|
|
256
|
+
})
|
|
257
|
+
// Add both instances to the API app's plugins; publish/import the item catalog.
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
The installed `economy.economy.shop()` service and ShopPort have five methods:
|
|
261
|
+
`offers`, `buy`, `receipt`, `exportSubject`, `eraseSubject`. `shop.offers` is a verified own
|
|
262
|
+
query and `shop.buy` an Action; the latter does not introduce an outer transaction
|
|
263
|
+
around the borrowed writer. `useShopOffers` / `useShopBuy` in `./web` use those
|
|
264
|
+
actual descriptors. Time-only availability needs an explicit refresh. Built-in
|
|
265
|
+
source-specific CDC/region/residency integration and player purchase recovery UI
|
|
266
|
+
remain acceptance/integration work.
|
|
267
|
+
|
|
268
|
+
The installed Inventory facade exposes `inventory.inventory.prepareGrant` and
|
|
269
|
+
`prepareCraft` for default SQL participation. They capture intentions and refuse
|
|
270
|
+
before binding, after deactivation or when Inventory/full crafting is replaced;
|
|
271
|
+
no SQL fallback is borrowed for a replacement. Exact-store group admission also
|
|
272
|
+
refuses accidental cross-app/physical-store units. Ordinary InventoryPort stays
|
|
273
|
+
eight methods. Prepared API, route and production discovery proof is pending.
|
|
274
|
+
|
|
275
|
+
A full `shop.service` supplies all five methods, its own durable authority/receipt/
|
|
276
|
+
privacy behavior and an explicit `version`. Required methods are captured without
|
|
277
|
+
accessor evaluation and retain their original receiver, including class methods.
|
|
278
|
+
Default Shop SQL roles/adapter/query sources are omitted, no fallback; actual replacement query sources must be declared. Combining a full service
|
|
279
|
+
with default inventory/context/price/system-account/clock/order/audit options refuses. The app
|
|
280
|
+
owns replacement schemas, placement/CDC, services and lifecycle; the installed
|
|
281
|
+
facade revokes calls after deactivation. Its privacy pair participates with the
|
|
282
|
+
Economy pair; Inventory hooks remain their own plugin's responsibility.
|
|
283
|
+
|
|
284
|
+
Server `/status`, strict Shared/Cloud/Local contracts and the existing bilingual
|
|
285
|
+
panel now show whether Shop is configured, its backend/rule/context/price slots,
|
|
286
|
+
redacted offer counts/limits and work budgets. Default Shop adds five roles;
|
|
287
|
+
replacement adds none. Rotation, built-in LiveOps and real payment remain false.
|
|
288
|
+
No owner, balance, order, accepted receipt, private attribute or purchase
|
|
289
|
+
outcome is exposed. Failure hides stale Shop content.
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
The default Shop inherits the root `priceResolver`; `shop.priceResolver` explicitly overrides it. An inherited or overridden custom resolver requires an explicit non-default `shop.version`, just as a custom context source does.
|
|
293
|
+
|
|
294
|
+
## Recover an uncertain virtual purchase
|
|
295
|
+
|
|
296
|
+
The optional Shop now also mounts `shop.receipt`, a read-only live query.
|
|
297
|
+
`receipt(subject, originalBuyInput)` and `useShopReceipt` return the original
|
|
298
|
+
request ID plus `not-observed`, `pending` or `fulfilled` with the actual
|
|
299
|
+
receipt. Missing/pending evidence is nonterminal: it cannot prove that a
|
|
300
|
+
purchase will never commit and cannot justify a new identity. Reads never seed
|
|
301
|
+
rows, update fences, call the clock, resolve current prices, prepare a
|
|
302
|
+
participant or buy again. Accepted history survives current offer/policy
|
|
303
|
+
changes; a changed accepted intention conflicts. Erased owners are refused.
|
|
304
|
+
|
|
305
|
+
`./web` exports `createShopPurchaseRecovery` and `browserShopPurchaseJournal`.
|
|
306
|
+
The journal stores only the original offer/version/server declaration digest/request/currency/quantity and an opaque
|
|
307
|
+
app/account/tenant scope, with a version. It stores no price, balance, item state,
|
|
308
|
+
credentials or response body. Supply actual browser storage and Web Locks after
|
|
309
|
+
client initialization; neither is replaced by an in-memory fallback. The journal
|
|
310
|
+
checks persistence before transport and holds the shared lock through settlement.
|
|
311
|
+
|
|
312
|
+
`selectScope(scope)` invalidates old continuations every time, including a switch
|
|
313
|
+
away and back. Use a stable opaque scope for the authenticated app/account/tenant
|
|
314
|
+
and call it whenever the session selection changes. `begin(input)` durably saves
|
|
315
|
+
the user-selected intention; it never sends a purchase. `retry(originalInput)` explicitly
|
|
316
|
+
submits that saved intention through the installed buy action and settles only a
|
|
317
|
+
matching fulfilled receipt. `observe(result)` may settle matching SQL evidence.
|
|
318
|
+
Failures, malformed/mismatching evidence, unknown/pending observations and scope
|
|
319
|
+
changes keep the original intention. `pending()` restores it after a reload.
|
|
320
|
+
No automatic replay, new-ID retry, optimistic grant or manual nonterminal dismissal.
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
import { browserShopPurchaseJournal, createShopPurchaseRecovery } from '@voltro/plugin-economy/web'
|
|
324
|
+
import type { ShopBuyInput, ShopOrderReceipt } from '@voltro/plugin-economy/shop'
|
|
325
|
+
|
|
326
|
+
export const purchaseRecovery = (
|
|
327
|
+
storage: Storage,
|
|
328
|
+
locks: LockManager,
|
|
329
|
+
runBuy: (input: ShopBuyInput) => Promise<ShopOrderReceipt>,
|
|
330
|
+
) => createShopPurchaseRecovery(browserShopPurchaseJournal(storage, locks), runBuy)
|
|
331
|
+
// On the client, runBuy is useShopBuy().run without handled-error options.
|
|
332
|
+
// Select the authenticated scope, begin a user-selected intention, then retry
|
|
333
|
+
// only on explicit confirmation. Pass useShopReceipt(originalInput).data to observe.
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Fixed-price quote confirmation, permanent withdrawal, account-erase
|
|
337
|
+
browser-journal cleanup, native browser locks, complete player integration and
|
|
338
|
+
full provider fulfillment remain open.
|
|
339
|
+
|
|
340
|
+
## Version-bound player Shop components
|
|
341
|
+
|
|
342
|
+
The `./components` entry now contains `ShopGrid`, `OfferCard`, `PurchaseDialog`
|
|
343
|
+
and `PurchaseRecovery`, using the existing shared UI primitives. They emit
|
|
344
|
+
intentions and render observations; they never grant items or change wallet
|
|
345
|
+
state optimistically.
|
|
346
|
+
|
|
347
|
+
Every offer view includes `offerDigest`, computed by the server from the complete
|
|
348
|
+
immutable declaration and rule version. A buy and its receipt require offer ID,
|
|
349
|
+
`offerVersion` and that digest. Fresh purchases compare them before preparation;
|
|
350
|
+
accepted replays still use immutable historical evidence. A different node with
|
|
351
|
+
a divergent same-version declaration cannot silently substitute another price or
|
|
352
|
+
gift. The browser preserves this identity in its original-intention journal.
|
|
353
|
+
It never computes an authorizing digest or sends a price/balance/item state.
|
|
354
|
+
|
|
355
|
+
`shopPurchaseTerms(view, intention)` shares the actual pure declaration/price
|
|
356
|
+
admission. It separates the currently displayed discount from the immutable
|
|
357
|
+
maximum charge, with exact decimal-string arithmetic. The dialog confirms that
|
|
358
|
+
maximum explicitly: the personalized discount is not a fixed quote. The server
|
|
359
|
+
may charge up to that ceiling under the saved declaration/rule identity, after
|
|
360
|
+
checking current eligibility/limits. A changed declaration/rule identity conflicts.
|
|
361
|
+
Fixed personalized-price quote confirmation and permanent withdrawal remain open.
|
|
362
|
+
|
|
363
|
+
The components accept EN/DE copy or a full `ShopUiCopy` override, translated titles,
|
|
364
|
+
currency names and a reward-rendering slot. Offer rows show declared gifts,
|
|
365
|
+
one-time policy and availability countdown using caller-supplied `at`; that clock
|
|
366
|
+
does not poll the backend. No remaining-stock claim follows from configured limits.
|
|
367
|
+
Real checkout is absent; mixed offers expose their virtual alternatives only.
|
|
368
|
+
The dialog uses native protected modal focus through the existing Dialog wrapper;
|
|
369
|
+
busy controls remain focusable and guard activation.
|
|
370
|
+
|
|
371
|
+
```tsx
|
|
372
|
+
import { ShopGrid, type ShopGridProps } from '@voltro/plugin-economy/components'
|
|
373
|
+
import { useShopOffers } from '@voltro/plugin-economy/web'
|
|
374
|
+
|
|
375
|
+
export const PlayerOffers = (props: Pick<ShopGridProps, 'at' | 'title' | 'onSelect'>) => {
|
|
376
|
+
const source = useShopOffers()
|
|
377
|
+
return <ShopGrid source={source} at={props.at} title={props.title} onSelect={props.onSelect} />
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
The parent owns the authenticated recovery scope and action admission. Obtain a
|
|
382
|
+
fresh request ID only for a new user-selected intention, save it with `begin`,
|
|
383
|
+
then call `retry` on explicit confirmation. Keep `PurchaseRecovery` above transient
|
|
384
|
+
offer failures/filters and use its saved request unchanged. A failed refresh hides
|
|
385
|
+
stale offer controls without removing saved purchase recovery. Closing the dialog
|
|
386
|
+
does not delete a request. `useShopReceipt(undefined)` skips the query; mount it
|
|
387
|
+
with the original saved input and pass matching data to `observe`. Only actual
|
|
388
|
+
fulfilled evidence settles it. Account-erase browser-journal cleanup remains open.
|
|
389
|
+
|
|
390
|
+
DOM fixtures substitute the modal methods and lock seam; they do not establish
|
|
391
|
+
native focus trapping, Web Locks, browser layout, dialect parity or a complete
|
|
392
|
+
game flow.
|
|
393
|
+
|
|
394
|
+
The explicit retry takes the exact original input shown to the user:
|
|
395
|
+
`retry(originalInput)`. It captures it before waiting for the shared lock and
|
|
396
|
+
compares it with the current durable reference before transport. If another tab
|
|
397
|
+
has settled the old request and reserved a new valid purchase, the stale control
|
|
398
|
+
conflicts rather than silently sending that new purchase. Pass the request
|
|
399
|
+
received by `PurchaseRecovery.onRetry`, not a newly minted input.
|
|
400
|
+
|
|
401
|
+
## Replace the complete wallet port
|
|
402
|
+
|
|
403
|
+
`EconomyPort` keeps four methods: `transfer`, `mine`, `auditWallet`,
|
|
404
|
+
`rebuildWallet`. `EconomySystemService` adds the required `exportSubject` and
|
|
405
|
+
`eraseSubject` pair for a full installed replacement. Supply that service
|
|
406
|
+
through `economyPlugin({ currencies, service, version, querySources })`. An
|
|
407
|
+
explicit non-default version and a nonempty, unique list of the actual app-owned
|
|
408
|
+
SQL tables for wallet query invalidation are required. The installed
|
|
409
|
+
`economy.mine` route uses those sources; it does not keep the default SQL
|
|
410
|
+
sources. Required methods are captured once without evaluating accessors; class
|
|
411
|
+
receivers are preserved through the same admission used by Shop replacements.
|
|
412
|
+
|
|
413
|
+
The factory omits all five default wallet tables and never constructs or falls
|
|
414
|
+
back to its default SQL adapter. `now`, `maxLedgerRows` and `maxExportRows` are
|
|
415
|
+
refused with a replacement. The app owns its SQL authority, declared schema,
|
|
416
|
+
conservation, caps, durable request receipts, complete audit/rebuild, lifecycle,
|
|
417
|
+
placement/CDC and privacy behavior. Normal app binding activates the facade;
|
|
418
|
+
deactivation revokes it. A replacement does not turn an unbound facade into a
|
|
419
|
+
working service. The four-method Effect tag and authenticated own-wallet route
|
|
420
|
+
keep the same output contract.
|
|
421
|
+
|
|
422
|
+
The installed facade still refuses player-authored transfer/audit/rebuild values,
|
|
423
|
+
undeclared transfer currencies and invalid own-subject identity before delegation.
|
|
424
|
+
Only system Subjects or verified services in `allowedServices` can transfer or
|
|
425
|
+
repair. Root price projection remains independent of the wallet backend. The
|
|
426
|
+
replacement must enforce its remaining state and tenant invariants; none is
|
|
427
|
+
proved by the presence of a method. Directly providing a different EconomyPort
|
|
428
|
+
Layer outside this plugin makes the app responsible for equivalent admission.
|
|
429
|
+
|
|
430
|
+
A default SQL Shop is refused over a fully replaced wallet: it cannot silently
|
|
431
|
+
charge a different default ledger. Supply a complete Shop replacement too, or
|
|
432
|
+
leave Shop absent. Each full Shop requires explicit `shop.querySources.offers`
|
|
433
|
+
and `shop.querySources.receipt`, nonempty unique actual app-owned SQL table lists,
|
|
434
|
+
and a non-default rule version. Default Shop refuses replacement query sources;
|
|
435
|
+
full Shop refuses default clock/participant/context/price/system-account and
|
|
436
|
+
order/audit bounds. The factory retains neither default Shop roles nor query
|
|
437
|
+
sources for that backend. Its dependent five-method ShopPort stays unchanged.
|
|
438
|
+
|
|
439
|
+
All four inspect layers now distinguish SQL/custom wallet backend and rule
|
|
440
|
+
version, normal app binding and conditional table roles. Default SQL work bounds
|
|
441
|
+
are `null` for a replaced wallet or Shop; panels explicitly label them
|
|
442
|
+
app-owned. No owners, wallet values, purchase outcomes or private replacement
|
|
443
|
+
table names are exposed. Failure hides stale configuration.
|
|
444
|
+
|
|
445
|
+
## EconomyPort fixture Layer
|
|
446
|
+
|
|
447
|
+
`./testing` exports `economyPortFixture({ currencies, allowedServices?, now?
|
|
448
|
+
})`: a test-only in-memory four-method port plus its actual `EconomyPort` Layer.
|
|
449
|
+
It reuses `planLedgerTransfer` and `applyWalletPosting` for exact balanced
|
|
450
|
+
rules, stable request receipts, own reads and ledger-derived audits. Provide
|
|
451
|
+
`fixture.layer` with `Effect.provide` when testing a dependent system. This
|
|
452
|
+
fixture grants no SQL, native, durable restart or production proof, and is not a
|
|
453
|
+
deployment backend. The complete system-contract conformance remains open.
|
|
454
|
+
|
|
455
|
+
The installed default `economy.economy.prepareTransfer` facade participates in same-store atomic game-currency Inventory crafting, refusing inactive/full-replacement SQL borrowing. Ordinary EconomyPort stays four methods.
|
|
456
|
+
|
|
457
|
+
## SQL sources and sinks aggregate
|
|
458
|
+
|
|
459
|
+
`readEconomyFlows(store, options)` and `economyFlowsAggregate(options)` live
|
|
460
|
+
only in `./server`, for the default SQL Economy tables. A replaced wallet owns
|
|
461
|
+
its report; these helpers never switch to its sources or a hot cache. Install
|
|
462
|
+
the factory explicitly as an app-owned `*.aggregate.ts` default export. The
|
|
463
|
+
existing aggregate discovery/storage/ refresh/inspection lifecycle owns its
|
|
464
|
+
materialization. No new always-on plugin source, timer or boot change. The API
|
|
465
|
+
Economy template now authors this file. The example tenant and bounds below are
|
|
466
|
+
illustrative configuration budgets.
|
|
467
|
+
|
|
468
|
+
```ts
|
|
469
|
+
// aggregates/economyFlows.aggregate.ts — server-only file convention
|
|
470
|
+
import { economyFlowsAggregate } from '@voltro/plugin-economy/server'
|
|
471
|
+
|
|
472
|
+
export default economyFlowsAggregate({
|
|
473
|
+
tenantId: 'game-demo', name: 'economy.flows', refresh: '1h',
|
|
474
|
+
maxRows: 10000, maxGroups: 1000,
|
|
475
|
+
})
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
The reader captures tenant/bounds before laziness and takes a bounded whole
|
|
479
|
+
ledger observation through the borrowed store transaction. It uses the actual
|
|
480
|
+
wallet-audit evidence reader, including durable committed/erased transaction
|
|
481
|
+
receipts, exact conserved pairs, account/identity and matching reason/reference/
|
|
482
|
+
actor checks. Missing pairs/receipts, corrupt values or exceeded work/group bounds
|
|
483
|
+
refuse the entire refresh. Pending receipts carry no value and are not counted.
|
|
484
|
+
Financial rows retained after subject erasure remain in the flow totals; no
|
|
485
|
+
owner, reference, actor, receipt body, wallet balance or tenant ID is output.
|
|
486
|
+
|
|
487
|
+
One accepted transfer counts once per UTC day/currency/reason: system→wallet is
|
|
488
|
+
a source, wallet→system a sink, wallet→wallet a separate transfer total. Both
|
|
489
|
+
sides are never double-counted. All sums use BigInt and canonical nonnegative
|
|
490
|
+
strings, so a report total may exceed one wallet's signed BIGINT while remaining
|
|
491
|
+
exact. Default bounds are 10,000 ledger rows/1,000 groups; configurable maxima
|
|
492
|
+
100,000/10,000 are admission budgets, not measured capacity. Full history is
|
|
493
|
+
recomputed; incremental retention/archive/large-history reporting is unfinished.
|
|
494
|
+
The projection does not promise a serializable point-in-time snapshot across
|
|
495
|
+
concurrent writes, or refresh a player's wallet value.
|
|
496
|
+
|
|
497
|
+
Normal aggregate replacement preserves framework stale/error metadata; authorize
|
|
498
|
+
all app query/GM/report reads using the actual aggregate APIs and tenant policy.
|
|
499
|
+
Direct server helpers have no player route.
|
|
500
|
+
|
|
501
|
+
## Bounded SQL consistency schedule
|
|
502
|
+
|
|
503
|
+
The default installed facade exposes
|
|
504
|
+
`economy.economy.checkConsistency(systemSubject, input)`, and `./server` exports
|
|
505
|
+
`economyConsistencySchedule` for an explicit app-owned `*.cron.tsx` file. The
|
|
506
|
+
API Economy template now authors that schedule. Existing scheduler admission,
|
|
507
|
+
coordination, overlap, watchdog and run/error tracking own execution; no private
|
|
508
|
+
timer, workflow queue, scheduler replacement or default boot source is added.
|
|
509
|
+
|
|
510
|
+
```ts
|
|
511
|
+
// schedules/economy.checkConsistency.cron.tsx
|
|
512
|
+
import { economyConsistencySchedule } from '@voltro/plugin-economy/server'
|
|
513
|
+
import { installed } from '../system/installed.server'
|
|
514
|
+
|
|
515
|
+
export default economyConsistencySchedule({
|
|
516
|
+
tenantId: 'game-demo', runner: installed.economy,
|
|
517
|
+
cron: '0 2 * * *', timezone: 'UTC',
|
|
518
|
+
maxLedgerRows: 10000, maxWallets: 1000,
|
|
519
|
+
})
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
Whole bounded SQL ledger evidence is admitted by the same actual conserved-pair
|
|
523
|
+
reader used by wallet audit/report. The union of live financial ownership and
|
|
524
|
+
actual wallet materializations includes a wallet even if its materialization was
|
|
525
|
+
deleted. Canonical materialized identities and known currencies must match.
|
|
526
|
+
Each selected wallet uses the existing actual audit under its ordinary durable
|
|
527
|
+
fences. Audit may seed/fence metadata; this job never invokes repair or changes
|
|
528
|
+
balances. It returns only checked/mismatch/ledger-row counts and a matches flag.
|
|
529
|
+
Erased ownership is excluded; retained anonymous financial evidence is still
|
|
530
|
+
validated. Foreign tenants are not selected. System Subjects only; player calls
|
|
531
|
+
refuse before SQL. Lazy input capture, inactive/replaced root refusal and no
|
|
532
|
+
fallback preserve the installed owner. A replacement owns its consistency job.
|
|
533
|
+
|
|
534
|
+
A mismatch fails the ordinary cron body with a redacted conflict code. Corrupt,
|
|
535
|
+
erased-during-run, unknown-currency, missing or oversized evidence makes the job
|
|
536
|
+
fail rather than report a prefix as green. Default whole bounds are 10,000 ledger
|
|
537
|
+
rows and 1,000 wallet/currency units; configurable maxima 100,000/10,000 remain
|
|
538
|
+
budgets. A transfer after enumeration belongs to the next run; this is not a
|
|
539
|
+
serializable fleet-wide cut. Operator-controlled rebuild remains the existing
|
|
540
|
+
separate trusted operation. No automatic compensation or missed-workflow crash
|
|
541
|
+
recovery is claimed; recurring scheduler behavior still needs actual execution.
|
|
542
|
+
|
|
543
|
+
No run number or support claim follows.
|
|
544
|
+
|
|
545
|
+
## Canonical system-contract fixture exports
|
|
546
|
+
|
|
547
|
+
`./testing` exports `priceResolverSystemFixture`, `economyFakeLayer` and
|
|
548
|
+
`economyPortSystemFixture` beside the existing declaration and configurable
|
|
549
|
+
four-method fixture. The slot probe uses the actual exported Price default and
|
|
550
|
+
an admitted composed override with a distinct result. The canonical fake Layer
|
|
551
|
+
creates one fresh memory fixture per Effect scope; its release closes that
|
|
552
|
+
fixture and escaped service calls refuse. Explicit `economyPortFixture(options)`
|
|
553
|
+
also exposes idempotent `close`; its ordinary Layer.succeed remains owned by the
|
|
554
|
+
caller's fixture lifetime. It calls shared pure ledger planning/wallet
|
|
555
|
+
arithmetic, no SQL imitation or production backend.
|
|
556
|
+
|
|
557
|
+
These exports fit the existing system suite's fixture shape; they do not
|
|
558
|
+
establish per-table privacy/event/inspect/mixin/upgrades or a complete
|
|
559
|
+
`systemContractConformance` registration/pass. SQL durability/native evidence
|
|
560
|
+
comes from the real adapter scenarios, not this memory Layer.
|
|
561
|
+
|
|
562
|
+
## Starter packs and explicit offer experiments
|
|
563
|
+
|
|
564
|
+
The browser-safe `./offer-policies` entry provides `defineStarterPack`, `starterPackAvailable`, `defineOfferExperiment`, `defaultExperimentBucket`, `resolveOfferExperiment` and `offerPolicyContextSource`. These functions compose the existing Shop declaration, trusted context and SQL writer. No new plugin, store, hot cache, parallel grant path or always-on boot work.
|
|
565
|
+
|
|
566
|
+
A Starter pack captures one existing offer with quantity1/per-player1/oneTime, an explicit positive first-session duration and optional exact currency multiplier. The multiplier becomes immutable offer content and is validated against the actual currency cap. Existing unversioned Shop counters accept the pack once per owner, including alternate request IDs; an accepted original request replays before clock/context even after the offer expires. The availability window is half-open from the trusted first session. This is per-offer first-purchase bonus, not an atomic account-wide never-paid rule. Account-merge identity continuity is still open.
|
|
567
|
+
|
|
568
|
+
Experiments capture explicit ID/version, half-open UTC dates, sorted weighted variants, complete distinct immutable Shop offers and metric names. Default bucketing is deterministic from tenant/experiment/version/trusted canonical key; it is no security RNG or durable assignment. The identity source must preserve this key across guest upgrade/merge. Real/mixed variants require distinct store products per configured channel. `realMoney:false` refuses real/mixed variants at declaration; virtual/free variants require no third-party credentials. Each variant keeps its own product/offer/digest, never a client-forged storefront price or a replacement real-price reference. Exposure events, persisted assignment/metrics/GM experiment reports and provider settlement remain open.
|
|
569
|
+
|
|
570
|
+
`offerPolicyContextSource` binds the real class/function receiver, captures configuration and reads trusted facts outside SQL callbacks. Its source receives only verified tenant/owner scalars, admitted offer and observation time; auth scopes/provider metadata are not forwarded. It combines existing eligibility and floor-validated virtual adjustments with Starter windows and the selected experiment offer. Altered declared content refuses. Add the actual resulting offers and this context source to the ordinary Shop configuration with an explicit nondefault rule version. The application still owns authoritative first-session/key/segment/flag/country facts. Snapshot eligibility is no serializable mutable-profile/spend-limit contract. Missing source data refuses; no dummy facts or fallback.
|
|
571
|
+
|
|
572
|
+
Existing inspect surfaces show the configured custom context; dedicated experiment/Starter controls remain open.
|
|
573
|
+
|
|
574
|
+
## Exact container disclosure, pity and server sampling
|
|
575
|
+
|
|
576
|
+
The browser-safe `./containers` entry captures `defineContainer` content with explicit currency registry/global money switch, immutable ID/version/shared-counter group, ordinary Price, bounded positive weighted drops and optional pity. Gifts use actual currency caps and exact signed-BIGINT amounts. `pity.guaranteed` names an existing rarity; `after` is the guaranteed attempt count, and optional `softFrom`/`softRampBps` use exact integer basis points. No float accumulation. Example counts and probabilities are app-owned test configuration.
|
|
577
|
+
|
|
578
|
+
`containerProbabilityContext` captures the real currency registry and declaration before evaluation. `defaultContainerProbability` preserves relative weight within guaranteed/non-guaranteed groups while increasing the guaranteed probability by an explicit basis-point increment from the soft threshold. `resolveContainerDistribution` admits detached nonnegative integer weights, all known unique drop IDs and a positive bounded total; custom slots cannot violate hard pity. `discloseContainer` exposes the same exact numerator/denominator values used by `resolveContainerRoll`. Hard pity permits only the guaranteed rarity on attempt `after`; a hit resets misses to zero. Without pity there is no miss counter. Public rational disclosure is exact and may exceed wallet BIGINT; it is not a floating percentage.
|
|
579
|
+
|
|
580
|
+
`defaultContainerRoll` is the single weighted selection default. `ContainerRandomPort` is explicit server authority; the `./server` exports `cryptoContainerRandom`, `cryptoContainerRandomLayer` and `sampleContainer`. The native source uses cryptographic rejection sampling, masks excess bits and refuses after a budget of128 rejected draws. No modulo bias, Math.random, player seed/state or automatic backend switch. Sample preparation captures caller data, evaluates the probability slot once and reuses that exact distribution after deferred draw. Custom roll must choose an admitted positive-weight outcome; custom random ports must supply uniform bounded draws.
|
|
581
|
+
|
|
582
|
+
This is **sampling, not opening/fulfillment**. No player container route, debit, grant, persistent group counter, SQL journal/rebuild, privacy role, age/country gate or atomic payment/pity acceptance is installed. Ordinary authoritative acceptance must commit the chosen result with actual prepared debit/grants and owned durable history; retry must never draw again. Dedicated four-layer container controls and the full legal/virtual-only contract remain open. Test-only random Layer and actual default/override/port probes are in `./testing`.
|
|
583
|
+
|
|
584
|
+
`offerExperimentFixture` and `experimentBucketSystemFixture` expose the actual deterministic default and a distinct admitted override through the server/testing entry.
|
|
585
|
+
|
|
586
|
+
## Code redemption adapter
|
|
587
|
+
|
|
588
|
+
`./codes` declares `defineCode`, CodePort and code intentions. A declaration contains immutable ID/version/campaign, actual capped currency/item gifts, half-open dates and explicit positive global/per-player limits. It contains no code token. Players supply only case-sensitive code and original request ID; amounts, recipients, gifts and state remain server-owned. Runtime code values are provided privately, never examples, credentials or placeholder tokens.
|
|
589
|
+
|
|
590
|
+
The server `dataStoreCodes` borrows one ordinary DataStore and the actual Economy/Inventory prepared participants. Four exported roles are `_voltro_eco_code_tenants`, `_voltro_eco_codes`, `_voltro_eco_code_subjects`, `_voltro_eco_code_claims`. SQL stores only scoped SHA-256 code digests and captured declarations, never plaintext codes. Issuing is trusted system/explicit service only. Definition/code identity is immutable; a changed declaration under the same code refuses.
|
|
591
|
+
|
|
592
|
+
Tenant/owner fences serialize acceptance and erasure. Bounded complete accepted claim history is the global/per-owner counter truth; no independent mutable counter cache. Actual wallet postings, item movements and code receipt commit through the same exact-store prepared group. Rejected grants roll back wallet value and leave a pending original identity, never accepted gifts. Captured completion time and code windows are checked before acceptance preparation. Terminal original retry returns before current clock/declaration/provider preparation. Accepted intent retains its original currency registry for replay/privacy after declaration removal. Own receipt reads distinguish pending/not-observed from fulfilled and never authorize a fresh nonce.
|
|
593
|
+
|
|
594
|
+
Export/erase methods are complete for these roles. Subject erasure clears owner/intent/digest/receipt bodies, preserves a permanent subject fence and accepted flag/code-key/owner-key evidence so global usage cannot reset. Shared definitions remain. Retained deterministic keys/digests are pseudonymous, not anonymous. Owner export after erasure is empty. Cross-system wallet/item privacy still runs through those owning plugins; code-only erasure cannot revoke gifts. Financial/movement child-reference conformance and archival retention require further evidence.
|
|
595
|
+
|
|
596
|
+
Optional `economyPlugin({ currencies, codes: { inventory } })` now installs CodePort, four default SQL roles, `economy.redeem` action and owned `economy.codes.receipt` live query. Omit inventory for currency-only gifts; item declarations without a participant refuse. No Code config means no Code tables, routes or Layer. `economy.economy.codes()` exposes the captured installed service. Trusted `create` accepts a privately supplied runtime code; no player or inspect issuance action is mounted. `useRedeemCode`/`useCodeReceipt` in `./web` use the actual browser-safe descriptors. The client sends only code/request identity; a missing or pending observation never authorizes a new request. Durable client recovery and account-erase client cleanup remain open; do not store code values in a browser journal or logs.
|
|
597
|
+
|
|
598
|
+
`defaultCodeEligibility` is the single exported default. `resolveCodeEligibility` captures the declaration, timestamp and accepted SQL counts; custom pure `codes.eligibility` may narrow admission, never raise hard dates/limits. Custom rules require explicit non-default `codes.version`. Callbacks run outside replayable SQL callbacks; acceptance rechecks captured count basis for custom rules and hard limits under fences. Persisted definitions bind rule versions; accepted receipts preserve historical rule identity and currencies without comparing current configuration on terminal retry. No cross-process/native race claim is inferred from these source checks.
|
|
599
|
+
|
|
600
|
+
A full `codes.service` replacement must implement all five CodeService methods, use non-default `codes.version` and supply nonempty actual `codes.querySources.receipt` tables. Required methods are captured with class receivers and without evaluating accessors. Replacement owns schema/CDC/authority/receipts/complete privacy; no default roles, bounds or fallback. Default participant/clock/policy/bound options are refused with a replacement. Default SQL Codes over a fully replaced wallet refuses. Installed creation retains trusted Subject checks, player routes derive own verified Subject, and deactivate revokes delegation. Normal export/erase invokes root Economy plus each configured optional system; destination Inventory privacy remains owned by Inventory.
|
|
601
|
+
|
|
602
|
+
Server `/status`, strict Shared/Cloud/Local contracts and the bilingual panel now show installed backend, rule/eligibility binding, item participant and work bound, never token, subject or accepted outcomes. Default Codes adds four roles; full replacement adds none and reports null SQL bounds. Referral and durable client recovery remain explicitly absent. Existing read-only providers carry the same observation; no new write capability is implied. `./testing` provides `codeEligibilityFixture`, the actual default/override probe, fresh scoped `codesFakeLayer` and actual installed CodePort probe. Production discovery/auth/live delivery, generic conformance, child-reference/archival proofs, GM issuance, referral workflow and complete monetization remain open.
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
## Durable SQL Container opening
|
|
606
|
+
|
|
607
|
+
`./server` now exports `dataStoreContainers`, captured `resolveContainersStoreOptions`, `containerDefinitionDigest` and four separate roles: container tenants, immutable definitions, subject fences and opening history. `ContainersPort`/five-method `ContainersService` lives in browser-safe `./containers`. The adapter borrows actual prepared Economy/Inventory writers; no alternate debit/grant path, hot data store or tick work.
|
|
608
|
+
|
|
609
|
+
Configured content uses real Price, optional allowed-country content and shared Pity groups with identical policies. `realMoney:false` refuses real/mixed declarations; free/virtual opens need no providers. Real-payment fulfillment remains absent. Required `accessSource` supplies trusted age/country/eligibility/virtual adjustments for the verified owner. No unknown-age/adult fallback. `defaultContainerEligibility` and `resolveContainerEligibility` preserve no-minors/country/eligibility guards; custom pure eligibility may narrow only. App source and rules require explicit non-default version. Actual age-verification/provider integration and approved legal exemptions remain open.
|
|
610
|
+
|
|
611
|
+
Own list reports exact Pity-adjusted probabilities, immutable declaration/rule identity and a server `disclosureDigest` tied to owner/history/distribution. An opening intention contains those observed identities, request ID and optional virtual currency, never a draw, seed, amount, probability or player state. Fresh opening conflicts if SQL history or same-version probability output no longer matches the observed disclosure. Current displayed discount is not a pinned quote; the captured declaration bounds payment. Source/RNG/custom policy callbacks occur outside replayable SQL callbacks.
|
|
612
|
+
|
|
613
|
+
One unresolved original request per owner serializes openings across groups. SQL reserves declaration, original currency registry, historical sequence/Pity and exact disclosed distribution before sampling. The admitted draw/drop/price is durably selected before actual participants prepare. Repeated pending requests reuse that selection; concurrent initial samplers may draw before one SQL selection wins, but only the admitted selection can fulfill. No guarantee that RNG is invoked exactly once across a pre-selection crash. A new nonce cannot bypass a pending original. Failed grants roll back wallet/items/accepted sequence and retain the original selection; fixing actual stock allows the same request to proceed. Permanent cancellation/GM repair of unresolved originals remains open; do not substitute a new request.
|
|
614
|
+
|
|
615
|
+
Prelude rechecks owner/pending/selected content/history/Pity under fences. Actual debit, chosen currency/item grants, accepted opening receipt and owner-slot release share one exact-store SQL group. Sequence-ordered accepted history is Pity truth/rebuild source; there is no mutable Pity counter/cache to reconstruct. Shared groups require matching historical policy. Hard Pity resets on a hit; fresh disclosure changes after acceptance.
|
|
616
|
+
|
|
617
|
+
Terminal original retry/own read admits historical content, rules, distribution and child receipts before current declarations/clock/facts/RNG/preparation. Export/erase owns these four roles after declarations change; erase removes subject/intention/selection/receipt bodies and leaves permanent owner fences/pseudonymous IDs. Shared definitions remain. Wallet/item erasure still runs through those owners. Factory/replacement/lifecycle are now available below.
|
|
618
|
+
|
|
619
|
+
Facts/RNG are explicit fixture seams, not age-verification or entropy proof. Native DDL/cleanup includes all four roles. `containerEligibilitySystemFixture` uses the actual default/override; canonical installed ContainersPort Layer is now available below; full registration remains open.
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
## Installed optional Containers
|
|
623
|
+
|
|
624
|
+
`economyPlugin({ currencies, containers: { containers, version, accessSource, inventory } })` now installs the captured SQL ContainersPort, four optional roles, own `economy.containers`/`economy.containers.receipt` queries and `economy.openContainer` action. Omit inventory only for currency-only drops. Required trusted access source supplies actual age/country facts with no adult default; it requires explicit non-default rule version. The root `realMoney` switch controls every declaration. Root price resolver is inherited unless explicitly overridden. Only configured systems add roles/routes/Layers; `economy.economy.containers()` refuses while absent or inactive.
|
|
625
|
+
|
|
626
|
+
Default receipt/list sources are actual Container SQL tables. Optional `containers.querySources` adds real app-owned invalidation tables for external access/price facts; clocks and non-SQL facts still require explicit freshness integration. A complete five-method `containers.service` replacement requires non-default version and actual nonempty list/receipt source tables. It owns schema/CDC/legal authority/receipts/privacy with no default SQL roles/bounds/fallback. Default participant/clock/strategies/random/source/bound options are refused with a replacement. Default SQL Containers over a completely replaced wallet refuses. Required methods retain class receivers without reading getters. Installed gateway preserves own verified Subject checks; replacement remains responsible for its legal/financial contract.
|
|
627
|
+
|
|
628
|
+
Normal bind/deactivate/export/erase invokes each configured optional system. Economy and Container hooks own their roles, while Inventory still owns destination erasure. Backend/rule identity, probability/roll/eligibility/price/RNG binding, redacted content counts/groups/Pity thresholds and work bounds are shown by server status, strict Shared/Cloud/Local decoder and bilingual configuration panel. No player Pity, draws, owner data, selections, outcomes or wallet values. Custom service reports app-owned RNG/null SQL bounds; real-payment/GM/client-recovery installation remains false. The existing read-only provider paths carry the new observation; refresh errors hide stale content and retain the reload control/focus. No GM write capability is implied.
|
|
629
|
+
|
|
630
|
+
`useContainers`, `useOpenContainer` and `useContainerReceipt` use the actual browser-safe descriptors. They neither persist a client journal nor replay automatically. Send only the observed declaration/disclosure identities and original request/currency choice. Missing/pending stays nonterminal. Uncertain selected originals cannot be discarded for a new nonce. Full player components, native/browser durable recovery, original cancellation/GM repair and real-payment fulfillment remain open.
|
|
631
|
+
|
|
632
|
+
`./testing` now exports actual installed scoped `containersPortFixture`, `containersFakeLayer`, `containersPortSystemFixture` and the actual default eligibility probe. Each scope uses a fresh memory store and revokes escaped service access through normal plugin deactivation. Explicit test facts/RNG are seams, not production age or entropy proofs. Templates await actual trusted age-source integration; no fake adult default is installed there.
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
## Subscription calendars and daily-grant rules
|
|
636
|
+
|
|
637
|
+
The browser-safe `./subscriptions` entry provides `defineSubscription`, `SubscriptionsPort`, replacement-method capture and intention admission. Content includes the existing Price, a day/week/month period, captured on-purchase/daily item and currency gifts, grace days and required duration/renewal copy keys plus an app-local cancellation path. The global `realMoney:false` switch refuses real/mixed declarations; virtual/free content requires no provider credentials. Cooling-off defaults to fourteen days. A shorter declaration requires an explicit application reason; this config is not a legal-compliance result. Perks and country/age authority remain unfinished.
|
|
638
|
+
|
|
639
|
+
`defaultSubscriptionPeriod` uses UTC, exact day/week durations and calendar months anchored to the original purchase time. A Jan31 origin clamps to Feb28/29 and returns to Mar31; it never derives the next month from the previously clamped date. `resolveSubscriptionPeriod` captures content and time before the slot. A custom resolver requires an explicit non-default rule identity and may narrow the canonical half-open window, never extend or overlap it. It may leave a gap. Work counts/time ranges are admission budgets, not capacity claims.
|
|
640
|
+
|
|
641
|
+
`defaultSubscriptionDailyGrant` decides eligibility in active or explicitly admitted grace windows. `resolveSubscriptionDailyGrant` uses an absolute UTC day, rejects duplicate claimed days and requires a named custom rule. A custom policy can deny an eligible grant but cannot permit a duplicate, early, expired or out-of-grace claim. The caller must supply complete authoritative SQL receipt history; this pure decision does not persist or prove idempotency. Local client timezone and client-supplied state are never grant authority.
|
|
642
|
+
|
|
643
|
+
`SubscriptionsPort` declares list/active/start/renew/claimDaily/cancel/export/erase. A complete replacement retains class receivers and refuses method getters. Player start intents carry observed content/version/digest, original request and currency choice. Daily/cancel intents carry only entitlement/request IDs. Renew is a trusted server intention; implementations must deny players and independently fence the entitlement/period. No price, dates, consent, claimed days or provider state is accepted as player value authority. Optional factory installation is described below.
|
|
644
|
+
|
|
645
|
+
No new tables, settlement, automatic debit, failure-grace transition, Inbox delivery, privacy implementation, SQL rebuild, panel installation, provider integration or canonical port fake Layer is implied. No support claim follows from these declarations.
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
## Ordinary virtual Subscription SQL adapter
|
|
649
|
+
|
|
650
|
+
`./server` now exports `dataStoreSubscriptions`, option admission, declaration digest and four Subscription table roles. Optional factory installation of this borrowed-DataStore adapter is described below. Explicit trusted access/renewal-consent source and non-default rules identity are mandatory; there is no consent fallback. The source receives verified owner/tenant scalars, captured content, start/renew kind and observation time. It runs outside replayable SQL callbacks. Real/provider settlement is absent; the adapter can select free or virtual alternatives through the actual Price guard.
|
|
651
|
+
|
|
652
|
+
Start uses observed declaration/version/digest, original nonce and currency choice. Actual prepared wallet debit/on-purchase currency gifts/item grants, accepted event and entitlement commit in one exact-store group. A genuine item/funds failure accepts no entitlement or value; participant pending admissions may remain. Complete sequence-ordered accepted events reconstruct current entitlements and UTC claimed days; there is no mutable entitlement/day cache. One existing unexpired/grace entitlement per content ID refuses another start. A new lifetime after expiry has its own origin and entitlement identity. Bounded complete history is required; archive/retention extension is open.
|
|
653
|
+
|
|
654
|
+
Daily claims carry only own entitlement/request IDs. Original accepted requests return historical receipts before clock/current declaration/source/preparation; another nonce cannot grant the same entitlement/UTC day. SQL prelude serializes owner history and checks the captured sequence basis. Parallel changed bases conflict and must reread; the framework does not silently retry callbacks or invent a new nonce. Preparation completion rechecks expiry and UTC-day crossings before acceptance. Child receipts use actual existing wallet/item writers, not duplicated transfer/grant code.
|
|
655
|
+
|
|
656
|
+
Renew is system-only and takes explicit tenant/owner/entitlement/request IDs. It requires original renewal consent plus current trusted consent/eligibility, due expiry and the next anchored window. It accepts at most one next period independently of nonce. It neither charges future periods early nor bulk-charges old missed months. Scheduling, durable consent acquisition/revocation events, payment-failure transitions and overdue repair remain open. Configured grace permits daily grants after expiry; cancellation suppresses that grace. This is not a verified provider-payment-failure policy. Cancel removes renewal authority without shortening an already paid active period or writing a refund; withdrawal/refund settlement is open.
|
|
657
|
+
|
|
658
|
+
Owning export/erase covers all four roles after current content/currencies change. Erase clears owner/event intent/receipt bodies and retains permanent subject fences plus pseudonymous event IDs/sequence/time; shared immutable declarations remain. Wallet and Inventory retain their separate privacy responsibilities. No Tick, hot cache, native connection or always-on boot work is introduced by this opt-in server factory.
|
|
659
|
+
|
|
660
|
+
Native DDL/cleanup includes each new role. Read/reconstruction, conservation, genuine grant rollback, UTC nonces, original replay, trusted renewal, class consent receiver, cancellation and owning privacy are source scenarios, not observed runtime results.
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
## Installed optional Subscriptions
|
|
664
|
+
|
|
665
|
+
`economyPlugin({ currencies, subscriptions: { subscriptions, version, accessSource, inventory } })` now installs the default SQL `SubscriptionsPort`, its four roles, normal lifecycle/privacy and own list/active/start/daily/cancel RPCs. Omit Inventory only when both grant declarations contain no items. Root `realMoney` and Price resolver are inherited; no source or consent is manufactured. No configuration means no Subscription roles/routes/Layer; absent or inactive facade access refuses. Trusted `renew` stays server-only, with no player renewal RPC or built-in scheduler. Earlier statements that this factory is absent are superseded.
|
|
666
|
+
|
|
667
|
+
A complete nine-method `subscriptions.service` replacement requires explicit non-default version, content and actual nonempty list/active query sources. It retains class receivers without invoking getters, owns authoritative schema/CDC/consent/receipts/lifecycle/privacy and has no default SQL roles, participant options, work bounds or fallback. Default SQL Subscriptions over a completely replaced wallet refuses. Default query sources can add actual app source tables; external fact and clock freshness remain explicit application integration. No native connection or Rooms/CLI boot hook is added.
|
|
668
|
+
|
|
669
|
+
Server status, strict Shared/Cloud/Local observations and EN/DE panel show backend/rule identity, period/daily/price slots, declaration IDs/periods/grant type counts/grace and SQL work bounds or app ownership. They expose no consent documents, owner IDs, active entitlements, receipts or values. Real-payment/automatic-renewal/GM/client-recovery capabilities remain false. Existing read-only selected-mount/replica providers carry the full observation; failure hides stale configuration and preserves the reload control/focus. No GM write permission follows from this panel.
|
|
670
|
+
|
|
671
|
+
`useSubscriptions`, `useActiveSubscriptions`, `useStartSubscription`, `useClaimSubscriptionDaily` and `useCancelSubscription` use the actual browser descriptors. They neither persist original intents nor automatically replay or schedule renewal. List displays declaration ceilings, not a pinned personalized quote. Applications must render required duration/renewal/cancellation disclosure and implement consent acquisition before supplying authority. Built-in player components, read-only original-intent recovery query/journal, Inbox/perks, consent/revocation events, failure/overdue repair and provider/refund/withdrawal remain open. Templates do not install fake consent.
|
|
672
|
+
|
|
673
|
+
`./testing` exports actual installed fresh memory-store `subscriptionsPortFixture`, scoped `subscriptionsFakeLayer` and the actual port/default slot probes. Normal plugin deactivation revokes escaped services. Consent facts are explicit test seams, not production/legal evidence.
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
## Read-only Subscription originals
|
|
677
|
+
|
|
678
|
+
`SubscriptionsPort` now has nine required methods, adding `receipt`; earlier eight-method references are superseded. `economy.subscriptions.receipt` and `useSubscriptionReceipt` observe a captured original start/daily/renew/cancel intention for the verified owner only. A start read carries its original declaration/version/digest/currency. Other reads carry entitlement/request IDs. Trusted renewal observation derives owner/tenant from the player's verified Subject; it does not grant renewal authority or mount a renewal action.
|
|
679
|
+
|
|
680
|
+
Default SQL observation performs reads only. It seeds no rows, evaluates no clock/current declaration/consent/strategy/prepared writer and neither reissues nor cancels an uncertain operation. Accepted originals use retained immutable definition/receipt evidence; changed accepted intents conflict. Missing evidence is `not-observed`, a nonterminal observation that cannot authorize a replacement nonce. Erased owners refuse. There is no pending event value row before the atomic group commits, so absence may coexist with an in-flight group. Full replacements must implement this ninth method and declare actual nonempty `querySources.receipt` tables alongside list/active.
|
|
681
|
+
|
|
682
|
+
Tests inspect all owned rows before/after the actual read and put throwing clock/authority/preparation controls behind it. Installed route/replacement/schema sources are aligned.
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
## Loyalty declarations and rules
|
|
686
|
+
|
|
687
|
+
The browser-safe `./loyalty` entry provides `defineLoyalty`, exact purchase-point projection, tier/perks strategies and the eight-method `LoyaltyPort`. Content captures ID/version/title, named real or virtual purchase sources with exact positive integer numerator/denominator, unique ordered tiers beginning at zero and XP multiplier/discount/badge/Inbox-gift perks. Gift admission reuses the actual Shop/currency caps. Virtual sources use the app currency registry; real sources require the global money switch and an ISO-style currency code. `realMoney:false` refuses real sources before binding and needs no provider credentials for virtual points. All limits are configured admission budgets, not measured capacity.
|
|
688
|
+
|
|
689
|
+
`loyaltyPointsForPurchase` projects trusted accepted amounts with exact BigInt arithmetic and floors once per purchase. It neither verifies a provider receipt nor writes points. `defaultLoyaltyTier` selects the highest earned threshold; named overrides may choose only an earned declared tier. `defaultLoyaltyPerks` returns its declared perks; the named perks slot may narrow only to a unique subset of those exact perks. Hidden, amplified or unearned perks refuse. These pure rules apply no Config override, XP/Price hook, badge or Inbox gift.
|
|
690
|
+
|
|
691
|
+
`projectLoyaltyPoints` is the one whole accepted-history arithmetic/graph projection used by the SQL writer and rebuild. It captures original purchase amount/weights and cumulative linked refunds; sequence gaps, duplicate purchases/refunds, time/refund regression, over-refund and signed-BIGINT overflow refuse. Partial reversals use the difference between original remaining points before and after the cumulative refund, rather than rounding each refund independently. A33-unit refund against1999 units at3/100 reduces59 points to58; a full refund removes the remaining58. Original-season reversals cannot consume current-season points. Arithmetic alone establishes neither receipt authenticity nor owner/complete-query custody; the SQL owner and trusted sources establish those separately.
|
|
692
|
+
|
|
693
|
+
## Durable and optional Loyalty
|
|
694
|
+
|
|
695
|
+
`./server` exports `dataStoreLoyalty`, `resolveLoyaltyStoreOptions` and five borrowed SQL roles: tenant fences, immutable definitions, subject fences/tail count, accepted entries and immutable seasons. It opens no native connection, uses no hot backend, reads/writes nothing in a tick and adds no CLI/Rooms boot hook. Complete ordered accepted entries are point truth and rebuild source; there is no materialized points cache. A durable per-subject end count rejects a missing history tail, in addition to sequence/owner/definition/season/arithmetic checks. Bounded complete history is required; archival expansion remains open.
|
|
696
|
+
|
|
697
|
+
Required `receiptSource.purchase/refund` methods must read actual accepted authority evidence and verify tenant, owner and canonical authority/purchase/refund identity. The source receives no player amount, weight, points or season. Purchase evidence carries the accepted amount/currency/kind/time and must refuse already-refunded or revoked purchases. Refund evidence carries the verified cumulative refunded amount. Explicit non-default rule version is mandatory. Sources/clock and strategy callbacks run outside replayable SQL callbacks. SQL serializes admission through tenant then subject fences and checks the unchanged complete tail before accepting an entry.
|
|
698
|
+
|
|
699
|
+
Purchase custody hashes tenant/logical loyalty ID/canonical authority/purchase independently of owner, content version and source aliases. Aliases and a new request nonce cannot credit the same receipt again. Refund custody likewise prevents replay/rebinding. Every reversal follows the original archived ratio, definition and season, even when current declarations have changed. Old-season refunds update the old season only. Seasonal programs require an explicit trusted `seasonSource`; accepted seasons retain immutable half-open bounds and reject changed identity or overlap. The current season view starts at zero without deleting old evidence or inventing a calendar. A logical program cannot silently change its decay mode after SQL admission.
|
|
700
|
+
|
|
701
|
+
`shopLoyaltyReceiptSource(store, { shop, refunds? })` reads an actual fulfilled default SQL Shop order and invokes its owning read-only original receipt service. It derives accepted virtual price and time from that receipt, never the current offer or a submitted price. Free/unsettled/foreign/mismatched orders refuse. This helper is for the default SQL Shop schema; a fully replaced Shop supplies its own receipt authority. The helper supplies no refund writer: omission of a verified refund source explicitly refuses reversals. It creates no automatic purchase consumer and does not make Shop purchase/refund and points one cross-system transaction. Catch-up, complete provider verification/refund settlement and cross-system consistency remain open.
|
|
702
|
+
|
|
703
|
+
`economyPlugin({ currencies, loyalty: { loyalty, version, receiptSource, seasonSource? } })` installs the default port, five roles, own `economy.loyalty`, `economy.loyalty.mine` and `economy.loyalty.receipt` queries, actual Tag and normal lifecycle/privacy hooks. `economy.economy.loyalty()` refuses while absent or inactive. There is no player credit/reverse/rebuild RPC. Those methods require a system Subject or a configured verified service; their captured intentions contain identities only. Player queries derive tenant/owner exclusively from Subject. `receipt` and trusted `rebuild` read accepted history without seeding rows, clock, current sources, slots or writes; missing is a nonterminal observation, never a new credit authority. Rebuild also works after current definitions are removed.
|
|
704
|
+
|
|
705
|
+
A complete `loyalty.service` replacement requires all eight receiver-bound methods, explicit non-default version/content and actual nonempty `querySources.list/mine/receipt` tables. Accessors refuse without invocation. It owns schema/CDC/receipts/season authority/privacy and has no default roles, bounds, source/strategy/clock options or fallback. Default sources may explicitly add actual app invalidation tables. Loyalty points are independent borrowed SQL truth over an explicitly supplied receipt source, so a replaced wallet does not acquire default wallet participants. Root real-money admission still applies. Time-only seasonal transitions require explicit refresh.
|
|
706
|
+
|
|
707
|
+
Export/erase owns all five roles after current content/currencies are removed. Erase clears owner/intent/evidence/point bodies and retains permanent subject fences, pseudonymous event identities, sequence/time and hashed purchase/refund custody. Shared immutable definitions and seasons survive erasure. Wallet, Shop and Inventory keep their own erase hooks; this port does not erase their values or infer merge continuity. A receipt cannot become fresh authority for another owner after erasure.
|
|
708
|
+
|
|
709
|
+
All four inspect layers and EN/DE panel show optional backend/version, tier/perks slots, source/season binding, redacted declaration type counts, conditional roles and SQL budgets or app ownership. They expose no owner, purchase reference, points, tier outcomes or refund evidence. Provider receipt verification, automatic consumption, perk effects and GM actions remain false. Shared/Local/Cloud read-only providers carry the same observation; failed refresh hides stale configuration and preserves reload focus. `useLoyaltyPrograms`, `useLoyalty` and `useLoyaltyReceipt` use actual browser-safe descriptors and perform no point writes or automatic recovery.
|
|
710
|
+
|
|
711
|
+
`./testing` supplies actual installed fresh memory-store `loyaltyPortFixture`, scoped `loyaltyFakeLayer`, a Tag-based port probe and real default tier/perks probes. The fixture uses an actual conserved virtual Shop purchase; its season/refund callbacks are explicitly test authority seams. Normal deactivation revokes escaped services.
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
## Optional Piggy banks
|
|
715
|
+
|
|
716
|
+
The browser-safe `./piggy-banks` entry exports `definePiggyBank`, the complete
|
|
717
|
+
seven-method `PiggyBanksPort`, exported fill/reset defaults and one shared
|
|
718
|
+
`projectPiggyBank` history projection. Declarations capture ID/version/title,
|
|
719
|
+
an actual registry currency, an exact positive cap, unique `fillFrom` source
|
|
720
|
+
IDs, an unlock `Price` and `reset: 'onUnlock'`. The cap cannot exceed the
|
|
721
|
+
currency's wallet cap. `realMoney:false` refuses real/mixed declarations before
|
|
722
|
+
binding; virtual/free unlocks require no provider credentials. Configuration
|
|
723
|
+
bounds are admission budgets, not measured throughput.
|
|
724
|
+
|
|
725
|
+
Install through `economyPlugin({ currencies, realMoney: false, piggyBanks: {
|
|
726
|
+
banks, version, fillSource, unlockSource } })`. `fillSource.read` is required
|
|
727
|
+
server authority: accepted tenant/owner, canonical authority/event receipt ID,
|
|
728
|
+
source ID, exact amount and settlement time. `unlockSource.read` is required
|
|
729
|
+
trusted eligibility and price-adjustment authority. Neither source has an
|
|
730
|
+
invented Match/age/provider fallback. Inputs and receiver methods are captured
|
|
731
|
+
before lazy Effects; getters refuse without invocation. Application code owns
|
|
732
|
+
receipt authenticity, revocation and source invalidation.
|
|
733
|
+
|
|
734
|
+
Canonical fill custody is tenant + logical bank + authority + receipt ID,
|
|
735
|
+
independent of request nonce, source alias or submitted owner. Each player's
|
|
736
|
+
accepted reward needs its own canonical receipt ID: a shared Match ID alone
|
|
737
|
+
cannot identify multiple players' rewards. Rebinding one receipt to another
|
|
738
|
+
owner, even after erasure, conflicts. Only trusted system/allowed-service calls
|
|
739
|
+
can `fill` or `rebuild`; players never submit amount, fill state, Match result
|
|
740
|
+
or reset state. `defaultPiggyFill` grants at most accepted amount and remaining
|
|
741
|
+
cap; named custom fill rules may narrow that envelope only. A fill at cap records
|
|
742
|
+
zero delta and consumes its receipt permanently; replay after reset cannot
|
|
743
|
+
create delayed value.
|
|
744
|
+
|
|
745
|
+
Four borrowed SQL roles own tenant fences, immutable bank definitions, subject
|
|
746
|
+
fences/end count and accepted events. Complete ordered accepted fill/unlock
|
|
747
|
+
history is available-value/cycle truth and rebuild source; no mutable fill
|
|
748
|
+
cache exists. Durable end count refuses a missing tail. Per-bank sequence,
|
|
749
|
+
monotonic time, owner/definition/evidence and exact arithmetic are checked.
|
|
750
|
+
The first admitted logical bank ID fixes its complete normalized definition,
|
|
751
|
+
registry and rules version; changing them under that ID conflicts. Use a new
|
|
752
|
+
bank ID or an explicit application migration. Live content evolution and
|
|
753
|
+
archival migration are unfinished.
|
|
754
|
+
|
|
755
|
+
Player `unlock` sends observed bank ID/version/digest, original request ID and
|
|
756
|
+
optional virtual currency only. Eligibility, price and reset callbacks run
|
|
757
|
+
outside replayable SQL. Actual prepared wallet debit, wallet grant, accepted
|
|
758
|
+
reset and receipt commit in one exact-store group using the existing wallet
|
|
759
|
+
writer. Failure rolls back payment and accepted reset/grant together; pending
|
|
760
|
+
wallet admissions may remain. Default reset grants all available value and
|
|
761
|
+
retains zero; a named custom reset retains a remainder and grants only the
|
|
762
|
+
difference, preventing double credit. Current prices are declarations, not
|
|
763
|
+
pinned personalized quotes. Real settlement is explicitly refused by this
|
|
764
|
+
writer; enabling the global money switch supplies no provider implementation.
|
|
765
|
+
|
|
766
|
+
Accepted original fill/unlock retries and owning `receipt`/`rebuild` read only
|
|
767
|
+
historical SQL, without row seeding, current declaration/clock/source/slot or a
|
|
768
|
+
fresh transfer. `not-observed` is nonterminal, never proof of rollback. Privacy
|
|
769
|
+
works after declarations change: export returns owning bank/evidence/receipt
|
|
770
|
+
history; erase clears owner/body/digest, retains custody, fences and end count,
|
|
771
|
+
and preserves shared definitions. Wallet and other destinations use their own
|
|
772
|
+
privacy hooks. Merge, retention and complete cross-system forensic reconciliation
|
|
773
|
+
remain open.
|
|
774
|
+
|
|
775
|
+
Complete `piggyBanks.service` replacement captures all seven methods and requires
|
|
776
|
+
explicit nondefault version/content plus actual nonempty
|
|
777
|
+
`querySources.list/receipt` tables. It owns schema/SQL/CDC/source authority/privacy;
|
|
778
|
+
there are no default roles/bounds/source/clock/strategy options or fallback.
|
|
779
|
+
Default SQL Piggy banks refuse a replaced root wallet because they need its
|
|
780
|
+
actual prepared writer. A complete replacement pair may supply its own service.
|
|
781
|
+
Default query sources can add actual app fact invalidation tables.
|
|
782
|
+
|
|
783
|
+
The installed player queries/actions and `usePiggyBanks`, `useUnlockPiggyBank`,
|
|
784
|
+
`usePiggyBankReceipt` hooks are browser-safe intent/observation paths. There is
|
|
785
|
+
no player fill/rebuild RPC. Four inspect layers and EN/DE panel expose backend,
|
|
786
|
+
version, slots, redacted configured caps/source counts, exact conditional roles
|
|
787
|
+
and SQL bounds or app ownership. No private fills, balances, events or receipts
|
|
788
|
+
appear. Automatic filling, real settlement, durable client recovery and GM
|
|
789
|
+
capabilities remain false. Failed refresh hides stale observations while
|
|
790
|
+
retaining reload focus.
|
|
791
|
+
|
|
792
|
+
`./testing` supplies a fresh installed `piggyPortFixture`, scoped
|
|
793
|
+
`piggyBanksFakeLayer`, actual Tag probe and default/override fill/reset probes.
|
|
794
|
+
Fixtures use actual conserved wallet transfers; fill/eligibility are explicitly
|
|
795
|
+
named test authority seams.
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
## Personalized, Flash and Bundle merchandising
|
|
799
|
+
|
|
800
|
+
`./offer-policies` exports browser-safe `definePersonalizedOffers`,
|
|
801
|
+
`defaultPersonalizedOfferSelector`, `resolvePersonalizedOffers`,
|
|
802
|
+
`defineFlashOffer`, `defaultFlashTriggerPolicy`, `resolveFlashOffer`,
|
|
803
|
+
`defineBundle` and `projectBundleValue`. These capture actual normalized Shop
|
|
804
|
+
content and the app currency registry. All `Price` declarations retain the
|
|
805
|
+
global real-money admission; `realMoney:false` supports virtual/free rules
|
|
806
|
+
without credentials and refuses real/mixed content. Enabling real references
|
|
807
|
+
never installs provider settlement. These rules introduce no tables, parallel
|
|
808
|
+
Shop/wallet writer, automatic Tick read or per-player state RPC.
|
|
809
|
+
|
|
810
|
+
A personalized pool declares ID/version, bounded slots, complete offers and
|
|
811
|
+
play-style affinity tags, plus explicit rotation anchor/duration. Trusted
|
|
812
|
+
context supplies canonical tenant/key, owned item IDs, play styles and time.
|
|
813
|
+
The default ranks unowned item kinds and matching declared styles, then uses
|
|
814
|
+
stable identity/rotation tie-breaking. Declaration order does not determine
|
|
815
|
+
selection. Rotation windows are half-open. A named selector override may choose
|
|
816
|
+
only unique declared offers within the slot bound; unknown/duplicate/excess
|
|
817
|
+
outputs refuse. Inputs and outputs are detached before lazy use. Canonical
|
|
818
|
+
identity across guest upgrade/merge and coherent fact snapshots belong to the
|
|
819
|
+
source; selection is not persisted exposure or account identity authority.
|
|
820
|
+
|
|
821
|
+
A Flash declaration captures one immutable one-time Shop offer, accepted trigger
|
|
822
|
+
kind and bounded duration. It forces single quantity/per-player lifetime one-time
|
|
823
|
+
limits; original Shop counters own once-only purchase. The trusted trigger
|
|
824
|
+
receipt includes canonical authority/event, tenant/owner, trigger kind and
|
|
825
|
+
accepted timestamp. Foreign, future or mismatched receipts refuse. The window
|
|
826
|
+
starts at that recorded time, is intersected with declaration availability and
|
|
827
|
+
never restarts from a refresh clock. The exported trigger default is replaceable
|
|
828
|
+
by a named narrowing slot; extending its envelope refuses. Missing explicit
|
|
829
|
+
trigger is unavailable, not fabricated eligibility. Event receipt durability,
|
|
830
|
+
revocation and selection of the original trigger remain source responsibilities.
|
|
831
|
+
A new window per later event is not a built-in rotation policy.
|
|
832
|
+
|
|
833
|
+
`offerPolicyContextSource` accepts optional `personalized`, `flashOffers`,
|
|
834
|
+
`personalizedSelector`, `flashTrigger` and explicit nondefault `policyVersion`.
|
|
835
|
+
Use that identity for the owning Shop rules version too. The required existing
|
|
836
|
+
facts source adds `personalization: { ownedItemIds, playStyles }` for pooled
|
|
837
|
+
offers and `flashTriggers: [{ flashId, trigger }]` for Flash offers. An explicit
|
|
838
|
+
null trigger means no window; omitted required records/facts refuse. Source
|
|
839
|
+
methods retain private receivers and getters refuse. Eligibility narrows the
|
|
840
|
+
existing app eligibility and composes with Starter/experiment rules. Both the
|
|
841
|
+
owning offer query and purchase use this same policy through ordinary Shop;
|
|
842
|
+
accepted original retries preflight historical SQL before current facts/clock,
|
|
843
|
+
even after expiry. No client ownership/style/trigger/window or amount is trusted.
|
|
844
|
+
|
|
845
|
+
`defineBundle({ offer, parts: [{ offer, qty }], showValue: 'sumOfParts' })`
|
|
846
|
+
requires parts to reproduce exactly the bundle's actual item/currency grants;
|
|
847
|
+
missing/duplicate/self-referential/mismatched parts refuse. Unit-price evidence
|
|
848
|
+
for `projectBundleValue` includes each part's ID/version, source reference,
|
|
849
|
+
observation time and normalized price. All parts must be represented once, with
|
|
850
|
+
explicit maximum age; stale/future/mismatched or amplified prices refuse.
|
|
851
|
+
The actual bundle price is admitted within its declaration too. Only matching
|
|
852
|
+
virtual currencies or real currency references are summed; absent comparability
|
|
853
|
+
produces no comparison, never an exchange rate. BigInt sums can exceed individual
|
|
854
|
+
SQL-wallet BIGINT bounds. `saving` is signed, so a more expensive bundle returns
|
|
855
|
+
a negative difference. Sum-of-parts display is not fabricated anchor-price
|
|
856
|
+
history, a pinned quote or legal/provider price verification.
|
|
857
|
+
|
|
858
|
+
`./server` adds `bundleValueFromShopReceipts({ shop, bundle, currencies,
|
|
859
|
+
version, maxPriceAgeMs }).read(subject, { at, bundlePrice, originals })`.
|
|
860
|
+
It captures the owning receipt method, checks verified own Subject and full
|
|
861
|
+
part content/rule digest, and reads accepted single-unit free or single-currency
|
|
862
|
+
virtual originals. Accepted price/time/order references supply the value;
|
|
863
|
+
missing/pending/future/stale/foreign/changed originals refuse. This adapter
|
|
864
|
+
accepts no real/mixed or multiple-alternative unit declarations. It reads no
|
|
865
|
+
current source/clock and creates no purchase or table. Purchase the bundle itself
|
|
866
|
+
through ordinary Shop. Verified refund state, provider/current-store prices,
|
|
867
|
+
atomic pricing snapshots and withdrawal/anchor history remain open.
|
|
868
|
+
|
|
869
|
+
The existing four-layer Shop configuration continues to label an explicit custom
|
|
870
|
+
context source; durable rotation/provider/LiveOps installation flags remain
|
|
871
|
+
false. These helpers do not install a new configuration page, player countdown,
|
|
872
|
+
comparison component, exposure event/ledger or automatic refresh. `./testing`
|
|
873
|
+
exports actual default/override selector and trigger probes.
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
## Shared wallet spend limits and complete-tail custody
|
|
877
|
+
|
|
878
|
+
`./spend-limits` exports captured currency-specific daily/monthly/minor rules,
|
|
879
|
+
`defaultSpendLimitPolicy`, `resolveSpendBudget`, `utcSpendWindows`, `projectSpend`
|
|
880
|
+
and `assertSpendBudget`. Install `spendLimits: { limits, source, version, policy? }`
|
|
881
|
+
on the ordinary `economyPlugin` or `dataStoreEconomy` options. Limits use exact
|
|
882
|
+
nonnegative integer amounts; null means no configured cap. Minor caps are
|
|
883
|
+
intersected with general caps. Policy version must be explicit and nondefault.
|
|
884
|
+
Named custom policy can only narrow caps and permission. No exchange rate or
|
|
885
|
+
cross-currency total is invented. Only declared limit currencies are covered.
|
|
886
|
+
|
|
887
|
+
Required `source.read({ tenantId, ownerId, ownerKind, currency, at })` returns
|
|
888
|
+
those exact scope fields, actual `allowed`/`minor` facts, `observedAt`, optional
|
|
889
|
+
parent daily/monthly caps and optional `blockedUntil`. Foreign/future/malformed
|
|
890
|
+
facts refuse; no adult, age or parental-consent fallback exists. Private source
|
|
891
|
+
receivers are captured and accessors refuse. Author source/policy callbacks run
|
|
892
|
+
outside replayable SQL. Fact freshness, revocation and atomic changes of an
|
|
893
|
+
external parental/profile system remain source responsibilities; a timestamp
|
|
894
|
+
field alone is not verification. A block timestamp is a generic restriction,
|
|
895
|
+
not a statutory cooling-off, consent, cancellation or refund implementation.
|
|
896
|
+
|
|
897
|
+
The existing prepared wallet writer owns the debit check under its ordinary
|
|
898
|
+
subject/currency fences. Shop, paid Crafting, Codes, Containers, Subscriptions,
|
|
899
|
+
Piggy unlocks and server transfers using that actual wallet participant share
|
|
900
|
+
this guard; there is no new Shop wrapper or separate counter cache. Complete
|
|
901
|
+
accepted owner/currency postings and their real paired transfer receipts supply
|
|
902
|
+
spend truth. UTC days and calendar months are half-open; credits/refunds do not
|
|
903
|
+
silently restore consumed allowance. Exact totals are recomputed in the SQL
|
|
904
|
+
commit callback and compared with captured policy, so multiple units in one
|
|
905
|
+
transaction cannot use the same remaining budget twice. Rejection rolls back
|
|
906
|
+
all accepted values/postings/counts/receipts in the group; pending admissions may
|
|
907
|
+
remain. Accepted original retries return before current source/clock/policy.
|
|
908
|
+
|
|
909
|
+
The existing subject role now retains `entriesCount` for the whole owner's
|
|
910
|
+
ledger across currencies. Each accepted wallet posting increments it in the same
|
|
911
|
+
transaction. Writer, audit/rebuild and unerased privacy require a complete
|
|
912
|
+
bounded owner history with that count; a deleted tail is refused rather than
|
|
913
|
+
rebuilding/spending from a prefix. `maxLedgerRows` therefore bounds complete
|
|
914
|
+
owner history, including all currencies. Erasure keeps the count and permanent
|
|
915
|
+
fence while removing owning fields; shared financial values remain under the
|
|
916
|
+
existing privacy policy. Rebuild repairs materialized wallets, never a missing
|
|
917
|
+
source ledger/count. Missing/invalid count is not automatically inferred from a
|
|
918
|
+
potentially incomplete history.
|
|
919
|
+
|
|
920
|
+
A fully replaced root wallet refuses default `spendLimits` options and owns its
|
|
921
|
+
own debit/source/SQL/privacy contract. Default configuration needs no extra SQL
|
|
922
|
+
role or secret and works with virtual currency. Provider payments, cross-Store/
|
|
923
|
+
Stripe real-money aggregation, debt/refund/withdrawal policy and complete legal
|
|
924
|
+
parent controls remain open. These options do not grant a player transfer route.
|
|
925
|
+
|
|
926
|
+
All four inspect layers and EN/DE section report configuration only: rules
|
|
927
|
+
version, policy/source, UTC period, complete-history budget and configured
|
|
928
|
+
currency/minor caps. No owner, actual minor status, parental facts, spend totals,
|
|
929
|
+
ledger or provider references appear. Default SQL root required; unknown/private
|
|
930
|
+
extras, relaxed minors and claimed provider/cooling/parent UI capability refuse.
|
|
931
|
+
Existing read-only host providers/reload/error/focus are reused; failure hides
|
|
932
|
+
stale caps. Provider/cooling/parent UI installation flags remain false.
|
|
933
|
+
|
|
934
|
+
`./testing` exports actual default/override policy probes, fresh installed
|
|
935
|
+
`spendLimitsPortFixture`, scoped `spendLimitsFakeLayer` and actual `EconomyPort`
|
|
936
|
+
Tag probe using the same writer. Named fixture age/parent facts are test seams;
|
|
937
|
+
normal deactivation revokes escaped wallets.
|
|
938
|
+
|
|
939
|
+
## Trade intentions and replaceable contract
|
|
940
|
+
|
|
941
|
+
`@voltro/plugin-economy/trades` contains captured `defineTradeRules`,
|
|
942
|
+
`tradeProposeIntent`, `tradeAcceptIntent`, `tradeCancelIntent`,
|
|
943
|
+
`resolveTradeEligibility`, `resolveTradeTax` and the full eight-method
|
|
944
|
+
`TradesPort`. The Tag alone installs no player routes or panels. The trusted four-role SQL
|
|
945
|
+
adapter is described below.
|
|
946
|
+
|
|
947
|
+
A proposal selects immutable item instance quantities and declared game-currency
|
|
948
|
+
consideration, target (or public target `null`), bounded lifetime and request ID.
|
|
949
|
+
Those terms never establish ownership or balances. Acceptance sends only the
|
|
950
|
+
observed trade ID/digest and request ID. Cancellation sends only ID/request ID.
|
|
951
|
+
Unknown player state and duplicate assets refuse. Real-money trading has no
|
|
952
|
+
channel. The actual owning service must bind the admitted terms and original
|
|
953
|
+
lifetime to durable custody before producing a receipt.
|
|
954
|
+
|
|
955
|
+
Trusted participant facts are tenant/owner-bound, include explicit permission,
|
|
956
|
+
TrustScore and actual item quantity/tradability/expiry/lock/reservation identity.
|
|
957
|
+
The default permits an already locked item only for this exact trade reservation;
|
|
958
|
+
foreign locks refuse. TrustScore has to come from the actual sanctions authority;
|
|
959
|
+
this module never invents it. Eligibility and named overrides can only narrow
|
|
960
|
+
permission, target, lifetime, ownership and availability. The adapter must reread
|
|
961
|
+
actual materialization under transaction fences; these facts are not a lock.
|
|
962
|
+
|
|
963
|
+
Default tax deducts the configured basis points from the receiver's gross amount,
|
|
964
|
+
rounding upward with exact integers. Gross equals net plus tax; an override may
|
|
965
|
+
reduce the declared tax only. The same-writer Wallet participant below owns the
|
|
966
|
+
actual net/tax sink pairs; the durable Trade service below binds that projection to
|
|
967
|
+
reservations, transfer, expiry, original replay and owner privacy. A tax projection does not move or mint value.
|
|
968
|
+
|
|
969
|
+
`./testing` exports `tradeFixture`, actual default/override
|
|
970
|
+
`tradeTaxSystemFixture` and `tradeEligibilitySystemFixture`. Installed Trade
|
|
971
|
+
Escrow/Inventory binding, owning SQL journal, replacement/scoped Layer, hooks,
|
|
972
|
+
player RPC and four inspect layers are described below. Automatic expiry, client
|
|
973
|
+
journal/components, marketplace search/fee and native parity remain open.
|
|
974
|
+
|
|
975
|
+
## Same-writer game-currency Wallet Escrow
|
|
976
|
+
|
|
977
|
+
Default `EconomySqlService` and the installed `economy.economy` facade expose
|
|
978
|
+
`prepareEscrowReserve`, `prepareEscrowRelease`, `prepareEscrowSettle`,
|
|
979
|
+
`prepareEscrowTaxSettle` and read-only `escrow`. Participant/input/observation
|
|
980
|
+
and `WalletEscrowSettlementReceipt` types are exported from
|
|
981
|
+
`@voltro/plugin-economy/server`. These require trusted system or explicitly
|
|
982
|
+
allowed-service authority. The four-method request EconomyPort stays unchanged.
|
|
983
|
+
Replaced root wallets cannot borrow the default SQL participant. The facade
|
|
984
|
+
captures inputs without invoking getters and refuses after normal deactivation.
|
|
985
|
+
|
|
986
|
+
Reserve takes owner, tenant, permanent reservation ID, declared game currency
|
|
987
|
+
and exact positive amount. The existing prepared Double-Entry writer debits
|
|
988
|
+
available wallet currency and credits the canonical per-owner/reservation/
|
|
989
|
+
currency system Escrow account. Only a private same-writer capability can use
|
|
990
|
+
`escrow.` system accounts; ordinary `transfer` and `prepareTransfer` refuse
|
|
991
|
+
minting or draining that namespace, including `escrow.tax.`. Actual key/digest
|
|
992
|
+
generation is shared. Spend/minor/parent limits apply to the reserve debit;
|
|
993
|
+
release never restores period allowance. No provider or room tick I/O is used.
|
|
994
|
+
|
|
995
|
+
Nullable server-only transaction `escrow` custody binds immutable reserve terms
|
|
996
|
+
and the accepted close mode, opaque target fence and actual receipts. Reserve
|
|
997
|
+
postings/custody and closing postings/custody each commit in one exact-store
|
|
998
|
+
group. Release returns the whole amount; ordinary settle pays the whole gross
|
|
999
|
+
to a different wallet. Tax settle takes an explicit nonnegative exact `taxAmount`
|
|
1000
|
+
and optional reserved-prefix `taxAccountId` (default `escrow.tax.default`). The
|
|
1001
|
+
Trade service must derive that amount through `resolveTradeTax`; this trusted
|
|
1002
|
+
participant does not invent a tax rate. Gross equals net plus tax. Net is an
|
|
1003
|
+
actual wallet payout; positive tax is a separate actual Double-Entry system
|
|
1004
|
+
pair to the bound sink. Both use the existing writer. No alternate wallet or
|
|
1005
|
+
financial source is installed.
|
|
1006
|
+
|
|
1007
|
+
`WalletEscrowSettlementReceipt` reports gross/net/tax and actual nullable
|
|
1008
|
+
`payout`/`taxReceipt`. Tax zero has no tax receipt. Tax equal to the entire
|
|
1009
|
+
reserved amount has no payout receipt or recipient wallet posting. No zero-value
|
|
1010
|
+
or synthetic financial receipt is fabricated. Recipient admission/privacy fences
|
|
1011
|
+
still apply. Recipient cap checks net; payout, tax, custody, counts and accepted
|
|
1012
|
+
receipts roll back together. A failed preparation may leave pending admission
|
|
1013
|
+
rows; pending opposite child intents are not evidence of a paid close.
|
|
1014
|
+
|
|
1015
|
+
Release, gross settle and tax settle share one permanent closing identity.
|
|
1016
|
+
Deterministic owner/currency/reserve fences and actual paired evidence admit
|
|
1017
|
+
only one target/mode/tax/sink. Conflicting retries refuse. Tax-zero and gross
|
|
1018
|
+
settle return the same actual payout original. Accepted retries precede current
|
|
1019
|
+
clock/spend callbacks; original reserve after close never resets or re-debits.
|
|
1020
|
+
Missing accepted custody/close marker, wrong amount/account/owner/time/reference
|
|
1021
|
+
or absent paired evidence refuses instead of reopening or inferring payment.
|
|
1022
|
+
The bounded prepared-unit budget applies to all descendants. Erased source or
|
|
1023
|
+
recipient refuses execution; originals do not bypass privacy fences. No partial
|
|
1024
|
+
close, FX, fee or real-money trading is performed.
|
|
1025
|
+
|
|
1026
|
+
`escrow` returns `not-observed`, `held`, `released` or `settled` after validating
|
|
1027
|
+
custody and real fund/payout/tax pairs, without seed, clock, sources, repair or
|
|
1028
|
+
writes. Accepted observations carry nullable `closingReceipt` and `taxReceipt`.
|
|
1029
|
+
Ordinary complete Double-Entry history/count rebuild remains wallet truth.
|
|
1030
|
+
Missing immutable Escrow custody is refused. Source-owned close observation can
|
|
1031
|
+
survive recipient erasure using original receipts and retained redacted financial
|
|
1032
|
+
pairs. Opaque fences are pseudonymous, not a legal anonymization claim.
|
|
1033
|
+
|
|
1034
|
+
Existing user/guest export includes own `escrows` only when present. Owning erase
|
|
1035
|
+
clears Escrow custody/receipts/digests, wallet identity and paired reference/actor
|
|
1036
|
+
fields, including the source-owned system-only tax transaction. Financial deltas
|
|
1037
|
+
and permanent owner fences remain. Later reserve/close cannot recreate an erased
|
|
1038
|
+
owner. Unsettled erased funds stay inaccessible; refund/disposition, guild
|
|
1039
|
+
lifecycle, arbitrary actor/reference erasure and legal retention require explicit
|
|
1040
|
+
policy. Flow reports show wallet-boundary `escrow.*` sources/sinks, not a newly
|
|
1041
|
+
minted supply claim. Complete Escrow/tax analytics classification remains open.
|
|
1042
|
+
|
|
1043
|
+
The written `realMoney:false` path needs no provider credentials. Inventory's
|
|
1044
|
+
four inspect layers retain `tradingInstalled:false`; these supporting
|
|
1045
|
+
participants do not install a player Trade service. Installed Trade
|
|
1046
|
+
binding/RPC/four panels are described below. Sanctions/eligibility, automatic
|
|
1047
|
+
expiry, components, replacement/scoped port conformance, recovery/consent and
|
|
1048
|
+
templates remain open.
|
|
1049
|
+
|
|
1050
|
+
## Atomic fresh counterparty and Trade Escrow composition
|
|
1051
|
+
|
|
1052
|
+
`EconomySqlService.prepareEscrowReserveTaxSettle` and the default-only revocable
|
|
1053
|
+
facade capture `WalletEscrowReserveTaxSettleInput`. They reserve the exact fresh
|
|
1054
|
+
amount and settle its net/tax in one actual SQL group. The returned
|
|
1055
|
+
`WalletEscrowFundedSettlementReceipt` contains real `reserve` and `settlement`
|
|
1056
|
+
receipts. Accepted originals precede current callbacks; changed
|
|
1057
|
+
amount/target/tax/sink refuses. No separate wallet writer or second database is
|
|
1058
|
+
used.
|
|
1059
|
+
|
|
1060
|
+
`./server` exports `tradeEscrowParticipant` and its structural types. It captures
|
|
1061
|
+
immutable trusted tenant/trade/proposer/offered/requested game-currency/item
|
|
1062
|
+
terms and the actual prepared Wallet and optional Inventory participants. The
|
|
1063
|
+
three methods `prepareReserve`, `prepareRelease`, `prepareExchange` require
|
|
1064
|
+
trusted system/explicit service authority and a mandatory exact-store prepared
|
|
1065
|
+
SQL admission. That admission belongs to the durable Trade writer and actual
|
|
1066
|
+
sanctions/eligibility authority. It is not optional, a player payload or a new
|
|
1067
|
+
clock/source check inside a replayable SQL callback. It must fence and revalidate
|
|
1068
|
+
the current proposal/expiry/participant/consent/eligibility state. The helper does
|
|
1069
|
+
not fabricate those facts or a durable Trade journal.
|
|
1070
|
+
|
|
1071
|
+
Reserve locks the proposer's actual selected items and moves actual available
|
|
1072
|
+
currency to its original Escrow. Exchange requires existing offered custody
|
|
1073
|
+
with the exact selected quantities/amounts. It transfers original or split items
|
|
1074
|
+
and closes offered currency; requested items and currency use compound fresh
|
|
1075
|
+
reserve/transfer and reserve/net/tax participants in the same group. Actual
|
|
1076
|
+
`resolveTradeTax` captures exact deductions outside SQL. Release frees only the
|
|
1077
|
+
proposer's original offered holdings. No parallel asset writer, new SQL table,
|
|
1078
|
+
real-money channel, exchange rate or newly minted Trade value is introduced.
|
|
1079
|
+
|
|
1080
|
+
The whole group includes required admission and every prepared descendant.
|
|
1081
|
+
Failure rolls back both owners' item/lock/history and wallet/fund/payout/tax/
|
|
1082
|
+
custody/receipt effects together; pending admissions can remain. Exact-store
|
|
1083
|
+
mismatch and work-budget overflow refuse before commit. Currency-only and
|
|
1084
|
+
empty one-sided legs are explicit; items require the actual Inventory participant.
|
|
1085
|
+
The normal per-unit recipient caps/capacity still apply in execution order;
|
|
1086
|
+
final-position capacity/netting for full inventories is not inferred. This
|
|
1087
|
+
fresh helper refuses already transferred/released offered custody. The durable
|
|
1088
|
+
Trade writer must return its accepted original before invoking it again.
|
|
1089
|
+
|
|
1090
|
+
The test admission exercises actual owner/store fencing only; it is not
|
|
1091
|
+
production sanctions, consent or Trade persistence proof. These fixtures use
|
|
1092
|
+
actual existing writers, real rollback and exact tax projection.
|
|
1093
|
+
|
|
1094
|
+
Installed lifecycle/Tag/RPC/four-layer binding of the durable eight-method
|
|
1095
|
+
`TradesPort` is described below. Expiry/recovery workflow, actual sanctions source,
|
|
1096
|
+
marketplace/gifting, player components, participant conformance and templates remain open.
|
|
1097
|
+
Inventory's existing `tradingInstalled:false` is retained. Trusted asset
|
|
1098
|
+
composition alone does not install player trading or complete `realMoney:false`.
|
|
1099
|
+
|
|
1100
|
+
## Durable SQL Trade adapter
|
|
1101
|
+
|
|
1102
|
+
`@voltro/plugin-economy/server` exports `dataStoreTrades`, `TradeStoreOptions`,
|
|
1103
|
+
`TradesSqlService` and four `tradeTables` roles: tenant fence/accepted-trade count,
|
|
1104
|
+
subject fence/accepted-operation count, proposal/closing custody and permanent
|
|
1105
|
+
request operations. The adapter implements all eight `TradesService` methods
|
|
1106
|
+
and a separate trusted `expire`. The request port stays eight methods.
|
|
1107
|
+
This is a trusted raw adapter; `economyPlugin` does not install it, its tables,
|
|
1108
|
+
Tag, player RPC or panels yet. Existing `tradingInstalled:false` stays accurate.
|
|
1109
|
+
|
|
1110
|
+
Options require declared game currencies, explicitly versioned Trade rules,
|
|
1111
|
+
actual same-store prepared Wallet/optional Inventory participants and
|
|
1112
|
+
`TradeAuthoritySource`. `authority.read` provides actual tenant/owner-bound
|
|
1113
|
+
permission, trust score and inventory facts. `authority.prepare` returns a
|
|
1114
|
+
required prepared SQL admission that revalidates those facts and owns actual
|
|
1115
|
+
revocation/eligibility/consent/expiry fences. Both callbacks run outside replayable
|
|
1116
|
+
SQL; the returned unit executes inside the Trade group. No default trust score,
|
|
1117
|
+
parental consent, sanctions adapter or permitted actor is fabricated. The named
|
|
1118
|
+
`eligibility` override only narrows the existing default Trade policy. An
|
|
1119
|
+
application source must integrate its real authority; fixture seams are no proof
|
|
1120
|
+
of that integration or of commit-time expiry freshness.
|
|
1121
|
+
|
|
1122
|
+
Proposal inputs are detached intentions; no balance, ownership or accepted-state
|
|
1123
|
+
payload is accepted. `captureTradeProposeIntention` captures pure bounded wire
|
|
1124
|
+
shape. An accepted original is compared before current currencies/rules/source/
|
|
1125
|
+
clock admission. Fresh proposals still validate actual declarations and bounds.
|
|
1126
|
+
Immutable terms include target or public target null, original lifetime,
|
|
1127
|
+
currencies/rules and exact captured tax projection. Offered items and currency
|
|
1128
|
+
are held through the actual existing writers in the same group as proposal
|
|
1129
|
+
custody and request receipt. Target privacy fences apply. Public proposals do not
|
|
1130
|
+
fabricate an unknown accepter; marketplace/listing fee/search remains separate.
|
|
1131
|
+
|
|
1132
|
+
Views disclose rule version and each offered/requested gross/net/tax, with
|
|
1133
|
+
server-derived nullable accepter ID. The digest binds original terms and tax.
|
|
1134
|
+
Accept sends only that trade ID/digest/request ID. Actual participant facts and
|
|
1135
|
+
prepared authority admission must permit the original lifetime/target/items.
|
|
1136
|
+
The same Trade tenant/owner fences admit one permanent closing mode/participant;
|
|
1137
|
+
both actual Escrow legs, taxes, journals/counts/custody and Trade receipt commit
|
|
1138
|
+
together. Cancel releases the proposer holdings without fresh eligibility facts.
|
|
1139
|
+
Trusted `expire` uses one canonical request after the captured deadline. No timer,
|
|
1140
|
+
cron, workflow scheduler or automatic cleanup is installed by this raw adapter.
|
|
1141
|
+
|
|
1142
|
+
Matching accepted retries validate SQL custody and actual original Inventory and
|
|
1143
|
+
Wallet evidence before current callbacks, including when the winner commits
|
|
1144
|
+
during another call's preparation. Changed kind/terms/digest/target or conflicting
|
|
1145
|
+
close refuses. Missing accepted operation/trade tail, original custody or paired
|
|
1146
|
+
asset evidence refuses instead of reseeding or reopening. Permanent accepted
|
|
1147
|
+
markers distinguish erased pending admissions from accepted trade counts;
|
|
1148
|
+
request/owner sequences retain a complete bounded accepted tail. Pending requests
|
|
1149
|
+
are not accepted value. Whole tenant/owner work exceeding the configured budget
|
|
1150
|
+
refuses. Execution-order asset caps/capacity remain conservative.
|
|
1151
|
+
|
|
1152
|
+
`mine` is tenant/participant scoped; it does not reveal all public listings.
|
|
1153
|
+
`receipt` is actor/request scoped and read-only. `rebuild` repairs only mutable
|
|
1154
|
+
Trade state from complete accepted proposal/closing/request custody and validated
|
|
1155
|
+
asset evidence; it never repairs missing history, redebits or re-grants assets.
|
|
1156
|
+
Wallet/Inventory own their materialization rebuilds. No current source callback
|
|
1157
|
+
or hidden cache becomes truth. Complete runtime CDC/query invalidation is still
|
|
1158
|
+
part of the installed binding.
|
|
1159
|
+
|
|
1160
|
+
User/guest export includes owned/targeted/accepted Trade custody and own requests.
|
|
1161
|
+
Erase clears related proposal/closing and all copied counterpart Trade receipt
|
|
1162
|
+
bodies, own identity and pending payloads, preserving permanent opaque fences/
|
|
1163
|
+
counts/markers and financial asset truth in the owning plugins. Repeated erase
|
|
1164
|
+
remains valid. Unsettled erased funds/locks require explicit disposition and
|
|
1165
|
+
coordinated owning-plugin privacy policy; this hook invents no refund or transfer.
|
|
1166
|
+
A partial peer Inventory erase can leave no full peer receipt for Trade proof;
|
|
1167
|
+
that incomplete proof is refused until coordinated privacy completes. Opaque
|
|
1168
|
+
keys are pseudonymous, not legal anonymization or retention decisions.
|
|
1169
|
+
|
|
1170
|
+
Native sources apply all four actual table roles and use independent participant
|
|
1171
|
+
pools. Shared canonical JSON serialization reuses the existing Shop
|
|
1172
|
+
implementation so JSON key order does not change declaration hashes.
|
|
1173
|
+
|
|
1174
|
+
Installed lifecycle/Tag/complete replacement/player query/action/four-layer
|
|
1175
|
+
binding is described below. Events/Mixins, actual sanctions/consent authority,
|
|
1176
|
+
expiry/ recovery workflows, public marketplace/gifting, player components and
|
|
1177
|
+
templates remain open.
|
|
1178
|
+
|
|
1179
|
+
## Installed Trade system
|
|
1180
|
+
|
|
1181
|
+
`economyPlugin({ currencies, trades: { rules, authority, inventory } })` now
|
|
1182
|
+
installs the actual eight-method `TradesPort`, conditional four Trade table roles,
|
|
1183
|
+
normal bind/deactivate/export/erase hooks and five browser-safe player routes.
|
|
1184
|
+
`authority` is required for the default SQL adapter: its real scoped facts and
|
|
1185
|
+
exact-store prepared revalidation unit must own sanctions, trust, consent and
|
|
1186
|
+
stock freshness. No default permission or production sanctions source is supplied.
|
|
1187
|
+
The installed Inventory plugin's `inventory` facade can provide the actual item
|
|
1188
|
+
participant; omit it only for currency-only trades. Default SQL Trades over a
|
|
1189
|
+
fully replaced root wallet refuses before binding. No room/tick or uninstalled
|
|
1190
|
+
boot path is added.
|
|
1191
|
+
|
|
1192
|
+
The optional `trades` configuration inherits declared currencies, root clock and
|
|
1193
|
+
trusted service names. Explicit `rules.version` is required. Named `eligibility`
|
|
1194
|
+
and `taxPolicy` slots reuse actual defaults and may only narrow admission/tax.
|
|
1195
|
+
`maxHistoryRows` bounds the complete tenant Trade history and each owner request
|
|
1196
|
+
history; it is a configured refusal budget, not capacity evidence. The shared
|
|
1197
|
+
`resolveTradeStoreOptions` captures participants/configuration before binding,
|
|
1198
|
+
including private method receivers, and rejects getters without invoking them.
|
|
1199
|
+
|
|
1200
|
+
A complete `trades.service` replacement must implement all eight owning methods,
|
|
1201
|
+
supply versioned `rules` and actual `querySources: { mine, receipt }`. It owns
|
|
1202
|
+
schema, authority, receipts, CDC and privacy. Default authority, item participant,
|
|
1203
|
+
clock, work-bound and policy options are refused with a replacement. No default
|
|
1204
|
+
Trade roles, SQL budgets or fallback are installed. The plugin still applies
|
|
1205
|
+
verified subject, own intention and trusted rebuild guards to captured methods;
|
|
1206
|
+
normal deactivation revokes escaped/lazy facade calls. In-progress operations
|
|
1207
|
+
are not claimed to be cancelled by deactivation.
|
|
1208
|
+
|
|
1209
|
+
Player RPCs are `economy.trades.mine`, `economy.trades.receipt`,
|
|
1210
|
+
`economy.trades.propose`, `economy.trades.accept`, `economy.trades.cancel`.
|
|
1211
|
+
`./web` exports `useTrades`, `useTradeReceipt`, `useProposeTrade`,
|
|
1212
|
+
`useAcceptTrade`, `useCancelTrade`. Proposals choose terms/target/lifetime/request;
|
|
1213
|
+
acceptance chooses the observed original digest/request; cancellation chooses
|
|
1214
|
+
trade/request. No balance, owner, permission, tax or resulting state is accepted.
|
|
1215
|
+
Views disclose original gross/net/tax, rule version and server-derived accepter.
|
|
1216
|
+
Wire schemas validate exact amounts and coherent tax/target/state disclosure.
|
|
1217
|
+
Queries observe original SQL plus actual asset receipt sources; explicit app fact
|
|
1218
|
+
sources may extend default sources. Currency-only installation omits Inventory
|
|
1219
|
+
sources. No client recovery journal or automatic retries are provided by hooks.
|
|
1220
|
+
|
|
1221
|
+
`plugin.economy.expireTrade` is trusted server-only for the default adapter. It
|
|
1222
|
+
uses the same permanent closing identity and actual original release; before the
|
|
1223
|
+
captured expiry it refuses. A full replacement exposes no synthetic expiry
|
|
1224
|
+
method/fallback. No player expiry route or automatic expiry scheduler is installed. The explicit
|
|
1225
|
+
app-owned batch/schedule factory is described below.
|
|
1226
|
+
|
|
1227
|
+
Server `/status`, Shared UI, Cloud decoder/proxy and the Local read provider
|
|
1228
|
+
share strict `tradesInstalled`/nullable `tradesSystem`, actual backend/rule/slot/
|
|
1229
|
+
authority/item participant/budget and exact conditional table roles. The bilingual
|
|
1230
|
+
configuration section shows configured tax, lifetime and minimum trust only;
|
|
1231
|
+
never player facts, subjects, terms, operations, assets or receipts. Replacements
|
|
1232
|
+
report app-owned authority/items, custom slots and null SQL budgets. The existing
|
|
1233
|
+
reload/failure/focus/provider path is reused; no new GM write capability appears.
|
|
1234
|
+
Public marketplace, automatic expiry, GM actions and client recovery remain false.
|
|
1235
|
+
|
|
1236
|
+
`./testing` exports fresh installed `tradesPortFixture`, scoped
|
|
1237
|
+
`tradesFakeLayer` and an actual `tradesPortSystemFixture` probe over the same
|
|
1238
|
+
wallet and Inventory writers. Named permission/trust fixture seams are not
|
|
1239
|
+
production sanctions or consent. Full generic `systemContractConformance`,
|
|
1240
|
+
Events/Mixins, production sanctions/consent/freshness, automatic
|
|
1241
|
+
expiry/recovery, marketplace/ gifting, coordinated erase/disposition, player
|
|
1242
|
+
components and templates remain open.
|
|
1243
|
+
|
|
1244
|
+
## Explicit bounded Trade expiry schedule
|
|
1245
|
+
|
|
1246
|
+
Default `TradesSqlService.expireDue` and installed `plugin.economy.expireDueTrades`
|
|
1247
|
+
accept trusted server subjects plus `{ tenantId, maxTrades? }`. `maxTrades`
|
|
1248
|
+
defaults to 1000 and is a refusal budget within the existing complete-history
|
|
1249
|
+
bound. A user or inactive/replaced/unconfigured default system refuses. Inputs
|
|
1250
|
+
and the runner's private receiver are captured before lazy execution. No player
|
|
1251
|
+
batch/expiry route, tick read or new SQL table is introduced.
|
|
1252
|
+
|
|
1253
|
+
The runner captures its clock outside SQL, reads the complete bounded tenant
|
|
1254
|
+
Trade snapshot, validates permanent accepted counts, proposal/closing/receipt
|
|
1255
|
+
metadata and distinct owner tails, then selects only proposed deadlines at or
|
|
1256
|
+
before that captured instant. The same captured snapshot and distinct-owner
|
|
1257
|
+
validation are reused during selection. Erased custody stays frozen and is not
|
|
1258
|
+
reopened. More due trades than `maxTrades` refuses before the first close;
|
|
1259
|
+
missing history or malformed metadata cannot be treated as an empty backlog.
|
|
1260
|
+
There is no unproved pagination cursor, archive or repaired-history shortcut.
|
|
1261
|
+
|
|
1262
|
+
Each selected trade closes through actual existing `expire` and original asset
|
|
1263
|
+
release in its own complete transaction. A competing accepted/cancelled/expired
|
|
1264
|
+
closing is counted only after re-reading valid durable Trade custody and actual
|
|
1265
|
+
asset evidence; a still-open or erased/corrupt failure stays failed. An
|
|
1266
|
+
interrupted batch may have committed earlier individual closes: the entire
|
|
1267
|
+
firing remains failed, and explicit retry selects remaining original custody.
|
|
1268
|
+
This is not a batch-wide transaction or exactly-once cron delivery claim.
|
|
1269
|
+
|
|
1270
|
+
Successful output contains counts and `capturedAt` only: `scannedRows`,
|
|
1271
|
+
`dueTrades`, `expiredTrades`, `alreadyClosedTrades`. `expiredTrades` counts
|
|
1272
|
+
matching original expiry receipts observed by this invocation; parallel jobs
|
|
1273
|
+
can both observe one receipt. It does not mean a count of fresh financial
|
|
1274
|
+
writes. Repeated invocation after closure sees no due trade. No owner, terms,
|
|
1275
|
+
request identity, asset or receipt body is returned by the batch report.
|
|
1276
|
+
|
|
1277
|
+
`./server` exports ordinary app-owned `tradeExpirySchedule({ tenantId, runner,
|
|
1278
|
+
maxTrades?, name?, cron?, timezone? })`. The `runner` can be the installed
|
|
1279
|
+
`plugin.economy` facade. Default cron is `* * * * *` in UTC; registration is
|
|
1280
|
+
explicit through the existing app schedule mechanism. The handler uses a
|
|
1281
|
+
server system subject, invokes the real runner and fails the firing for errors
|
|
1282
|
+
or inconsistent counts. The plugin itself registers no automatic job, so the
|
|
1283
|
+
configuration panel still reports `automaticExpiryInstalled:false`. Full
|
|
1284
|
+
replacements receive no default batch/expiry fallback.
|
|
1285
|
+
|
|
1286
|
+
Accepted owner-history validation now also checks every operation's tenant,
|
|
1287
|
+
owner key/identity, admission, kind/state, permanent request ID/digest and exact
|
|
1288
|
+
receipt shape even when the requested original belongs to a different trade.
|
|
1289
|
+
Per-trade operation selection reads the complete bounded set and selects
|
|
1290
|
+
accepted sequence markers; pending opposing requests do not hide a later
|
|
1291
|
+
accepted receipt behind a fixed four-row prefix. No missing tail/count is inferred.
|
|
1292
|
+
|
|
1293
|
+
The direct schedule-body fixture is not execution by the scheduler engine or a
|
|
1294
|
+
durable cron/crash proof.
|
|
1295
|
+
|
|
1296
|
+
## Explicit client Trade intention recovery
|
|
1297
|
+
|
|
1298
|
+
`./web` exports `createTradeRecovery`, `browserTradeIntentionJournal`,
|
|
1299
|
+
`captureTradeIntention` and the public journal/slot/reference/transport/recovery
|
|
1300
|
+
contracts. The transport supplies the three installed propose/accept/cancel
|
|
1301
|
+
Actions; propose receives canonical string amounts, suitable for the wire schema.
|
|
1302
|
+
A caller supplies a stable app plus authenticated tenant/account scope without
|
|
1303
|
+
credentials and updates it on every account/session/target change. The server
|
|
1304
|
+
still derives real owner/tenant from its authenticated Subject; a client scope
|
|
1305
|
+
is not proof of authority.
|
|
1306
|
+
|
|
1307
|
+
A saved `TradeIntention` is one of `{ kind:'propose', input }`,
|
|
1308
|
+
`{ kind:'accept', input }`, `{ kind:'cancel', input }`. Only captured intentions
|
|
1309
|
+
are persisted, with version1 and scope. Selected item references, quantities and
|
|
1310
|
+
consideration are intentionally part of proposal recovery, never statements of
|
|
1311
|
+
ownership/balance, player facts, trust, secrets or received asset state. The
|
|
1312
|
+
browser namespace is `voltro:trade-intention:v1:<scope>` with a configured
|
|
1313
|
+
32768-character refusal bound. It stores one unresolved intention per scope;
|
|
1314
|
+
no expiry deletes an uncertain request and no new nonce replaces it.
|
|
1315
|
+
|
|
1316
|
+
`begin` captures and requires a matching storage read acknowledgement before
|
|
1317
|
+
transport is possible. `retry(original)` acquires the actual cross-tab Web Lock,
|
|
1318
|
+
rechecks saved scope/operation/terms/request and holds that lock through response
|
|
1319
|
+
and settlement. It sends only the saved original, never automatically replays
|
|
1320
|
+
or invents a request ID. Failure keeps the acknowledged record. Every
|
|
1321
|
+
`selectScope` invalidates queued/in-flight continuations, including switching
|
|
1322
|
+
away and back; a late accepted response cannot clear the old original under a
|
|
1323
|
+
new session. Explicit observation can later reconcile that original.
|
|
1324
|
+
|
|
1325
|
+
`observe` accepts detached `TradeObservation`; not-observed stays unresolved.
|
|
1326
|
+
Only a matching original receipt may clear. Proposal receipts must match target,
|
|
1327
|
+
complete offered/requested terms and original duration with state proposed;
|
|
1328
|
+
accepted receipts match trade/digest with accepted state; cancellation matches
|
|
1329
|
+
trade with cancelled state. All check matching request/observation identity and
|
|
1330
|
+
complete coherent gross/net/tax, lifetime, target/accepter and rule shape.
|
|
1331
|
+
A proposal's original receipt remains proposed even if its current trade later
|
|
1332
|
+
closes; a current list snapshot is not the proposal operation receipt. Missing,
|
|
1333
|
+
changed, malformed or erased evidence never grants dismissal authority.
|
|
1334
|
+
|
|
1335
|
+
Pure `captureTradeView`, `captureTradeReceipt`, `captureTradeObservation` in
|
|
1336
|
+
`./trades` reuse one shape validator across RPC, owning SQL request history and
|
|
1337
|
+
client recovery. They reject getters/unknown fields and validate disclosure;
|
|
1338
|
+
shape validation is not server asset/permission proof. Missing owner history
|
|
1339
|
+
cannot become an empty accepted tail. Shop and Trade now use one internal
|
|
1340
|
+
acknowledgement/scope/lock/storage/retry algorithm; Shop retains its public
|
|
1341
|
+
contracts, original namespace and domain-specific purchase matching. No parallel
|
|
1342
|
+
Shop implementation or alternative financial writer remains.
|
|
1343
|
+
|
|
1344
|
+
The caller must provide actual browser storage and Web Locks. Missing capabilities
|
|
1345
|
+
refuse; there is no memory fallback. Factory-provided journals must honor the
|
|
1346
|
+
same cross-tab and durable acknowledgement contract. Captured journal/transport/
|
|
1347
|
+
storage/lock methods preserve private receivers and refuse accessors. Hooks do
|
|
1348
|
+
not automatically construct a journal, select identity, retry, or add a player
|
|
1349
|
+
screen. The server configuration observation therefore still reports
|
|
1350
|
+
`clientRecoveryInstalled:false`; explicit app integration is required.
|
|
1351
|
+
|
|
1352
|
+
The deterministic lock/storage seam does not prove actual browser cross-tab,
|
|
1353
|
+
storage-event, engine SDK or crash behavior. Player recovery components are
|
|
1354
|
+
described below; template/consumer integration, production sanctions and
|
|
1355
|
+
consent, gifting/public marketplace and coordinated privacy/disposition remain
|
|
1356
|
+
open; no acceptance, capacity or complete virtual-only stack claim.
|
|
1357
|
+
|
|
1358
|
+
## Player Trade disclosure and recovery components
|
|
1359
|
+
|
|
1360
|
+
`./components` exports `TradeTerms`, `TradeList`, `TradeAcceptDialog`,
|
|
1361
|
+
`TradeIntentionRecovery`, their prop contracts and bilingual `tradeUiCopy`.
|
|
1362
|
+
They extend the existing semantic UI/dialog/button system and accept app-owned
|
|
1363
|
+
source, action, journal and content-label callbacks. No transport, credential,
|
|
1364
|
+
financial writer or new visual system is embedded in the components.
|
|
1365
|
+
|
|
1366
|
+
`TradeTerms` captures a coherent original server view before displaying proposer
|
|
1367
|
+
and counterparty item selections plus gross/net/tax for each game currency,
|
|
1368
|
+
including a zero net payout. Unknown or inconsistent disclosure shows an error.
|
|
1369
|
+
Instance/currency label callbacks resolve actual app content; the component
|
|
1370
|
+
never invents an item identity, price, permission or trust score. Terms are
|
|
1371
|
+
original server disclosure, not a live wallet or ownership report.
|
|
1372
|
+
|
|
1373
|
+
`TradeList` renders own/targeted/accepted views only for its current `actorId`;
|
|
1374
|
+
foreign participant observations and duplicate/invalid views refuse. This is
|
|
1375
|
+
presentation scoping, not server authority. A failed or loading source hides
|
|
1376
|
+
stale actions. Review/Cancel are guarded by original state and actor selection;
|
|
1377
|
+
new review additionally requires an observed lifetime that is still open.
|
|
1378
|
+
Deadline display uses the chosen locale, and time is supplied explicitly; no
|
|
1379
|
+
hidden server clock or per-tick read is introduced. The source list has a
|
|
1380
|
+
configured 1000-view refusal bound, not a support/capacity claim.
|
|
1381
|
+
|
|
1382
|
+
`TradeAcceptDialog` uses the existing native modal with labeled title/help,
|
|
1383
|
+
both sides and explicit original acceptance intention. Confirmation requires
|
|
1384
|
+
matching trade/digest, actor, proposed state, original open lifetime and no
|
|
1385
|
+
busy/error/result state. The callback sends only that captured intention;
|
|
1386
|
+
the parent acknowledges it through recovery before the actual Action.
|
|
1387
|
+
An accepted original is displayed only when request/trade/digest/accepter and
|
|
1388
|
+
all immutable terms/dates/rules/taxes match; a late response after the displayed
|
|
1389
|
+
deadline remains valid while a new acceptance stays disabled. Close/ESC never
|
|
1390
|
+
clears a saved uncertain request. Native stacking/focus behavior needs later
|
|
1391
|
+
browser evidence.
|
|
1392
|
+
|
|
1393
|
+
`TradeIntentionRecovery` belongs above transient lists, errors and dialogs. It
|
|
1394
|
+
checks reference version/expected account scope and captured operation before
|
|
1395
|
+
showing an unresolved request; a foreign or unauthenticated scope hides it
|
|
1396
|
+
without deleting the durable record. It shows selected proposal consideration
|
|
1397
|
+
without inventing tax before a server proposal receipt exists. Recovery reference
|
|
1398
|
+
is secondary in a disclosure. Retry and optional receipt-check controls invoke
|
|
1399
|
+
only saved original callbacks; there is no dismiss, new nonce or silent storage
|
|
1400
|
+
clear. Busy controls remain mounted/focusable with guarded `aria-disabled`.
|
|
1401
|
+
Raw exception/provider bodies are not exposed to players.
|
|
1402
|
+
|
|
1403
|
+
They use actual installed wallet/Inventory Trade writers and the journal;
|
|
1404
|
+
native-dialog methods and storage/locks remain explicit DOM seams, not proof of
|
|
1405
|
+
keyboard stacking, responsive/mobile, actual transport or cross-tab browser
|
|
1406
|
+
behavior. A complete app session/provider/journal parent integration and
|
|
1407
|
+
templates remain open.
|
|
1408
|
+
|
|
1409
|
+
## Inventory-backed Trade authority
|
|
1410
|
+
|
|
1411
|
+
`inventoryTradeAuthoritySource(store, { permissions, inventory, items })` from
|
|
1412
|
+
`@voltro/plugin-economy/server` composes the actual bound Inventory service with
|
|
1413
|
+
an explicit `TradePermissionSource`. The permission source must provide both
|
|
1414
|
+
`read` and `prepare`; there is no default allowance, trust score, adult status or
|
|
1415
|
+
consent. Its read returns tenant/owner-bound `allowed` and `trustScore`. Its
|
|
1416
|
+
prepared SQL unit must fence the owning policy records and revalidate those
|
|
1417
|
+
captured facts. A read-only Sanctions port cannot fulfill this atomic contract.
|
|
1418
|
+
Until that integration exists, leave Trades uninstalled or fail admission.
|
|
1419
|
+
|
|
1420
|
+
The helper reads actual own possessions and original reservation evidence.
|
|
1421
|
+
Tradability comes from explicitly supplied item contracts keyed by item ID and
|
|
1422
|
+
schema version. An unknown contract refuses. Another trade's lock never becomes
|
|
1423
|
+
this trade's reservation. The existing actual Inventory/Wallet writers still
|
|
1424
|
+
revalidate stock, expiry and conservation inside the same commit; a shaped
|
|
1425
|
+
client object or the helper's read alone never establishes admission.
|
|
1426
|
+
|
|
1427
|
+
`captureTradeAuthorityAdmission` captures one proposer or two distinct accepting
|
|
1428
|
+
participants, the original trade/tenant, phase and open lifetime. The policy
|
|
1429
|
+
receives detached permission facts rather than item attributes or author-owned
|
|
1430
|
+
arrays. The helper admits its prepared unit against the exact supplied SQL
|
|
1431
|
+
store before any writer executes. A foreign-store unit refuses. Policies must
|
|
1432
|
+
implement their actual sanctions, consent and privacy fences; the helper creates
|
|
1433
|
+
no policy table, automatic sanctions installation or synthetic permission.
|
|
1434
|
+
|
|
1435
|
+
The source fixtures contain a clearly named test-only app-policy table and use
|
|
1436
|
+
actual borrowed SQL plus Inventory/Wallet writers. Five source scenarios and one
|
|
1437
|
+
scenario per six native variants cover custody, revocation between read and
|
|
1438
|
+
commit, full rollback, receiver/getter admission, removed contracts, unavailable
|
|
1439
|
+
facts and original receipt replay after revocation.
|
|
1440
|
+
|
|
1441
|
+
## Actual SQL sanctions in Trade admission
|
|
1442
|
+
|
|
1443
|
+
`sanctionsTradePermissionSource(store, sanctions.sanctions.admission())` from
|
|
1444
|
+
`@voltro/plugin-economy/server` supplies the required policy for
|
|
1445
|
+
`inventoryTradeAuthoritySource`. It calls the actual server-only prepared
|
|
1446
|
+
Sanctions capability at enforcement point `trade`, with the Trade writer's
|
|
1447
|
+
captured time. Each participant receives real active-measure and conservative
|
|
1448
|
+
strike-derived trust checks; no `allowed:true`, trust bonus or policy table is
|
|
1449
|
+
invented by the adapter. Receiver-safe methods and detached input admission
|
|
1450
|
+
preserve tenant/owner binding. Permission units must belong to the same SQL store
|
|
1451
|
+
as Inventory, Wallets and Trades. The returned guarded group revalidates actual
|
|
1452
|
+
sanction records atomically with the assets.
|
|
1453
|
+
|
|
1454
|
+
Actual `tradeBan`, general bans, insufficient trust, unavailable service, erased
|
|
1455
|
+
identity and changed prepared records refuse new acceptance. A participant may
|
|
1456
|
+
still cancel an existing hold through the ordinary Trade release path. Accepted
|
|
1457
|
+
original proposal/accept receipts remain recoverable before current policies,
|
|
1458
|
+
never grant fresh authority. Policy errors retain redacted forbidden/conflict
|
|
1459
|
+
semantics across the SQL boundary. Proposal and acceptance acquire policy before
|
|
1460
|
+
Trade/asset admission. Additional consent or legal gates require their own real
|
|
1461
|
+
prepared policy; this adapter establishes sanctions only.
|
|
1462
|
+
|
|
1463
|
+
The `api-economy` source factory now installs actual Sanctions, versioned
|
|
1464
|
+
Trades, Inventory and Wallets in one application. Binding supplies the exact
|
|
1465
|
+
borrowed store to these helpers; there is no substitute wallet, fake sanction
|
|
1466
|
+
service, player balance write or provider credential. An explicit bounded app
|
|
1467
|
+
schedule expires held proposals.
|
|
1468
|
+
|
|
1469
|
+
|
|
1470
|
+
## Explicit IAP verification foundation
|
|
1471
|
+
|
|
1472
|
+
`./iap` is browser-safe and exports
|
|
1473
|
+
`defineIapCatalog`, `iapReceiptIntent` and `iapProductsCsv`. A product explicitly
|
|
1474
|
+
maps an existing captured Shop offer, channel and consumable/non-consumable/
|
|
1475
|
+
subscription kind to the offer's real or mixed store SKU. Duplicate SKU or offer
|
|
1476
|
+
mappings per channel refuse. Non-consumables and subscriptions require one unit; non-consumables also require a per-player cap of one.
|
|
1477
|
+
The catalog preserves the actual offer grants; it never provisions store products.
|
|
1478
|
+
CSV exports identifiers and kinds for console preparation, without receipts,
|
|
1479
|
+
reference prices or credentials. A sync-products CLI is not installed yet.
|
|
1480
|
+
|
|
1481
|
+
An empty catalog works with `realMoney:false` without provider configuration.
|
|
1482
|
+
Nonempty real catalogs and all verification adapters refuse when money is disabled.
|
|
1483
|
+
The receipt intent contains only channel, offer ID/version, request ID and bounded
|
|
1484
|
+
opaque receipt. Tenant, owner, amount, transaction identity, quantity and payment
|
|
1485
|
+
state are not player inputs. Do not log raw receipt evidence.
|
|
1486
|
+
|
|
1487
|
+
The server-only `IapVerificationPort`, `iapVerificationPort`,
|
|
1488
|
+
`appleIapVerificationPort` and `googleIapVerificationPort` require explicit trusted
|
|
1489
|
+
account authority. `IapAccountSource.read` returns the server-owned stable account
|
|
1490
|
+
binding for the authenticated tenant/owner and provider environment. It must be
|
|
1491
|
+
unique across all tenants sharing an application. Never accept that binding from
|
|
1492
|
+
an RPC input. The generic replacement is deliberate server authority responsible
|
|
1493
|
+
for authenticity, fresh provider status and account binding; its output shape
|
|
1494
|
+
alone proves none of those properties.
|
|
1495
|
+
|
|
1496
|
+
Apple uses the official server library with supplied DER trust roots and online
|
|
1497
|
+
certificate checks. It verifies the submitted StoreKit 2 JWS, requests its current
|
|
1498
|
+
transaction from the App Store Server API and verifies that JWS too. The two
|
|
1499
|
+
transaction projections must agree. Bundle, environment, SKU, type and
|
|
1500
|
+
`appAccountToken` must match; family sharing, revocation, upgraded subscriptions,
|
|
1501
|
+
advanced commerce and malformed data refuse. Production requires `appAppleId`.
|
|
1502
|
+
Only Sandbox and Production are admitted; unsigned Xcode/LocalTesting modes are
|
|
1503
|
+
excluded. `AppleIapCredentialSource.read` supplies rotated App Store Connect
|
|
1504
|
+
signing key/key ID/issuer ID. Missing credentials or verification failures never
|
|
1505
|
+
switch provider or fulfill a payment. The upstream contracts are in the
|
|
1506
|
+
[Apple library](https://github.com/apple/app-store-server-library-node).
|
|
1507
|
+
|
|
1508
|
+
Google reads the fixed authenticated [Products v2 endpoint](https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.productsv2/getproductpurchasev2)
|
|
1509
|
+
using an explicitly supplied server-side service-account OAuth source with
|
|
1510
|
+
androidpublisher authority. HTTP has abort/time/byte budgets and refuses redirects.
|
|
1511
|
+
A successful purchase must match the server account and exactly one configured
|
|
1512
|
+
product. Pending/cancelled, partially refunded, rental/preorder and ambiguous
|
|
1513
|
+
multi-line purchases refuse. Sandbox requires an explicit test-card purchase;
|
|
1514
|
+
production rejects test purchases. Consumption and acknowledgement observations
|
|
1515
|
+
are retained. They do not authorize regranting an already processed purchase.
|
|
1516
|
+
Google subscriptions remain separate unfinished work.
|
|
1517
|
+
|
|
1518
|
+
`VerifiedIapPurchase` is opaque process-local evidence; copying or serializing it
|
|
1519
|
+
loses its verification capability. Transaction custody identity includes channel,
|
|
1520
|
+
environment, application and provider transaction, independent of request, tenant
|
|
1521
|
+
or owner. Google tokens are hashed, never exposed as transaction IDs. Apple paid
|
|
1522
|
+
amounts retain their provider milliunit scale. Google Products v2 supplies no paid
|
|
1523
|
+
amount here, so `paid` remains null. Reference prices never fill that gap or produce
|
|
1524
|
+
Loyalty spend credit. Evidence alone does not grant assets, record an order,
|
|
1525
|
+
establish consent or implement privacy/retention.
|
|
1526
|
+
|
|
1527
|
+
The explicit SQL adapter described below now owns durable receipt custody and
|
|
1528
|
+
atomic fulfillment. Optional lifecycle/RPC/four-layer configuration binding is
|
|
1529
|
+
described below; `paymentsInstalled` reports configuration only. Provider roots,
|
|
1530
|
+
credentials, products and sandbox accounts are operator decisions. No capacity
|
|
1531
|
+
claim follows from the configured work budgets.
|
|
1532
|
+
|
|
1533
|
+
|
|
1534
|
+
## Explicit SQL IAP fulfillment
|
|
1535
|
+
|
|
1536
|
+
`dataStoreIap(store, options)` borrows the app store and supplies the four-method
|
|
1537
|
+
`IapService`/`IapPort`: redeem, receipt, exportSubject and eraseSubject. It requires
|
|
1538
|
+
`realMoney:true`, an immutable operator-owned namespace, matching currencies,
|
|
1539
|
+
verification service, mandatory prepared SQL purchase admission, actual prepared
|
|
1540
|
+
Wallet/Inventory participants and an explicit source account. It opens no store,
|
|
1541
|
+
registers no timer and changes no boot path. `captureIapService` binds all four
|
|
1542
|
+
replacement methods, including private class receivers, without executing getters.
|
|
1543
|
+
The optional Economy plugin IAP binding described below installs this adapter.
|
|
1544
|
+
|
|
1545
|
+
Four server tables own namespace work counts, subjects, orders and global receipt
|
|
1546
|
+
custody. The receipt primary key derives from provider application/environment/
|
|
1547
|
+
channel/transaction independently of tenant, owner, request and work namespace.
|
|
1548
|
+
Pending verified custody binds the original order permanently; aborted groups may
|
|
1549
|
+
retain pending admissions, never accepted value. A fresh alias or owner/tenant/
|
|
1550
|
+
namespace rebind returns `already_redeemed`. The exact accepted original request
|
|
1551
|
+
returns its stored receipt before current provider, clock, policy or participant
|
|
1552
|
+
callbacks. Changed intentions conflict. Raw receipt evidence is hashed and never
|
|
1553
|
+
stored in order snapshots or exposed in player receipts.
|
|
1554
|
+
|
|
1555
|
+
Fresh fulfillment requires opaque verification capability, exact matching currency
|
|
1556
|
+
configuration and a prepared `IapPurchaseAdmissionSource`. The application must
|
|
1557
|
+
provide actual age/consent/spend authority under its own SQL fences; no permissive
|
|
1558
|
+
facts or consent default exists. Namespace, subject and receipt fences, admission,
|
|
1559
|
+
actual wallet transfers, catalog inventory grants and accepted order/custody commit
|
|
1560
|
+
as one exact-store group. A foreign-store participant, revoked policy, capacity
|
|
1561
|
+
failure or malformed returned receipt rolls back accepted state. Fresh evidence
|
|
1562
|
+
already consumed or acknowledged refuses; historic originals still recover first.
|
|
1563
|
+
Subscriptions refuse here until their entitlement implementation exists.
|
|
1564
|
+
|
|
1565
|
+
One-time/per-player, UTC-day and global quantity limits use whole bounded accepted
|
|
1566
|
+
SQL history. Non-consumable declarations require a single unit and per-player cap
|
|
1567
|
+
of one. Accepted offer/channel/version configuration digests remain immutable even
|
|
1568
|
+
after body erase. Accepted offer quantities/days retain global consumption after
|
|
1569
|
+
erasure. Namespace and subject row counts detect missing history; exceeding the
|
|
1570
|
+
complete work budget refuses instead of scanning a prefix or repairing from a
|
|
1571
|
+
partial selection. No process lock or second monetary writer is introduced.
|
|
1572
|
+
|
|
1573
|
+
Export includes own pending/fulfilled order observations without raw receipts.
|
|
1574
|
+
Erase removes owner links and full order/provider bodies, fences recreation and
|
|
1575
|
+
retains permanent receipt custody, counters and minimal quantity metadata. These
|
|
1576
|
+
keys are pseudonymous, not an anonymization claim. Wallet and Inventory own
|
|
1577
|
+
their separate export/erase hooks; this adapter neither erases those systems nor
|
|
1578
|
+
invents refunds, retention periods or disposition of paid assets.
|
|
1579
|
+
|
|
1580
|
+
They exercise real Wallet/Inventory writes, original recovery, custody,
|
|
1581
|
+
rollback, SQL-policy revocation, erasure and missing-history refusal.
|
|
1582
|
+
|
|
1583
|
+
|
|
1584
|
+
## SQL-owned IAP account bindings
|
|
1585
|
+
|
|
1586
|
+
`dataStoreIapAccounts(store, options)` implements the server-only five-method
|
|
1587
|
+
`IapAccountsPort`/`IapAccountBindingService`: read, observe, prepareBinding, exportSubject and
|
|
1588
|
+
eraseSubject. Its real-money/application/channel/environment configuration is
|
|
1589
|
+
captured once. `read` captures the server-selected tenant/owner, mints a public
|
|
1590
|
+
pseudonymous UUID at runtime and persists one stable binding under the actual SQL
|
|
1591
|
+
owner fence. A second process/pool reads that same token. Different owners,
|
|
1592
|
+
tenants, applications, environments and channels have distinct records; requesting
|
|
1593
|
+
a channel/environment outside the configured service refuses. Tokens are account
|
|
1594
|
+
identifiers for StoreKit/Play, not secrets or authentication credentials.
|
|
1595
|
+
|
|
1596
|
+
Two SQL roles own account bodies and global token-uniqueness custody. Account keys
|
|
1597
|
+
bind actual application/channel/environment/tenant/owner; token custody is global
|
|
1598
|
+
within application/channel/environment, independent of tenant. Entropy is captured
|
|
1599
|
+
before replayable SQL, never supplied by the player. Erase removes owner/token
|
|
1600
|
+
bodies, marks custody erased and permanently refuses recreation. Export returns
|
|
1601
|
+
only the selected binding. Retained keys are pseudonymous, not anonymous. Account
|
|
1602
|
+
merge, migration, external-token adoption and retention are unfinished.
|
|
1603
|
+
|
|
1604
|
+
`IapAccountSource` now requires both read and prepareBinding. Verification evidence
|
|
1605
|
+
privately retains its captured account source/token; no account token enters
|
|
1606
|
+
player evidence, order snapshots or financial receipts. The default IAP SQL
|
|
1607
|
+
writer prepares that binding and puts its exact-store guard before purchase
|
|
1608
|
+
admission and asset writes. Account erasure after provider verification therefore
|
|
1609
|
+
refuses the fresh commit. Accepted originals are read from their financial SQL
|
|
1610
|
+
receipt first and can recover after separate account erasure until order privacy
|
|
1611
|
+
removes their body. Do not treat a read-only account lookup as atomic admission.
|
|
1612
|
+
Complete account replacements own their real prepared SQL authority and privacy.
|
|
1613
|
+
|
|
1614
|
+
Standalone SQL adapters require their own account and financial privacy wiring.
|
|
1615
|
+
Storage/account guards use actual SQL; their provider verifier remains a
|
|
1616
|
+
deliberate synthetic seam.
|
|
1617
|
+
|
|
1618
|
+
|
|
1619
|
+
## Installed IAP readiness, privacy and configuration
|
|
1620
|
+
|
|
1621
|
+
`economyPlugin({ currencies, realMoney: true, iap: { version, products, providers,
|
|
1622
|
+
namespace, admission, inventory, systemAccount } })` now installs optional IAP.
|
|
1623
|
+
The default SQL binding requires explicit immutable version/namespace/source
|
|
1624
|
+
account, actual prepared age/consent/spend admission and actual Inventory
|
|
1625
|
+
preparation when products grant items. It reuses the installed default Wallet;
|
|
1626
|
+
default SQL fulfillment over a replaced root Wallet refuses. Subscription
|
|
1627
|
+
products refuse this default installation until entitlement fulfillment exists.
|
|
1628
|
+
|
|
1629
|
+
Each provider declares channel, sandbox/production and actual application ID,
|
|
1630
|
+
plus a synchronous server `create(context)` factory. Channels are unique; all
|
|
1631
|
+
products need a provider and each provider needs products. The captured context
|
|
1632
|
+
contains only that channel's actual catalog, currencies, app/environment, clock
|
|
1633
|
+
and borrowed-store account registry. Factories return complete initialize/verify
|
|
1634
|
+
services. Verified app/environment/catalog/currency evidence must match the
|
|
1635
|
+
installed declarations. Provider shape is not authentication authority; use the
|
|
1636
|
+
actual Apple/Google adapters or an explicitly trusted server replacement.
|
|
1637
|
+
|
|
1638
|
+
```ts
|
|
1639
|
+
import {
|
|
1640
|
+
googleIapVerificationPort,
|
|
1641
|
+
type GoogleIapAccessTokenSource,
|
|
1642
|
+
type IapProviderDefinition,
|
|
1643
|
+
} from '@voltro/plugin-economy/server'
|
|
1644
|
+
|
|
1645
|
+
// Example application identity only; provision the actual app and SKU in the store console.
|
|
1646
|
+
export const playProvider = (
|
|
1647
|
+
accessTokens: GoogleIapAccessTokenSource,
|
|
1648
|
+
): IapProviderDefinition => ({
|
|
1649
|
+
channel: 'android',
|
|
1650
|
+
environment: 'production',
|
|
1651
|
+
applicationId: 'studio.example.game',
|
|
1652
|
+
create: ({ realMoney, environment, applicationId, products, currencies, accounts, now }) =>
|
|
1653
|
+
googleIapVerificationPort({
|
|
1654
|
+
realMoney, environment, applicationId, products, currencies, accounts, now,
|
|
1655
|
+
accessTokens,
|
|
1656
|
+
}),
|
|
1657
|
+
})
|
|
1658
|
+
```
|
|
1659
|
+
|
|
1660
|
+
Binding constructs adapters without reading credentials or importing the Apple
|
|
1661
|
+
SDK. Only configured IAP contributes `onActivate`; it awaits every captured
|
|
1662
|
+
provider's required `initialize` before exposing IAP operations. Apple reads and
|
|
1663
|
+
validates supplied App Store Connect credentials and constructs its real SDK
|
|
1664
|
+
trust verifier. Google reads and admits the supplied server token. These checks
|
|
1665
|
+
are setup admission, not proof of remote authorization, token expiry or a valid
|
|
1666
|
+
purchase. Every verification reads the credential source again for rotation.
|
|
1667
|
+
Missing required credentials or defective roots fail initialization; no unsigned,
|
|
1668
|
+
fake credential, automatic provider switch or fallback is supplied. Deactivation
|
|
1669
|
+
revokes the facade; a late initializer cannot reopen a deactivated generation.
|
|
1670
|
+
|
|
1671
|
+
`economy.economy.iap()` returns initialize/account plus the four-method IapPort
|
|
1672
|
+
facade. `economy.iap.account` is an Action: verified own user/guest selects channel
|
|
1673
|
+
only, the server persists/returns its public account UUID. `economy.iap.redeem`
|
|
1674
|
+
is an Action accepting only `IapReceiptIntent`. Preserve the original request on
|
|
1675
|
+
uncertain responses. Do not log raw receipts; retain the Audit plugin's input
|
|
1676
|
+
redaction. Neither Action wraps the owning adapters in another transaction.
|
|
1677
|
+
`useIapAccount` and `useRedeemIap` are in `./web`.
|
|
1678
|
+
|
|
1679
|
+
`receipt` now takes `IapReceiptReference`: channel/offer/version/request and a
|
|
1680
|
+
SHA-256 receipt digest, never the raw receipt. `createIapReceiptReference(intent)`
|
|
1681
|
+
in `./iap` captures the input before Web Crypto hashing the canonical JSON string
|
|
1682
|
+
representation used by SQL. `iapReceiptReference` admits stored references.
|
|
1683
|
+
`economy.iap.receipt` and `useIapReceipt` use this read-only hash reference; they
|
|
1684
|
+
never verify, seed or grant. Missing/pending observations remain nonterminal;
|
|
1685
|
+
a changed reference conflicts. This is not a durable recovery journal.
|
|
1686
|
+
|
|
1687
|
+
Installed export composes own account and financial observations. Erase first
|
|
1688
|
+
revokes account bindings, then removes financial bodies, before the root Wallet
|
|
1689
|
+
hook erases its owner. Destination Inventory keeps its own installed privacy
|
|
1690
|
+
hook. The sequence is retryable, not an atomic cross-plugin erase or paid-asset
|
|
1691
|
+
refund/disposition policy. Removed account/receipt identities remain fenced.
|
|
1692
|
+
|
|
1693
|
+
A complete `IapSystemService` replacement implements initialize/account/redeem/
|
|
1694
|
+
receipt/complete/exportSubject/eraseSubject, supplies captured products/version and actual
|
|
1695
|
+
receipt query sources, and forbids all default SQL options. It owns provider,
|
|
1696
|
+
account, schema, authority, CDC, grants, conservation, activation and privacy;
|
|
1697
|
+
there are no default IAP roles, borrowed default Wallet, SQL budgets or fallback.
|
|
1698
|
+
Private class receivers work; getters and partial services refuse.
|
|
1699
|
+
|
|
1700
|
+
The four configuration layers show exact optional six SQL roles, provider
|
|
1701
|
+
applications, product mappings, binding/source/version and work budgets. Full
|
|
1702
|
+
replacement has no provider declaration list or default work bounds. The
|
|
1703
|
+
`paymentsInstalled`/`iapInstalled` flags report configured receipt fulfillment,
|
|
1704
|
+
not provider health or successful purchases. Explicit completion authority is
|
|
1705
|
+
reported per channel below. Notifications and refund debt are conditional on the
|
|
1706
|
+
explicit optional binding below; subscription entitlements, GM actions and
|
|
1707
|
+
installed client recovery remain false.
|
|
1708
|
+
|
|
1709
|
+
|
|
1710
|
+
## Explicit provider completion after accepted SQL
|
|
1711
|
+
|
|
1712
|
+
`dataStoreIap` now additionally exposes server-only
|
|
1713
|
+
`IapSqlService.prepareCompletion(subject, originalIntent)`. This is a read-only
|
|
1714
|
+
accepted-original path: own verified subject, exact request/evidence digest,
|
|
1715
|
+
fulfilled order, complete accepted body and matching global SQL custody. Missing,
|
|
1716
|
+
pending, changed, erased or corrupt evidence refuses. No order is seeded, grant
|
|
1717
|
+
replayed, clock consulted or provider called. The result is an opaque
|
|
1718
|
+
`AcceptedIapCompletion` capability with order ID only. Raw receipt remains in its
|
|
1719
|
+
private captured resolver; JSON/shape copying cannot restore authority. Exercising
|
|
1720
|
+
the capability re-reads the owning SQL original, so later erasure/removal refuses
|
|
1721
|
+
before provider I/O. This is not an atomic lock spanning SQL and a remote store.
|
|
1722
|
+
|
|
1723
|
+
`IapCompletionPort`/`IapCompletionService` has required initialize/complete methods.
|
|
1724
|
+
`iapCompletionPort` captures a deliberate trusted server completion authority,
|
|
1725
|
+
actual catalog/app/environment/currencies and account source. It admits the real
|
|
1726
|
+
accepted SQL capability and current account before provider completion. It does
|
|
1727
|
+
not verify a new purchase, fulfill, reverse money or accept player state. Complete
|
|
1728
|
+
replacements own actual provider observations; output shape alone proves nothing.
|
|
1729
|
+
Private receivers are captured, required getters refused, defects redacted.
|
|
1730
|
+
|
|
1731
|
+
```ts
|
|
1732
|
+
import { Effect } from 'effect'
|
|
1733
|
+
import type {
|
|
1734
|
+
IapSqlService,
|
|
1735
|
+
IapCompletionService,
|
|
1736
|
+
} from '@voltro/plugin-economy/server'
|
|
1737
|
+
import type { IapReceiptIntent } from '@voltro/plugin-economy/iap'
|
|
1738
|
+
import type { Subject } from '@voltro/protocol'
|
|
1739
|
+
|
|
1740
|
+
export const completeAccepted = (
|
|
1741
|
+
orders: IapSqlService,
|
|
1742
|
+
provider: IapCompletionService,
|
|
1743
|
+
subject: Subject,
|
|
1744
|
+
originalIntent: IapReceiptIntent,
|
|
1745
|
+
) => Effect.gen(function* () {
|
|
1746
|
+
const evidence = yield* orders.prepareCompletion(subject, originalIntent)
|
|
1747
|
+
return yield* provider.complete(evidence)
|
|
1748
|
+
})
|
|
1749
|
+
```
|
|
1750
|
+
|
|
1751
|
+
`googleIapCompletionPort` takes the same operator-owned options as the Google
|
|
1752
|
+
verifier. Its credential source is read on every invocation; explicit initialize
|
|
1753
|
+
checks setup admission only. Completion reuses the verifier's bounded fixed
|
|
1754
|
+
Products-v2 GET. Current owner, application, environment, SKU, quantity, purchase
|
|
1755
|
+
time/value and token identity must match the accepted original. Cancellation,
|
|
1756
|
+
partial refunds or changed purchase identity refuse before a provider POST.
|
|
1757
|
+
Consumables use the official [consume endpoint](https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.products/consume);
|
|
1758
|
+
non-consumables use [acknowledge](https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.products/acknowledge),
|
|
1759
|
+
with an empty body or empty JSON respectively. No developer payload, nonce or
|
|
1760
|
+
new transaction is minted. Existing terminal provider state needs no POST.
|
|
1761
|
+
|
|
1762
|
+
After a successful or uncertain POST, one fresh authenticated GET must observe
|
|
1763
|
+
matching consumed/acknowledged state. POST response alone is not completion;
|
|
1764
|
+
nonterminal state returns unavailable while accepted SQL assets/custody remain.
|
|
1765
|
+
An explicit same-capability/original-intention retry observes current state and
|
|
1766
|
+
never grants again or automatically sends a second POST after one uncertain call.
|
|
1767
|
+
HTTP uses redirect refusal, abort/time/byte budgets and redacted failures. No
|
|
1768
|
+
reference price is converted into paid amount or Loyalty credit.
|
|
1769
|
+
|
|
1770
|
+
This standalone port is implemented. Installed completion/RPC and encrypted
|
|
1771
|
+
notification/refund binding are described below. StoreKit client finish,
|
|
1772
|
+
subscriptions, Steam/Web and full workflow integration remain separate open
|
|
1773
|
+
work. Configuration panels report the actual configured completion factory.
|
|
1774
|
+
Synthetic HTTP/OAuth fixtures use only newly generated transient test material;
|
|
1775
|
+
they are not real Google authorization, sandbox purchase or production evidence.
|
|
1776
|
+
|
|
1777
|
+
|
|
1778
|
+
## Installed explicit provider completion
|
|
1779
|
+
|
|
1780
|
+
A provider may additionally declare `createCompletion(context)`. Its captured
|
|
1781
|
+
initialize/complete service is constructed during binding without I/O, and its
|
|
1782
|
+
initializer is awaited during IAP activation. Missing/failed readiness keeps the
|
|
1783
|
+
IAP facade unavailable. A channel without completion authority has no implied
|
|
1784
|
+
completion fallback. The complete system replacement now requires seven methods,
|
|
1785
|
+
including `complete(subject, originalIntent)`, and owns its completion lifecycle.
|
|
1786
|
+
|
|
1787
|
+
The installed facade prepares a read-only opaque capability from the exact own
|
|
1788
|
+
accepted SQL order, then delegates to the configured channel authority. A
|
|
1789
|
+
changed receipt/owner/request cannot finish another purchase. It never redeems
|
|
1790
|
+
again, invokes the purchase verifier, grants items or posts Wallet value.
|
|
1791
|
+
Deactivation before handoff to the authority refuses. No-IAP applications
|
|
1792
|
+
receive no additional activation hook or completion route.
|
|
1793
|
+
|
|
1794
|
+
```ts
|
|
1795
|
+
import { economyPlugin } from '@voltro/plugin-economy'
|
|
1796
|
+
import {
|
|
1797
|
+
googleIapProvider,
|
|
1798
|
+
type IapPluginOptions, type GoogleIapAccessTokenSource,
|
|
1799
|
+
} from '@voltro/plugin-economy/server'
|
|
1800
|
+
import { Effect } from 'effect'
|
|
1801
|
+
|
|
1802
|
+
const installPlayEconomy = (
|
|
1803
|
+
currencies: Parameters<typeof economyPlugin>[0]['currencies'],
|
|
1804
|
+
iap: Omit<IapPluginOptions, 'providers' | 'service' | 'querySources'>,
|
|
1805
|
+
applicationId: string,
|
|
1806
|
+
accessTokens: GoogleIapAccessTokenSource,
|
|
1807
|
+
) => economyPlugin({
|
|
1808
|
+
currencies, realMoney: true, iap: {
|
|
1809
|
+
...iap, providers: [googleIapProvider({
|
|
1810
|
+
environment: 'sandbox', applicationId, accessTokens,
|
|
1811
|
+
})],
|
|
1812
|
+
},
|
|
1813
|
+
})
|
|
1814
|
+
|
|
1815
|
+
const completeOriginal = (
|
|
1816
|
+
installed: ReturnType<typeof installPlayEconomy>,
|
|
1817
|
+
subject: Parameters<ReturnType<typeof installed.economy.iap>['complete']>[0],
|
|
1818
|
+
originalIntent: Parameters<ReturnType<typeof installed.economy.iap>['complete']>[1],
|
|
1819
|
+
) => Effect.gen(function* () {
|
|
1820
|
+
const service = installed.economy.iap()
|
|
1821
|
+
const accepted = yield* service.redeem(subject, originalIntent)
|
|
1822
|
+
const observedCompletion = yield* service.complete(subject, originalIntent)
|
|
1823
|
+
return { accepted, observedCompletion }
|
|
1824
|
+
})
|
|
1825
|
+
```
|
|
1826
|
+
|
|
1827
|
+
The account, prepared purchase-admission source, item participant, catalog and OAuth
|
|
1828
|
+
source are actual application inputs, not supplied credentials or permissive policy
|
|
1829
|
+
defaults. The example targets sandbox declarations; a real test purchase/credential
|
|
1830
|
+
is still required for validation. Provider completion is a remote operation after
|
|
1831
|
+
SQL acceptance, not one distributed transaction. If it fails or its reply is lost,
|
|
1832
|
+
retain the original accepted financial receipt and retry **only complete** with the
|
|
1833
|
+
same intention. Never mint another request or redeem another grant to finish it.
|
|
1834
|
+
`economy.iap.complete` is an explicit Action mounted only for configured completion
|
|
1835
|
+
or a complete replacement. `useCompleteIap` exposes it without automatic retries or
|
|
1836
|
+
a journal. It takes the original opaque mutation receipt, no player completion
|
|
1837
|
+
state; keep audit input redacted. A returned observation is not durable SQL proof
|
|
1838
|
+
of provider state and contains no raw receipt, account token or credential.
|
|
1839
|
+
|
|
1840
|
+
All four configuration layers now report `providerCompletionInstalled` and each
|
|
1841
|
+
configured provider's `completionInstalled`. These report declaration bindings,
|
|
1842
|
+
not readiness, healthy credentials or successful purchases. A complete
|
|
1843
|
+
replacement owns its implementation and has no default provider list. Google
|
|
1844
|
+
completes by a fresh matching authenticated terminal read, including after
|
|
1845
|
+
uncertain POST. StoreKit transaction finishing remains an explicit native-client
|
|
1846
|
+
responsibility; no server-only `finish` callback substitutes for it.
|
|
1847
|
+
|
|
1848
|
+
Synthetic finish authority proves no StoreKit purchase.
|
|
1849
|
+
|
|
1850
|
+
`googleIapProvider({ applicationId, environment, accessTokens, timeoutMs?,
|
|
1851
|
+
maxResponseBytes? })` captures one installed Play declaration and creates both
|
|
1852
|
+
actual verification and explicit completion adapters. It supplies no OAuth
|
|
1853
|
+
token, price or permissive purchase policy. Provider construction does not call
|
|
1854
|
+
the credential source. The installed context carries captured product
|
|
1855
|
+
definitions; the helper removes its channel before strict Google options
|
|
1856
|
+
admission.
|
|
1857
|
+
|
|
1858
|
+
|
|
1859
|
+
## IAP hash-reference recovery
|
|
1860
|
+
|
|
1861
|
+
`./iap-recovery` now supplies `createIapPurchaseRecovery`, the journal/source/
|
|
1862
|
+
transport interfaces and `browserIapPurchaseJournal`. One journal entry stores only
|
|
1863
|
+
version, stable authenticated app/tenant/account scope and the original
|
|
1864
|
+
channel/offer/version/request/receipt SHA256 reference. No raw receipt, OAuth token,
|
|
1865
|
+
provider account UUID, amount, grant or completion state is saved. Scope keys must
|
|
1866
|
+
contain no credentials. An actual store/secure receipt source must implement
|
|
1867
|
+
`restore(reference)`; a reference cannot reconstruct or authorize a purchase.
|
|
1868
|
+
|
|
1869
|
+
```ts
|
|
1870
|
+
import {
|
|
1871
|
+
browserIapPurchaseJournal, createIapPurchaseRecovery,
|
|
1872
|
+
type IapReceiptRecoverySource, type IapPurchaseRecoveryTransport,
|
|
1873
|
+
} from '@voltro/plugin-economy/iap-recovery'
|
|
1874
|
+
import type { IapReceiptIntent } from '@voltro/plugin-economy/iap'
|
|
1875
|
+
|
|
1876
|
+
const connectRecovery = (
|
|
1877
|
+
accountScope: string,
|
|
1878
|
+
actualStoreReceipts: IapReceiptRecoverySource,
|
|
1879
|
+
transport: IapPurchaseRecoveryTransport,
|
|
1880
|
+
) => {
|
|
1881
|
+
const recovery = createIapPurchaseRecovery(
|
|
1882
|
+
browserIapPurchaseJournal(localStorage, navigator.locks),
|
|
1883
|
+
actualStoreReceipts,
|
|
1884
|
+
transport,
|
|
1885
|
+
)
|
|
1886
|
+
recovery.selectScope(accountScope)
|
|
1887
|
+
return recovery
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
const submitSelectedPurchase = async (
|
|
1891
|
+
recovery: ReturnType<typeof connectRecovery>,
|
|
1892
|
+
originalIntent: IapReceiptIntent,
|
|
1893
|
+
) => {
|
|
1894
|
+
const saved = await recovery.begin(originalIntent)
|
|
1895
|
+
return recovery.retry(saved.request)
|
|
1896
|
+
}
|
|
1897
|
+
```
|
|
1898
|
+
|
|
1899
|
+
`begin` captures the original, computes its hash and requires matching durable
|
|
1900
|
+
storage acknowledgement before any server mutation. `pending` only reads; it does
|
|
1901
|
+
not restore, purchase or finish. User-triggered `retry(saved.request)` first reads
|
|
1902
|
+
the original hash-only SQL observation. Accepted SQL evidence skips current
|
|
1903
|
+
verification and another redemption. Missing/pending reuses the exact original
|
|
1904
|
+
intention after the actual source restores evidence matching **every** saved field
|
|
1905
|
+
and hash. Never generate a new request to recover a purchase.
|
|
1906
|
+
|
|
1907
|
+
The explicit completion callback receives the original and its actual accepted
|
|
1908
|
+
financial receipt. For configured Play it calls the installed complete Action; for
|
|
1909
|
+
StoreKit an actual native adapter must call and observe transaction.finish after SQL
|
|
1910
|
+
acceptance. Successful callback output must match receipt order, channel and time.
|
|
1911
|
+
Neither callback shape nor a synthetic finish is a provider-authenticity proof.
|
|
1912
|
+
Financial `fulfilled` alone, missing/pending and uncertain redemption/completion all
|
|
1913
|
+
keep the journal entry. Only matching completed observations clear it. No automatic
|
|
1914
|
+
retry, purchase, acknowledgement or finish is scheduled.
|
|
1915
|
+
|
|
1916
|
+
The coordinator reuses the existing intention acknowledgement/lock/settlement core.
|
|
1917
|
+
It holds the actual supplied lock through read, restore, hash, mutation, completion
|
|
1918
|
+
and clearing, rechecking scope generation between asynchronous boundaries. Every
|
|
1919
|
+
scope selection invalidates queued work, including away and back. A scope change
|
|
1920
|
+
cannot cancel an already dispatched remote operation; its original journal remains
|
|
1921
|
+
for explicit recovery. Another reference conflicts until the original settles.
|
|
1922
|
+
Missing Web Locks/storage and failed acknowledgement refuse, with no memory fallback.
|
|
1923
|
+
|
|
1924
|
+
Native implementations provide actual durable acknowledgement and matching
|
|
1925
|
+
process/ client locking through IapPurchaseJournal. The configuration flag
|
|
1926
|
+
clientRecoveryInstalled remains false because these app/engine bindings are not
|
|
1927
|
+
installed by the server plugin. This exported source coordinator does not
|
|
1928
|
+
establish any of those implementations.
|
|
1929
|
+
|
|
1930
|
+
Browser-safe receipt admission now shares exact canonical paid units, asset IDs,
|
|
1931
|
+
positive transfer amounts, unique child receipts/instances and valid timestamps
|
|
1932
|
+
with the default SQL receipt decoder and server completion result admission. It
|
|
1933
|
+
decodes observations only; actual own SQL/provider authority remains required.
|
|
1934
|
+
No decoded object alone grants assets or completes providers.
|
|
1935
|
+
|
|
1936
|
+
|
|
1937
|
+
## Full installed IAP port and scoped fixtures
|
|
1938
|
+
|
|
1939
|
+
The installed server layer now exposes `IapSystemPort` with all seven methods:
|
|
1940
|
+
initialize, account, redeem, receipt, complete, exportSubject and eraseSubject.
|
|
1941
|
+
`captureIapSystemService` captures real private class receivers and refuses partial
|
|
1942
|
+
methods/accessors. The ordinary browser-safe `IapPort` keeps its four financial/
|
|
1943
|
+
privacy methods; both Tags resolve the same installed owning facade. A full system
|
|
1944
|
+
replacement omits default accounts/SQL layers, while no configured IAP contributes
|
|
1945
|
+
neither IAP Tag. Readiness/privacy/lifecycle admission still belongs to the owning
|
|
1946
|
+
system; resolving a Tag does not independently authorize a purchase.
|
|
1947
|
+
|
|
1948
|
+
```ts
|
|
1949
|
+
import { Effect } from 'effect'
|
|
1950
|
+
import { IapSystemPort } from '@voltro/plugin-economy/server'
|
|
1951
|
+
import { iapFakeLayer } from '@voltro/plugin-economy/testing'
|
|
1952
|
+
|
|
1953
|
+
const observeFixtureAccount = Effect.scoped(Effect.gen(function* () {
|
|
1954
|
+
const iap = yield* IapSystemPort
|
|
1955
|
+
const subject = {
|
|
1956
|
+
type: 'user' as const, kind: 'guest' as const,
|
|
1957
|
+
id: 'fixture.account', tenantId: 'fixture',
|
|
1958
|
+
}
|
|
1959
|
+
return yield* iap.account(subject, 'ios')
|
|
1960
|
+
}).pipe(Effect.provide(iapFakeLayer)))
|
|
1961
|
+
```
|
|
1962
|
+
|
|
1963
|
+
`./testing` exports `iapPortFixture`, fresh scoped `iapFakeLayer`,
|
|
1964
|
+
`iapPortSystemFixture` and `iapSystemPortSystemFixture`. These use actual
|
|
1965
|
+
installed SQL account/order/custody/Wallet/Inventory services with explicit
|
|
1966
|
+
synthetic verifier/ finish authorities and real lifecycle release. Their probes
|
|
1967
|
+
resolve actual Tags, replay original SQL receipts without another grant,
|
|
1968
|
+
complete accepted orders and observe permanent account erase fences. Scoped
|
|
1969
|
+
release revokes escaped facades. Fixtures supply no real provider credential,
|
|
1970
|
+
crypto purchase or permissive production policy.
|
|
1971
|
+
|
|
1972
|
+
These are concrete port fixtures for Plan21, not a complete generic
|
|
1973
|
+
`systemContractConformance` registration. Full strategy
|
|
1974
|
+
defaults/events/mixins/all- table privacy/inspect/stages registration, app-owned
|
|
1975
|
+
replacement integration, compiled ordinary boot and every other remaining
|
|
1976
|
+
acceptance requirement stay open.
|
|
1977
|
+
|
|
1978
|
+
|
|
1979
|
+
## Installed Apple declaration
|
|
1980
|
+
|
|
1981
|
+
`appleIapProvider({ applicationId, environment, appleRootCertificates, appAppleId?,
|
|
1982
|
+
credentials })` captures the existing actual StoreKit verifier as an installed IAP
|
|
1983
|
+
declaration. It uses the same application/environment/root/credential admission as
|
|
1984
|
+
`appleIapVerificationPort`, copies the deployment's public root bytes before lazy
|
|
1985
|
+
binding, and captures the actual credential-source receiver. Construction does not
|
|
1986
|
+
read credentials, import the SDK or contact Apple. Production requires the numeric
|
|
1987
|
+
Apple app ID as well as the application bundle ID. Missing real credentials fail
|
|
1988
|
+
activation; no generated third-party credential or unsigned purchase mode exists.
|
|
1989
|
+
|
|
1990
|
+
```ts
|
|
1991
|
+
import { economyPlugin } from '@voltro/plugin-economy'
|
|
1992
|
+
import {
|
|
1993
|
+
appleIapProvider,
|
|
1994
|
+
type IapPluginOptions, type AppleIapCredentialSource,
|
|
1995
|
+
} from '@voltro/plugin-economy/server'
|
|
1996
|
+
|
|
1997
|
+
const installAppleEconomy = (
|
|
1998
|
+
currencies: Parameters<typeof economyPlugin>[0]['currencies'],
|
|
1999
|
+
iap: Omit<IapPluginOptions, 'providers' | 'service' | 'querySources'>,
|
|
2000
|
+
applicationId: string,
|
|
2001
|
+
trustedDeploymentRoots: readonly Uint8Array[],
|
|
2002
|
+
credentials: AppleIapCredentialSource,
|
|
2003
|
+
) => economyPlugin({
|
|
2004
|
+
currencies, realMoney: true, iap: {
|
|
2005
|
+
...iap, providers: [appleIapProvider({
|
|
2006
|
+
environment: 'sandbox', applicationId,
|
|
2007
|
+
appleRootCertificates: trustedDeploymentRoots, credentials,
|
|
2008
|
+
})],
|
|
2009
|
+
},
|
|
2010
|
+
})
|
|
2011
|
+
```
|
|
2012
|
+
|
|
2013
|
+
The factory removes the installed channel before the strict Apple options decoder
|
|
2014
|
+
and supplies actual captured product definitions. It does not add a server completion
|
|
2015
|
+
factory. StoreKit transaction.finish belongs to an actual native client **after** the
|
|
2016
|
+
matching SQL grant has been accepted; its recovery source/finisher can use the
|
|
2017
|
+
explicit hash-reference coordinator above. The inspect channel correctly reports
|
|
2018
|
+
completionInstalled:false for this declaration; this is not an unfinished HTTP
|
|
2019
|
+
fallback to a server-only finish callback.
|
|
2020
|
+
|
|
2021
|
+
Their public root construction seam is not an Apple trust chain; missing
|
|
2022
|
+
credentials refuse before SDK construction. No new table, event, credential,
|
|
2023
|
+
support or capacity claim is introduced.
|
|
2024
|
+
|
|
2025
|
+
|
|
2026
|
+
## Authenticated notification ports
|
|
2027
|
+
|
|
2028
|
+
`appleIapNotificationPort` uses the existing SDK to verify Notifications-v2 and nested
|
|
2029
|
+
signed transactions/renewals against captured deployment roots and app/environment.
|
|
2030
|
+
`googleIapNotificationPort` uses the shared RS256/JWKS verifier with actual configured
|
|
2031
|
+
HTTPS audience, service-account email, verified-email claim and exact subscription.
|
|
2032
|
+
RTDN supports one-time, subscription, voided, pending-review and test hints; it does
|
|
2033
|
+
not declare sandbox/production. These follow the primary [Apple SDK](https://apple.github.io/app-store-server-library-node/classes/SignedDataVerifier.html),
|
|
2034
|
+
[push authentication](https://docs.cloud.google.com/pubsub/docs/authenticate-push-subscriptions)
|
|
2035
|
+
and [RTDN reference](https://developer.android.com/google/play/billing/rtdn-reference).
|
|
2036
|
+
|
|
2037
|
+
Both server-only `IapNotificationPort` services require initialize/verify.
|
|
2038
|
+
Verification returns opaque process-local evidence; `iapNotificationNotice`
|
|
2039
|
+
reads its redacted metadata. Copies/serialized references refuse. Raw purchase
|
|
2040
|
+
tokens and signed data remain private. Captured byte/time budgets are not
|
|
2041
|
+
delivery-capacity claims. No shared webhook secret, token query-string, player
|
|
2042
|
+
grant/state, SQL write or automatic financial adjustment is added.
|
|
2043
|
+
|
|
2044
|
+
Notification authentication **does not establish current financial state**.
|
|
2045
|
+
Fresh provider lookup, accepted SQL owner/custody, deduplication, ordering,
|
|
2046
|
+
durable receipts, renewals/refunds/debt and paid-asset disposition remain
|
|
2047
|
+
required construction work. The verifier alone installs no route; the explicit
|
|
2048
|
+
optional installed notification/refund binding is described below. Do not
|
|
2049
|
+
acknowledge provider delivery as durably processed using this verifier alone.
|
|
2050
|
+
|
|
2051
|
+
`./testing` exports fresh scoped `iapNotificationPortFixture`,
|
|
2052
|
+
`iapNotificationFakeLayer` and `iapNotificationPortSystemFixture`. Their
|
|
2053
|
+
explicit synthetic TEST authority has no purchase identity or grant/refund
|
|
2054
|
+
authority; normal scope release permanently revokes verify/initialize. Shared
|
|
2055
|
+
verifier and SDK mocks only measure seam configuration and refusal, not real
|
|
2056
|
+
cryptographic or delivery behavior.
|
|
2057
|
+
|
|
2058
|
+
## Accepted SQL notification targets
|
|
2059
|
+
|
|
2060
|
+
`IapSqlService.prepareNotification(deployment, authenticatedNotice)` is a trusted
|
|
2061
|
+
server-only read helper. Capture the realMoney/channel/app/environment declaration
|
|
2062
|
+
from server configuration; never take it from the player or the push body. The
|
|
2063
|
+
notice must be the original process-local evidence returned by an authenticated
|
|
2064
|
+
notification adapter. The helper looks up the globally claimed transaction identity,
|
|
2065
|
+
checks its actual namespace, owner privacy and accepted SQL custody, and decodes the
|
|
2066
|
+
original immutable order snapshot and financial receipt. It performs no seeding,
|
|
2067
|
+
clock read, current verifier/admission callback, asset write or provider call.
|
|
2068
|
+
|
|
2069
|
+
```ts
|
|
2070
|
+
import { observeAcceptedIapNotificationTarget } from '@voltro/plugin-economy/server'
|
|
2071
|
+
|
|
2072
|
+
const target = yield* orders.prepareNotification(deployment, authenticatedNotice)
|
|
2073
|
+
const original = yield* observeAcceptedIapNotificationTarget(target)
|
|
2074
|
+
```
|
|
2075
|
+
|
|
2076
|
+
The opaque target exposes only orderId/noticeId. `observeAcceptedIapNotificationTarget`
|
|
2077
|
+
repeats the actual SQL/privacy reads before returning the original receipt and the
|
|
2078
|
+
redacted authenticated notice. Spread/JSON copies have no authority. Erasure,
|
|
2079
|
+
missing/corrupt custody, foreign namespace/app/SKU/original transaction and changed
|
|
2080
|
+
order ownership refuse; a saved target cannot bind another order. A nullable RTDN
|
|
2081
|
+
environment is resolved only through the captured declaration and a matching actual
|
|
2082
|
+
accepted SQL identity. That establishes SQL scope, never current Google state.
|
|
2083
|
+
TEST notices and review hints without a transaction identity cannot select a purchase.
|
|
2084
|
+
|
|
2085
|
+
This read-only capability authorizes neither a grant nor a refund. Current
|
|
2086
|
+
provider status must be looked up separately before a financial adjustment;
|
|
2087
|
+
provider I/O and SQL erasure are not one atomic transaction. No raw push
|
|
2088
|
+
material or owner identity is projected through this observation helper. No new
|
|
2089
|
+
tables or installed webhook route are added by this read helper alone; optional
|
|
2090
|
+
installed binding is described below.
|
|
2091
|
+
|
|
2092
|
+
## Fresh IAP provider status
|
|
2093
|
+
|
|
2094
|
+
`IapCurrentStatePort`/`iapCurrentStatePort` is a server-only initialize/read seam.
|
|
2095
|
+
It requires the original opaque accepted SQL notification target and captured
|
|
2096
|
+
realMoney/app/channel/environment/account configuration. `IapAccountsPort` now
|
|
2097
|
+
also supplies `observe`: it reads and validates an existing SQL binding without
|
|
2098
|
+
minting, seeding or fence updates. Missing, corrupt or erased bindings refuse.
|
|
2099
|
+
This observation path, rather than the allocating `read`, gates the new status
|
|
2100
|
+
service. Original SQL and current account are rechecked after external I/O and on
|
|
2101
|
+
every subsequent use of the opaque status evidence.
|
|
2102
|
+
|
|
2103
|
+
```ts
|
|
2104
|
+
import { iapCurrentPurchaseState } from '@voltro/plugin-economy/server'
|
|
2105
|
+
|
|
2106
|
+
const accepted = yield* orders.prepareNotification(deployment, authenticatedNotice)
|
|
2107
|
+
const verified = yield* currentProvider.read(accepted)
|
|
2108
|
+
const status = yield* iapCurrentPurchaseState(verified)
|
|
2109
|
+
```
|
|
2110
|
+
|
|
2111
|
+
`googleIapCurrentStatePort` uses the same bounded, authenticated, fixed Products v2
|
|
2112
|
+
GET as the verifier/completion adapters. It reads the purchase token only from the
|
|
2113
|
+
private authenticated notice, checks its hash against accepted SQL, and compares
|
|
2114
|
+
actual account, app/environment, SKU, purchase time and quantity. Current
|
|
2115
|
+
refundableQuantity gives an exact cumulative share `(quantity - refundableQuantity)
|
|
2116
|
+
/ quantity`; provider consumption/acknowledgement can advance. A missing/error,
|
|
2117
|
+
pending or inconsistent cancelled response never implies a full refund.
|
|
2118
|
+
|
|
2119
|
+
`appleIapCurrentStatePort` reads current operator credentials, calls the real SDK
|
|
2120
|
+
getTransactionInfo and verifies that response with captured public Apple roots,
|
|
2121
|
+
online checks and app/environment scope. Purchase identity/value/account admission
|
|
2122
|
+
is shared with the original verifier. Legacy date-only/full refund gives 1/1;
|
|
2123
|
+
REFUND_PRORATED uses exact integer milli-percentage / 100000. Contradictory fields,
|
|
2124
|
+
unsupported Family Sharing revoke and foreign/changed purchase facts refuse.
|
|
2125
|
+
|
|
2126
|
+
Opaque evidence exposes only orderId. `iapCurrentPurchaseState` returns redacted
|
|
2127
|
+
state, exact refundNumerator/refundDenominator, observedAt and optional revokedAt.
|
|
2128
|
+
Copying this projection does not restore evidence. maxAgeMs (default 30000) is an
|
|
2129
|
+
evidence lifetime budget; expiry or clock rewind refuses. It is not a provider
|
|
2130
|
+
consistency or latency guarantee. SQL/account erasure and provider I/O remain
|
|
2131
|
+
separate transactions; a future financial writer still needs its own SQL fence.
|
|
2132
|
+
No refund, debt, entitlement, provider mutation, new table or installed webhook
|
|
2133
|
+
route comes from this read service. notificationInstalled/refundDebtInstalled stay
|
|
2134
|
+
false; no raw token, signing key or owner identity reaches its public projection.
|
|
2135
|
+
|
|
2136
|
+
Twelve source scenarios cover existing-account observation, actual SQL/account
|
|
2137
|
+
revalidation, provider callsites/projection/transport refusal and scoped Tag
|
|
2138
|
+
release; one native independent-pool scenario per six variants is authored.
|
|
2139
|
+
Provider HTTP/SDK/authenticated-notice seams are explicit; no real purchase,
|
|
2140
|
+
push, signature/OCSP or sandbox delivery proof. `./testing` exports fresh
|
|
2141
|
+
iapCurrentStatePortFixture/iapCurrentStateFakeLayer/iapCurrentStatePortSystemFixture
|
|
2142
|
+
with actual accepted SQL and an explicit active-provider seam.
|
|
2143
|
+
|
|
2144
|
+
Provider field contracts: [Google Products v2](https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.productsv2),
|
|
2145
|
+
[Apple transaction payload](https://apple.github.io/app-store-server-library-node/interfaces/JWSTransactionDecodedPayload.html)
|
|
2146
|
+
and [Apple revocation types](https://apple.github.io/app-store-server-library-node/enums/RevocationType.html).
|
|
2147
|
+
|
|
2148
|
+
## Durable encrypted IAP notice custody
|
|
2149
|
+
|
|
2150
|
+
The explicit server-only `./iap-notifications` entry adds
|
|
2151
|
+
`IapNotificationCustodyPort`/`dataStoreIapNotificationCustody`. Its three table roles
|
|
2152
|
+
are notice namespace/count admission, subject privacy/count admission and immutable
|
|
2153
|
+
notice identities with pending/expired/erased custody. Importing the ordinary root
|
|
2154
|
+
or `./server` does not import these new table declarations. This is trusted worker
|
|
2155
|
+
custody, not a player route or a completed refund processor.
|
|
2156
|
+
|
|
2157
|
+
```ts
|
|
2158
|
+
import { dataStoreIapNotificationCustody } from '@voltro/plugin-economy/iap-notifications'
|
|
2159
|
+
|
|
2160
|
+
const custody = dataStoreIapNotificationCustody(store, {
|
|
2161
|
+
namespace,
|
|
2162
|
+
deployment,
|
|
2163
|
+
orders,
|
|
2164
|
+
bodyRetentionMs,
|
|
2165
|
+
})
|
|
2166
|
+
yield* custody.initialize()
|
|
2167
|
+
const saved = yield* custody.retain(authenticatedNotice)
|
|
2168
|
+
const resumedNotice = yield* custody.restore(saved.id)
|
|
2169
|
+
const original = yield* orders.prepareNotification(deployment, resumedNotice)
|
|
2170
|
+
const current = yield* currentProvider.read(original)
|
|
2171
|
+
```
|
|
2172
|
+
|
|
2173
|
+
Declare actual deployment app/channel/environment, a server-owned namespace and
|
|
2174
|
+
explicit bodyRetentionMs. Construction performs no SQL/crypto/provider I/O.
|
|
2175
|
+
initialize requires the registered framework FieldCipher; retain refuses missing
|
|
2176
|
+
cipher before notice admission/seeding and never stores cleartext. Explicit cipher
|
|
2177
|
+
sources may provide the same FieldCipher codec/row-binding contract; the default
|
|
2178
|
+
reads the registered cipher per operation so configured key rotation is observed.
|
|
2179
|
+
Keys and provider credentials come from the real deployment, never examples.
|
|
2180
|
+
|
|
2181
|
+
retain first resolves the authenticated notice to actual accepted IAP SQL/privacy.
|
|
2182
|
+
Its immutable identity is channel/environment/app/noticeId; changed content, order
|
|
2183
|
+
or owner conflicts. Namespace/subject fences serialize bounded whole-namespace
|
|
2184
|
+
counts and privacy. The body authenticates namespace/deployment/order/owner/times/
|
|
2185
|
+
notice/private material/fingerprint and is sealed to its exact table/column/row
|
|
2186
|
+
address using the existing framework codec, as in the connection vault. sealedBody
|
|
2187
|
+
is deliberately plain server-only text holding ciphertext; automatic field
|
|
2188
|
+
middleware must not encrypt/decrypt it a second time. Unbounded SQL text follows
|
|
2189
|
+
the existing LONGTEXT/NVARCHAR(MAX) mapping; plaintext and sealed-body work are
|
|
2190
|
+
separately bounded. There is no plaintext or unbound-envelope fallback.
|
|
2191
|
+
|
|
2192
|
+
restore requires actual retained SQL custody, row-bound authenticated decryption,
|
|
2193
|
+
matching metadata/body/fingerprint, current custody privacy and the original IAP
|
|
2194
|
+
SQL order again. It can issue a new process-local authenticated notice from that
|
|
2195
|
+
trusted encrypted SQL provenance after a process/pool restart. A public notice,
|
|
2196
|
+
serialized opaque object, copied ciphertext or changed metadata cannot do that.
|
|
2197
|
+
The restored notice still needs a fresh provider read before any financial action.
|
|
2198
|
+
Expiry is checked when the Effect runs and after original SQL revalidation.
|
|
2199
|
+
Restored evidence also carries an internal async SQL-custody validity check: every
|
|
2200
|
+
accepted-target use rechecks this exact encrypted row, privacy and expiry before
|
|
2201
|
+
entering the IAP transaction. Holding a restored notice does not bypass pruning
|
|
2202
|
+
or erasure. The two transactions remain separate, without a nested SQL lease.
|
|
2203
|
+
|
|
2204
|
+
pending returns bounded redacted receipt identities; it is not a processed-status
|
|
2205
|
+
or refund guarantee. Explicit prune removes expired private bodies, keeps identity
|
|
2206
|
+
and refuses payload recreation on redelivery. No automatic retention schedule is
|
|
2207
|
+
installed yet. Export returns owned metadata without raw tokens/ciphertext; erase
|
|
2208
|
+
removes owner/body and permanently fences recreation. Erase also works without a
|
|
2209
|
+
cipher or with corrupt ciphertext. Detected original IAP erasure during retain/
|
|
2210
|
+
restore triggers custody cleanup. These are separate SQL transactions; a future
|
|
2211
|
+
installed privacy/financial workflow must still coordinate its own fences. No
|
|
2212
|
+
cross-store/distributed atomicity is claimed.
|
|
2213
|
+
|
|
2214
|
+
One native scenario per six variants covers real cipher, independent-pool
|
|
2215
|
+
restore, a payload beyond 65535 bytes, expiry and cipher-free erase. This is not
|
|
2216
|
+
executed dialect evidence. ./testing exports
|
|
2217
|
+
iapNotificationCustodyPortFixture/iapNotificationCustodyFakeLayer/
|
|
2218
|
+
iapNotificationCustodyPortSystemFixture with fresh SQL and a privately owned
|
|
2219
|
+
real framework cipher; it does not replace the process-global key. The
|
|
2220
|
+
authenticated notice is an explicit test seam. The standalone custody port
|
|
2221
|
+
installs no default tables or route; the optional installed notification/refund
|
|
2222
|
+
binding below contributes those declarations. No provider delivery, load or
|
|
2223
|
+
support claim follows from this source.
|
|
2224
|
+
|
|
2225
|
+
### Current IAP state in a SQL mutation group
|
|
2226
|
+
|
|
2227
|
+
`IapSqlService.prepareCurrentState(proof)` returns an actual-store-bound
|
|
2228
|
+
`PreparedSqlMutation<IapCurrentPurchaseState>` for trusted financial consumers.
|
|
2229
|
+
It repeats original accepted IAP custody, account binding and, for restored
|
|
2230
|
+
notices, encrypted notification custody/expiry inside the same SQL transaction.
|
|
2231
|
+
The account source must supply both `observe` and `prepareBinding`. A foreign
|
|
2232
|
+
store participant or serialized evidence refuses; there is no fallback.
|
|
2233
|
+
|
|
2234
|
+
Preparation reads the opaque provider observation once. Commit uses SQL and the
|
|
2235
|
+
captured server clock, without a provider request or nested SQL transaction.
|
|
2236
|
+
The original IAP fence precedes account and notification fences. Evidence
|
|
2237
|
+
lifetime is checked again when the group runs. Consumers must preserve this
|
|
2238
|
+
lock order and finish their financial group with their own freshness check.
|
|
2239
|
+
The participant itself grants or reverses no assets and cannot guarantee that
|
|
2240
|
+
an external provider has remained unchanged since its response.
|
|
2241
|
+
|
|
2242
|
+
Five authored memory-store scenarios cover same-store composition, post-prepare
|
|
2243
|
+
erasure, wallet rollback, time expiry, mixed-store refusal and encrypted-notice
|
|
2244
|
+
revocation/tampering. The native six-variant scenario also exercises account
|
|
2245
|
+
erasure from a second pool after preparation.
|
|
2246
|
+
|
|
2247
|
+
### SQL IAP refunds, reversals and hard-currency debt
|
|
2248
|
+
|
|
2249
|
+
The server-only `@voltro/plugin-economy/iap-refunds` entry declares four optional
|
|
2250
|
+
refund roles and exposes `IapRefundPort` plus `dataStoreIapRefunds`. Its five
|
|
2251
|
+
methods are `reconcile`, `rebuild`, `observe`, `exportSubject` and `eraseSubject`.
|
|
2252
|
+
It is an explicit trusted server participant; the ordinary IAP installation
|
|
2253
|
+
does not yet install notification routes or refund workers. Do not expose
|
|
2254
|
+
`reconcile` as a player-authored status/amount mutation. It accepts only the
|
|
2255
|
+
original opaque `VerifiedIapCurrentState` produced from accepted SQL and fresh
|
|
2256
|
+
provider lookup. `realMoney:false` refuses this real-money component; virtual
|
|
2257
|
+
wallets and virtual shop capabilities remain separate.
|
|
2258
|
+
|
|
2259
|
+
Supply actual same-store `IapSqlService` and `EconomySqlService`, an explicit
|
|
2260
|
+
`debtPolicy: 'allowNegative' | 'blockSpend'`, a namespace and, for item orders,
|
|
2261
|
+
a same-store asset participant with a nondefault `assetsVersion`. There is one
|
|
2262
|
+
immutable refund cursor per accepted original across namespaces. Changing the
|
|
2263
|
+
namespace cannot authorize another refund of the same purchase. SQL adjustment
|
|
2264
|
+
history, owner fences, original IAP/account/retained-notice fences and balanced
|
|
2265
|
+
wallet/item movement receipts commit together. No provider I/O runs in the
|
|
2266
|
+
financial transaction. Whole-history/SQL-unit bounds refuse excess work.
|
|
2267
|
+
|
|
2268
|
+
Currency reversal uses exact cumulative `floor(original * numerator /
|
|
2269
|
+
denominator)`. Each adjustment books only the difference from the preceding
|
|
2270
|
+
accepted cumulative amount. Older observed states return the newer SQL receipt;
|
|
2271
|
+
contradictory states at the same observation time refuse. A later fresh active
|
|
2272
|
+
observation compensates an earlier refund through the original source account.
|
|
2273
|
+
Repeated accepted observations cannot book the same adjustment again. `rebuild`
|
|
2274
|
+
repairs only the refund projection from complete accepted adjustment history and
|
|
2275
|
+
actual balanced wallet/item evidence, without replaying grants.
|
|
2276
|
+
|
|
2277
|
+
`EconomySqlService.prepareIapRefund` uses the existing wallet writer and requires
|
|
2278
|
+
the opaque current-state proof and original accepted wallet receipt. Only
|
|
2279
|
+
hard-currency refund debits can create debt. Ordinary credits repay signed debt;
|
|
2280
|
+
ordinary debits cannot create or deepen it. `allowNegative` permits otherwise
|
|
2281
|
+
funded spending in other currencies; `blockSpend` refuses all wallet debits for
|
|
2282
|
+
that owner while a wallet has blocked debt. Existing blocked debt cannot be
|
|
2283
|
+
weakened by a later refund policy. Nullable wallet policy and owner sequence/policy
|
|
2284
|
+
on SQL ledger movements preserve the policy for wallet rebuild. The signed
|
|
2285
|
+
financial ledger remains the source of the balance, with exact BIGINT overflow
|
|
2286
|
+
and positive cap checks. Soft-currency debits remain nonnegative.
|
|
2287
|
+
|
|
2288
|
+
`inventoryIapRefundAssets` reuses actual `InventorySqlService.prepareConsume` and
|
|
2289
|
+
`prepareGrant`. The studio must explicitly choose `disposition: 'requireAll' |
|
|
2290
|
+
'recoverRemaining'`. `requireAll` refuses the entire financial group when the
|
|
2291
|
+
requested items cannot be recovered. `recoverRemaining` removes only remaining
|
|
2292
|
+
owned eligible quantities and durably reports unrecoverable consumed, moved or
|
|
2293
|
+
locked quantities. It never pursues another owner or counts missing items as
|
|
2294
|
+
removed. A refund reversal restores only quantities actually recovered, with
|
|
2295
|
+
original attributes through current inventory admission, and tracks actual
|
|
2296
|
+
restored instance receipts for a later refund. The adapter does not invent an
|
|
2297
|
+
expired-item restitution policy or bypass changed catalog rules. A custom asset
|
|
2298
|
+
participant supplies prepared same-store mutations, pure receipt admission and
|
|
2299
|
+
SQL-only audit of its accepted movement history.
|
|
2300
|
+
|
|
2301
|
+
Export returns owned reconciliation metadata. Erasure removes receipt bodies and
|
|
2302
|
+
ownership, preserves immutable identities and fences recreation; it does not
|
|
2303
|
+
silently cancel financial postings. The separate original IAP, account, wallet,
|
|
2304
|
+
inventory and notification privacy hooks must also be wired into the application
|
|
2305
|
+
cascade. Notification processing, installed refund/debt inspect wiring, refund
|
|
2306
|
+
misuse events/sanctions and store subscriptions/entitlements remain unfinished.
|
|
2307
|
+
|
|
2308
|
+
Seven actual memory-SQL scenarios, two scoped Tag/lifetime sources and a case
|
|
2309
|
+
for each of six native variants have been authored, including fractional
|
|
2310
|
+
rounding, replay, reversal, restored-instance recovery, cross-namespace refusal,
|
|
2311
|
+
debt repayment/rebuild, explicit item disposition, late privacy revocation,
|
|
2312
|
+
expiry, bounds and cross-pool reads. They have not run; provider/notification
|
|
2313
|
+
projections in these fixtures are explicit seams.
|
|
2314
|
+
|
|
2315
|
+
### Notification processor, SQL completion and explicit ingress
|
|
2316
|
+
|
|
2317
|
+
The `./iap-notifications` server entry also exposes the seven-method
|
|
2318
|
+
`IapNotificationProcessorPort`: `initialize`, `ingest`, `process`, `pending`,
|
|
2319
|
+
`prune`, `exportSubject` and `eraseSubject`. `iapNotificationProcessor` captures
|
|
2320
|
+
actual notification, current-state, custody, original-order and refund ports.
|
|
2321
|
+
It authenticates delivery, retains the private body under the framework cipher,
|
|
2322
|
+
restores SQL provenance, reads fresh provider state, executes the financial
|
|
2323
|
+
reconciliation group and marks SQL custody `processed`. Failed provider or
|
|
2324
|
+
financial work leaves pending SQL for explicit retry.
|
|
2325
|
+
|
|
2326
|
+
Custody now has eight methods, adding `markProcessed(id, receipt)`. It accepts
|
|
2327
|
+
only the process-local capability attached to a successful actual SQL refund
|
|
2328
|
+
result. A spread copy or JSON projection cannot authorize the marker. The marker
|
|
2329
|
+
joins fresh original/account/retained-notice and actual accepted refund-journal
|
|
2330
|
+
fences on the same store before changing custody. It is a second SQL commit
|
|
2331
|
+
after the financial group; a lost marker reply retries accepted history, without
|
|
2332
|
+
repeating financial value. Duplicate authenticated processed delivery avoids
|
|
2333
|
+
another provider call. Retention removes the processed private body while
|
|
2334
|
+
preserving the immutable completion marker. Owned erasure removes bodies and
|
|
2335
|
+
refund ownership through both ports. This is not an atomic privacy cascade over
|
|
2336
|
+
all application systems.
|
|
2337
|
+
|
|
2338
|
+
`iapNotificationHttpRoute({ path, maxBodyBytes, processor })` returns a normal
|
|
2339
|
+
exact POST plugin HTTP declaration. Register it in `httpRoutes`; the helper does
|
|
2340
|
+
not start a listener. Provider signatures or push bearer authentication supply
|
|
2341
|
+
authority; browser cookies and query parameters do not. It applies the explicit
|
|
2342
|
+
byte budget, fatal UTF-8 decoding, precise method refusal, no-store responses
|
|
2343
|
+
and redacted authentication/retry errors. A success returns 204 only after the
|
|
2344
|
+
processor completes. This declared route is origin-exempt because its concrete
|
|
2345
|
+
notification verifier requires provider authority independent of cookies. A full
|
|
2346
|
+
processor replacement must preserve that authentication contract.
|
|
2347
|
+
|
|
2348
|
+
`runIapNotificationWork({ processor, maxNotices })` prunes retained bodies and
|
|
2349
|
+
processes the whole bounded pending set. Excess refuses without accepting a
|
|
2350
|
+
prefix. Earlier successful notices survive a later firing failure and a retry
|
|
2351
|
+
reads SQL again. `iapNotificationSchedule({ name, cron, timezone, processor,
|
|
2352
|
+
maxNotices })` returns an ordinary discovered schedule descriptor. Cadence,
|
|
2353
|
+
registration and body retention are operator-owned; no timer or worker is
|
|
2354
|
+
started by construction.
|
|
2355
|
+
|
|
2356
|
+
They include pending retry, copied-financial-capability refusal, late account
|
|
2357
|
+
erasure, processed-body expiry, SQL marker replay and malformed HTTP admission.
|
|
2358
|
+
Handler invocation is not normal gateway discovery/startup; synthetic provider
|
|
2359
|
+
and authentication fixtures are not genuine delivery/signature evidence.
|
|
2360
|
+
Optional IAP plugin binding and four-layer configuration panels are described
|
|
2361
|
+
below.
|
|
2362
|
+
|
|
2363
|
+
### Optional installed notification and refund system
|
|
2364
|
+
|
|
2365
|
+
`IapPluginOptions.notifications` now installs the actual notification/refund
|
|
2366
|
+
chain when explicitly configured. Its `version` must be nondefault. The SQL
|
|
2367
|
+
default requires operator-selected `bodyRetentionMs` and `debtPolicy`:
|
|
2368
|
+
`allowNegative` keeps hard-currency refund debt while permitting funded spending
|
|
2369
|
+
in other currencies; `blockSpend` blocks owner wallet debits until debt is repaid.
|
|
2370
|
+
Ordinary credits repay debt. No retention, pricing or region decision is guessed.
|
|
2371
|
+
Item orders additionally require an actual prepared `assets` participant and
|
|
2372
|
+
nondefault `assetsVersion`; `inventoryIapRefundAssets` supplies the described
|
|
2373
|
+
SQL recovery participant over the app's actual Inventory adapter. Currency-only
|
|
2374
|
+
orders may omit it. The same original/account/notice/current-state/refund/wallet/
|
|
2375
|
+
item SQL authorities described above govern every financial adjustment.
|
|
2376
|
+
|
|
2377
|
+
The binding requires both `createNotifications` and `createCurrentState` on each
|
|
2378
|
+
configured provider. Only iOS and Android have the default chain. Apple provider
|
|
2379
|
+
construction shares the captured trust configuration with purchase verification
|
|
2380
|
+
and fresh-state lookup. Google requires explicit authenticated push configuration
|
|
2381
|
+
in `googleIapProvider({ ...purchaseOptions, notifications: { audience,
|
|
2382
|
+
serviceAccountEmail, subscription } })`; these three identities are app-owned,
|
|
2383
|
+
not derived from the package name. Missing factories refuse notification
|
|
2384
|
+
installation before credential access. Without `notifications`, purchase-only
|
|
2385
|
+
installations construct neither factory. Every initializer is awaited before
|
|
2386
|
+
IAP activation; missing required credentials or framework cipher refuse readiness.
|
|
2387
|
+
Declared IAP systems carry `network:outbound:*` permission for their explicitly
|
|
2388
|
+
supplied provider authorities, like Billing. Virtual-only installations acquire
|
|
2389
|
+
no IAP outbound permission or routes and refuse real-money declarations first.
|
|
2390
|
+
|
|
2391
|
+
The normal plugin declaration adds exact POST paths `/_voltro/economy/iap/ios`
|
|
2392
|
+
and/or `/_voltro/economy/iap/android`. `pathPrefix` may select one safe common
|
|
2393
|
+
prefix; `maxBodyBytes` is the explicit ingress work budget, default 262144 bytes.
|
|
2394
|
+
Default `maxNoticeRows` and `maxRefundRows` are 10000-row work budgets, never
|
|
2395
|
+
capacity results. SQL custody is isolated per provider deployment; one shared
|
|
2396
|
+
refund journal keeps global original-order idempotence. No raw custody or refund
|
|
2397
|
+
Tag is ambiguously installed for multiple deployments.
|
|
2398
|
+
|
|
2399
|
+
`economy.economy.iapNotifications()` and the server-only
|
|
2400
|
+
`IapNotificationsSystemPort` expose `initialize`, `processor(channel)`,
|
|
2401
|
+
`exportSubject` and `eraseSubject`. Resolve a channel-selected seven-method
|
|
2402
|
+
processor only after activation. Escaped processors and pending old-generation
|
|
2403
|
+
outcomes refuse after deactivation or rebinding. Explicit same-store rebinding
|
|
2404
|
+
reconstructs services from SQL without regranting assets. Initialization never
|
|
2405
|
+
starts its own timer, listener or worker.
|
|
2406
|
+
|
|
2407
|
+
For ordinary file discovery, use this in an app-owned `*.cron.tsx` file. The
|
|
2408
|
+
cadence and budget below are examples, not an operator retention or deployment
|
|
2409
|
+
promise. `economy` is the actual configured plugin instance exported by that app.
|
|
2410
|
+
|
|
2411
|
+
```ts
|
|
2412
|
+
import { installedIapNotificationSchedule } from '@voltro/plugin-economy/iap-notifications'
|
|
2413
|
+
import { economy } from '../plugins/economy'
|
|
2414
|
+
|
|
2415
|
+
export default installedIapNotificationSchedule({
|
|
2416
|
+
system: economy.economy.iapNotifications(),
|
|
2417
|
+
channel: 'ios',
|
|
2418
|
+
name: 'iap.notifications.ios',
|
|
2419
|
+
cron: '*/5 * * * *',
|
|
2420
|
+
timezone: 'UTC',
|
|
2421
|
+
maxNotices: 1000,
|
|
2422
|
+
})
|
|
2423
|
+
```
|
|
2424
|
+
|
|
2425
|
+
The descriptor captures the facade at discovery and resolves the selected
|
|
2426
|
+
processor at firing, so discovery needs neither activation nor credentials.
|
|
2427
|
+
Register one schedule per configured deployment. Pruning and pending retry use
|
|
2428
|
+
the existing bounded processor work; there is no automatic background runtime.
|
|
2429
|
+
|
|
2430
|
+
A complete four-method `IapNotificationsSystemService` replacement supplies
|
|
2431
|
+
`service` and an explicit version, owns provider authentication/current-state/
|
|
2432
|
+
finance/queue/retention/privacy, and forbids default SQL/cipher/debt/assets/bounds
|
|
2433
|
+
options. The plugin still enforces exact HTTP paths/body budget and lifetime.
|
|
2434
|
+
A complete IAP replacement requires this complete notification replacement;
|
|
2435
|
+
default SQL notifications never borrow a replaced IAP or Wallet. Private class
|
|
2436
|
+
receivers are captured. Default construction adds seven notification/refund
|
|
2437
|
+
roles; replacement adds none. Parent initialization/export/erase include the
|
|
2438
|
+
selected notification system. Default erase revokes accounts first, then notice
|
|
2439
|
+
and refund bodies, then original IAP and root Wallet privacy. Inventory retains
|
|
2440
|
+
its own installed privacy hook. Partial privacy work is retryable, not atomic
|
|
2441
|
+
across systems.
|
|
2442
|
+
|
|
2443
|
+
Server inspect, Shared UI, Local transport and Cloud schema/proxy now require the
|
|
2444
|
+
same exact `notificationSystem` shape, conditional seven roles, provider channels/
|
|
2445
|
+
paths, binding/version, debt/item policies and work budgets. No private token,
|
|
2446
|
+
notice body, owner, balance or refund outcome is exposed. Optional configuration
|
|
2447
|
+
sets `notificationInstalled` and `refundDebtInstalled`; absent configuration
|
|
2448
|
+
requires false flags and null status. Replacement shows app-owned policy/bounds.
|
|
2449
|
+
Both EN/DE panels retain the existing refresh/error/focus behavior. This is
|
|
2450
|
+
configuration, not provider readiness, authenticated delivery or financial success.
|
|
2451
|
+
|
|
2452
|
+
`./testing` additionally exports `iapNotificationsSystemPortFixture`,
|
|
2453
|
+
`iapNotificationsSystemFakeLayer` and `iapNotificationsSystemPortSystemFixture`.
|
|
2454
|
+
Each resolves the actual installed SystemPort and processes a real SQL refund
|
|
2455
|
+
with a privately owned framework cipher. Provider authentication/current-state
|
|
2456
|
+
are explicit synthetic seams. Scope release uses normal plugin deactivation; all
|
|
2457
|
+
four outer methods and escaped processors become unavailable.
|
|
2458
|
+
|
|
2459
|
+
### Accepted refund events and explicit sanctions classification
|
|
2460
|
+
|
|
2461
|
+
Every accepted refund/reversal journal entry now commits `eventState: pending`
|
|
2462
|
+
and null `eventDeliveryVersion` in the same financial transaction. These two
|
|
2463
|
+
server-only columns belong to the existing adjustment table; no second journal
|
|
2464
|
+
or new table role is created. A financial rollback creates neither value nor a
|
|
2465
|
+
pending event. Event acknowledgement only changes delivery metadata, never
|
|
2466
|
+
accepted amounts, asset movements or financial bodies. Refund projection rebuild
|
|
2467
|
+
does not replay or recreate delivered events.
|
|
2468
|
+
|
|
2469
|
+
`dataStoreIapRefunds` returns `IapRefundSqlService`, which includes its actual
|
|
2470
|
+
three-method `events` port: `pending(subject)`, `read(subject, opaqueEvent)` and
|
|
2471
|
+
`acknowledge(subject, opaqueEvent, deliveryVersion)`. Only system Subjects or
|
|
2472
|
+
verified tenant service accounts with `economy:events:write` may consume it.
|
|
2473
|
+
The complete bounded journal and actual accepted order/financial history are
|
|
2474
|
+
checked under refund namespace/subject fences. A pending object carries only its
|
|
2475
|
+
event ID; copies, JSON and objects from another service cannot authorize reads
|
|
2476
|
+
or acknowledgement. The same actual capability/version acknowledgement is
|
|
2477
|
+
idempotent; a changed version refuses. Delivered entries do not reopen. Refund
|
|
2478
|
+
erasure removes owners/bodies and delivery versions, marks event custody erased
|
|
2479
|
+
and revokes saved pending authority. No raw receipt/token or expired encrypted
|
|
2480
|
+
notification is needed to resume the already accepted financial journal.
|
|
2481
|
+
|
|
2482
|
+
The installed default notification system now resolves this one shared queue
|
|
2483
|
+
through `IapRefundEventsPort` or `economy.economy.iapRefundEvents()`. It is
|
|
2484
|
+
lifetime- guarded. Complete notification replacements may supply their own
|
|
2485
|
+
`refundEvents` port; that option is forbidden for the SQL default. An absent
|
|
2486
|
+
replacement queue contributes no Tag and returns unavailable, never default SQL.
|
|
2487
|
+
The replacement owns its queue/privacy contract. All four inspect layers now
|
|
2488
|
+
require `eventQueue: sql | custom | null` and both panels show configured queue
|
|
2489
|
+
ownership and the need for an explicit delivery schedule. They display no
|
|
2490
|
+
pending owner/event/outcome enumeration. Optional descriptor discovery ships the
|
|
2491
|
+
browser- safe `./events` declarations `economy.refunded` and
|
|
2492
|
+
`economy.refundReversed` only when an event queue is configured; the production
|
|
2493
|
+
build copies that source tree. Descriptor guards require `economy:events:read`.
|
|
2494
|
+
|
|
2495
|
+
`deliverIapRefundEvents({ events, publisher, version, maxEvents, sanctions,
|
|
2496
|
+
abusePolicy })` reads the whole pending set. `maxEvents` defaults to a 1000-row
|
|
2497
|
+
work budget; excess refuses before any prefix delivery. `publisher.read(tenantId)`
|
|
2498
|
+
must supply the actual tenant-bound framework `EventPublishContext`, including
|
|
2499
|
+
its configured durable workflow audience. Foreign/null tenants and deferred
|
|
2500
|
+
transaction contexts refuse. The helper uses the existing `publishEvent`, with
|
|
2501
|
+
its real key/payload admission and size guard. Publication happens outside the
|
|
2502
|
+
financial SQL group, and acknowledgement follows successful nondeferred
|
|
2503
|
+
publication. A lost reply between publication and acknowledgement can duplicate
|
|
2504
|
+
an ephemeral event; consumers must deduplicate its stable event ID. SQL pending
|
|
2505
|
+
custody survives process loss. A successful acknowledgement says the framework
|
|
2506
|
+
publisher accepted delivery, not that a workflow finished or a client received
|
|
2507
|
+
it. Without a durable audience, the EventBus retains its ordinary ephemeral
|
|
2508
|
+
semantics. `iapRefundEventSchedule` returns a normal app-discovered descriptor;
|
|
2509
|
+
no timer, event bus, tenant, listener or worker is invented or started.
|
|
2510
|
+
|
|
2511
|
+
The exported pure default `noRefundAbuseSignal` returns null. An explicit
|
|
2512
|
+
`abusePolicy` over actual SQL event context may return a bounded confidence
|
|
2513
|
+
value; it requires an actual supplied `SanctionsService.recordSignal`. The
|
|
2514
|
+
context reports bounded observed originals, currently refunded originals and
|
|
2515
|
+
refund adjustments, not all purchases or an invented refund rate. Classified
|
|
2516
|
+
positive refund states become trusted `rmt` signals keyed to the original order;
|
|
2517
|
+
replay/partial updates cannot increment a second strike for that same accepted
|
|
2518
|
+
original and identical classification. Changed classification conflicts.
|
|
2519
|
+
Reversals do not run the classifier. The Sanctions default keeps `rmt`
|
|
2520
|
+
review-only; automatic measures require its explicit escalation strategy.
|
|
2521
|
+
Missing or failing installed consumers leave SQL pending, do not repeat
|
|
2522
|
+
financial value and are not silently bypassed. External
|
|
2523
|
+
publication/classification and erasure are not one distributed atomic
|
|
2524
|
+
transaction.
|
|
2525
|
+
|
|
2526
|
+
Installed Tag/lifetime sources and strict Shared/Cloud/Local/ ENDE DOM sources
|
|
2527
|
+
are extended. `./testing` exports `iapRefundEventsPortFixture`,
|
|
2528
|
+
`iapRefundEventsFakeLayer` and `iapRefundEventsPortSystemFixture`; release
|
|
2529
|
+
revokes actual account/journal authority plus the facade. Whole-journal cost is
|
|
2530
|
+
bounded by configuration but not measured. Full entitlement, sanctions workflow/
|
|
2531
|
+
GM/template/engine integration and remaining monetization stay open.
|
|
2532
|
+
|
|
2533
|
+
|
|
2534
|
+
### Own IAP refund disposition
|
|
2535
|
+
|
|
2536
|
+
`IapPort.receipt(subject, originalHashReference)` and `useIapReceipt` now expose
|
|
2537
|
+
`partially-refunded` and `refunded` when a configured disposition reader admits
|
|
2538
|
+
accepted SQL refund history. Both contain the unchanged original `receipt` plus
|
|
2539
|
+
`refund: { sequence, state, refundNumerator, refundDenominator, observedAt }`.
|
|
2540
|
+
Missing and pending originals keep their existing nonterminal states. No private
|
|
2541
|
+
notice, provider token, fingerprint, owner or refund movement is returned.
|
|
2542
|
+
|
|
2543
|
+
The default installed notification system supplies the actual shared refund
|
|
2544
|
+
adapter. The original order, retained purchase custody, refund namespace/subject,
|
|
2545
|
+
complete bounded adjustment history, original wallet transfers and configured
|
|
2546
|
+
item movement participant are read in one borrowed SQL transaction. The read
|
|
2547
|
+
seeds no rows, updates no admission fences, opens no nested transaction, calls no
|
|
2548
|
+
provider and evaluates no clock or current catalog. Invalid, erased, incomplete,
|
|
2549
|
+
over-budget or inconsistent history refuses rather than displaying `fulfilled`.
|
|
2550
|
+
The original purchase remains immutable SQL truth; this projection grants nothing
|
|
2551
|
+
and is not a provider observation or store-completion authority. A budget is not
|
|
2552
|
+
measured capacity.
|
|
2553
|
+
|
|
2554
|
+
Standalone `dataStoreIap` accepts an explicit `refundDisposition: { read }` SQL
|
|
2555
|
+
reader. `dataStoreIapRefunds(...).readDisposition` implements this contract using
|
|
2556
|
+
the caller's transaction and admitted `IapSqlOriginalPurchase`; it cannot refund,
|
|
2557
|
+
repair or grant. Asset audit receives that admitted original purchase without
|
|
2558
|
+
requiring a retained notification body. Default installed queries conditionally
|
|
2559
|
+
include all four refund table dependencies for LiveQuery invalidation; absent
|
|
2560
|
+
notification configuration adds none.
|
|
2561
|
+
|
|
2562
|
+
A complete notification replacement over default SQL IAP may supply its own
|
|
2563
|
+
`refundDisposition` reader and must declare actual nonempty IAP
|
|
2564
|
+
`querySources.receipt` dependencies. The reader preserves its receiver and must
|
|
2565
|
+
obey the borrowed-transaction read contract. No default refund adapter is loaned
|
|
2566
|
+
into that replacement. Omission reports no disposition reader and exposes only
|
|
2567
|
+
the original order. A complete IAP replacement owns its own `receipt` contract
|
|
2568
|
+
and cannot configure this default-reader seam. The four inspect layers require
|
|
2569
|
+
`notificationSystem.orderDisposition: sql | custom | null` and show configuration
|
|
2570
|
+
ownership only, never an order's refund outcome.
|
|
2571
|
+
|
|
2572
|
+
`captureIapObservation` shares strict admission between the installed facade,
|
|
2573
|
+
wire schema and recovery. Financial `partially-refunded`/`refunded` observations
|
|
2574
|
+
retain an uncertain completion journal; they cannot replace verified store
|
|
2575
|
+
completion. Explicit recovery reuses the accepted original and never redeems it
|
|
2576
|
+
again. A failed completion keeps the original hash reference for an explicit
|
|
2577
|
+
retry. Client finishing may still fail for a revoked store transaction; no success
|
|
2578
|
+
or completion is inferred from its financial disposition.
|
|
2579
|
+
|
|
2580
|
+
This section makes no support claim.
|
|
2581
|
+
|
|
2582
|
+
|
|
2583
|
+
### Accepted store subscription periods
|
|
2584
|
+
|
|
2585
|
+
`dataStoreIap` can now fulfill a verified `subscription` transaction through the
|
|
2586
|
+
same actual account/eligibility/Wallet/Inventory/order/custody prepared group.
|
|
2587
|
+
Quantity is exactly one and consumption must be `none`; `expiresAt` must exceed
|
|
2588
|
+
`purchasedAt` and acceptance time. A later distinct verified transaction can
|
|
2589
|
+
fulfill its own original once. Replaying an accepted original returns its receipt
|
|
2590
|
+
before current verification or clock and never grants another period. Existing
|
|
2591
|
+
logical-offer limits continue to apply. No timer, automatic renewal or purchase
|
|
2592
|
+
consent is manufactured.
|
|
2593
|
+
|
|
2594
|
+
The server-only extended SQL adapter adds `subscriptionPeriods(subject)`. It
|
|
2595
|
+
reads complete bounded own order/custody history and returns only
|
|
2596
|
+
`orderId`, `offerId`, `offerVersion`, `channel`, `startsAt`, `expiresAt` and accepted
|
|
2597
|
+
`refund` disposition. It does not seed, mutate, open a nested transaction, evaluate
|
|
2598
|
+
a clock or call a provider. A real configured refund disposition reader is
|
|
2599
|
+
required when an accepted subscription exists; missing admission refuses instead
|
|
2600
|
+
of asserting an unrefunded entitlement. Historical receipts remain readable after
|
|
2601
|
+
current catalog removal. Owner erasure, changed custody or corrupt history
|
|
2602
|
+
refuses. Structural IAP erasure no longer decodes private accepted payloads before
|
|
2603
|
+
redaction; valid bounded owner/custody linkage remains required and financial
|
|
2604
|
+
values are retained.
|
|
2605
|
+
|
|
2606
|
+
`./iap-entitlements` is browser-safe and exports `IapEntitlementsPort`, its
|
|
2607
|
+
four-method service capture, `defaultIapEntitlementPolicy` and
|
|
2608
|
+
`resolveIapEntitlement`. `./server` exports `iapEntitlementsPort` with explicit
|
|
2609
|
+
`realMoney: true`, version, actual order source, optional pure policy and server
|
|
2610
|
+
clock. A fresh clock is evaluated after the actual SQL source resolves. Partial
|
|
2611
|
+
refunds remain eligible by default; a named pure policy may narrow that
|
|
2612
|
+
decision, including refusing partial refunds, but cannot restore a fully
|
|
2613
|
+
refunded, future or expired period. Duplicate order projections and nonboolean
|
|
2614
|
+
decisions refuse. `entitled(subject, offerId)` returns `{ offerId, entitled,
|
|
2615
|
+
expiresAt, observedAt }`; `expiresAt` is null when denied. The service delegates
|
|
2616
|
+
export/erase to the captured order owner. It creates no SQL role, listener or
|
|
2617
|
+
background schedule. `realMoney: false` refuses before order binding; existing
|
|
2618
|
+
virtual Subscription services remain usable independently.
|
|
2619
|
+
|
|
2620
|
+
This is an observation of accepted SQL periods, not a fresh provider status or
|
|
2621
|
+
prepared value-write capability. No grace, billing retry, automatic renewal,
|
|
2622
|
+
upgrade/proration or unseen provider period is inferred. An application needs its
|
|
2623
|
+
actual provider renewal delivery before a later transaction appears in SQL.
|
|
2624
|
+
Apple's documented transaction `expiresDate` is static per transaction; a renewal
|
|
2625
|
+
creates another transaction with a new expiry. See [Apple expiresDate](https://developer.apple.com/documentation/appstoreserverapi/expiresdate)
|
|
2626
|
+
and [originalTransactionId](https://developer.apple.com/documentation/appstoreserverapi/originaltransactionid).
|
|
2627
|
+
|
|
2628
|
+
Deliberately supplied completion authorities may now accept a subscription's
|
|
2629
|
+
actual original SQL capability. That does not install a native finish callback:
|
|
2630
|
+
StoreKit finish remains client-owned, and the current Google Products-v2 adapter
|
|
2631
|
+
still refuses subscriptions. Installed default admission and entitlement routes
|
|
2632
|
+
are described below; the configured flag is conditional.
|
|
2633
|
+
|
|
2634
|
+
Provider projection/completion fixtures are deliberate seams, never crypto,
|
|
2635
|
+
StoreKit or sandbox evidence. No support or capacity claim.
|
|
2636
|
+
|
|
2637
|
+
### Installed store subscription entitlements
|
|
2638
|
+
|
|
2639
|
+
Optional `iap.entitlements: { version, policy? }` now binds the default accepted
|
|
2640
|
+
SQL-period port. A subscription product in the default installed IAP system
|
|
2641
|
+
requires this configuration. Default entitlements require the actual installed
|
|
2642
|
+
refund disposition reader; an absent reader refuses construction. Explicit
|
|
2643
|
+
nondefault rule identity is required. The named pure policy may only narrow
|
|
2644
|
+
accepted, unexpired, non-fully-refunded periods. Configuring this port does not
|
|
2645
|
+
make an incompatible provider verifier accept subscriptions. The Google adapter
|
|
2646
|
+
now selects its separate Subscription-v2/Orders path described below.
|
|
2647
|
+
|
|
2648
|
+
`economy.economy.iapEntitlements()` and `IapEntitlementsPort` resolve the same
|
|
2649
|
+
lifecycle-guarded four-method service. Owning activation initializes it before the
|
|
2650
|
+
parent becomes active; deactivation and generation changes revoke late reads.
|
|
2651
|
+
Default history shares the existing IAP/refund SQL roles and owning parent privacy.
|
|
2652
|
+
A complete `iap.entitlements.service` replacement owns expiry/history/privacy,
|
|
2653
|
+
requires actual nonempty `querySources`, captures class receivers, forbids a
|
|
2654
|
+
borrowed default policy and cascades its own export/erase. Full IAP replacement
|
|
2655
|
+
cannot borrow the default SQL entitlement implementation.
|
|
2656
|
+
|
|
2657
|
+
The optional `economy.iap.entitled` query and `useIapEntitlement` accept only
|
|
2658
|
+
`{ offerId }` for the verified own user. Actual original/custody/financial/refund
|
|
2659
|
+
and explicit app dependencies invalidate the default query. A complete replacement
|
|
2660
|
+
uses only its declared sources. Mere passage of time is not a database change;
|
|
2661
|
+
`economy.iap.checkEntitlement` and `useCheckIapEntitlement` explicitly obtain a
|
|
2662
|
+
fresh server observation, including after `expiresAt`. The application must check
|
|
2663
|
+
again when it needs freshness. Privileged operations must evaluate their actual
|
|
2664
|
+
server authority; a cached client boolean is not permission to grant value.
|
|
2665
|
+
|
|
2666
|
+
The four configuration layers report conditional
|
|
2667
|
+
`subscriptionEntitlementsInstalled` and exact `entitlementsSystem` backend/version/
|
|
2668
|
+
policy/SQL-history bound or app ownership. They never expose player periods,
|
|
2669
|
+
refund results or readiness. Failed reads hide stale configuration and retain
|
|
2670
|
+
reload focus. No configured source means no entitlement Tag, routes or RPC imports.
|
|
2671
|
+
Virtual/free Subscription systems remain independent under `realMoney: false`.
|
|
2672
|
+
|
|
2673
|
+
### Google subscription originals and pinned financial status
|
|
2674
|
+
|
|
2675
|
+
`googleIapProvider`, `googleIapVerificationPort`, `googleIapCompletionPort` and
|
|
2676
|
+
`googleIapCurrentStatePort` share bounded authenticated transport. Subscription
|
|
2677
|
+
configuration requires `subscriptionPlans`, each naming `offerId`, `offerVersion`,
|
|
2678
|
+
`basePlanId` and `storeOfferId` (explicit null for no store offer). Both purchase
|
|
2679
|
+
and financial order must match that mapping and the real account/app/environment.
|
|
2680
|
+
No missing plan, out-of-app linking, replacement, add-on or proration fallback.
|
|
2681
|
+
|
|
2682
|
+
The verifier reads Subscription-v2 and its latest successful Orders record. Each
|
|
2683
|
+
paid original has the actual order ID as transaction identity; the token hash
|
|
2684
|
+
identifies its chain. Exact single-line paid Money is recorded at nanounit scale,
|
|
2685
|
+
never from reference prices. Only a processed, unrefunded, current accepted period
|
|
2686
|
+
can grant; server clock/consent/prepared value admission still applies. A new paid
|
|
2687
|
+
order can fulfill once under a new original request. Reusing the token cannot
|
|
2688
|
+
reuse a previously accepted order as new value. This adds no automatic renewal
|
|
2689
|
+
worker or native client journal.
|
|
2690
|
+
|
|
2691
|
+
Completion revalidates the accepted SQL original and reads that pinned financial
|
|
2692
|
+
order even after a later renewal. Subscription acknowledgement uses the official
|
|
2693
|
+
endpoint, then fresh authenticated state; an uncertain POST is never a completion
|
|
2694
|
+
receipt by itself. Current financial reads likewise pin the original order.
|
|
2695
|
+
Subscription cancellation/expiry is not a monetary refund. Full refund state
|
|
2696
|
+
revokes; processed partial refund totals reduce to an exact bounded fraction,
|
|
2697
|
+
with no rounding. Unrepresentable fractions refuse. Ordinary payment, privacy,
|
|
2698
|
+
refund debt and item writers remain the existing SQL paths.
|
|
2699
|
+
|
|
2700
|
+
RTDN subscription messages without an order ID now retain a chain hash and null
|
|
2701
|
+
transaction identity; the financial processor refuses to choose an original from
|
|
2702
|
+
that hint. A subscription Void RTDN carries the exact order ID and chain hash,
|
|
2703
|
+
then the fresh lookup authorizes any adjustment. One-time token identities remain
|
|
2704
|
+
unchanged. Automatic renewal routing, missed-period ingestion, account linking,
|
|
2705
|
+
grace/deferral/plan changes and historical provider expiry extensions remain open.
|
|
2706
|
+
Paid period snapshots are used conservatively with current provider admission;
|
|
2707
|
+
the SQL entitlement is not a claim of all current provider access.
|
|
2708
|
+
|
|
2709
|
+
See
|
|
2710
|
+
[Subscription-v2](https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.subscriptionsv2),
|
|
2711
|
+
[Orders](https://developers.google.com/android-publisher/api-ref/rest/v3/orders),
|
|
2712
|
+
[acknowledgement](https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.subscriptions/acknowledge)
|
|
2713
|
+
and [RTDN](https://developer.android.com/google/play/billing/rtdn-reference).
|
|
2714
|
+
Generated OAuth/fetch/notice seams prove no Play purchase, cryptography, Pub/Sub
|
|
2715
|
+
delivery or sandbox.
|
|
2716
|
+
|
|
2717
|
+
|
|
2718
|
+
## Production license admission
|
|
2719
|
+
|
|
2720
|
+
Declare the actual `licensingPlugin()` before this plugin in the API app. Production and staging activation require its verified active or grace deployment-wide license to grant `economy`. Explicit development/test mode stays free without recurring per-instance notices; CLI owns the actual template first-boot receipt. Tenant-scoped licenses refuse until actual tenant-bound authority exists. This server-only gate preserves normal schema, store binding and cleanup; it runs before the original activation hook, not before schema/store binding.
|