payment-kit 1.27.2 → 1.29.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/__blocklet__.js +37 -0
- package/api/ocap-1.30-subpath-shims.d.ts +35 -0
- package/api/src/crons/index.ts +32 -0
- package/api/src/crons/metering-subscription-detection.ts +12 -14
- package/api/src/crons/overdue-detection.ts +51 -74
- package/api/src/crons/retry-pending-events.ts +58 -0
- package/api/src/integrations/app-store/apple-root-certs.ts +26 -0
- package/api/src/integrations/app-store/client.ts +369 -0
- package/api/src/integrations/app-store/handlers/index.ts +46 -0
- package/api/src/integrations/app-store/handlers/subscription.ts +635 -0
- package/api/src/integrations/app-store/node-apple-receipt-verify.d.ts +17 -0
- package/api/src/integrations/app-store/notification-routing.ts +18 -0
- package/api/src/integrations/app-store/signed-data-verifier.ts +150 -0
- package/api/src/integrations/arcblock/nft.ts +6 -2
- package/api/src/integrations/arcblock/stake.ts +3 -2
- package/api/src/integrations/arcblock/token.ts +4 -4
- package/api/src/integrations/blocklet/notification.ts +1 -1
- package/api/src/integrations/ethereum/tx.ts +29 -0
- package/api/src/integrations/google-play/client.ts +276 -0
- package/api/src/integrations/google-play/handlers/index.ts +69 -0
- package/api/src/integrations/google-play/handlers/subscription.ts +565 -0
- package/api/src/integrations/google-play/handlers/voided.ts +106 -0
- package/api/src/integrations/google-play/setup.ts +43 -0
- package/api/src/integrations/google-play/verify.ts +251 -0
- package/api/src/integrations/iap-reconcile.ts +415 -0
- package/api/src/integrations/stripe/handlers/invoice.ts +70 -53
- package/api/src/integrations/stripe/handlers/payment-intent.ts +8 -1
- package/api/src/integrations/stripe/resource.ts +8 -0
- package/api/src/libs/audit.ts +70 -24
- package/api/src/libs/auth.ts +49 -2
- package/api/src/libs/chain-error.ts +31 -0
- package/api/src/libs/entitlement.ts +399 -0
- package/api/src/libs/env.ts +2 -0
- package/api/src/libs/error.ts +15 -0
- package/api/src/libs/event.ts +42 -1
- package/api/src/libs/invoice.ts +69 -34
- package/api/src/libs/notification/template/customer-auto-recharge-daily-limit-exceeded.ts +1 -3
- package/api/src/libs/notification/template/customer-auto-recharge-failed.ts +1 -3
- package/api/src/libs/notification/template/customer-credit-grant-granted.ts +1 -3
- package/api/src/libs/notification/template/customer-credit-insufficient.ts +1 -3
- package/api/src/libs/notification/template/customer-credit-low-balance.ts +1 -3
- package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -3
- package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -3
- package/api/src/libs/notification/template/one-time-payment-refund-succeeded.ts +1 -3
- package/api/src/libs/notification/template/one-time-payment-succeeded.ts +1 -3
- package/api/src/libs/notification/template/subscription-renew-failed.ts +1 -3
- package/api/src/libs/notification/template/subscription-slippage-exceeded.ts +1 -3
- package/api/src/libs/notification/template/subscription-slippage-warning.ts +1 -3
- package/api/src/libs/notification/template/subscription-succeeded.ts +1 -1
- package/api/src/libs/pagination.ts +14 -9
- package/api/src/libs/payment.ts +25 -10
- package/api/src/libs/security.ts +51 -0
- package/api/src/libs/session.ts +1 -1
- package/api/src/libs/subscription.ts +13 -1
- package/api/src/libs/timing.ts +35 -0
- package/api/src/libs/util.ts +29 -15
- package/api/src/libs/wallet-migration.ts +72 -53
- package/api/src/queues/auto-recharge.ts +1 -1
- package/api/src/queues/credit-consume.ts +94 -12
- package/api/src/queues/credit-grant.ts +4 -0
- package/api/src/queues/event.ts +39 -21
- package/api/src/queues/invoice.ts +1 -0
- package/api/src/queues/payment.ts +83 -15
- package/api/src/queues/refund.ts +84 -71
- package/api/src/queues/subscription.ts +1 -0
- package/api/src/queues/webhook.ts +12 -2
- package/api/src/routes/checkout-sessions.ts +82 -43
- package/api/src/routes/connect/change-payment.ts +2 -0
- package/api/src/routes/connect/change-plan.ts +2 -0
- package/api/src/routes/connect/pay.ts +12 -3
- package/api/src/routes/connect/setup.ts +3 -1
- package/api/src/routes/connect/shared.ts +52 -39
- package/api/src/routes/connect/subscribe.ts +4 -1
- package/api/src/routes/credit-grants.ts +25 -17
- package/api/src/routes/donations.ts +2 -2
- package/api/src/routes/entitlements.ts +105 -0
- package/api/src/routes/events.ts +2 -2
- package/api/src/routes/index.ts +12 -2
- package/api/src/routes/integrations/app-store.ts +267 -0
- package/api/src/routes/integrations/google-play.ts +324 -0
- package/api/src/routes/meter-events.ts +16 -6
- package/api/src/routes/payment-links.ts +1 -1
- package/api/src/routes/payment-methods.ts +131 -1
- package/api/src/routes/settings.ts +1 -1
- package/api/src/routes/tax-rates.ts +1 -1
- package/api/src/store/migrations/20260526-iap-foundation.ts +105 -0
- package/api/src/store/models/customer.ts +37 -1
- package/api/src/store/models/entitlement-grant.ts +118 -0
- package/api/src/store/models/entitlement-product.ts +48 -0
- package/api/src/store/models/entitlement.ts +86 -0
- package/api/src/store/models/index.ts +9 -0
- package/api/src/store/models/invoice.ts +20 -0
- package/api/src/store/models/payment-method.ts +66 -1
- package/api/src/store/models/price.ts +23 -14
- package/api/src/store/models/refund.ts +10 -0
- package/api/src/store/models/subscription.ts +14 -0
- package/api/src/store/models/types.ts +32 -0
- package/api/tests/integrations/app-store/client.spec.ts +335 -0
- package/api/tests/integrations/app-store/handlers.spec.ts +480 -0
- package/api/tests/integrations/app-store/notifications.spec.ts +381 -0
- package/api/tests/integrations/app-store/signed-data-verifier.spec.ts +72 -0
- package/api/tests/integrations/app-store/webhook-routing.spec.ts +27 -0
- package/api/tests/integrations/google-play/handlers.spec.ts +341 -0
- package/api/tests/integrations/google-play/verify.spec.ts +215 -0
- package/api/tests/integrations/iap-reconcile.spec.ts +237 -0
- package/api/tests/libs/entitlement.spec.ts +347 -0
- package/api/tests/libs/wallet-migration.spec.ts +4 -4
- package/api/tests/queues/credit-consume-batch.spec.ts +5 -2
- package/api/tests/queues/credit-consume.spec.ts +8 -4
- package/api/tests/routes/credit-grants.spec.ts +1 -0
- package/blocklet.yml +1 -1
- package/cloudflare/MIGRATION-CHALLENGES.md +676 -0
- package/cloudflare/MIGRATION-RUNBOOK.md +777 -0
- package/cloudflare/README.md +499 -0
- package/cloudflare/STAGING-MIGRATION-GUIDE.md +602 -0
- package/cloudflare/build.ts +151 -0
- package/cloudflare/did-connect-auth.ts +527 -0
- package/cloudflare/docs/2026-04-22-sdk-1.30.9-upgrade-retro.md +324 -0
- package/cloudflare/docs/2026-04-24-queue-ops-followup.md +218 -0
- package/cloudflare/docs/cf-queues-ops-alert-analysis.md +663 -0
- package/cloudflare/docs/cf-workers-local-dev-and-fixes.md +284 -0
- package/cloudflare/docs/cleanup-tasks-2026-05.md +62 -0
- package/cloudflare/docs/payment-kit-platform-analysis-2026-04-20.md +354 -0
- package/cloudflare/frontend-shims/buffer-polyfill.ts +9 -0
- package/cloudflare/frontend-shims/js-sdk.ts +43 -0
- package/cloudflare/frontend-shims/mime-types.ts +46 -0
- package/cloudflare/frontend-shims/session.ts +24 -0
- package/cloudflare/frontend-shims/vite-plugin-noop.ts +6 -0
- package/cloudflare/index.html +40 -0
- package/cloudflare/migrate-to-d1.js +252 -0
- package/cloudflare/migrations/0001_initial_schema.sql +82 -0
- package/cloudflare/migrations/0002_indexes.sql +75 -0
- package/cloudflare/migrations/0003_locks_and_constraints.sql +18 -0
- package/cloudflare/migrations/0004_iap_foundation.sql +72 -0
- package/cloudflare/migrations/0005_iap_tenant_backfill.sql +112 -0
- package/cloudflare/run-build.js +391 -0
- package/cloudflare/scripts/test-decrypt.js +102 -0
- package/cloudflare/shims/arcblock-ws.ts +20 -0
- package/cloudflare/shims/axios-http-adapter.ts +4 -0
- package/cloudflare/shims/axios-lite.ts +117 -0
- package/cloudflare/shims/blocklet-sdk/auth-service.ts +33 -0
- package/cloudflare/shims/blocklet-sdk/cdn.ts +3 -0
- package/cloudflare/shims/blocklet-sdk/component-api.ts +35 -0
- package/cloudflare/shims/blocklet-sdk/component.ts +18 -0
- package/cloudflare/shims/blocklet-sdk/config.ts +8 -0
- package/cloudflare/shims/blocklet-sdk/did.ts +14 -0
- package/cloudflare/shims/blocklet-sdk/env.ts +12 -0
- package/cloudflare/shims/blocklet-sdk/eventbus.ts +3 -0
- package/cloudflare/shims/blocklet-sdk/fallback.ts +3 -0
- package/cloudflare/shims/blocklet-sdk/index.ts +11 -0
- package/cloudflare/shims/blocklet-sdk/logger.ts +11 -0
- package/cloudflare/shims/blocklet-sdk/middlewares.ts +15 -0
- package/cloudflare/shims/blocklet-sdk/notification.ts +11 -0
- package/cloudflare/shims/blocklet-sdk/security.ts +53 -0
- package/cloudflare/shims/blocklet-sdk/session.ts +8 -0
- package/cloudflare/shims/blocklet-sdk/verify-session.ts +44 -0
- package/cloudflare/shims/blocklet-sdk/verify-sign.ts +38 -0
- package/cloudflare/shims/blocklet-sdk/wallet-authenticator.ts +3 -0
- package/cloudflare/shims/blocklet-sdk/wallet-handler.ts +6 -0
- package/cloudflare/shims/blocklet-sdk/wallet.ts +103 -0
- package/cloudflare/shims/cookie-parser.ts +3 -0
- package/cloudflare/shims/cors.ts +21 -0
- package/cloudflare/shims/cron.ts +189 -0
- package/cloudflare/shims/crypto-js-warn.ts +7 -0
- package/cloudflare/shims/did-space-js.ts +17 -0
- package/cloudflare/shims/did-space.ts +11 -0
- package/cloudflare/shims/error.ts +18 -0
- package/cloudflare/shims/express-compat/index.ts +80 -0
- package/cloudflare/shims/express-compat/types.ts +41 -0
- package/cloudflare/shims/fastq.ts +105 -0
- package/cloudflare/shims/lock.ts +115 -0
- package/cloudflare/shims/mime-types.ts +56 -0
- package/cloudflare/shims/nedb-storage.ts +9 -0
- package/cloudflare/shims/node-child-process.ts +9 -0
- package/cloudflare/shims/node-fs.ts +20 -0
- package/cloudflare/shims/node-http.ts +13 -0
- package/cloudflare/shims/node-https.ts +4 -0
- package/cloudflare/shims/node-misc.ts +15 -0
- package/cloudflare/shims/node-net.ts +8 -0
- package/cloudflare/shims/node-os.ts +14 -0
- package/cloudflare/shims/node-tty.ts +8 -0
- package/cloudflare/shims/node-zlib.ts +17 -0
- package/cloudflare/shims/noop.ts +26 -0
- package/cloudflare/shims/payment-vendor.ts +14 -0
- package/cloudflare/shims/querystring.ts +12 -0
- package/cloudflare/shims/queue.ts +611 -0
- package/cloudflare/shims/rolldown-runtime.ts +43 -0
- package/cloudflare/shims/sequelize-d1/datatypes.ts +24 -0
- package/cloudflare/shims/sequelize-d1/helpers.ts +46 -0
- package/cloudflare/shims/sequelize-d1/index.ts +34 -0
- package/cloudflare/shims/sequelize-d1/model.ts +1176 -0
- package/cloudflare/shims/sequelize-d1/operators.ts +306 -0
- package/cloudflare/shims/sequelize-d1/retry.ts +85 -0
- package/cloudflare/shims/sequelize-d1/sequelize-class.ts +119 -0
- package/cloudflare/shims/sequelize-d1/timing.ts +81 -0
- package/cloudflare/shims/sequelize-d1/types.ts +35 -0
- package/cloudflare/shims/stripe-cf.ts +29 -0
- package/cloudflare/shims/ws-lite.ts +103 -0
- package/cloudflare/shims/xss.ts +3 -0
- package/cloudflare/tests/shims/cron.spec.ts +210 -0
- package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +87 -0
- package/cloudflare/tests/shims/queue-scheduled.spec.ts +186 -0
- package/cloudflare/vite.config.ts +162 -0
- package/cloudflare/worker.ts +1608 -0
- package/cloudflare/wrangler.json +63 -0
- package/cloudflare/wrangler.jsonc +75 -0
- package/cloudflare/wrangler.staging.json +67 -0
- package/cloudflare/wrangler.toml +28 -0
- package/jest.config.js +4 -12
- package/package.json +30 -22
- package/scripts/seed-google-play.ts +79 -0
- package/src/app.tsx +62 -4
- package/src/components/customer/link.tsx +9 -13
- package/src/components/customer/notification-preference.tsx +3 -2
- package/src/components/filter-toolbar.tsx +4 -0
- package/src/components/invoice/list.tsx +9 -1
- package/src/components/invoice-pdf/utils.ts +2 -1
- package/src/components/layout/admin.tsx +39 -5
- package/src/components/layout/user-cf.tsx +77 -0
- package/src/components/payment-intent/actions.tsx +23 -3
- package/src/components/payment-method/app-store.tsx +103 -0
- package/src/components/payment-method/form.tsx +7 -1
- package/src/components/payment-method/google-play.tsx +85 -0
- package/src/components/safe-did-address.tsx +75 -0
- package/src/components/subscription/list.tsx +20 -0
- package/src/libs/patch-user-card.ts +25 -0
- package/src/libs/util.ts +5 -7
- package/src/locales/en.tsx +63 -0
- package/src/locales/zh.tsx +63 -0
- package/src/pages/admin/billing/meter-events/index.tsx +4 -0
- package/src/pages/admin/billing/subscriptions/detail.tsx +80 -0
- package/src/pages/admin/customers/customers/detail.tsx +8 -2
- package/src/pages/admin/customers/customers/index.tsx +2 -2
- package/src/pages/admin/overview.tsx +3 -1
- package/src/pages/admin/settings/payment-methods/create.tsx +12 -0
- package/src/pages/admin/settings/payment-methods/index.tsx +1 -1
- package/src/pages/customer/subscription/detail.tsx +4 -4
- package/tsconfig.api.json +1 -6
- package/tsconfig.json +3 -4
- package/tsconfig.types.json +2 -1
- package/vite.config.ts +6 -1
|
@@ -6,7 +6,7 @@ import pick from 'lodash/pick';
|
|
|
6
6
|
import { InferAttributes, Op, WhereOptions } from 'sequelize';
|
|
7
7
|
import cloneDeep from 'lodash/cloneDeep';
|
|
8
8
|
import merge from 'lodash/merge';
|
|
9
|
-
import
|
|
9
|
+
import Joi from 'joi';
|
|
10
10
|
import { ensureWebhookRegistered, cleanupStripeWebhook, validateStripeKeys } from '../integrations/stripe/setup';
|
|
11
11
|
import logger from '../libs/logger';
|
|
12
12
|
import { authenticate } from '../libs/security';
|
|
@@ -183,6 +183,124 @@ router.post('/', auth, async (req, res) => {
|
|
|
183
183
|
return res.json({ ...method.toJSON(), payment_currencies: [currency.toJSON()] });
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
if (raw.type === 'google_play') {
|
|
187
|
+
if (!raw.settings.google_play?.package_name) {
|
|
188
|
+
return res.status(400).json({ error: 'google_play package_name is required' });
|
|
189
|
+
}
|
|
190
|
+
if (!raw.settings.google_play?.service_account_json) {
|
|
191
|
+
return res.status(400).json({ error: 'google_play service_account_json is required' });
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
const parsed = JSON.parse(raw.settings.google_play.service_account_json);
|
|
195
|
+
if (!parsed.client_email || !parsed.private_key) {
|
|
196
|
+
return res.status(400).json({ error: 'service_account_json missing client_email or private_key' });
|
|
197
|
+
}
|
|
198
|
+
} catch {
|
|
199
|
+
return res.status(400).json({ error: 'service_account_json is not valid JSON' });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const exist = await PaymentMethod.findOne({
|
|
203
|
+
where: { type: 'google_play', livemode: raw.livemode },
|
|
204
|
+
});
|
|
205
|
+
if (exist) {
|
|
206
|
+
return res.status(400).json({ error: 'google_play payment method already exists for this livemode' });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
raw.settings = pick(PaymentMethod.encryptSettings(raw.settings), ['google_play']) as PaymentMethodSettings;
|
|
210
|
+
raw.logo = raw.logo || getUrl('/methods/google-play.png');
|
|
211
|
+
raw.features = { recurring: true, refund: true, dispute: false };
|
|
212
|
+
raw.confirmation = { type: 'callback' };
|
|
213
|
+
|
|
214
|
+
const method = await PaymentMethod.create(raw as TPaymentMethod);
|
|
215
|
+
|
|
216
|
+
// Create a default USD currency for the PaymentMethod (mirrors stripe path).
|
|
217
|
+
const currency = await PaymentCurrency.create({
|
|
218
|
+
livemode: method.livemode,
|
|
219
|
+
active: method.active,
|
|
220
|
+
locked: true,
|
|
221
|
+
is_base_currency: false,
|
|
222
|
+
payment_method_id: method.id,
|
|
223
|
+
type: 'standard',
|
|
224
|
+
name: 'Dollar',
|
|
225
|
+
description: 'US Dollar (Google Play)',
|
|
226
|
+
logo: getUrl('/currencies/dollar.png'),
|
|
227
|
+
symbol: 'USD',
|
|
228
|
+
decimal: 2,
|
|
229
|
+
maximum_precision: 2,
|
|
230
|
+
minimum_payment_amount: '1',
|
|
231
|
+
maximum_payment_amount: '100000000000',
|
|
232
|
+
contract: '',
|
|
233
|
+
metadata: {},
|
|
234
|
+
});
|
|
235
|
+
await method.update({ default_currency_id: currency.id });
|
|
236
|
+
|
|
237
|
+
return res.json({ ...method.toJSON(), payment_currencies: [currency.toJSON()] });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (raw.type === 'app_store') {
|
|
241
|
+
if (!raw.settings.app_store?.bundle_id) {
|
|
242
|
+
return res.status(400).json({ error: 'app_store bundle_id is required' });
|
|
243
|
+
}
|
|
244
|
+
const env = raw.settings.app_store?.environment;
|
|
245
|
+
if (env !== 'production' && env !== 'sandbox') {
|
|
246
|
+
return res.status(400).json({ error: 'app_store environment must be production or sandbox' });
|
|
247
|
+
}
|
|
248
|
+
// Server API credentials are optional — StoreKit 2 JWS verify doesn't need them.
|
|
249
|
+
// But if any of the three is set, all three must be set together.
|
|
250
|
+
const hasAnyServerCred = !!(
|
|
251
|
+
raw.settings.app_store?.issuer_id ||
|
|
252
|
+
raw.settings.app_store?.key_id ||
|
|
253
|
+
raw.settings.app_store?.private_key_pem
|
|
254
|
+
);
|
|
255
|
+
if (hasAnyServerCred) {
|
|
256
|
+
if (
|
|
257
|
+
!raw.settings.app_store?.issuer_id ||
|
|
258
|
+
!raw.settings.app_store?.key_id ||
|
|
259
|
+
!raw.settings.app_store?.private_key_pem
|
|
260
|
+
) {
|
|
261
|
+
return res.status(400).json({
|
|
262
|
+
error: 'app_store Server API credentials must include all of issuer_id, key_id, private_key_pem',
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const exist = await PaymentMethod.findOne({
|
|
268
|
+
where: { type: 'app_store', livemode: raw.livemode },
|
|
269
|
+
});
|
|
270
|
+
if (exist) {
|
|
271
|
+
return res.status(400).json({ error: 'app_store payment method already exists for this livemode' });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
raw.settings = pick(PaymentMethod.encryptSettings(raw.settings), ['app_store']) as PaymentMethodSettings;
|
|
275
|
+
raw.logo = raw.logo || getUrl('/methods/app-store.png');
|
|
276
|
+
raw.features = { recurring: true, refund: false, dispute: false };
|
|
277
|
+
raw.confirmation = { type: 'callback' };
|
|
278
|
+
|
|
279
|
+
const method = await PaymentMethod.create(raw as TPaymentMethod);
|
|
280
|
+
|
|
281
|
+
const currency = await PaymentCurrency.create({
|
|
282
|
+
livemode: method.livemode,
|
|
283
|
+
active: method.active,
|
|
284
|
+
locked: true,
|
|
285
|
+
is_base_currency: false,
|
|
286
|
+
payment_method_id: method.id,
|
|
287
|
+
type: 'standard',
|
|
288
|
+
name: 'Dollar',
|
|
289
|
+
description: 'US Dollar (App Store)',
|
|
290
|
+
logo: getUrl('/currencies/dollar.png'),
|
|
291
|
+
symbol: 'USD',
|
|
292
|
+
decimal: 2,
|
|
293
|
+
maximum_precision: 2,
|
|
294
|
+
minimum_payment_amount: '1',
|
|
295
|
+
maximum_payment_amount: '100000000000',
|
|
296
|
+
contract: '',
|
|
297
|
+
metadata: {},
|
|
298
|
+
});
|
|
299
|
+
await method.update({ default_currency_id: currency.id });
|
|
300
|
+
|
|
301
|
+
return res.json({ ...method.toJSON(), payment_currencies: [currency.toJSON()] });
|
|
302
|
+
}
|
|
303
|
+
|
|
186
304
|
// FIXME: support bitcoin payment methods
|
|
187
305
|
|
|
188
306
|
return res.status(400).json({ error: 'payment method type is not supported' });
|
|
@@ -259,6 +377,18 @@ router.get('/types', auth, (_, res) => {
|
|
|
259
377
|
description: 'Pay with base compatible chains',
|
|
260
378
|
logo: getUrl('/methods/base.png'),
|
|
261
379
|
},
|
|
380
|
+
{
|
|
381
|
+
type: 'google_play',
|
|
382
|
+
name: 'Google Play',
|
|
383
|
+
description: 'Subscriptions purchased via Google Play in-app billing',
|
|
384
|
+
logo: getUrl('/methods/google-play.png'),
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
type: 'app_store',
|
|
388
|
+
name: 'App Store',
|
|
389
|
+
description: 'Subscriptions purchased via Apple App Store StoreKit',
|
|
390
|
+
logo: getUrl('/methods/app-store.png'),
|
|
391
|
+
},
|
|
262
392
|
]);
|
|
263
393
|
});
|
|
264
394
|
|
|
@@ -3,7 +3,7 @@ import pick from 'lodash/pick';
|
|
|
3
3
|
import { Op, type WhereOptions } from 'sequelize';
|
|
4
4
|
|
|
5
5
|
import Joi from 'joi';
|
|
6
|
-
import
|
|
6
|
+
import merge from 'lodash/merge';
|
|
7
7
|
import { PaymentCurrency } from '../store/models/payment-currency';
|
|
8
8
|
import { PaymentMethod } from '../store/models/payment-method';
|
|
9
9
|
import { authenticate } from '../libs/security';
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { DataTypes } from 'sequelize';
|
|
2
|
+
import { createIndexIfNotExists, safeApplyColumnChanges, type Migration } from '../migrate';
|
|
3
|
+
import models from '../models';
|
|
4
|
+
|
|
5
|
+
export const up: Migration = async ({ context }) => {
|
|
6
|
+
// 1. Customer: per-channel UUID for IAP appAccountToken / obfuscatedAccountId mapping (D-004)
|
|
7
|
+
await safeApplyColumnChanges(context, {
|
|
8
|
+
customers: [
|
|
9
|
+
{ name: 'app_store_uuid', field: { type: DataTypes.STRING(36), allowNull: true } },
|
|
10
|
+
{ name: 'google_play_uuid', field: { type: DataTypes.STRING(36), allowNull: true } },
|
|
11
|
+
],
|
|
12
|
+
});
|
|
13
|
+
// SQLite/D1 allows multiple NULLs in UNIQUE columns, so a plain UNIQUE index
|
|
14
|
+
// is functionally equivalent to a partial one (WHERE col IS NOT NULL).
|
|
15
|
+
await createIndexIfNotExists(context, 'customers', ['app_store_uuid'], 'idx_customers_app_store_uuid', {
|
|
16
|
+
unique: true,
|
|
17
|
+
});
|
|
18
|
+
await createIndexIfNotExists(context, 'customers', ['google_play_uuid'], 'idx_customers_google_play_uuid', {
|
|
19
|
+
unique: true,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// 2. Subscription: channel + environment (D-005)
|
|
23
|
+
await safeApplyColumnChanges(context, {
|
|
24
|
+
subscriptions: [
|
|
25
|
+
{ name: 'channel', field: { type: DataTypes.STRING(20), allowNull: true } },
|
|
26
|
+
{
|
|
27
|
+
name: 'environment',
|
|
28
|
+
field: { type: DataTypes.STRING(20), allowNull: true, defaultValue: 'production' },
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// 3. Invoice: three-segment amounts for cross-channel accounting (D-001 A)
|
|
34
|
+
await safeApplyColumnChanges(context, {
|
|
35
|
+
invoices: [
|
|
36
|
+
{ name: 'gross_amount', field: { type: DataTypes.STRING(32), allowNull: true } },
|
|
37
|
+
{ name: 'platform_fee', field: { type: DataTypes.STRING(32), defaultValue: '0' } },
|
|
38
|
+
{ name: 'net_amount', field: { type: DataTypes.STRING(32), allowNull: true } },
|
|
39
|
+
],
|
|
40
|
+
});
|
|
41
|
+
// Backfill: existing Stripe / on-chain rows have no platform fee, so gross = net = total
|
|
42
|
+
await context.sequelize.query(
|
|
43
|
+
'UPDATE invoices SET gross_amount = total, net_amount = total WHERE gross_amount IS NULL'
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// 4. Refund: source (merchant_initiated | platform_initiated)
|
|
47
|
+
await safeApplyColumnChanges(context, {
|
|
48
|
+
refunds: [
|
|
49
|
+
{
|
|
50
|
+
name: 'source',
|
|
51
|
+
field: { type: DataTypes.STRING(30), allowNull: true, defaultValue: 'merchant_initiated' },
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// 5. Entitlement tables (D-003 B)
|
|
57
|
+
await context.createTable('entitlements', models.Entitlement.GENESIS_ATTRIBUTES);
|
|
58
|
+
await createIndexIfNotExists(context, 'entitlements', ['key'], 'idx_entitlements_key', { unique: true });
|
|
59
|
+
|
|
60
|
+
await context.createTable('entitlement_products', models.EntitlementProduct.GENESIS_ATTRIBUTES);
|
|
61
|
+
|
|
62
|
+
await context.createTable('entitlement_grants', models.EntitlementGrant.GENESIS_ATTRIBUTES);
|
|
63
|
+
await createIndexIfNotExists(
|
|
64
|
+
context,
|
|
65
|
+
'entitlement_grants',
|
|
66
|
+
['customer_id', 'entitlement_id', 'status'],
|
|
67
|
+
'idx_entitlement_grants_lookup'
|
|
68
|
+
);
|
|
69
|
+
await createIndexIfNotExists(
|
|
70
|
+
context,
|
|
71
|
+
'entitlement_grants',
|
|
72
|
+
['source_subscription_id'],
|
|
73
|
+
'idx_entitlement_grants_source_sub'
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// Note: PaymentMethod.type ENUM extension to include 'app_store' / 'google_play'
|
|
77
|
+
// is intentionally NOT changed at DB level — on D1/SQLite the column is already TEXT
|
|
78
|
+
// with no CHECK constraint, so any value is accepted. The model-level type union
|
|
79
|
+
// is updated in models/payment-method.ts when A1/A2 lands.
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const down: Migration = async ({ context }) => {
|
|
83
|
+
await context.removeIndex('entitlement_grants', 'idx_entitlement_grants_source_sub');
|
|
84
|
+
await context.removeIndex('entitlement_grants', 'idx_entitlement_grants_lookup');
|
|
85
|
+
await context.dropTable('entitlement_grants');
|
|
86
|
+
|
|
87
|
+
await context.dropTable('entitlement_products');
|
|
88
|
+
|
|
89
|
+
await context.removeIndex('entitlements', 'idx_entitlements_key');
|
|
90
|
+
await context.dropTable('entitlements');
|
|
91
|
+
|
|
92
|
+
await context.removeColumn('refunds', 'source');
|
|
93
|
+
|
|
94
|
+
await context.removeColumn('invoices', 'net_amount');
|
|
95
|
+
await context.removeColumn('invoices', 'platform_fee');
|
|
96
|
+
await context.removeColumn('invoices', 'gross_amount');
|
|
97
|
+
|
|
98
|
+
await context.removeColumn('subscriptions', 'environment');
|
|
99
|
+
await context.removeColumn('subscriptions', 'channel');
|
|
100
|
+
|
|
101
|
+
await context.removeIndex('customers', 'idx_customers_google_play_uuid');
|
|
102
|
+
await context.removeIndex('customers', 'idx_customers_app_store_uuid');
|
|
103
|
+
await context.removeColumn('customers', 'google_play_uuid');
|
|
104
|
+
await context.removeColumn('customers', 'app_store_uuid');
|
|
105
|
+
};
|
|
@@ -13,9 +13,9 @@ import {
|
|
|
13
13
|
} from 'sequelize';
|
|
14
14
|
|
|
15
15
|
import merge from 'lodash/merge';
|
|
16
|
+
import { getLock } from '../../libs/lock';
|
|
16
17
|
import { createEvent } from '../../libs/audit';
|
|
17
18
|
import CustomError from '../../libs/error';
|
|
18
|
-
import { getLock } from '../../libs/lock';
|
|
19
19
|
import { createCodeGenerator, createIdGenerator } from '../../libs/util';
|
|
20
20
|
import type { CustomerAddress, CustomerPreferences, CustomerShipping } from './types';
|
|
21
21
|
|
|
@@ -57,6 +57,10 @@ export class Customer extends Model<InferAttributes<Customer>, InferCreationAttr
|
|
|
57
57
|
};
|
|
58
58
|
declare next_invoice_sequence?: number;
|
|
59
59
|
|
|
60
|
+
// Per-channel UUID for IAP appAccountToken / obfuscatedAccountId mapping (D-004)
|
|
61
|
+
declare app_store_uuid?: string;
|
|
62
|
+
declare google_play_uuid?: string;
|
|
63
|
+
|
|
60
64
|
// TODO: following fields not supported
|
|
61
65
|
// declare preferred_locales?: string[];
|
|
62
66
|
// declare tax_exempt?: LiteralUnion<'exempt' | 'none' | 'reverse', string>;
|
|
@@ -143,6 +147,16 @@ export class Customer extends Model<InferAttributes<Customer>, InferCreationAttr
|
|
|
143
147
|
type: DataTypes.NUMBER,
|
|
144
148
|
defaultValue: 1,
|
|
145
149
|
},
|
|
150
|
+
app_store_uuid: {
|
|
151
|
+
type: DataTypes.STRING(36),
|
|
152
|
+
allowNull: true,
|
|
153
|
+
unique: true,
|
|
154
|
+
},
|
|
155
|
+
google_play_uuid: {
|
|
156
|
+
type: DataTypes.STRING(36),
|
|
157
|
+
allowNull: true,
|
|
158
|
+
unique: true,
|
|
159
|
+
},
|
|
146
160
|
created_at: {
|
|
147
161
|
type: DataTypes.DATE,
|
|
148
162
|
defaultValue: DataTypes.NOW,
|
|
@@ -156,6 +170,28 @@ export class Customer extends Model<InferAttributes<Customer>, InferCreationAttr
|
|
|
156
170
|
};
|
|
157
171
|
|
|
158
172
|
public async getInvoiceNumber() {
|
|
173
|
+
// Optimized path for CF Workers: atomic UPDATE...RETURNING in 1 D1 round trip.
|
|
174
|
+
// Original lock-based approach: lock.acquire → reload → increment(UPDATE + reload) → release
|
|
175
|
+
// = 2 D1 round trips for lock + 3 D1 round trips for data = ~1000ms at ~350ms/RT
|
|
176
|
+
// Optimized: single UPDATE...RETURNING = 1 D1 round trip = ~350ms
|
|
177
|
+
//
|
|
178
|
+
// SQLite serializes writes at the DB level, so the application-level lock is
|
|
179
|
+
// redundant for both D1 and Blocklet Server's SQLite.
|
|
180
|
+
const d1 = (globalThis as any).__CF_ENV__?.DB;
|
|
181
|
+
if (d1) {
|
|
182
|
+
try {
|
|
183
|
+
const sql =
|
|
184
|
+
// eslint-disable-next-line @typescript-eslint/quotes
|
|
185
|
+
'UPDATE "customers" SET "next_invoice_sequence" = COALESCE("next_invoice_sequence", 1) + 1 WHERE "id" = ? RETURNING "next_invoice_sequence" - 1 as "prev_sequence"';
|
|
186
|
+
const result = await d1.prepare(sql).bind(this.id).first();
|
|
187
|
+
const sequence = (result as any)?.prev_sequence ?? 1;
|
|
188
|
+
return `${this.invoice_prefix}-${padStart(sequence.toString(), 4, '0')}`;
|
|
189
|
+
} catch (err) {
|
|
190
|
+
console.warn('[getInvoiceNumber] D1 RETURNING failed, falling back:', (err as any)?.message);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Blocklet Server path (or D1 fallback): use lock-based approach
|
|
159
195
|
const lock = getLock(`${this.id}-invoice-number`);
|
|
160
196
|
await lock.acquire();
|
|
161
197
|
await this.reload();
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/lines-between-class-members */
|
|
2
|
+
import { CreationOptional, DataTypes, InferAttributes, InferCreationAttributes, Model } from 'sequelize';
|
|
3
|
+
import type { LiteralUnion } from 'type-fest';
|
|
4
|
+
|
|
5
|
+
import { createIdGenerator } from '../../libs/util';
|
|
6
|
+
|
|
7
|
+
export const nextEntitlementGrantId = createIdGenerator('eg', 24);
|
|
8
|
+
|
|
9
|
+
// One Subscription (or other source) grants an Entitlement to a Customer.
|
|
10
|
+
// Multiple grants for the same (customer, entitlement) can coexist across channels;
|
|
11
|
+
// the "currently effective" grant is determined by status + active_until.
|
|
12
|
+
// eslint-disable-next-line prettier/prettier
|
|
13
|
+
export class EntitlementGrant extends Model<InferAttributes<EntitlementGrant>, InferCreationAttributes<EntitlementGrant>> {
|
|
14
|
+
declare id: CreationOptional<string>;
|
|
15
|
+
declare livemode: boolean;
|
|
16
|
+
|
|
17
|
+
declare entitlement_id: string;
|
|
18
|
+
declare customer_id: string;
|
|
19
|
+
|
|
20
|
+
declare source_subscription_id?: string;
|
|
21
|
+
declare source_channel: LiteralUnion<
|
|
22
|
+
'stripe' | 'arcblock' | 'ethereum' | 'base' | 'app_store' | 'google_play',
|
|
23
|
+
string
|
|
24
|
+
>;
|
|
25
|
+
|
|
26
|
+
declare active_from: number;
|
|
27
|
+
declare active_until: number;
|
|
28
|
+
|
|
29
|
+
declare status: LiteralUnion<'active' | 'canceled' | 'expired' | 'voided', string>;
|
|
30
|
+
|
|
31
|
+
declare metadata?: Record<string, any>;
|
|
32
|
+
|
|
33
|
+
declare created_at: CreationOptional<Date>;
|
|
34
|
+
declare updated_at: CreationOptional<Date>;
|
|
35
|
+
|
|
36
|
+
public static readonly GENESIS_ATTRIBUTES = {
|
|
37
|
+
id: {
|
|
38
|
+
type: DataTypes.STRING(30),
|
|
39
|
+
primaryKey: true,
|
|
40
|
+
allowNull: false,
|
|
41
|
+
defaultValue: nextEntitlementGrantId,
|
|
42
|
+
},
|
|
43
|
+
livemode: {
|
|
44
|
+
type: DataTypes.BOOLEAN,
|
|
45
|
+
allowNull: false,
|
|
46
|
+
},
|
|
47
|
+
entitlement_id: {
|
|
48
|
+
type: DataTypes.STRING(30),
|
|
49
|
+
allowNull: false,
|
|
50
|
+
},
|
|
51
|
+
customer_id: {
|
|
52
|
+
type: DataTypes.STRING(18),
|
|
53
|
+
allowNull: false,
|
|
54
|
+
},
|
|
55
|
+
source_subscription_id: {
|
|
56
|
+
type: DataTypes.STRING(30),
|
|
57
|
+
allowNull: true,
|
|
58
|
+
},
|
|
59
|
+
source_channel: {
|
|
60
|
+
type: DataTypes.STRING(20),
|
|
61
|
+
allowNull: false,
|
|
62
|
+
},
|
|
63
|
+
active_from: {
|
|
64
|
+
type: DataTypes.INTEGER,
|
|
65
|
+
allowNull: false,
|
|
66
|
+
},
|
|
67
|
+
active_until: {
|
|
68
|
+
type: DataTypes.INTEGER,
|
|
69
|
+
allowNull: false,
|
|
70
|
+
},
|
|
71
|
+
status: {
|
|
72
|
+
type: DataTypes.STRING(20),
|
|
73
|
+
allowNull: false,
|
|
74
|
+
defaultValue: 'active',
|
|
75
|
+
},
|
|
76
|
+
metadata: {
|
|
77
|
+
type: DataTypes.JSON,
|
|
78
|
+
allowNull: true,
|
|
79
|
+
},
|
|
80
|
+
created_at: {
|
|
81
|
+
type: DataTypes.DATE,
|
|
82
|
+
defaultValue: DataTypes.NOW,
|
|
83
|
+
allowNull: false,
|
|
84
|
+
},
|
|
85
|
+
updated_at: {
|
|
86
|
+
type: DataTypes.DATE,
|
|
87
|
+
defaultValue: DataTypes.NOW,
|
|
88
|
+
allowNull: false,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
public static initialize(sequelize: any) {
|
|
93
|
+
this.init(EntitlementGrant.GENESIS_ATTRIBUTES, {
|
|
94
|
+
sequelize,
|
|
95
|
+
modelName: 'EntitlementGrant',
|
|
96
|
+
tableName: 'entitlement_grants',
|
|
97
|
+
createdAt: 'created_at',
|
|
98
|
+
updatedAt: 'updated_at',
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public static associate(models: any) {
|
|
103
|
+
this.belongsTo(models.Entitlement, {
|
|
104
|
+
foreignKey: 'entitlement_id',
|
|
105
|
+
as: 'entitlement',
|
|
106
|
+
});
|
|
107
|
+
this.belongsTo(models.Customer, {
|
|
108
|
+
foreignKey: 'customer_id',
|
|
109
|
+
as: 'customer',
|
|
110
|
+
});
|
|
111
|
+
this.belongsTo(models.Subscription, {
|
|
112
|
+
foreignKey: 'source_subscription_id',
|
|
113
|
+
as: 'subscription',
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export type TEntitlementGrant = InferAttributes<EntitlementGrant>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/lines-between-class-members */
|
|
2
|
+
import { CreationOptional, DataTypes, InferAttributes, InferCreationAttributes, Model } from 'sequelize';
|
|
3
|
+
|
|
4
|
+
// Join table: which products unlock which entitlements (many-to-many)
|
|
5
|
+
// eslint-disable-next-line prettier/prettier
|
|
6
|
+
export class EntitlementProduct extends Model<InferAttributes<EntitlementProduct>, InferCreationAttributes<EntitlementProduct>> {
|
|
7
|
+
declare entitlement_id: string;
|
|
8
|
+
declare product_id: string;
|
|
9
|
+
declare created_at: CreationOptional<Date>;
|
|
10
|
+
declare updated_at: CreationOptional<Date>;
|
|
11
|
+
|
|
12
|
+
public static readonly GENESIS_ATTRIBUTES = {
|
|
13
|
+
entitlement_id: {
|
|
14
|
+
type: DataTypes.STRING(30),
|
|
15
|
+
primaryKey: true,
|
|
16
|
+
allowNull: false,
|
|
17
|
+
},
|
|
18
|
+
product_id: {
|
|
19
|
+
type: DataTypes.STRING(30),
|
|
20
|
+
primaryKey: true,
|
|
21
|
+
allowNull: false,
|
|
22
|
+
},
|
|
23
|
+
created_at: {
|
|
24
|
+
type: DataTypes.DATE,
|
|
25
|
+
defaultValue: DataTypes.NOW,
|
|
26
|
+
allowNull: false,
|
|
27
|
+
},
|
|
28
|
+
updated_at: {
|
|
29
|
+
type: DataTypes.DATE,
|
|
30
|
+
defaultValue: DataTypes.NOW,
|
|
31
|
+
allowNull: false,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
public static initialize(sequelize: any) {
|
|
36
|
+
this.init(EntitlementProduct.GENESIS_ATTRIBUTES, {
|
|
37
|
+
sequelize,
|
|
38
|
+
modelName: 'EntitlementProduct',
|
|
39
|
+
tableName: 'entitlement_products',
|
|
40
|
+
createdAt: 'created_at',
|
|
41
|
+
updatedAt: 'updated_at',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public static associate() {}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type TEntitlementProduct = InferAttributes<EntitlementProduct>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/lines-between-class-members */
|
|
2
|
+
import { CreationOptional, DataTypes, InferAttributes, InferCreationAttributes, Model } from 'sequelize';
|
|
3
|
+
|
|
4
|
+
import { createIdGenerator } from '../../libs/util';
|
|
5
|
+
|
|
6
|
+
export const nextEntitlementId = createIdGenerator('ent', 24);
|
|
7
|
+
|
|
8
|
+
// eslint-disable-next-line prettier/prettier
|
|
9
|
+
export class Entitlement extends Model<InferAttributes<Entitlement>, InferCreationAttributes<Entitlement>> {
|
|
10
|
+
declare id: CreationOptional<string>;
|
|
11
|
+
declare livemode: boolean;
|
|
12
|
+
|
|
13
|
+
declare key: string;
|
|
14
|
+
declare name?: string;
|
|
15
|
+
declare description?: string;
|
|
16
|
+
|
|
17
|
+
declare metadata?: Record<string, any>;
|
|
18
|
+
|
|
19
|
+
declare created_at: CreationOptional<Date>;
|
|
20
|
+
declare updated_at: CreationOptional<Date>;
|
|
21
|
+
|
|
22
|
+
public static readonly GENESIS_ATTRIBUTES = {
|
|
23
|
+
id: {
|
|
24
|
+
type: DataTypes.STRING(30),
|
|
25
|
+
primaryKey: true,
|
|
26
|
+
allowNull: false,
|
|
27
|
+
defaultValue: nextEntitlementId,
|
|
28
|
+
},
|
|
29
|
+
livemode: {
|
|
30
|
+
type: DataTypes.BOOLEAN,
|
|
31
|
+
allowNull: false,
|
|
32
|
+
},
|
|
33
|
+
key: {
|
|
34
|
+
type: DataTypes.STRING(64),
|
|
35
|
+
allowNull: false,
|
|
36
|
+
unique: true,
|
|
37
|
+
},
|
|
38
|
+
name: {
|
|
39
|
+
type: DataTypes.STRING(255),
|
|
40
|
+
allowNull: true,
|
|
41
|
+
},
|
|
42
|
+
description: {
|
|
43
|
+
type: DataTypes.TEXT,
|
|
44
|
+
allowNull: true,
|
|
45
|
+
},
|
|
46
|
+
metadata: {
|
|
47
|
+
type: DataTypes.JSON,
|
|
48
|
+
allowNull: true,
|
|
49
|
+
},
|
|
50
|
+
created_at: {
|
|
51
|
+
type: DataTypes.DATE,
|
|
52
|
+
defaultValue: DataTypes.NOW,
|
|
53
|
+
allowNull: false,
|
|
54
|
+
},
|
|
55
|
+
updated_at: {
|
|
56
|
+
type: DataTypes.DATE,
|
|
57
|
+
defaultValue: DataTypes.NOW,
|
|
58
|
+
allowNull: false,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
public static initialize(sequelize: any) {
|
|
63
|
+
this.init(Entitlement.GENESIS_ATTRIBUTES, {
|
|
64
|
+
sequelize,
|
|
65
|
+
modelName: 'Entitlement',
|
|
66
|
+
tableName: 'entitlements',
|
|
67
|
+
createdAt: 'created_at',
|
|
68
|
+
updatedAt: 'updated_at',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public static associate(models: any) {
|
|
73
|
+
this.belongsToMany(models.Product, {
|
|
74
|
+
through: models.EntitlementProduct,
|
|
75
|
+
foreignKey: 'entitlement_id',
|
|
76
|
+
otherKey: 'product_id',
|
|
77
|
+
as: 'products',
|
|
78
|
+
});
|
|
79
|
+
this.hasMany(models.EntitlementGrant, {
|
|
80
|
+
foreignKey: 'entitlement_id',
|
|
81
|
+
as: 'grants',
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type TEntitlement = InferAttributes<Entitlement>;
|
|
@@ -39,6 +39,9 @@ import { PriceQuote } from './price-quote';
|
|
|
39
39
|
import { ArchiveMetadata } from './archive-metadata';
|
|
40
40
|
import { ArchiveLock } from './archive-lock';
|
|
41
41
|
import { RevenueSnapshot } from './revenue-snapshot';
|
|
42
|
+
import { Entitlement } from './entitlement';
|
|
43
|
+
import { EntitlementGrant } from './entitlement-grant';
|
|
44
|
+
import { EntitlementProduct } from './entitlement-product';
|
|
42
45
|
|
|
43
46
|
const models = {
|
|
44
47
|
CheckoutSession,
|
|
@@ -81,6 +84,9 @@ const models = {
|
|
|
81
84
|
ArchiveMetadata,
|
|
82
85
|
ArchiveLock,
|
|
83
86
|
RevenueSnapshot,
|
|
87
|
+
Entitlement,
|
|
88
|
+
EntitlementGrant,
|
|
89
|
+
EntitlementProduct,
|
|
84
90
|
};
|
|
85
91
|
|
|
86
92
|
export function initialize(sequelize: any) {
|
|
@@ -137,6 +143,9 @@ export * from './price-quote';
|
|
|
137
143
|
export * from './archive-metadata';
|
|
138
144
|
export * from './archive-lock';
|
|
139
145
|
export * from './revenue-snapshot';
|
|
146
|
+
export * from './entitlement';
|
|
147
|
+
export * from './entitlement-grant';
|
|
148
|
+
export * from './entitlement-product';
|
|
140
149
|
|
|
141
150
|
export type TPriceExpanded = TPrice & {
|
|
142
151
|
object: 'price';
|
|
@@ -84,6 +84,14 @@ export class Invoice extends Model<InferAttributes<Invoice>, InferCreationAttrib
|
|
|
84
84
|
declare tax: string;
|
|
85
85
|
declare total: string;
|
|
86
86
|
|
|
87
|
+
// Three-segment amounts for cross-channel accounting (D-001 A).
|
|
88
|
+
// gross_amount = what the user actually paid (= total for non-IAP channels).
|
|
89
|
+
// platform_fee = amount Apple / Google withheld (0 for Stripe / on-chain).
|
|
90
|
+
// net_amount = what we actually received (= gross_amount - platform_fee).
|
|
91
|
+
declare gross_amount?: string;
|
|
92
|
+
declare platform_fee?: string;
|
|
93
|
+
declare net_amount?: string;
|
|
94
|
+
|
|
87
95
|
declare amount_due: string; // total - amount_paid
|
|
88
96
|
declare amount_paid: string;
|
|
89
97
|
declare amount_remaining: string; // amount_due - amount_paid
|
|
@@ -250,6 +258,18 @@ export class Invoice extends Model<InferAttributes<Invoice>, InferCreationAttrib
|
|
|
250
258
|
type: DataTypes.STRING(32),
|
|
251
259
|
allowNull: false,
|
|
252
260
|
},
|
|
261
|
+
gross_amount: {
|
|
262
|
+
type: DataTypes.STRING(32),
|
|
263
|
+
allowNull: true,
|
|
264
|
+
},
|
|
265
|
+
platform_fee: {
|
|
266
|
+
type: DataTypes.STRING(32),
|
|
267
|
+
defaultValue: '0',
|
|
268
|
+
},
|
|
269
|
+
net_amount: {
|
|
270
|
+
type: DataTypes.STRING(32),
|
|
271
|
+
allowNull: true,
|
|
272
|
+
},
|
|
253
273
|
subtotal: {
|
|
254
274
|
type: DataTypes.STRING(32),
|
|
255
275
|
allowNull: false,
|