payment-kit 1.29.1 → 1.29.3
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/api/dev.ts +41 -2
- package/api/hono.d.ts +42 -0
- package/api/node-sqlite.d.ts +12 -0
- package/api/src/bootstrap.ts +47 -0
- package/api/src/crons/base.ts +3 -3
- package/api/src/crons/currency.ts +1 -1
- package/api/src/crons/index.ts +41 -37
- package/api/src/crons/metering-subscription-detection.ts +1 -1
- package/api/src/crons/overdue-detection.ts +2 -2
- package/api/src/crons/retry-pending-events.ts +6 -0
- package/api/src/crons/tenant-fanout.ts +82 -0
- package/api/src/host-node/did-connect-runtime-node.ts +33 -0
- package/api/src/host-node/serve-static-arc.ts +68 -0
- package/api/src/host-node/serve-static.ts +41 -0
- package/api/src/index.ts +22 -161
- package/api/src/integrations/app-store/client.ts +3 -4
- package/api/src/integrations/app-store/handlers/subscription.ts +7 -7
- package/api/src/integrations/app-store/signed-data-verifier.ts +3 -2
- package/api/src/integrations/arcblock/token.ts +21 -7
- package/api/src/integrations/google-play/handlers/subscription.ts +6 -6
- package/api/src/integrations/google-play/handlers/voided.ts +2 -2
- package/api/src/integrations/google-play/verify.ts +3 -2
- package/api/src/integrations/iap-reconcile.ts +3 -5
- package/api/src/integrations/stripe/handlers/invoice.ts +2 -2
- package/api/src/integrations/stripe/handlers/subscription.ts +3 -3
- package/api/src/libs/archive/query.ts +19 -0
- package/api/src/libs/audit.ts +61 -4
- package/api/src/libs/auth.ts +247 -47
- package/api/src/libs/context.ts +89 -1
- package/api/src/libs/currency.ts +2 -2
- package/api/src/libs/dayjs.ts +8 -2
- package/api/src/libs/did-connect/runtime-did-connect-js.ts +88 -0
- package/api/src/libs/did-connect/tenant-identity.ts +221 -0
- package/api/src/libs/drivers/auth-storage.ts +118 -0
- package/api/src/libs/drivers/cron.ts +264 -0
- package/api/src/libs/drivers/db.ts +170 -0
- package/api/src/libs/drivers/identity.ts +142 -0
- package/api/src/libs/drivers/index.ts +40 -0
- package/api/src/libs/drivers/locks.ts +226 -0
- package/api/src/libs/drivers/migrate-runner.ts +70 -0
- package/api/src/libs/drivers/queue.ts +104 -0
- package/api/src/libs/drivers/secrets.ts +194 -0
- package/api/src/libs/env.ts +170 -54
- package/api/src/libs/exchange-rate/service.ts +7 -6
- package/api/src/libs/http-fetch-adapter.ts +60 -0
- package/api/src/libs/invoice.ts +1 -1
- package/api/src/libs/lock.ts +51 -47
- package/api/src/libs/logger.ts +48 -8
- package/api/src/libs/notification/index.ts +1 -1
- package/api/src/libs/notification/template/customer-credit-low-balance.ts +2 -1
- package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -1
- package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -1
- package/api/src/libs/overdraft-protection.ts +1 -1
- package/api/src/libs/payout.ts +1 -1
- package/api/src/libs/queue/index.ts +271 -52
- package/api/src/libs/queue/runtime.ts +175 -0
- package/api/src/libs/resource.ts +3 -3
- package/api/src/libs/secrets.ts +38 -0
- package/api/src/libs/session.ts +3 -2
- package/api/src/libs/subscription.ts +5 -5
- package/api/src/libs/tenant.ts +92 -0
- package/api/src/libs/url.ts +3 -3
- package/api/src/libs/util.ts +21 -13
- package/api/src/middlewares/hono/cdn.ts +63 -0
- package/api/src/middlewares/hono/context.ts +80 -0
- package/api/src/middlewares/hono/csrf.ts +83 -0
- package/api/src/middlewares/hono/fallback.ts +194 -0
- package/api/src/middlewares/hono/pipeline.ts +73 -0
- package/api/src/middlewares/hono/resource-mount.ts +42 -0
- package/api/src/middlewares/hono/resource.ts +63 -0
- package/api/src/middlewares/hono/security.ts +209 -0
- package/api/src/middlewares/hono/session.ts +114 -0
- package/api/src/middlewares/hono/xss.ts +61 -0
- package/api/src/queues/auto-recharge.ts +12 -10
- package/api/src/queues/checkout-session.ts +38 -21
- package/api/src/queues/credit-consume.ts +40 -36
- package/api/src/queues/credit-grant.ts +25 -18
- package/api/src/queues/credit-reconciliation.ts +7 -5
- package/api/src/queues/discount-status.ts +9 -6
- package/api/src/queues/event.ts +41 -11
- package/api/src/queues/exchange-rate-health.ts +49 -30
- package/api/src/queues/invoice.ts +18 -15
- package/api/src/queues/notification.ts +14 -7
- package/api/src/queues/payment.ts +64 -37
- package/api/src/queues/payout.ts +37 -21
- package/api/src/queues/refund.ts +36 -18
- package/api/src/queues/subscription.ts +83 -53
- package/api/src/queues/token-transfer.ts +15 -10
- package/api/src/queues/usage-record.ts +8 -5
- package/api/src/queues/vendors/commission.ts +7 -5
- package/api/src/queues/vendors/fulfillment-coordinator.ts +17 -13
- package/api/src/queues/vendors/fulfillment.ts +4 -2
- package/api/src/queues/vendors/return-processor.ts +5 -3
- package/api/src/queues/vendors/return-scanner.ts +5 -4
- package/api/src/queues/vendors/status-check.ts +10 -7
- package/api/src/queues/webhook.ts +60 -32
- package/api/src/routes/connect/shared.ts +1 -2
- package/api/src/routes/connect/subscribe.ts +3 -3
- package/api/src/routes/{archive.ts → hono/archive.ts} +69 -64
- package/api/src/routes/{auto-recharge-configs.ts → hono/auto-recharge-configs.ts} +39 -28
- package/api/src/routes/{checkout-sessions.ts → hono/checkout-sessions.ts} +790 -923
- package/api/src/routes/{coupons.ts → hono/coupons.ts} +93 -76
- package/api/src/routes/{credit-grants.ts → hono/credit-grants.ts} +140 -126
- package/api/src/routes/hono/credit-tokens.ts +43 -0
- package/api/src/routes/{credit-transactions.ts → hono/credit-transactions.ts} +37 -29
- package/api/src/routes/{customers.ts → hono/customers.ts} +199 -224
- package/api/src/routes/{donations.ts → hono/donations.ts} +41 -32
- package/api/src/routes/{entitlements.ts → hono/entitlements.ts} +28 -25
- package/api/src/routes/{events.ts → hono/events.ts} +107 -71
- package/api/src/routes/{exchange-rate-providers.ts → hono/exchange-rate-providers.ts} +138 -126
- package/api/src/routes/hono/exchange-rates.ts +77 -0
- package/api/src/routes/hono/index.ts +115 -0
- package/api/src/routes/{integrations → hono/integrations}/app-store.ts +68 -48
- package/api/src/routes/{integrations → hono/integrations}/google-play.ts +78 -58
- package/api/src/routes/hono/integrations/stripe.ts +74 -0
- package/api/src/routes/{invoices.ts → hono/invoices.ts} +253 -244
- package/api/src/routes/{meter-events.ts → hono/meter-events.ts} +120 -110
- package/api/src/routes/hono/meters.ts +288 -0
- package/api/src/routes/hono/passports.ts +73 -0
- package/api/src/routes/{payment-currencies.ts → hono/payment-currencies.ts} +219 -197
- package/api/src/routes/{payment-intents.ts → hono/payment-intents.ts} +136 -132
- package/api/src/routes/{payment-links.ts → hono/payment-links.ts} +145 -128
- package/api/src/routes/{payment-methods.ts → hono/payment-methods.ts} +125 -93
- package/api/src/routes/{payment-stats.ts → hono/payment-stats.ts} +30 -25
- package/api/src/routes/{payouts.ts → hono/payouts.ts} +55 -47
- package/api/src/routes/{prices.ts → hono/prices.ts} +265 -242
- package/api/src/routes/{pricing-table.ts → hono/pricing-table.ts} +94 -87
- package/api/src/routes/{products.ts → hono/products.ts} +172 -159
- package/api/src/routes/{promotion-codes.ts → hono/promotion-codes.ts} +207 -185
- package/api/src/routes/hono/redirect.ts +24 -0
- package/api/src/routes/{refunds.ts → hono/refunds.ts} +98 -83
- package/api/src/routes/{settings.ts → hono/settings.ts} +64 -55
- package/api/src/routes/{subscription-items.ts → hono/subscription-items.ts} +64 -57
- package/api/src/routes/{subscriptions.ts → hono/subscriptions.ts} +475 -528
- package/api/src/routes/{tax-rates.ts → hono/tax-rates.ts} +71 -70
- package/api/src/routes/hono/tool.ts +69 -0
- package/api/src/routes/{usage-records.ts → hono/usage-records.ts} +47 -42
- package/api/src/routes/{vendor.ts → hono/vendor.ts} +315 -167
- package/api/src/routes/{webhook-attempts.ts → hono/webhook-attempts.ts} +17 -13
- package/api/src/routes/hono/webhook-endpoints.ts +126 -0
- package/api/src/service.ts +814 -0
- package/api/src/store/migrations/20230911-seeding.ts +2 -1
- package/api/src/store/migrations/20260609-remove-did-space-jobs.ts +23 -0
- package/api/src/store/migrations/20260610-tenant-columns.ts +40 -0
- package/api/src/store/migrations/20260611-tenant-backfill.ts +33 -0
- package/api/src/store/models/auto-recharge-config.ts +22 -10
- package/api/src/store/models/checkout-session.ts +15 -14
- package/api/src/store/models/coupon.ts +29 -20
- package/api/src/store/models/credit-grant.ts +38 -29
- package/api/src/store/models/credit-transaction.ts +32 -21
- package/api/src/store/models/customer.ts +19 -17
- package/api/src/store/models/discount.ts +11 -2
- package/api/src/store/models/entitlement-grant.ts +21 -9
- package/api/src/store/models/entitlement-product.ts +21 -9
- package/api/src/store/models/entitlement.ts +19 -10
- package/api/src/store/models/event.ts +18 -9
- package/api/src/store/models/exchange-rate-provider.ts +17 -4
- package/api/src/store/models/invoice-item.ts +18 -9
- package/api/src/store/models/invoice.ts +16 -8
- package/api/src/store/models/meter-event.ts +27 -9
- package/api/src/store/models/meter.ts +31 -22
- package/api/src/store/models/payment-currency.ts +25 -8
- package/api/src/store/models/payment-intent.ts +15 -6
- package/api/src/store/models/payment-link.ts +15 -6
- package/api/src/store/models/payment-method.ts +38 -22
- package/api/src/store/models/payment-stat.ts +18 -9
- package/api/src/store/models/payout.ts +15 -6
- package/api/src/store/models/price-quote.ts +17 -8
- package/api/src/store/models/price.ts +24 -12
- package/api/src/store/models/pricing-table.ts +29 -20
- package/api/src/store/models/product-vendor.ts +20 -10
- package/api/src/store/models/product.ts +15 -6
- package/api/src/store/models/promotion-code.ts +14 -6
- package/api/src/store/models/refund.ts +15 -6
- package/api/src/store/models/revenue-snapshot.ts +21 -9
- package/api/src/store/models/setting.ts +18 -9
- package/api/src/store/models/setup-intent.ts +36 -27
- package/api/src/store/models/subscription-item.ts +21 -9
- package/api/src/store/models/subscription-schedule.ts +21 -9
- package/api/src/store/models/subscription.ts +21 -10
- package/api/src/store/models/tax-rate.ts +29 -21
- package/api/src/store/models/usage-record.ts +11 -2
- package/api/src/store/models/webhook-attempt.ts +18 -9
- package/api/src/store/models/webhook-endpoint.ts +18 -9
- package/api/src/store/scoped-core.ts +55 -0
- package/api/src/store/scoped.ts +247 -0
- package/api/src/store/sequelize.ts +82 -23
- package/api/src/store/sql-migrations.ts +20 -0
- package/api/src/store/tenant-backfill.ts +260 -0
- package/api/src/store/tenant-model.ts +124 -0
- package/api/src/store/tenant-tables.ts +50 -0
- package/api/tests/bootstrap/bootstrap.spec.ts +162 -0
- package/api/tests/crons/tenant-fanout.spec.ts +158 -0
- package/api/tests/embedded/embedded-multi-mode-d3.spec.ts +257 -0
- package/api/tests/fixtures/bare-query-violation.ts +13 -0
- package/api/tests/fixtures/core-env-violation.ts +10 -0
- package/api/tests/fixtures/host-read-violation.ts +19 -0
- package/api/tests/fixtures/tenants.ts +4 -0
- package/api/tests/integrations/iap-tenant.spec.ts +284 -0
- package/api/tests/libs/archive-query.spec.ts +26 -0
- package/api/tests/libs/audit-tenant.spec.ts +153 -0
- package/api/tests/libs/context.spec.ts +204 -0
- package/api/tests/libs/core-config.spec.ts +115 -0
- package/api/tests/libs/cron-driver-d2.spec.ts +237 -0
- package/api/tests/libs/crons-conservation-d2.spec.ts +52 -0
- package/api/tests/libs/did-connect-runtime-js.spec.ts +98 -0
- package/api/tests/libs/did-connect-tenant-identity.spec.ts +159 -0
- package/api/tests/libs/lock-tenant.spec.ts +66 -0
- package/api/tests/libs/scoped.spec.ts +222 -0
- package/api/tests/libs/secrets-facade.spec.ts +52 -0
- package/api/tests/libs/service-host.spec.ts +37 -0
- package/api/tests/libs/tenancy-slot-authority.spec.ts +209 -0
- package/api/tests/libs/tenant-middleware.spec.ts +42 -0
- package/api/tests/libs/tenant-scanner.spec.ts +120 -0
- package/api/tests/middlewares/hono/cdn.spec.ts +70 -0
- package/api/tests/middlewares/hono/context.spec.ts +113 -0
- package/api/tests/middlewares/hono/csrf.spec.ts +136 -0
- package/api/tests/middlewares/hono/fallback.spec.ts +67 -0
- package/api/tests/middlewares/hono/pipeline.spec.ts +47 -0
- package/api/tests/middlewares/hono/security.spec.ts +181 -0
- package/api/tests/middlewares/hono/session.spec.ts +42 -0
- package/api/tests/middlewares/hono/xss.spec.ts +81 -0
- package/api/tests/models/tenant-backfill.spec.ts +287 -0
- package/api/tests/models/tenant-columns-model.spec.ts +46 -0
- package/api/tests/models/tenant-columns.spec.ts +161 -0
- package/api/tests/queues/credit-consume-batch.spec.ts +8 -1
- package/api/tests/queues/credit-consume.spec.ts +8 -1
- package/api/tests/queues/event-tenant.spec.ts +292 -0
- package/api/tests/queues/exchange-rate-health-tenant-d6.spec.ts +62 -0
- package/api/tests/queues/queue-parity.spec.ts +249 -0
- package/api/tests/queues/queue-runtime-surface.spec.ts +277 -0
- package/api/tests/queues/queue-teardown-d2.spec.ts +127 -0
- package/api/tests/queues/tenant-matrix-a.spec.ts +245 -0
- package/api/tests/queues/tenant-matrix-b.spec.ts +168 -0
- package/api/tests/routes/connect/hono-attach.spec.ts +107 -0
- package/api/tests/service/collapse.spec.ts +96 -0
- package/api/tests/service/didconnect-storage-slot.spec.ts +60 -0
- package/api/tests/service/fail-closed-http.spec.ts +79 -0
- package/api/tests/service/static-arc-handler.spec.ts +101 -0
- package/api/tests/service/static-externalized.spec.ts +48 -0
- package/api/tests/store/tenant-crosscut.spec.ts +202 -0
- package/api/tests/store/tenant-model-spike.spec.ts +177 -0
- package/api/tests/store/tenant-model.spec.ts +162 -0
- package/api/tests/store/tenant-residual.spec.ts +196 -0
- package/api/third.d.ts +4 -0
- package/blocklet.yml +1 -1
- package/cloudflare/MIGRATION-RUNBOOK.md +3 -8
- package/cloudflare/README.md +34 -27
- package/cloudflare/STAGING-MIGRATION-GUIDE.md +3 -15
- package/cloudflare/build.ts +33 -13
- package/cloudflare/cf-adapter.ts +419 -0
- package/cloudflare/did-connect-runtime.ts +96 -0
- package/cloudflare/did-connect-token-storage.ts +151 -0
- package/cloudflare/esbuild-cf-config.cjs +407 -0
- package/cloudflare/migrations/0006_tenant_columns.sql +46 -0
- package/cloudflare/migrations/0007_tenant_backfill_indexes.sql +65 -0
- package/cloudflare/migrations/0008_schema_parity.sql +16 -0
- package/cloudflare/migrations/0009_remove_did_space_jobs.sql +5 -0
- package/cloudflare/queue-runtime-mode.ts +13 -0
- package/cloudflare/run-build.js +33 -403
- package/cloudflare/scripts/cf-package-import-probe.mjs +90 -0
- package/cloudflare/scripts/didconnect-mock-smoke.mjs +140 -0
- package/cloudflare/shims/blocklet-sdk/asset-host-transformer.ts +20 -0
- package/cloudflare/shims/blocklet-sdk/config.ts +8 -1
- package/cloudflare/shims/blocklet-sdk/login.ts +12 -0
- package/cloudflare/shims/blocklet-sdk/service-api.ts +14 -0
- package/cloudflare/shims/blocklet-sdk/session.ts +4 -2
- package/cloudflare/shims/blocklet-sdk/util-constants.ts +8 -0
- package/cloudflare/shims/blocklet-sdk/wallet-authenticator.ts +16 -1
- package/cloudflare/shims/blocklet-sdk/wallet-handler.ts +18 -3
- package/cloudflare/shims/cron.ts +38 -158
- package/cloudflare/shims/events.ts +124 -0
- package/cloudflare/shims/fastq.ts +15 -1
- package/cloudflare/shims/nedb-storage.ts +16 -8
- package/cloudflare/shims/xss.ts +8 -0
- package/cloudflare/tenant-middleware.ts +36 -0
- package/cloudflare/tests/cf-adapter.spec.ts +244 -0
- package/cloudflare/tests/did-connect-token-storage.spec.ts +105 -0
- package/cloudflare/tests/tenant-middleware.spec.ts +160 -0
- package/cloudflare/tests/worker-handler-gate.spec.ts +69 -0
- package/cloudflare/vite.config.ts +53 -45
- package/cloudflare/worker.ts +261 -448
- package/cloudflare/wrangler.json +0 -6
- package/cloudflare/wrangler.jsonc +0 -6
- package/cloudflare/wrangler.local-e2e.jsonc +25 -0
- package/cloudflare/wrangler.staging.json +0 -6
- package/jest.config.js +3 -1
- package/package.json +33 -38
- package/scripts/bootstrap-inject.ts +166 -0
- package/scripts/core-env-whitelist.json +1 -0
- package/scripts/e2e-12b-runtime.ts +149 -0
- package/scripts/e2e-core-config.ts +125 -0
- package/scripts/e2e-d1-tenancy.ts +116 -0
- package/scripts/e2e-d2-cron-queue.ts +139 -0
- package/scripts/e2e-d3-embedded-multi.ts +171 -0
- package/scripts/e2e-hono-s2.ts +125 -0
- package/scripts/e2e-hono-s3e.ts +135 -0
- package/scripts/e2e-hono-s4.ts +114 -0
- package/scripts/e2e-migration-contract.ts +100 -0
- package/scripts/e2e-s0.ts +61 -0
- package/scripts/e2e-s1.ts +107 -0
- package/scripts/e2e-s2.ts +178 -0
- package/scripts/e2e-s3.ts +110 -0
- package/scripts/e2e-s4.ts +191 -0
- package/scripts/e2e-s5.ts +139 -0
- package/scripts/e2e-s6.ts +127 -0
- package/scripts/e2e-tenant-model.ts +119 -0
- package/scripts/e2e-tenant-worker.ts +199 -0
- package/scripts/gen-sql-migrations.js +46 -0
- package/scripts/phase8-codemod.js +219 -0
- package/scripts/phase9a-env-getters-codemod.js +82 -0
- package/scripts/scan-core-env.js +109 -0
- package/scripts/scan-tenant-queries.js +235 -0
- package/scripts/schema-drift-guard.ts +210 -0
- package/scripts/tenant-scan-whitelist.json +1 -0
- package/src/app.tsx +2 -1
- package/src/env.d.ts +13 -1
- package/src/libs/service-host.ts +13 -0
- package/tsconfig.json +1 -1
- package/vite.arc.config.ts +159 -0
- package/api/src/libs/did-space.ts +0 -235
- package/api/src/libs/middleware.ts +0 -50
- package/api/src/libs/security.ts +0 -192
- package/api/src/queues/space.ts +0 -662
- package/api/src/routes/credit-tokens.ts +0 -38
- package/api/src/routes/exchange-rates.ts +0 -87
- package/api/src/routes/index.ts +0 -142
- package/api/src/routes/integrations/stripe.ts +0 -61
- package/api/src/routes/meters.ts +0 -274
- package/api/src/routes/passports.ts +0 -68
- package/api/src/routes/redirect.ts +0 -20
- package/api/src/routes/tool.ts +0 -65
- package/api/src/routes/webhook-endpoints.ts +0 -126
- package/api/tests/routes/credit-grants.spec.ts +0 -1261
- package/cloudflare/did-connect-auth.ts +0 -527
- package/cloudflare/shims/did-space-js.ts +0 -17
- package/cloudflare/shims/did-space.ts +0 -11
- package/cloudflare/shims/express-compat/index.ts +0 -80
- package/cloudflare/shims/express-compat/types.ts +0 -41
- package/cloudflare/shims/lock.ts +0 -115
- package/cloudflare/shims/queue.ts +0 -611
- package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +0 -87
- package/cloudflare/tests/shims/queue-scheduled.spec.ts +0 -186
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { BN, fromUnitToToken } from '@ocap/util';
|
|
2
2
|
import { Op } from 'sequelize';
|
|
3
3
|
import pAll from 'p-all';
|
|
4
|
+
import {
|
|
5
|
+
isCfWorker,
|
|
6
|
+
creditLowBalanceThresholdPercentage,
|
|
7
|
+
creditBatchSize,
|
|
8
|
+
creditBatchWindowMs,
|
|
9
|
+
creditQueueConcurrency,
|
|
10
|
+
} from '../libs/env';
|
|
11
|
+
import { systemFindAll, systemFindByPk, systemFindOne } from '../store/scoped';
|
|
4
12
|
|
|
5
13
|
import { getLock } from '../libs/lock';
|
|
6
14
|
import logger from '../libs/logger';
|
|
7
|
-
import createQueue from '../libs/queue';
|
|
8
|
-
import { createEvent } from '../libs/audit';
|
|
15
|
+
import createQueue, { assertJobObjectTenant } from '../libs/queue';
|
|
16
|
+
import { createEvent, reportAuditFailure } from '../libs/audit';
|
|
9
17
|
import { MeterEvent, CreditGrant, CreditTransaction, Customer, Subscription, TMeterExpanded } from '../store/models';
|
|
10
18
|
import { getCachedMeterExpanded } from '../libs/reference-cache';
|
|
11
19
|
|
|
@@ -20,7 +28,7 @@ import { addTokenTransferJob } from './token-transfer';
|
|
|
20
28
|
// In Blocklet Server (no Queue ops limit), use the full MAX_RETRY_COUNT.
|
|
21
29
|
// After max retries, mark as requires_action — retryFailedEventsForCustomer()
|
|
22
30
|
// picks them up when credit is granted.
|
|
23
|
-
const CREDIT_MAX_RETRY = (
|
|
31
|
+
const CREDIT_MAX_RETRY = isCfWorker() ? 5 : MAX_RETRY_COUNT;
|
|
24
32
|
|
|
25
33
|
type CreditConsumptionJob = {
|
|
26
34
|
meterEventId: string;
|
|
@@ -82,7 +90,7 @@ async function checkLowBalance(
|
|
|
82
90
|
if (totalCreditAmountBn.lte(new BN(0))) return;
|
|
83
91
|
const remainingAmountBn = new BN(remainingBalance);
|
|
84
92
|
// Get threshold percentage from env var, default to 10%
|
|
85
|
-
const thresholdPercentage =
|
|
93
|
+
const thresholdPercentage = creditLowBalanceThresholdPercentage();
|
|
86
94
|
const threshold = totalCreditAmountBn.mul(new BN(thresholdPercentage)).div(new BN(100));
|
|
87
95
|
if (remainingAmountBn.gt(new BN(0)) && remainingAmountBn.lte(threshold)) {
|
|
88
96
|
const percentage = remainingAmountBn.mul(new BN(100)).div(totalCreditAmountBn).toString();
|
|
@@ -94,7 +102,7 @@ async function checkLowBalance(
|
|
|
94
102
|
percentage,
|
|
95
103
|
subscription_id: context.subscription?.id,
|
|
96
104
|
},
|
|
97
|
-
}).catch(
|
|
105
|
+
}).catch(reportAuditFailure);
|
|
98
106
|
}
|
|
99
107
|
} catch (error: any) {
|
|
100
108
|
logger.error('Failed to check low balance', {
|
|
@@ -118,7 +126,7 @@ async function loadReferenceData(
|
|
|
118
126
|
|
|
119
127
|
const [meter, customer] = await Promise.all([
|
|
120
128
|
getCachedMeterExpanded(meterEvent.event_name) as Promise<TMeterExpanded | null>,
|
|
121
|
-
Customer
|
|
129
|
+
systemFindByPk(Customer, customerId),
|
|
122
130
|
]);
|
|
123
131
|
|
|
124
132
|
if (!meter) {
|
|
@@ -163,7 +171,7 @@ async function consumeAvailableCredits(
|
|
|
163
171
|
const currencyId = context.meter.currency_id!;
|
|
164
172
|
const meterEventId = context.meterEvent.id;
|
|
165
173
|
|
|
166
|
-
const existingTransactions = await CreditTransaction
|
|
174
|
+
const existingTransactions = await systemFindAll(CreditTransaction, {
|
|
167
175
|
where: {
|
|
168
176
|
source: meterEventId,
|
|
169
177
|
},
|
|
@@ -300,7 +308,7 @@ async function handlePostConsumptionEvents(
|
|
|
300
308
|
currency_id: currencyId,
|
|
301
309
|
subscription_id: context.subscription?.id,
|
|
302
310
|
},
|
|
303
|
-
}).catch(
|
|
311
|
+
}).catch(reportAuditFailure);
|
|
304
312
|
}
|
|
305
313
|
|
|
306
314
|
// 如果有关联订阅且订阅活跃,将其标记为逾期
|
|
@@ -338,7 +346,7 @@ async function handlePostConsumptionEvents(
|
|
|
338
346
|
currency_id: currencyId,
|
|
339
347
|
subscription_id: context.subscription?.id,
|
|
340
348
|
},
|
|
341
|
-
}).catch(
|
|
349
|
+
}).catch(reportAuditFailure);
|
|
342
350
|
}
|
|
343
351
|
|
|
344
352
|
if (!insufficientTriggered) {
|
|
@@ -479,7 +487,7 @@ async function createCreditTransaction(
|
|
|
479
487
|
});
|
|
480
488
|
|
|
481
489
|
// 重新查询已存在的 transaction
|
|
482
|
-
const duplicateTransaction = await CreditTransaction
|
|
490
|
+
const duplicateTransaction = await systemFindOne(CreditTransaction, {
|
|
483
491
|
where: {
|
|
484
492
|
source: meterEventId,
|
|
485
493
|
credit_grant_id: creditGrantId,
|
|
@@ -508,11 +516,12 @@ export async function handleCreditConsumption(job: CreditConsumptionJob) {
|
|
|
508
516
|
logger.info('Starting credit consumption job', { meterEventId });
|
|
509
517
|
|
|
510
518
|
// Pre-check before acquiring lock
|
|
511
|
-
const preCheckEvent = await MeterEvent
|
|
519
|
+
const preCheckEvent = await systemFindByPk(MeterEvent, meterEventId);
|
|
512
520
|
if (!preCheckEvent) {
|
|
513
521
|
logger.warn('Skipping credit consumption job: MeterEvent not found', { meterEventId });
|
|
514
522
|
return;
|
|
515
523
|
}
|
|
524
|
+
assertJobObjectTenant(preCheckEvent);
|
|
516
525
|
if (preCheckEvent.status === 'completed' || preCheckEvent.status === 'canceled') {
|
|
517
526
|
logger.info('Skipping credit consumption job: MeterEvent already processed', {
|
|
518
527
|
meterEventId,
|
|
@@ -554,8 +563,8 @@ export async function handleCreditConsumption(job: CreditConsumptionJob) {
|
|
|
554
563
|
|
|
555
564
|
// Fresh loads inside lock for consistency
|
|
556
565
|
const [freshEvent, freshSubscription] = await Promise.all([
|
|
557
|
-
MeterEvent
|
|
558
|
-
context._subscriptionId ? Subscription
|
|
566
|
+
systemFindByPk(MeterEvent, meterEventId),
|
|
567
|
+
context._subscriptionId ? systemFindByPk(Subscription, context._subscriptionId) : Promise.resolve(null),
|
|
559
568
|
]);
|
|
560
569
|
if (!freshEvent) {
|
|
561
570
|
logger.warn('MeterEvent disappeared after lock acquired', { meterEventId });
|
|
@@ -691,7 +700,7 @@ export async function handleCreditConsumption(job: CreditConsumptionJob) {
|
|
|
691
700
|
|
|
692
701
|
// Handle retry logic with more robust error handling
|
|
693
702
|
try {
|
|
694
|
-
const meterEvent = await MeterEvent
|
|
703
|
+
const meterEvent = await systemFindByPk(MeterEvent, meterEventId);
|
|
695
704
|
if (meterEvent && !['completed', 'canceled', 'requires_action'].includes(meterEvent.status)) {
|
|
696
705
|
const attemptCount = meterEvent.attempt_count + 1;
|
|
697
706
|
const nonRetryable = isNonRetryableCreditError(error);
|
|
@@ -762,9 +771,6 @@ export async function handleCreditConsumption(job: CreditConsumptionJob) {
|
|
|
762
771
|
// ============================================================================
|
|
763
772
|
// Batch credit consumption
|
|
764
773
|
// ============================================================================
|
|
765
|
-
const CREDIT_BATCH_SIZE = Math.max(1, parseInt(process.env.CREDIT_BATCH_SIZE || '50', 10));
|
|
766
|
-
const CREDIT_BATCH_WINDOW_MS = Math.max(10, parseInt(process.env.CREDIT_BATCH_WINDOW_MS || '3000', 10));
|
|
767
|
-
|
|
768
774
|
type PendingBatch = { eventIds: string[]; timer: NodeJS.Timeout | null };
|
|
769
775
|
const pendingBatches = new Map<string, PendingBatch>();
|
|
770
776
|
// Track in-flight batch jobs per key to avoid head-of-line blocking.
|
|
@@ -785,13 +791,13 @@ function addToBatch(customerId: string, eventName: string, meterEventId: string,
|
|
|
785
791
|
|
|
786
792
|
batch.eventIds.push(meterEventId);
|
|
787
793
|
|
|
788
|
-
if (batch.eventIds.length >=
|
|
794
|
+
if (batch.eventIds.length >= creditBatchSize()) {
|
|
789
795
|
flushBatch(key);
|
|
790
796
|
return;
|
|
791
797
|
}
|
|
792
798
|
|
|
793
799
|
if (!batch.timer) {
|
|
794
|
-
batch.timer = setTimeout(() => flushBatch(key),
|
|
800
|
+
batch.timer = setTimeout(() => flushBatch(key), creditBatchWindowMs());
|
|
795
801
|
}
|
|
796
802
|
}
|
|
797
803
|
|
|
@@ -864,7 +870,7 @@ async function handleBatchCreditConsumptionInner(meterEventIds: string[], batchS
|
|
|
864
870
|
// ==========================================
|
|
865
871
|
// Pre-check: filter already-processed events
|
|
866
872
|
// ==========================================
|
|
867
|
-
const preCheckEvents = await MeterEvent
|
|
873
|
+
const preCheckEvents = await systemFindAll(MeterEvent, {
|
|
868
874
|
where: { id: { [Op.in]: meterEventIds } },
|
|
869
875
|
});
|
|
870
876
|
|
|
@@ -894,7 +900,7 @@ async function handleBatchCreditConsumptionInner(meterEventIds: string[], batchS
|
|
|
894
900
|
// ==========================================
|
|
895
901
|
const [meter, customer] = await Promise.all([
|
|
896
902
|
getCachedMeterExpanded(eventName) as Promise<TMeterExpanded | null>,
|
|
897
|
-
Customer
|
|
903
|
+
systemFindByPk(Customer, customerId),
|
|
898
904
|
]);
|
|
899
905
|
|
|
900
906
|
if (!meter || !meter.currency_id || !customer) {
|
|
@@ -925,7 +931,7 @@ async function handleBatchCreditConsumptionInner(meterEventIds: string[], batchS
|
|
|
925
931
|
// Fresh read inside lock
|
|
926
932
|
// ==========================================
|
|
927
933
|
const processableIds = processableEvents.map((e) => e.id);
|
|
928
|
-
const freshEvents = await MeterEvent
|
|
934
|
+
const freshEvents = await systemFindAll(MeterEvent, {
|
|
929
935
|
where: { id: { [Op.in]: processableIds } },
|
|
930
936
|
});
|
|
931
937
|
|
|
@@ -940,7 +946,7 @@ async function handleBatchCreditConsumptionInner(meterEventIds: string[], batchS
|
|
|
940
946
|
let priceIds: string[] | undefined;
|
|
941
947
|
|
|
942
948
|
if (subscriptionId) {
|
|
943
|
-
subscription = await Subscription
|
|
949
|
+
subscription = await systemFindByPk(Subscription, subscriptionId);
|
|
944
950
|
if (!subscription) {
|
|
945
951
|
logger.warn('Batch: Subscription not found inside lock, skipping', {
|
|
946
952
|
customerId,
|
|
@@ -965,7 +971,7 @@ async function handleBatchCreditConsumptionInner(meterEventIds: string[], batchS
|
|
|
965
971
|
// Batch idempotency check
|
|
966
972
|
// ==========================================
|
|
967
973
|
const allEventIds = freshProcessable.map((e) => e.id);
|
|
968
|
-
const existingTransactions = await CreditTransaction
|
|
974
|
+
const existingTransactions = await systemFindAll(CreditTransaction, {
|
|
969
975
|
where: { source: { [Op.in]: allEventIds } },
|
|
970
976
|
});
|
|
971
977
|
const txBySource = new Map<string, CreditTransaction[]>();
|
|
@@ -1249,11 +1255,6 @@ async function handleBatchCreditConsumptionInner(meterEventIds: string[], batchS
|
|
|
1249
1255
|
// Queue setup
|
|
1250
1256
|
// ============================================================================
|
|
1251
1257
|
|
|
1252
|
-
const creditQueueConcurrency = Math.max(
|
|
1253
|
-
1,
|
|
1254
|
-
Math.min(20, parseInt(process.env.CREDIT_QUEUE_CONCURRENCY || '5', 10) || 5)
|
|
1255
|
-
);
|
|
1256
|
-
|
|
1257
1258
|
export const creditQueue = createQueue<CreditConsumptionJob | BatchCreditConsumptionJob>({
|
|
1258
1259
|
name: 'credit-consumption',
|
|
1259
1260
|
onJob: (job) => {
|
|
@@ -1263,7 +1264,7 @@ export const creditQueue = createQueue<CreditConsumptionJob | BatchCreditConsump
|
|
|
1263
1264
|
return handleCreditConsumption(job as CreditConsumptionJob);
|
|
1264
1265
|
},
|
|
1265
1266
|
options: {
|
|
1266
|
-
concurrency: creditQueueConcurrency,
|
|
1267
|
+
concurrency: creditQueueConcurrency(),
|
|
1267
1268
|
maxRetries: 0,
|
|
1268
1269
|
enableScheduledJob: true,
|
|
1269
1270
|
},
|
|
@@ -1308,11 +1309,12 @@ const addCreditConsumptionJob = async (
|
|
|
1308
1309
|
}
|
|
1309
1310
|
|
|
1310
1311
|
if (!options.skipStatusCheck) {
|
|
1311
|
-
const meterEvent = await MeterEvent
|
|
1312
|
+
const meterEvent = await systemFindByPk(MeterEvent, meterEventId);
|
|
1312
1313
|
if (!meterEvent) {
|
|
1313
1314
|
logger.warn('Cannot add credit consumption job: MeterEvent not found', { meterEventId });
|
|
1314
1315
|
return;
|
|
1315
1316
|
}
|
|
1317
|
+
assertJobObjectTenant(meterEvent);
|
|
1316
1318
|
|
|
1317
1319
|
if (meterEvent.status === 'completed' || meterEvent.status === 'canceled') {
|
|
1318
1320
|
logger.debug('Skipping credit consumption job: MeterEvent already processed', {
|
|
@@ -1342,7 +1344,7 @@ creditQueue.on('retry', ({ id, job }) => {
|
|
|
1342
1344
|
});
|
|
1343
1345
|
|
|
1344
1346
|
export async function startCreditConsumeQueue(): Promise<void> {
|
|
1345
|
-
const lock = getLock('startCreditConsumeQueue');
|
|
1347
|
+
const lock = getLock('startCreditConsumeQueue', { scope: 'global' });
|
|
1346
1348
|
if (lock.locked) {
|
|
1347
1349
|
return;
|
|
1348
1350
|
}
|
|
@@ -1359,7 +1361,7 @@ export async function startCreditConsumeQueue(): Promise<void> {
|
|
|
1359
1361
|
|
|
1360
1362
|
do {
|
|
1361
1363
|
// eslint-disable-next-line no-await-in-loop
|
|
1362
|
-
batchEvents = await MeterEvent
|
|
1364
|
+
batchEvents = await systemFindAll(MeterEvent, {
|
|
1363
1365
|
where: {
|
|
1364
1366
|
status: ['pending', 'requires_capture', 'processing'],
|
|
1365
1367
|
},
|
|
@@ -1491,13 +1493,14 @@ events.on('customer.credit_grant.granted', async (creditGrant: CreditGrant) => {
|
|
|
1491
1493
|
});
|
|
1492
1494
|
|
|
1493
1495
|
async function retryFailedEventsForCustomer(creditGrant: CreditGrant): Promise<void> {
|
|
1494
|
-
const grant = await CreditGrant
|
|
1496
|
+
const grant = await systemFindByPk(CreditGrant, creditGrant.id);
|
|
1495
1497
|
if (!grant) {
|
|
1496
1498
|
logger.error('Credit grant not found', {
|
|
1497
1499
|
creditGrantId: creditGrant.id,
|
|
1498
1500
|
});
|
|
1499
1501
|
return;
|
|
1500
1502
|
}
|
|
1503
|
+
assertJobObjectTenant(grant);
|
|
1501
1504
|
|
|
1502
1505
|
const customerId = grant.customer_id;
|
|
1503
1506
|
const currencyId = grant.currency_id;
|
|
@@ -1583,8 +1586,9 @@ async function retryFailedEventsForCustomer(creditGrant: CreditGrant): Promise<v
|
|
|
1583
1586
|
|
|
1584
1587
|
let chunkIndex = 0;
|
|
1585
1588
|
for (const [, subEventIds] of bySubscription) {
|
|
1586
|
-
|
|
1587
|
-
|
|
1589
|
+
const batchSize = creditBatchSize();
|
|
1590
|
+
for (let i = 0; i < subEventIds.length; i += batchSize) {
|
|
1591
|
+
const chunk = subEventIds.slice(i, i + batchSize);
|
|
1588
1592
|
creditQueue.push({
|
|
1589
1593
|
id: `retry-batch-${customerId}-${Date.now()}-${chunkIndex++}`,
|
|
1590
1594
|
job: { meterEventIds: chunk } as any,
|
|
@@ -4,7 +4,8 @@ import { Op } from 'sequelize';
|
|
|
4
4
|
import { BN, fromTokenToUnit, fromUnitToToken } from '@ocap/util';
|
|
5
5
|
import pAll from 'p-all';
|
|
6
6
|
import dayjs from '../libs/dayjs';
|
|
7
|
-
import createQueue from '../libs/queue';
|
|
7
|
+
import createQueue, { assertJobObjectTenant } from '../libs/queue';
|
|
8
|
+
import { systemFindAll, systemFindByPk } from '../store/scoped';
|
|
8
9
|
import {
|
|
9
10
|
AutoRechargeConfig,
|
|
10
11
|
CreditGrant,
|
|
@@ -48,7 +49,7 @@ type CreditGrantJob =
|
|
|
48
49
|
* Non on-chain credits finish immediately; on-chain credits wait for mint success
|
|
49
50
|
*/
|
|
50
51
|
async function activateGrant(creditGrant: CreditGrant) {
|
|
51
|
-
const paymentCurrency = await PaymentCurrency
|
|
52
|
+
const paymentCurrency = await systemFindByPk(PaymentCurrency, creditGrant.currency_id);
|
|
52
53
|
const isOnChainCredit = paymentCurrency && PaymentCurrency.isOnChainCredit(paymentCurrency);
|
|
53
54
|
|
|
54
55
|
logger.info('Activating credit grant', {
|
|
@@ -76,10 +77,11 @@ async function activateGrant(creditGrant: CreditGrant) {
|
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
// Verify customer
|
|
79
|
-
const customer = await Customer
|
|
80
|
+
const customer = await systemFindByPk(Customer, creditGrant.customer_id);
|
|
80
81
|
if (!customer) {
|
|
81
82
|
throw new Error(`Customer DID not found for credit grant ${creditGrant.id}`);
|
|
82
83
|
}
|
|
84
|
+
assertJobObjectTenant(customer);
|
|
83
85
|
|
|
84
86
|
const customerState = await getAccountState(paymentCurrency, customer.did);
|
|
85
87
|
|
|
@@ -161,7 +163,7 @@ export async function expireGrant(creditGrant: CreditGrant) {
|
|
|
161
163
|
return;
|
|
162
164
|
}
|
|
163
165
|
|
|
164
|
-
const paymentCurrency = await PaymentCurrency
|
|
166
|
+
const paymentCurrency = await systemFindByPk(PaymentCurrency, creditGrant.currency_id);
|
|
165
167
|
if (!paymentCurrency) {
|
|
166
168
|
logger.error('Payment currency not found for credit grant transfer', {
|
|
167
169
|
creditGrantId: creditGrant.id,
|
|
@@ -171,9 +173,10 @@ export async function expireGrant(creditGrant: CreditGrant) {
|
|
|
171
173
|
await creditGrant.update({ status: 'expired' });
|
|
172
174
|
return;
|
|
173
175
|
}
|
|
176
|
+
assertJobObjectTenant(paymentCurrency);
|
|
174
177
|
|
|
175
178
|
// Get customer DID
|
|
176
|
-
const customer = await Customer
|
|
179
|
+
const customer = await systemFindByPk(Customer, creditGrant.customer_id);
|
|
177
180
|
if (!customer?.did) {
|
|
178
181
|
logger.error('Customer DID not found for credit grant transfer', {
|
|
179
182
|
creditGrantId: creditGrant.id,
|
|
@@ -280,11 +283,12 @@ const handleCreditGrantJob = async (job: CreditGrantJob) => {
|
|
|
280
283
|
|
|
281
284
|
const { creditGrantId, action } = job as { creditGrantId: string; action: 'activate' | 'expire' };
|
|
282
285
|
|
|
283
|
-
const creditGrant = await CreditGrant
|
|
286
|
+
const creditGrant = await systemFindByPk(CreditGrant, creditGrantId);
|
|
284
287
|
if (!creditGrant) {
|
|
285
288
|
logger.error('Credit grant not found', { creditGrantId });
|
|
286
289
|
return;
|
|
287
290
|
}
|
|
291
|
+
assertJobObjectTenant(creditGrant);
|
|
288
292
|
|
|
289
293
|
const now = dayjs().unix();
|
|
290
294
|
|
|
@@ -411,7 +415,7 @@ export async function scheduleCreditGrantJobs(creditGrant: CreditGrant) {
|
|
|
411
415
|
export const startCreditGrantQueue = async () => {
|
|
412
416
|
logger.info('Starting credit grant queue...');
|
|
413
417
|
|
|
414
|
-
const grantsToSchedule = await CreditGrant
|
|
418
|
+
const grantsToSchedule = await systemFindAll(CreditGrant, {
|
|
415
419
|
where: {
|
|
416
420
|
[Op.or]: [
|
|
417
421
|
{ status: 'pending' },
|
|
@@ -424,7 +428,7 @@ export const startCreditGrantQueue = async () => {
|
|
|
424
428
|
order: [['created_at', 'ASC']],
|
|
425
429
|
});
|
|
426
430
|
|
|
427
|
-
const invoicesToSchedule = await Invoice
|
|
431
|
+
const invoicesToSchedule = await systemFindAll(Invoice, {
|
|
428
432
|
where: {
|
|
429
433
|
status: 'paid',
|
|
430
434
|
metadata: {
|
|
@@ -482,7 +486,7 @@ async function recoverCreditScheduleJobs(): Promise<void> {
|
|
|
482
486
|
try {
|
|
483
487
|
// Find subscriptions with active credit schedules
|
|
484
488
|
// We filter for non-null credit_schedule_state in application code
|
|
485
|
-
const allActiveSubscriptions = await Subscription
|
|
489
|
+
const allActiveSubscriptions = await systemFindAll(Subscription, {
|
|
486
490
|
where: {
|
|
487
491
|
status: ['active', 'trialing'],
|
|
488
492
|
},
|
|
@@ -589,7 +593,7 @@ creditGrantQueue.on('finished', ({ id }) => {
|
|
|
589
593
|
|
|
590
594
|
async function handleInvoiceCredit(invoiceId: string) {
|
|
591
595
|
try {
|
|
592
|
-
const invoice = (await Invoice
|
|
596
|
+
const invoice = (await systemFindByPk(Invoice, invoiceId, {
|
|
593
597
|
include: [
|
|
594
598
|
{
|
|
595
599
|
model: Customer,
|
|
@@ -619,7 +623,7 @@ async function handleInvoiceCredit(invoiceId: string) {
|
|
|
619
623
|
customerId: invoice.customer.id,
|
|
620
624
|
});
|
|
621
625
|
if (invoice.metadata?.auto_recharge?.config_id) {
|
|
622
|
-
const autoRechargeConfig = await AutoRechargeConfig
|
|
626
|
+
const autoRechargeConfig = await systemFindByPk(AutoRechargeConfig, invoice.metadata?.auto_recharge?.config_id);
|
|
623
627
|
if (autoRechargeConfig) {
|
|
624
628
|
await autoRechargeConfig.updateDailyStats(invoice.total);
|
|
625
629
|
}
|
|
@@ -635,7 +639,7 @@ async function handleInvoiceCredit(invoiceId: string) {
|
|
|
635
639
|
|
|
636
640
|
const items = await Promise.all(
|
|
637
641
|
lineItems.map(async (lineItem) => {
|
|
638
|
-
const price = (await Price
|
|
642
|
+
const price = (await systemFindByPk(Price, lineItem.price_id, {
|
|
639
643
|
include: [{ model: Product, as: 'product' }],
|
|
640
644
|
})) as TPriceExpanded | null;
|
|
641
645
|
|
|
@@ -664,7 +668,7 @@ async function handleInvoiceCredit(invoiceId: string) {
|
|
|
664
668
|
}
|
|
665
669
|
|
|
666
670
|
const quantity = lineItem.quantity || 1;
|
|
667
|
-
const currency = await PaymentCurrency
|
|
671
|
+
const currency = await systemFindByPk(PaymentCurrency, creditConfig.currency_id);
|
|
668
672
|
|
|
669
673
|
if (!currency) {
|
|
670
674
|
logger.error('Currency not found', { currencyId: creditConfig.currency_id, invoiceId });
|
|
@@ -726,7 +730,7 @@ async function handleInvoiceCredit(invoiceId: string) {
|
|
|
726
730
|
return;
|
|
727
731
|
}
|
|
728
732
|
|
|
729
|
-
const existingGrants = await CreditGrant
|
|
733
|
+
const existingGrants = await systemFindAll(CreditGrant, {
|
|
730
734
|
where: {
|
|
731
735
|
customer_id: invoice.customer.id,
|
|
732
736
|
metadata: {
|
|
@@ -972,7 +976,7 @@ events.on('invoice.paid', async (invoice: Invoice) => {
|
|
|
972
976
|
try {
|
|
973
977
|
await addInvoiceCreditJob(invoice.id);
|
|
974
978
|
if (invoice.subscription_id) {
|
|
975
|
-
const subscription = await Subscription
|
|
979
|
+
const subscription = await systemFindByPk(Subscription, invoice.subscription_id);
|
|
976
980
|
if (subscription) {
|
|
977
981
|
const jobs = await activateAfterFirstPaymentSchedules(subscription, invoice.status_transitions?.paid_at);
|
|
978
982
|
await Promise.all(jobs.map((job) => addScheduledCreditJob(job.jobId, job.job, job.runAt, true)));
|
|
@@ -989,11 +993,12 @@ events.on('invoice.paid', async (invoice: Invoice) => {
|
|
|
989
993
|
events.on('customer.credit_grant.created', async (data: { id: string }) => {
|
|
990
994
|
try {
|
|
991
995
|
// Fetch instance since event emits dataValues (plain object)
|
|
992
|
-
const creditGrant = await CreditGrant
|
|
996
|
+
const creditGrant = await systemFindByPk(CreditGrant, data.id);
|
|
993
997
|
if (!creditGrant) {
|
|
994
998
|
logger.error('Credit grant not found for scheduling', { creditGrantId: data.id });
|
|
995
999
|
return;
|
|
996
1000
|
}
|
|
1001
|
+
assertJobObjectTenant(creditGrant);
|
|
997
1002
|
await scheduleCreditGrantJobs(creditGrant);
|
|
998
1003
|
logger.info('Credit grant jobs scheduled', {
|
|
999
1004
|
creditGrantId: creditGrant.id,
|
|
@@ -1053,7 +1058,7 @@ events.on('credit-grant.queued', async (id, job, args = {}) => {
|
|
|
1053
1058
|
* Used by both subscription.created and subscription.started events
|
|
1054
1059
|
*/
|
|
1055
1060
|
async function initializeAndScheduleCreditJobs(subscriptionId: string, eventName: string): Promise<void> {
|
|
1056
|
-
const sub = await Subscription
|
|
1061
|
+
const sub = await systemFindByPk(Subscription, subscriptionId);
|
|
1057
1062
|
if (!sub) {
|
|
1058
1063
|
logger.warn('Subscription not found for credit schedule initialization', {
|
|
1059
1064
|
subscriptionId,
|
|
@@ -1061,6 +1066,7 @@ async function initializeAndScheduleCreditJobs(subscriptionId: string, eventName
|
|
|
1061
1066
|
});
|
|
1062
1067
|
return;
|
|
1063
1068
|
}
|
|
1069
|
+
assertJobObjectTenant(sub);
|
|
1064
1070
|
|
|
1065
1071
|
// initializeCreditSchedule only works for active/trialing subscriptions
|
|
1066
1072
|
// and skips if already initialized
|
|
@@ -1147,10 +1153,11 @@ events.on('customer.subscription.trial_start', async (subscription: Subscription
|
|
|
1147
1153
|
*/
|
|
1148
1154
|
events.on('customer.subscription.deleted', async (subscription: Subscription) => {
|
|
1149
1155
|
try {
|
|
1150
|
-
const sub = await Subscription
|
|
1156
|
+
const sub = await systemFindByPk(Subscription, subscription.id);
|
|
1151
1157
|
if (!sub) {
|
|
1152
1158
|
return;
|
|
1153
1159
|
}
|
|
1160
|
+
assertJobObjectTenant(sub);
|
|
1154
1161
|
|
|
1155
1162
|
const jobIds = await stopCreditSchedule(sub);
|
|
1156
1163
|
|
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
|
|
8
8
|
import { BN } from '@ocap/util';
|
|
9
9
|
import logger from '../libs/logger';
|
|
10
|
-
import createQueue from '../libs/queue';
|
|
10
|
+
import createQueue, { assertJobObjectTenant } from '../libs/queue';
|
|
11
11
|
import { getAccountState, getCustomerTokenBalance } from '../integrations/arcblock/token';
|
|
12
12
|
import { CreditGrant, CreditTransaction, Customer, PaymentCurrency } from '../store/models';
|
|
13
13
|
import { events } from '../libs/event';
|
|
14
|
+
import { systemFindAll, systemFindByPk } from '../store/scoped';
|
|
14
15
|
|
|
15
16
|
type ReconciliationJob = {
|
|
16
17
|
customerId: string;
|
|
@@ -39,7 +40,7 @@ type BalanceInfo = {
|
|
|
39
40
|
*/
|
|
40
41
|
async function getBalanceInfo(customerId: string, currencyId: string): Promise<BalanceInfo> {
|
|
41
42
|
// Get all granted credit grants that have been minted on-chain
|
|
42
|
-
const grants = await CreditGrant
|
|
43
|
+
const grants = await systemFindAll(CreditGrant, {
|
|
43
44
|
where: {
|
|
44
45
|
customer_id: customerId,
|
|
45
46
|
currency_id: currencyId,
|
|
@@ -53,7 +54,7 @@ async function getBalanceInfo(customerId: string, currencyId: string): Promise<B
|
|
|
53
54
|
.toString();
|
|
54
55
|
|
|
55
56
|
// Get pending transfers (consumed but token not yet transferred on-chain)
|
|
56
|
-
const pendingTransactions = await CreditTransaction
|
|
57
|
+
const pendingTransactions = await systemFindAll(CreditTransaction, {
|
|
57
58
|
where: {
|
|
58
59
|
customer_id: customerId,
|
|
59
60
|
transfer_status: 'pending',
|
|
@@ -98,13 +99,14 @@ async function handleReconciliation(job: ReconciliationJob) {
|
|
|
98
99
|
});
|
|
99
100
|
|
|
100
101
|
try {
|
|
101
|
-
const currency = await PaymentCurrency
|
|
102
|
+
const currency = await systemFindByPk(PaymentCurrency, currencyId);
|
|
102
103
|
if (!currency || !currency.isOnChainCredit()) {
|
|
103
104
|
logger.warn('Currency not found or not on-chain credit', { currencyId });
|
|
104
105
|
return;
|
|
105
106
|
}
|
|
106
107
|
|
|
107
|
-
const customer = await Customer
|
|
108
|
+
const customer = await systemFindByPk(Customer, customerId);
|
|
109
|
+
assertJobObjectTenant(customer);
|
|
108
110
|
if (!customer?.did) {
|
|
109
111
|
logger.warn('Customer not found for reconciliation', { customerId });
|
|
110
112
|
return;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import pAll from 'p-all';
|
|
2
|
-
import createQueue from '../libs/queue';
|
|
2
|
+
import createQueue, { assertJobObjectTenant } from '../libs/queue';
|
|
3
3
|
import { Coupon, PromotionCode } from '../store/models';
|
|
4
4
|
import logger from '../libs/logger';
|
|
5
5
|
import { events } from '../libs/event';
|
|
6
6
|
import { validCoupon, validPromotionCode } from '../libs/discount/coupon';
|
|
7
|
+
import { systemFindAll, systemFindByPk } from '../store/scoped';
|
|
7
8
|
|
|
8
9
|
export type DiscountType = 'coupon' | 'promotion-code';
|
|
9
10
|
|
|
@@ -30,10 +31,11 @@ export async function processDiscountStatus(job: DiscountStatusJobData) {
|
|
|
30
31
|
* Process coupon status check
|
|
31
32
|
*/
|
|
32
33
|
async function processCouponStatus(couponId: string) {
|
|
33
|
-
const coupon = await Coupon
|
|
34
|
+
const coupon = await systemFindByPk(Coupon, couponId);
|
|
34
35
|
if (!coupon) {
|
|
35
36
|
return;
|
|
36
37
|
}
|
|
38
|
+
assertJobObjectTenant(coupon);
|
|
37
39
|
|
|
38
40
|
// Skip if already invalid
|
|
39
41
|
if (!coupon.valid) {
|
|
@@ -54,7 +56,7 @@ async function processCouponStatus(couponId: string) {
|
|
|
54
56
|
max_redemptions: coupon.max_redemptions,
|
|
55
57
|
times_redeemed: coupon.times_redeemed,
|
|
56
58
|
});
|
|
57
|
-
const promotionCodes = await PromotionCode
|
|
59
|
+
const promotionCodes = await systemFindAll(PromotionCode, { where: { coupon_id: couponId, active: true } });
|
|
58
60
|
await pAll(
|
|
59
61
|
promotionCodes.map(
|
|
60
62
|
(promotionCode) => () =>
|
|
@@ -76,11 +78,12 @@ async function processCouponStatus(couponId: string) {
|
|
|
76
78
|
* Process promotion code status check
|
|
77
79
|
*/
|
|
78
80
|
async function processPromotionCodeStatus(promotionCodeId: string) {
|
|
79
|
-
const promotionCode = await PromotionCode
|
|
81
|
+
const promotionCode = await systemFindByPk(PromotionCode, promotionCodeId);
|
|
80
82
|
if (!promotionCode) {
|
|
81
83
|
logger.warn('Promotion code not found for status check', { promotionCodeId });
|
|
82
84
|
return;
|
|
83
85
|
}
|
|
86
|
+
assertJobObjectTenant(promotionCode);
|
|
84
87
|
|
|
85
88
|
// Skip if already inactive
|
|
86
89
|
if (!promotionCode.active) {
|
|
@@ -146,8 +149,8 @@ export const discountStatusQueue = createQueue<DiscountStatusJobData>({
|
|
|
146
149
|
export const startDiscountStatusQueue = async () => {
|
|
147
150
|
logger.info('Starting discount status queue...');
|
|
148
151
|
|
|
149
|
-
const coupons = await Coupon
|
|
150
|
-
const promotionCodes = await PromotionCode
|
|
152
|
+
const coupons = await systemFindAll(Coupon, { where: { valid: true } });
|
|
153
|
+
const promotionCodes = await systemFindAll(PromotionCode, { where: { active: true } });
|
|
151
154
|
await pAll(
|
|
152
155
|
coupons.map(
|
|
153
156
|
(coupon) => () =>
|
package/api/src/queues/event.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { Op } from 'sequelize';
|
|
2
|
+
import { isCfWorker } from '../libs/env';
|
|
3
|
+
import { systemFindAll, systemFindByPk } from '../store/scoped';
|
|
2
4
|
|
|
5
|
+
import { withTenant } from '../libs/context';
|
|
3
6
|
import { events } from '../libs/event';
|
|
4
7
|
import logger from '../libs/logger';
|
|
5
8
|
import createQueue from '../libs/queue';
|
|
9
|
+
import { resolveRowTenant } from '../libs/tenant';
|
|
6
10
|
import { Event } from '../store/models/event';
|
|
7
11
|
import { WebhookAttempt } from '../store/models/webhook-attempt';
|
|
8
12
|
import { WebhookEndpoint } from '../store/models/webhook-endpoint';
|
|
@@ -15,7 +19,7 @@ type EventJob = {
|
|
|
15
19
|
export const handleEvent = async (job: EventJob) => {
|
|
16
20
|
logger.info('Starting to handle event', job);
|
|
17
21
|
|
|
18
|
-
const event = await Event
|
|
22
|
+
const event = await systemFindByPk(Event, job.eventId);
|
|
19
23
|
if (!event) {
|
|
20
24
|
logger.warn('Event not found', job);
|
|
21
25
|
return;
|
|
@@ -26,7 +30,12 @@ export const handleEvent = async (job: EventJob) => {
|
|
|
26
30
|
return;
|
|
27
31
|
}
|
|
28
32
|
|
|
29
|
-
|
|
33
|
+
// Phase 4 (W1-3): fanout only to endpoints of the event's own tenant.
|
|
34
|
+
// resolveRowTenant fails closed for NULL-tenant events in multi mode.
|
|
35
|
+
const eventTenant = resolveRowTenant(event);
|
|
36
|
+
const webhooks = await systemFindAll(WebhookEndpoint, {
|
|
37
|
+
where: { status: 'enabled', livemode: event.livemode, instance_did: eventTenant },
|
|
38
|
+
});
|
|
30
39
|
const eventWebhooks = webhooks.filter((webhook) => webhook.enabled_events.includes(event.type));
|
|
31
40
|
if (eventWebhooks.length === 0) {
|
|
32
41
|
logger.info('no webhook endpoint for event', job);
|
|
@@ -75,22 +84,43 @@ export const eventQueue = createQueue<EventJob>({
|
|
|
75
84
|
});
|
|
76
85
|
|
|
77
86
|
export const startEventQueue = async () => {
|
|
78
|
-
|
|
87
|
+
// Cross-tenant recovery scan: fetch instance_did too so the recovered push
|
|
88
|
+
// carries its tenant. In multi mode startup runs with NO tenant context, so a
|
|
89
|
+
// push without instance_did would hit injectJobTenant -> getInstanceDid ->
|
|
90
|
+
// TENANT_CONTEXT_MISSING (a FATAL unhandledRejection from the async forEach
|
|
91
|
+
// below). The handler re-derives the tenant from the row, but the PUSH itself
|
|
92
|
+
// must already be tenant-stamped to survive multi mode.
|
|
93
|
+
const docs = await systemFindAll(Event, {
|
|
79
94
|
where: {
|
|
80
95
|
pending_webhooks: { [Op.gt]: 0 },
|
|
81
96
|
},
|
|
82
|
-
attributes: ['id'],
|
|
97
|
+
attributes: ['id', 'instance_did'],
|
|
83
98
|
});
|
|
84
99
|
|
|
85
100
|
logger.info(`Found ${docs.length} events with pending webhooks`);
|
|
86
101
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
102
|
+
// Sequential + per-doc try/catch: one bad row must never crash startup
|
|
103
|
+
// recovery (await in the original forEach left rejections unhandled). The
|
|
104
|
+
// push runs inside withTenant(event.instance_did) so injectJobTenant stamps
|
|
105
|
+
// the correct tenant — in multi mode there is no ambient context at startup.
|
|
106
|
+
for (const x of docs) {
|
|
107
|
+
if (!x.instance_did) {
|
|
108
|
+
logger.warn('skip pending-webhook event with no tenant', { id: x.id });
|
|
109
|
+
} else {
|
|
110
|
+
try {
|
|
111
|
+
// eslint-disable-next-line no-await-in-loop -- bounded startup recovery
|
|
112
|
+
await withTenant(x.instance_did, async () => {
|
|
113
|
+
const exist = await eventQueue.get(x.id);
|
|
114
|
+
if (!exist) {
|
|
115
|
+
logger.info(`Pushing event ${x.id} to queue`);
|
|
116
|
+
eventQueue.push({ id: x.id, job: { eventId: x.id }, persist: false });
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
} catch (error) {
|
|
120
|
+
logger.error('failed to recover pending-webhook event', { id: x.id, error });
|
|
121
|
+
}
|
|
92
122
|
}
|
|
93
|
-
}
|
|
123
|
+
}
|
|
94
124
|
|
|
95
125
|
logger.info('Finished starting event queue');
|
|
96
126
|
};
|
|
@@ -100,7 +130,7 @@ eventQueue.on('failed', ({ id, job, error }) => {
|
|
|
100
130
|
});
|
|
101
131
|
|
|
102
132
|
events.on('event.created', async (event) => {
|
|
103
|
-
if ((
|
|
133
|
+
if (isCfWorker()) {
|
|
104
134
|
// CF Workers: execute inline to save 2 CF Queue ops per event.
|
|
105
135
|
// eventQueue only dispatches webhooks — lightweight DB lookup + webhookQueue.push.
|
|
106
136
|
// Webhook delivery still goes through webhookQueue with full retry guarantees.
|