payment-kit 1.27.2 → 1.28.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 +10 -0
- package/api/src/crons/metering-subscription-detection.ts +12 -14
- package/api/src/crons/overdue-detection.ts +51 -74
- 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/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 +32 -16
- package/api/src/libs/auth.ts +49 -2
- package/api/src/libs/chain-error.ts +31 -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/session.ts +1 -1
- package/api/src/libs/timing.ts +35 -0
- package/api/src/libs/util.ts +16 -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 +14 -2
- 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/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/meter-events.ts +16 -6
- package/api/src/routes/payment-links.ts +1 -1
- package/api/src/routes/payment-methods.ts +1 -1
- package/api/src/routes/settings.ts +1 -1
- package/api/src/routes/tax-rates.ts +1 -1
- package/api/src/store/models/customer.ts +23 -1
- package/api/src/store/models/payment-method.ts +4 -0
- package/api/src/store/models/price.ts +23 -14
- 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/run-build.js +390 -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-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 +585 -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 +1157 -0
- package/cloudflare/shims/sequelize-d1/operators.ts +293 -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-scheduled.spec.ts +186 -0
- package/cloudflare/vite.config.ts +162 -0
- package/cloudflare/worker.ts +1553 -0
- package/cloudflare/wrangler.json +63 -0
- package/cloudflare/wrangler.jsonc +69 -0
- package/cloudflare/wrangler.staging.json +66 -0
- package/cloudflare/wrangler.toml +28 -0
- package/jest.config.js +4 -12
- package/package.json +26 -22
- 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/safe-did-address.tsx +75 -0
- package/src/libs/patch-user-card.ts +25 -0
- package/src/libs/util.ts +5 -7
- package/src/pages/admin/billing/meter-events/index.tsx +4 -0
- package/src/pages/admin/customers/customers/detail.tsx +2 -2
- package/src/pages/admin/customers/customers/index.tsx +2 -2
- package/src/pages/admin/overview.tsx +3 -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
package/__blocklet__.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Polyfill global for browser
|
|
2
|
+
if (typeof globalThis !== 'undefined' && typeof global === 'undefined') {
|
|
3
|
+
window.global = globalThis;
|
|
4
|
+
}
|
|
5
|
+
if (typeof globalThis !== 'undefined' && typeof Buffer === 'undefined') {
|
|
6
|
+
// Buffer polyfill will be loaded by vite-plugin-node-polyfills if configured
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// CF Workers shim for window.blocklet
|
|
10
|
+
// This file replaces the auto-generated __blocklet__.js from Blocklet Server
|
|
11
|
+
window.blocklet = window.blocklet || {};
|
|
12
|
+
Object.assign(window.blocklet, {
|
|
13
|
+
prefix: '',
|
|
14
|
+
groupPrefix: '',
|
|
15
|
+
did: 'did:abt:test',
|
|
16
|
+
componentId: null,
|
|
17
|
+
serverVersion: '3.0.0',
|
|
18
|
+
languages: [
|
|
19
|
+
{ code: 'en', name: 'English' },
|
|
20
|
+
{ code: 'zh', name: '简体中文' }
|
|
21
|
+
],
|
|
22
|
+
appPid: 'payment-kit-cf',
|
|
23
|
+
appId: 'payment-kit-cf',
|
|
24
|
+
appName: 'Payment Kit',
|
|
25
|
+
appLogo: '',
|
|
26
|
+
appDescription: 'Decentralized Payment System',
|
|
27
|
+
appUrl: window.location.origin,
|
|
28
|
+
GA_MEASUREMENT_ID: '',
|
|
29
|
+
theme: { prefer: 'light' },
|
|
30
|
+
navigation: [{ title: 'Home', link: '/', icon: '' }],
|
|
31
|
+
webWalletUrl: '',
|
|
32
|
+
componentMountPoints: [],
|
|
33
|
+
sessionPermissions: { canReadAll: true, canWriteAll: true },
|
|
34
|
+
settings: {
|
|
35
|
+
session: { cacheTtl: 3600 },
|
|
36
|
+
},
|
|
37
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Type shims for @ocap/* / @arcblock/* 1.30.x CBOR-only subpath entries.
|
|
2
|
+
//
|
|
3
|
+
// These packages ship runtime + `.d.mts` type files under the package.json
|
|
4
|
+
// `exports` map. tsc under `moduleResolution: node` cannot resolve `.d.mts`
|
|
5
|
+
// through the exports map, producing:
|
|
6
|
+
// error TS2307: Cannot find module '@arcblock/did-util/cbor' or its
|
|
7
|
+
// corresponding type declarations.
|
|
8
|
+
//
|
|
9
|
+
// Switching moduleResolution to `bundler`/`node16` would fix resolution but
|
|
10
|
+
// require matching `module: esnext`, which changes the emitted JS format for
|
|
11
|
+
// api/dist runtime. Not worth the blast radius.
|
|
12
|
+
//
|
|
13
|
+
// Instead: declare loose types here so tsc is satisfied. Runtime resolution
|
|
14
|
+
// still uses the real packages via Node/esbuild's own module loader (they
|
|
15
|
+
// handle .cjs + exports map correctly).
|
|
16
|
+
|
|
17
|
+
declare module '@arcblock/did-util/cbor' {
|
|
18
|
+
// biome-ignore lint/suspicious/noExplicitAny: loose shim; real types live in .d.mts
|
|
19
|
+
type AnyFn = (...args: any[]) => any;
|
|
20
|
+
export const toAssetAddress: AnyFn;
|
|
21
|
+
export const toDelegateAddress: AnyFn;
|
|
22
|
+
export const toFactoryAddress: AnyFn;
|
|
23
|
+
export const toItxAddress: AnyFn;
|
|
24
|
+
export const toRollupAddress: AnyFn;
|
|
25
|
+
export const toStakeAddress: AnyFn;
|
|
26
|
+
export const toTokenAddress: AnyFn;
|
|
27
|
+
export const toTokenFactoryAddress: AnyFn;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
declare module '@ocap/asset/mint/client' {
|
|
31
|
+
// biome-ignore lint/suspicious/noExplicitAny: loose shim; real types live in .d.mts
|
|
32
|
+
type AnyFn = (...args: any[]) => any;
|
|
33
|
+
export const formatFactoryState: AnyFn;
|
|
34
|
+
export const preMintFromFactory: AnyFn;
|
|
35
|
+
}
|
package/api/src/crons/index.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
import logger from '../libs/logger';
|
|
25
25
|
import { startCreditConsumeQueue } from '../queues/credit-consume';
|
|
26
26
|
import { startDepositVaultQueue } from '../queues/payment';
|
|
27
|
+
import { startRefundQueue } from '../queues/refund';
|
|
27
28
|
import { startSubscriptionQueue } from '../queues/subscription';
|
|
28
29
|
import { startVendorStatusCheckSchedule } from '../queues/vendors/status-check';
|
|
29
30
|
import { CheckoutSession } from '../store/models';
|
|
@@ -85,6 +86,15 @@ function init() {
|
|
|
85
86
|
fn: startSubscriptionQueue,
|
|
86
87
|
options: { runOnInit: false },
|
|
87
88
|
},
|
|
89
|
+
{
|
|
90
|
+
// Recover pending refunds that were missed (e.g. after deploy or failed dispatch).
|
|
91
|
+
// Previously invoked unconditionally on every CF scheduled tick; now throttled
|
|
92
|
+
// to every 5 minutes to stay within CF Queue free-tier ops budget.
|
|
93
|
+
name: 'refund.recovery',
|
|
94
|
+
time: '0 */5 * * * *',
|
|
95
|
+
fn: startRefundQueue,
|
|
96
|
+
options: { runOnInit: false },
|
|
97
|
+
},
|
|
88
98
|
{
|
|
89
99
|
name: 'checkoutSession.cleanup.expired',
|
|
90
100
|
time: expiredSessionCleanupCronTime,
|
|
@@ -119,20 +119,18 @@ export class MeteringSubscriptionDetectionTemplate implements BaseEmailTemplate<
|
|
|
119
119
|
});
|
|
120
120
|
|
|
121
121
|
const meteringInvoices: any[] = [];
|
|
122
|
-
await
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
})
|
|
135
|
-
);
|
|
122
|
+
const prices = (await Price.findAll()).map((x) => x.toJSON());
|
|
123
|
+
for (const invoice of invoices) {
|
|
124
|
+
const invoiceJson = invoice.toJSON();
|
|
125
|
+
// @ts-ignore
|
|
126
|
+
(invoiceJson.lines || []).forEach((item) => {
|
|
127
|
+
item.price = prices.find((x) => x.id === item.price_id);
|
|
128
|
+
});
|
|
129
|
+
// @ts-ignore
|
|
130
|
+
if ((invoiceJson.lines || []).some((line) => line.price?.recurring?.usage_type === 'metered')) {
|
|
131
|
+
meteringInvoices.push(invoice);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
136
134
|
const meteringInvoiceIds = Array.from(new Set(meteringInvoices.map((invoice) => invoice.id)));
|
|
137
135
|
const meteringSubscriptionIds = Array.from(
|
|
138
136
|
new Set(meteringInvoices.map((invoice) => invoice?.subscription_id).filter(Boolean) as string[])
|
|
@@ -102,19 +102,19 @@ export class HealthReportTemplate implements BaseEmailTemplate<HealthReportConte
|
|
|
102
102
|
const startTime = dayjs.unix(start).toISOString();
|
|
103
103
|
const endTime = dayjs.unix(end).toISOString();
|
|
104
104
|
|
|
105
|
-
// 1. Get all currency configurations
|
|
106
|
-
const allCurrencies = await
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
[Op.between]: [startTime, endTime],
|
|
105
|
+
// 1. Get all currency configurations + pending meter events in parallel
|
|
106
|
+
const [allCurrencies, events] = await Promise.all([
|
|
107
|
+
PaymentCurrency.findAll(),
|
|
108
|
+
MeterEvent.findAll({
|
|
109
|
+
where: {
|
|
110
|
+
status: ['pending', 'requires_capture', 'requires_action'],
|
|
111
|
+
created_at: {
|
|
112
|
+
[Op.between]: [startTime, endTime],
|
|
113
|
+
},
|
|
115
114
|
},
|
|
116
|
-
},
|
|
117
|
-
|
|
115
|
+
}),
|
|
116
|
+
]);
|
|
117
|
+
const currencyMap = new Map(allCurrencies.map((c) => [c.id, c]));
|
|
118
118
|
|
|
119
119
|
if (events.length === 0) {
|
|
120
120
|
return { customerCount: 0, pendingAmounts: '0', exceedsThreshold: false };
|
|
@@ -209,25 +209,21 @@ export class HealthReportTemplate implements BaseEmailTemplate<HealthReportConte
|
|
|
209
209
|
const startTime = dayjs.unix(start).toISOString();
|
|
210
210
|
const endTime = dayjs.unix(end).toISOString();
|
|
211
211
|
|
|
212
|
-
// Get all successful payments
|
|
213
|
-
const payments = await
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
[Op.between]: [startTime, endTime],
|
|
212
|
+
// Get all successful payments + refunds in parallel
|
|
213
|
+
const [payments, refunds] = await Promise.all([
|
|
214
|
+
PaymentIntent.findAll({
|
|
215
|
+
where: {
|
|
216
|
+
status: 'succeeded',
|
|
217
|
+
created_at: { [Op.between]: [startTime, endTime] },
|
|
218
218
|
},
|
|
219
|
-
},
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
where: {
|
|
225
|
-
status: 'succeeded',
|
|
226
|
-
created_at: {
|
|
227
|
-
[Op.between]: [startTime, endTime],
|
|
219
|
+
}),
|
|
220
|
+
Refund.findAll({
|
|
221
|
+
where: {
|
|
222
|
+
status: 'succeeded',
|
|
223
|
+
created_at: { [Op.between]: [startTime, endTime] },
|
|
228
224
|
},
|
|
229
|
-
},
|
|
230
|
-
|
|
225
|
+
}),
|
|
226
|
+
]);
|
|
231
227
|
|
|
232
228
|
// Aggregate total revenue by currency
|
|
233
229
|
const revenueByCurrency = new Map<string, BN>();
|
|
@@ -282,33 +278,18 @@ export class HealthReportTemplate implements BaseEmailTemplate<HealthReportConte
|
|
|
282
278
|
const startTime = dayjs.unix(start).toISOString();
|
|
283
279
|
const endTime = dayjs.unix(end).toISOString();
|
|
284
280
|
|
|
285
|
-
//
|
|
286
|
-
const newSubscriptions = await
|
|
287
|
-
|
|
288
|
-
created_at: {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
canceled_at: {
|
|
298
|
-
[Op.between]: [start, end],
|
|
299
|
-
},
|
|
300
|
-
},
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
// Subscriptions that became past_due in the last 24 hours
|
|
304
|
-
const pastDueSubscriptions = await Subscription.count({
|
|
305
|
-
where: {
|
|
306
|
-
status: 'past_due',
|
|
307
|
-
updated_at: {
|
|
308
|
-
[Op.between]: [startTime, endTime],
|
|
309
|
-
},
|
|
310
|
-
},
|
|
311
|
-
});
|
|
281
|
+
// All 3 counts are independent — run in parallel
|
|
282
|
+
const [newSubscriptions, canceledSubscriptions, pastDueSubscriptions] = await Promise.all([
|
|
283
|
+
Subscription.count({
|
|
284
|
+
where: { created_at: { [Op.between]: [startTime, endTime] } },
|
|
285
|
+
}),
|
|
286
|
+
Subscription.count({
|
|
287
|
+
where: { canceled_at: { [Op.between]: [start, end] } },
|
|
288
|
+
}),
|
|
289
|
+
Subscription.count({
|
|
290
|
+
where: { status: 'past_due', updated_at: { [Op.between]: [startTime, endTime] } },
|
|
291
|
+
}),
|
|
292
|
+
]);
|
|
312
293
|
|
|
313
294
|
return {
|
|
314
295
|
newSubscriptions,
|
|
@@ -405,26 +386,22 @@ export class HealthReportTemplate implements BaseEmailTemplate<HealthReportConte
|
|
|
405
386
|
const startTime = dayjs.unix(start).toISOString();
|
|
406
387
|
const endTime = dayjs.unix(end).toISOString();
|
|
407
388
|
|
|
408
|
-
// Successful payments
|
|
409
|
-
const succeededPayments = await PaymentIntent.findAll({
|
|
410
|
-
where: {
|
|
411
|
-
status: 'succeeded',
|
|
412
|
-
created_at: {
|
|
413
|
-
[Op.between]: [startTime, endTime],
|
|
414
|
-
},
|
|
415
|
-
},
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
// Failed payments (canceled status)
|
|
389
|
+
// Successful + failed payments in parallel
|
|
419
390
|
const failedStatuses = ['canceled', 'requires_payment_method', 'requires_action'];
|
|
420
|
-
const failedPayments = await
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
[Op.between]: [startTime, endTime],
|
|
391
|
+
const [succeededPayments, failedPayments] = await Promise.all([
|
|
392
|
+
PaymentIntent.findAll({
|
|
393
|
+
where: {
|
|
394
|
+
status: 'succeeded',
|
|
395
|
+
created_at: { [Op.between]: [startTime, endTime] },
|
|
425
396
|
},
|
|
426
|
-
},
|
|
427
|
-
|
|
397
|
+
}),
|
|
398
|
+
PaymentIntent.findAll({
|
|
399
|
+
where: {
|
|
400
|
+
status: { [Op.in]: failedStatuses },
|
|
401
|
+
created_at: { [Op.between]: [startTime, endTime] },
|
|
402
|
+
},
|
|
403
|
+
}),
|
|
404
|
+
]);
|
|
428
405
|
|
|
429
406
|
const succeededCount = succeededPayments.length;
|
|
430
407
|
const failedCount = failedPayments.length;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isEthereumDid, isValid } from '@arcblock/did';
|
|
2
|
-
import { formatFactoryState, preMintFromFactory } from '@ocap/asset';
|
|
2
|
+
import { formatFactoryState, preMintFromFactory } from '@ocap/asset/mint/client';
|
|
3
3
|
import merge from 'lodash/merge';
|
|
4
4
|
|
|
5
5
|
import { wallet } from '../../libs/auth';
|
|
@@ -62,6 +62,7 @@ export async function mintNftForCheckoutSession(id: string) {
|
|
|
62
62
|
inputs: inputs || {},
|
|
63
63
|
owner: nftOwner,
|
|
64
64
|
issuer: { wallet, name: appState.moniker },
|
|
65
|
+
encoding: 'protobuf',
|
|
65
66
|
});
|
|
66
67
|
logger.info('nft preMint for checkoutSession', { id, inputs, nftOwner, factory, preMint });
|
|
67
68
|
|
|
@@ -88,7 +89,10 @@ export async function mintNftForCheckoutSession(id: string) {
|
|
|
88
89
|
factory,
|
|
89
90
|
address: preMint.address,
|
|
90
91
|
assets: [],
|
|
91
|
-
|
|
92
|
+
// preMint.variables is Record<string,unknown>; VariableInput expects
|
|
93
|
+
// typed value which we can't narrow here. Cast to any — chain side
|
|
94
|
+
// validates the actual schema.
|
|
95
|
+
variables: Object.entries(preMint.variables).map(([key, value]) => ({ name: key, value })) as any,
|
|
92
96
|
owner: nftOwner,
|
|
93
97
|
},
|
|
94
98
|
},
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import assert from 'assert';
|
|
4
4
|
|
|
5
5
|
import { isEthereumDid } from '@arcblock/did';
|
|
6
|
-
import { toStakeAddress } from '@arcblock/did-util';
|
|
6
|
+
import { toStakeAddress } from '@arcblock/did-util/cbor';
|
|
7
7
|
import { BN, fromUnitToToken, toBN } from '@ocap/util';
|
|
8
8
|
|
|
9
9
|
import { Op } from 'sequelize';
|
|
@@ -46,7 +46,8 @@ export async function ensureStakedForGas() {
|
|
|
46
46
|
const [hash] = await client.stake({
|
|
47
47
|
to: wallet.address,
|
|
48
48
|
message: 'stake-for-gas',
|
|
49
|
-
|
|
49
|
+
// @ts-ignore - token.decimal may be string from DB
|
|
50
|
+
tokens: [{ address: token.address, value: fromUnitToToken(txConfig.txGas.minStake, Number(token.decimal)) }],
|
|
50
51
|
wallet,
|
|
51
52
|
});
|
|
52
53
|
logger.info(`staked for gas on chain ${host}`, { hash });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { toStakeAddress, toTokenAddress, toTokenFactoryAddress } from '@arcblock/did-util';
|
|
1
|
+
import { toStakeAddress, toTokenAddress, toTokenFactoryAddress } from '@arcblock/did-util/cbor';
|
|
2
2
|
import { fromPublicKeyHash, toTypeInfo } from '@arcblock/did';
|
|
3
3
|
import stringify from 'json-stable-stringify';
|
|
4
4
|
import { BN, fromTokenToUnit, fromUnitToToken, toBN, toBase58, toBase64 } from '@ocap/util';
|
|
@@ -138,7 +138,7 @@ async function createTokenVC(data: { tokenAddress: string; symbol: string; websi
|
|
|
138
138
|
vc.proof = proof;
|
|
139
139
|
|
|
140
140
|
const isValid = await verifyVC({
|
|
141
|
-
vc,
|
|
141
|
+
vc: vc as any,
|
|
142
142
|
ownerDid: wallet.address,
|
|
143
143
|
trustedIssuers: wallet.address,
|
|
144
144
|
});
|
|
@@ -298,8 +298,8 @@ export async function createToken(data: { name: string; symbol: string; decimal?
|
|
|
298
298
|
reserveAddress: forgeState.token.address,
|
|
299
299
|
};
|
|
300
300
|
|
|
301
|
-
factoryItx.token.address = toTokenAddress(factoryItx.token);
|
|
302
|
-
factoryItx.address = toTokenFactoryAddress(factoryItx);
|
|
301
|
+
factoryItx.token.address = toTokenAddress(factoryItx.token, 'protobuf');
|
|
302
|
+
factoryItx.address = toTokenFactoryAddress(factoryItx, 'protobuf');
|
|
303
303
|
|
|
304
304
|
// Step 1: Create and publish VC
|
|
305
305
|
logger.info('Creating and publishing token VC', { symbol: data.symbol });
|
|
@@ -1,3 +1,32 @@
|
|
|
1
|
+
// ===========================================================================
|
|
2
|
+
// CF Workers compatibility issue:
|
|
3
|
+
//
|
|
4
|
+
// waitForEvmTxReceipt polls every 3s for up to 30 minutes (timeout: 30*60*1000).
|
|
5
|
+
// waitForEvmTxConfirm polls every 3s for up to 30 minutes.
|
|
6
|
+
//
|
|
7
|
+
// CF Workers request handlers have a ~30s wall-clock limit (paid plan).
|
|
8
|
+
// Even CF Cron Triggers have a ~15min limit. Both are far below 30 minutes.
|
|
9
|
+
//
|
|
10
|
+
// Impact: Base chain (and all EVM chain) payments fail during onAuth because
|
|
11
|
+
// executeEvmTransaction awaits waitForEvmTxReceipt which times out.
|
|
12
|
+
//
|
|
13
|
+
// Proposed fix (requires significant refactoring):
|
|
14
|
+
// 1. In executeEvmTransaction, do ONE immediate check for the receipt.
|
|
15
|
+
// If the tx is already confirmed (common for fast chains like Base), return.
|
|
16
|
+
// 2. If not confirmed yet, persist a "pending_evm_confirmation" job to the queue
|
|
17
|
+
// with the txHash, block_height, and callback info.
|
|
18
|
+
// 3. Add a cron job (e.g., every 1 minute) that polls pending EVM confirmations:
|
|
19
|
+
// - Query the Job table for pending_evm_confirmation jobs
|
|
20
|
+
// - For each, call provider.getTransactionReceipt(txHash)
|
|
21
|
+
// - If confirmed, execute the post-confirmation logic (update invoice, etc.)
|
|
22
|
+
// - If not confirmed and not timed out, leave for next cron cycle
|
|
23
|
+
// - If timed out (e.g., 30 min since creation), mark as failed
|
|
24
|
+
// 4. The onAuth handler returns immediately after the initial check + job creation,
|
|
25
|
+
// similar to how waitForEvmTxConfirm is already fire-and-forget in pay.ts.
|
|
26
|
+
//
|
|
27
|
+
// This decouples the long-polling from the request lifecycle.
|
|
28
|
+
// ===========================================================================
|
|
29
|
+
|
|
1
30
|
import type { JsonRpcProvider, TransactionReceipt, TransactionResponse } from 'ethers';
|
|
2
31
|
import waitFor from 'p-wait-for';
|
|
3
32
|
|
|
@@ -181,59 +181,76 @@ export async function ensureStripeInvoice(stripeInvoice: any, subscription: Subs
|
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
const invoiceNumber = await customer.getInvoiceNumber();
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
184
|
+
try {
|
|
185
|
+
// @ts-ignore
|
|
186
|
+
invoice = await Invoice.create({
|
|
187
|
+
number: invoiceNumber,
|
|
188
|
+
...pick(stripeInvoice, [
|
|
189
|
+
'amount_due',
|
|
190
|
+
'amount_paid',
|
|
191
|
+
'amount_remaining',
|
|
192
|
+
'amount_shipping',
|
|
193
|
+
'attempt_count',
|
|
194
|
+
'attempted',
|
|
195
|
+
'auto_advance',
|
|
196
|
+
'billing_reason',
|
|
197
|
+
'collection_method',
|
|
198
|
+
'custom_fields',
|
|
199
|
+
'customer_address',
|
|
200
|
+
'customer_email',
|
|
201
|
+
'customer_name',
|
|
202
|
+
'customer_phone',
|
|
203
|
+
'description',
|
|
204
|
+
'discounts',
|
|
205
|
+
'due_date',
|
|
206
|
+
'effective_at',
|
|
207
|
+
'ending_balance',
|
|
208
|
+
'livemode',
|
|
209
|
+
'paid_out_of_band',
|
|
210
|
+
'paid',
|
|
211
|
+
'period_end',
|
|
212
|
+
'period_start',
|
|
213
|
+
'starting_balance',
|
|
214
|
+
'status_transitions',
|
|
215
|
+
'status',
|
|
216
|
+
'subtotal_excluding_tax',
|
|
217
|
+
'subtotal',
|
|
218
|
+
'tax',
|
|
219
|
+
'total',
|
|
220
|
+
'last_finalization_error',
|
|
221
|
+
]),
|
|
222
|
+
discounts: processDiscounts,
|
|
223
|
+
total_discount_amounts: processTotalDiscounts,
|
|
224
|
+
currency_id: subscription.currency_id,
|
|
225
|
+
customer_id: subscription.customer_id,
|
|
226
|
+
default_payment_method_id: subscription.default_payment_method_id as string,
|
|
227
|
+
payment_intent_id: '',
|
|
228
|
+
subscription_id: subscription.id,
|
|
229
|
+
checkout_session_id: checkoutSession?.id,
|
|
230
|
+
statement_descriptor: stripeInvoice.statement_descriptor || '',
|
|
231
|
+
|
|
232
|
+
payment_settings: subscription.payment_settings,
|
|
233
|
+
metadata: {
|
|
234
|
+
stripe_id: stripeInvoice.id,
|
|
235
|
+
stripe_discounts: stripeInvoice.discounts,
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
} catch (err: any) {
|
|
239
|
+
// DB-level safety net: unique constraint on metadata.stripe_id prevents duplicate mirroring
|
|
240
|
+
// even when the in-memory lock fails (CF Workers multi-isolate race condition)
|
|
241
|
+
if (err.name === 'SequelizeUniqueConstraintError' || err.message?.includes('UNIQUE constraint failed')) {
|
|
242
|
+
logger.warn('Duplicate stripe invoice creation caught by unique constraint', {
|
|
243
|
+
stripeInvoiceId: stripeInvoice.id,
|
|
244
|
+
subscriptionId: subscription.id,
|
|
245
|
+
});
|
|
246
|
+
invoice = await Invoice.findOne({ where: { 'metadata.stripe_id': stripeInvoice.id } });
|
|
247
|
+
if (invoice) {
|
|
248
|
+
lock.release();
|
|
249
|
+
return invoice;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
throw err;
|
|
253
|
+
}
|
|
237
254
|
if (checkoutSession) {
|
|
238
255
|
await checkoutSession.update({ invoice_id: invoice.id });
|
|
239
256
|
}
|
|
@@ -6,6 +6,7 @@ import type Stripe from 'stripe';
|
|
|
6
6
|
|
|
7
7
|
import dayjs from '../../../libs/dayjs';
|
|
8
8
|
import logger from '../../../libs/logger';
|
|
9
|
+
// eslint-disable-next-line import/no-cycle
|
|
9
10
|
import { handlePaymentSucceed } from '../../../queues/payment';
|
|
10
11
|
import { Invoice, PaymentIntent, PaymentMethod, Subscription, TEventExpanded } from '../../../store/models';
|
|
11
12
|
import { handleStripeInvoiceCreated } from './invoice';
|
|
@@ -27,7 +28,13 @@ export async function handleStripePaymentSucceed(paymentIntent: PaymentIntent, e
|
|
|
27
28
|
|
|
28
29
|
const checkoutSessionId = event?.data?.object?.metadata?.checkoutSessionId;
|
|
29
30
|
if (checkoutSessionId) {
|
|
30
|
-
|
|
31
|
+
// Directly await invoice creation instead of fire-and-forget EventEmitter
|
|
32
|
+
// (CF Workers may terminate before async EventEmitter listeners complete)
|
|
33
|
+
try {
|
|
34
|
+
events.emit('checkout.session.pending_invoice', { checkoutSessionId, paymentIntentId: paymentIntent.id });
|
|
35
|
+
} catch (e: any) {
|
|
36
|
+
logger.error('pending_invoice event handler error', { checkoutSessionId, error: e?.message });
|
|
37
|
+
}
|
|
31
38
|
}
|
|
32
39
|
|
|
33
40
|
await handlePaymentSucceed(paymentIntent, triggerRenew);
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
TLineItemExpanded,
|
|
28
28
|
} from '../../store/models';
|
|
29
29
|
import { syncStripeInvoice } from './handlers/invoice';
|
|
30
|
+
// eslint-disable-next-line import/no-cycle
|
|
30
31
|
import { syncStripePayment } from './handlers/payment-intent';
|
|
31
32
|
import { getLock } from '../../libs/lock';
|
|
32
33
|
import { getPriceUintAmountByCurrency } from '../../libs/price';
|
|
@@ -212,6 +213,13 @@ export async function ensureStripePaymentIntent(
|
|
|
212
213
|
let stripeIntent = null;
|
|
213
214
|
if (internal.payment_details?.stripe?.payment_intent_id) {
|
|
214
215
|
stripeIntent = await client.paymentIntents.retrieve(internal.payment_details.stripe.payment_intent_id);
|
|
216
|
+
// Ensure Stripe metadata has local PI id + checkoutSessionId for webhook matching
|
|
217
|
+
const meta = stripeIntent.metadata || {};
|
|
218
|
+
if (meta.id !== internal.id || (checkoutSessionId && meta.checkoutSessionId !== checkoutSessionId)) {
|
|
219
|
+
stripeIntent = await client.paymentIntents.update(stripeIntent.id, {
|
|
220
|
+
metadata: { ...meta, appPid: env.appPid, id: internal.id, checkoutSessionId: checkoutSessionId || '' },
|
|
221
|
+
});
|
|
222
|
+
}
|
|
215
223
|
} else {
|
|
216
224
|
const customer = await ensureStripePaymentCustomer(internal, method);
|
|
217
225
|
stripeIntent = await client.paymentIntents.create({
|