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.
Files changed (184) hide show
  1. package/__blocklet__.js +37 -0
  2. package/api/ocap-1.30-subpath-shims.d.ts +35 -0
  3. package/api/src/crons/index.ts +10 -0
  4. package/api/src/crons/metering-subscription-detection.ts +12 -14
  5. package/api/src/crons/overdue-detection.ts +51 -74
  6. package/api/src/integrations/arcblock/nft.ts +6 -2
  7. package/api/src/integrations/arcblock/stake.ts +3 -2
  8. package/api/src/integrations/arcblock/token.ts +4 -4
  9. package/api/src/integrations/blocklet/notification.ts +1 -1
  10. package/api/src/integrations/ethereum/tx.ts +29 -0
  11. package/api/src/integrations/stripe/handlers/invoice.ts +70 -53
  12. package/api/src/integrations/stripe/handlers/payment-intent.ts +8 -1
  13. package/api/src/integrations/stripe/resource.ts +8 -0
  14. package/api/src/libs/audit.ts +32 -16
  15. package/api/src/libs/auth.ts +49 -2
  16. package/api/src/libs/chain-error.ts +31 -0
  17. package/api/src/libs/error.ts +15 -0
  18. package/api/src/libs/event.ts +42 -1
  19. package/api/src/libs/invoice.ts +69 -34
  20. package/api/src/libs/notification/template/customer-auto-recharge-daily-limit-exceeded.ts +1 -3
  21. package/api/src/libs/notification/template/customer-auto-recharge-failed.ts +1 -3
  22. package/api/src/libs/notification/template/customer-credit-grant-granted.ts +1 -3
  23. package/api/src/libs/notification/template/customer-credit-insufficient.ts +1 -3
  24. package/api/src/libs/notification/template/customer-credit-low-balance.ts +1 -3
  25. package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -3
  26. package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -3
  27. package/api/src/libs/notification/template/one-time-payment-refund-succeeded.ts +1 -3
  28. package/api/src/libs/notification/template/one-time-payment-succeeded.ts +1 -3
  29. package/api/src/libs/notification/template/subscription-renew-failed.ts +1 -3
  30. package/api/src/libs/notification/template/subscription-slippage-exceeded.ts +1 -3
  31. package/api/src/libs/notification/template/subscription-slippage-warning.ts +1 -3
  32. package/api/src/libs/notification/template/subscription-succeeded.ts +1 -1
  33. package/api/src/libs/pagination.ts +14 -9
  34. package/api/src/libs/payment.ts +25 -10
  35. package/api/src/libs/session.ts +1 -1
  36. package/api/src/libs/timing.ts +35 -0
  37. package/api/src/libs/util.ts +16 -15
  38. package/api/src/libs/wallet-migration.ts +72 -53
  39. package/api/src/queues/auto-recharge.ts +1 -1
  40. package/api/src/queues/credit-consume.ts +94 -12
  41. package/api/src/queues/credit-grant.ts +4 -0
  42. package/api/src/queues/event.ts +14 -2
  43. package/api/src/queues/invoice.ts +1 -0
  44. package/api/src/queues/payment.ts +83 -15
  45. package/api/src/queues/refund.ts +84 -71
  46. package/api/src/queues/subscription.ts +1 -0
  47. package/api/src/routes/checkout-sessions.ts +82 -43
  48. package/api/src/routes/connect/change-payment.ts +2 -0
  49. package/api/src/routes/connect/change-plan.ts +2 -0
  50. package/api/src/routes/connect/pay.ts +12 -3
  51. package/api/src/routes/connect/setup.ts +3 -1
  52. package/api/src/routes/connect/shared.ts +52 -39
  53. package/api/src/routes/connect/subscribe.ts +4 -1
  54. package/api/src/routes/credit-grants.ts +25 -17
  55. package/api/src/routes/donations.ts +2 -2
  56. package/api/src/routes/meter-events.ts +16 -6
  57. package/api/src/routes/payment-links.ts +1 -1
  58. package/api/src/routes/payment-methods.ts +1 -1
  59. package/api/src/routes/settings.ts +1 -1
  60. package/api/src/routes/tax-rates.ts +1 -1
  61. package/api/src/store/models/customer.ts +23 -1
  62. package/api/src/store/models/payment-method.ts +4 -0
  63. package/api/src/store/models/price.ts +23 -14
  64. package/api/tests/libs/wallet-migration.spec.ts +4 -4
  65. package/api/tests/queues/credit-consume-batch.spec.ts +5 -2
  66. package/api/tests/queues/credit-consume.spec.ts +8 -4
  67. package/api/tests/routes/credit-grants.spec.ts +1 -0
  68. package/blocklet.yml +1 -1
  69. package/cloudflare/MIGRATION-CHALLENGES.md +676 -0
  70. package/cloudflare/MIGRATION-RUNBOOK.md +777 -0
  71. package/cloudflare/README.md +499 -0
  72. package/cloudflare/STAGING-MIGRATION-GUIDE.md +602 -0
  73. package/cloudflare/build.ts +151 -0
  74. package/cloudflare/did-connect-auth.ts +527 -0
  75. package/cloudflare/docs/2026-04-22-sdk-1.30.9-upgrade-retro.md +324 -0
  76. package/cloudflare/docs/2026-04-24-queue-ops-followup.md +218 -0
  77. package/cloudflare/docs/cf-queues-ops-alert-analysis.md +663 -0
  78. package/cloudflare/docs/cf-workers-local-dev-and-fixes.md +284 -0
  79. package/cloudflare/docs/cleanup-tasks-2026-05.md +62 -0
  80. package/cloudflare/docs/payment-kit-platform-analysis-2026-04-20.md +354 -0
  81. package/cloudflare/frontend-shims/buffer-polyfill.ts +9 -0
  82. package/cloudflare/frontend-shims/js-sdk.ts +43 -0
  83. package/cloudflare/frontend-shims/mime-types.ts +46 -0
  84. package/cloudflare/frontend-shims/session.ts +24 -0
  85. package/cloudflare/frontend-shims/vite-plugin-noop.ts +6 -0
  86. package/cloudflare/index.html +40 -0
  87. package/cloudflare/migrate-to-d1.js +252 -0
  88. package/cloudflare/migrations/0001_initial_schema.sql +82 -0
  89. package/cloudflare/migrations/0002_indexes.sql +75 -0
  90. package/cloudflare/migrations/0003_locks_and_constraints.sql +18 -0
  91. package/cloudflare/run-build.js +390 -0
  92. package/cloudflare/scripts/test-decrypt.js +102 -0
  93. package/cloudflare/shims/arcblock-ws.ts +20 -0
  94. package/cloudflare/shims/axios-http-adapter.ts +4 -0
  95. package/cloudflare/shims/axios-lite.ts +117 -0
  96. package/cloudflare/shims/blocklet-sdk/auth-service.ts +33 -0
  97. package/cloudflare/shims/blocklet-sdk/cdn.ts +3 -0
  98. package/cloudflare/shims/blocklet-sdk/component-api.ts +35 -0
  99. package/cloudflare/shims/blocklet-sdk/component.ts +18 -0
  100. package/cloudflare/shims/blocklet-sdk/config.ts +8 -0
  101. package/cloudflare/shims/blocklet-sdk/did.ts +14 -0
  102. package/cloudflare/shims/blocklet-sdk/env.ts +12 -0
  103. package/cloudflare/shims/blocklet-sdk/eventbus.ts +3 -0
  104. package/cloudflare/shims/blocklet-sdk/fallback.ts +3 -0
  105. package/cloudflare/shims/blocklet-sdk/index.ts +11 -0
  106. package/cloudflare/shims/blocklet-sdk/logger.ts +11 -0
  107. package/cloudflare/shims/blocklet-sdk/middlewares.ts +15 -0
  108. package/cloudflare/shims/blocklet-sdk/notification.ts +11 -0
  109. package/cloudflare/shims/blocklet-sdk/security.ts +53 -0
  110. package/cloudflare/shims/blocklet-sdk/session.ts +8 -0
  111. package/cloudflare/shims/blocklet-sdk/verify-sign.ts +38 -0
  112. package/cloudflare/shims/blocklet-sdk/wallet-authenticator.ts +3 -0
  113. package/cloudflare/shims/blocklet-sdk/wallet-handler.ts +6 -0
  114. package/cloudflare/shims/blocklet-sdk/wallet.ts +103 -0
  115. package/cloudflare/shims/cookie-parser.ts +3 -0
  116. package/cloudflare/shims/cors.ts +21 -0
  117. package/cloudflare/shims/cron.ts +189 -0
  118. package/cloudflare/shims/crypto-js-warn.ts +7 -0
  119. package/cloudflare/shims/did-space-js.ts +17 -0
  120. package/cloudflare/shims/did-space.ts +11 -0
  121. package/cloudflare/shims/error.ts +18 -0
  122. package/cloudflare/shims/express-compat/index.ts +80 -0
  123. package/cloudflare/shims/express-compat/types.ts +41 -0
  124. package/cloudflare/shims/fastq.ts +105 -0
  125. package/cloudflare/shims/lock.ts +115 -0
  126. package/cloudflare/shims/mime-types.ts +56 -0
  127. package/cloudflare/shims/nedb-storage.ts +9 -0
  128. package/cloudflare/shims/node-child-process.ts +9 -0
  129. package/cloudflare/shims/node-fs.ts +20 -0
  130. package/cloudflare/shims/node-http.ts +13 -0
  131. package/cloudflare/shims/node-https.ts +4 -0
  132. package/cloudflare/shims/node-misc.ts +15 -0
  133. package/cloudflare/shims/node-net.ts +8 -0
  134. package/cloudflare/shims/node-os.ts +14 -0
  135. package/cloudflare/shims/node-tty.ts +8 -0
  136. package/cloudflare/shims/node-zlib.ts +17 -0
  137. package/cloudflare/shims/noop.ts +26 -0
  138. package/cloudflare/shims/payment-vendor.ts +14 -0
  139. package/cloudflare/shims/querystring.ts +12 -0
  140. package/cloudflare/shims/queue.ts +585 -0
  141. package/cloudflare/shims/rolldown-runtime.ts +43 -0
  142. package/cloudflare/shims/sequelize-d1/datatypes.ts +24 -0
  143. package/cloudflare/shims/sequelize-d1/helpers.ts +46 -0
  144. package/cloudflare/shims/sequelize-d1/index.ts +34 -0
  145. package/cloudflare/shims/sequelize-d1/model.ts +1157 -0
  146. package/cloudflare/shims/sequelize-d1/operators.ts +293 -0
  147. package/cloudflare/shims/sequelize-d1/retry.ts +85 -0
  148. package/cloudflare/shims/sequelize-d1/sequelize-class.ts +119 -0
  149. package/cloudflare/shims/sequelize-d1/timing.ts +81 -0
  150. package/cloudflare/shims/sequelize-d1/types.ts +35 -0
  151. package/cloudflare/shims/stripe-cf.ts +29 -0
  152. package/cloudflare/shims/ws-lite.ts +103 -0
  153. package/cloudflare/shims/xss.ts +3 -0
  154. package/cloudflare/tests/shims/cron.spec.ts +210 -0
  155. package/cloudflare/tests/shims/queue-scheduled.spec.ts +186 -0
  156. package/cloudflare/vite.config.ts +162 -0
  157. package/cloudflare/worker.ts +1553 -0
  158. package/cloudflare/wrangler.json +63 -0
  159. package/cloudflare/wrangler.jsonc +69 -0
  160. package/cloudflare/wrangler.staging.json +66 -0
  161. package/cloudflare/wrangler.toml +28 -0
  162. package/jest.config.js +4 -12
  163. package/package.json +26 -22
  164. package/src/app.tsx +62 -4
  165. package/src/components/customer/link.tsx +9 -13
  166. package/src/components/customer/notification-preference.tsx +3 -2
  167. package/src/components/filter-toolbar.tsx +4 -0
  168. package/src/components/invoice/list.tsx +9 -1
  169. package/src/components/invoice-pdf/utils.ts +2 -1
  170. package/src/components/layout/admin.tsx +39 -5
  171. package/src/components/layout/user-cf.tsx +77 -0
  172. package/src/components/payment-intent/actions.tsx +23 -3
  173. package/src/components/safe-did-address.tsx +75 -0
  174. package/src/libs/patch-user-card.ts +25 -0
  175. package/src/libs/util.ts +5 -7
  176. package/src/pages/admin/billing/meter-events/index.tsx +4 -0
  177. package/src/pages/admin/customers/customers/detail.tsx +2 -2
  178. package/src/pages/admin/customers/customers/index.tsx +2 -2
  179. package/src/pages/admin/overview.tsx +3 -1
  180. package/src/pages/customer/subscription/detail.tsx +4 -4
  181. package/tsconfig.api.json +1 -6
  182. package/tsconfig.json +3 -4
  183. package/tsconfig.types.json +2 -1
  184. package/vite.config.ts +6 -1
@@ -8,14 +8,40 @@ import { context } from './context';
8
8
 
9
9
  const API_VERSION = '2023-09-05';
10
10
 
11
- export async function createEvent(scope: string, type: LiteralUnion<EventType, string>, model: any, options: any = {}) {
11
+ export function createEvent(
12
+ scope: string,
13
+ type: LiteralUnion<EventType, string>,
14
+ model: any,
15
+ options: any = {}
16
+ ): Promise<void> {
17
+ // Context-aware tracking for CF Workers:
18
+ // - HTTP requests: use ctx.waitUntil (non-blocking, like Blocklet's fire-and-forget)
19
+ // - Queue consumer/Cron: use __cfPendingJobs__ (blocking, listeners must complete)
20
+ // - Blocklet Server: no tracking needed (long-lived process)
21
+ const promise = doCreateEvent(scope, type, model, options);
22
+ const isHttp = (globalThis as any).__cfHttpContext__;
23
+ if (isHttp) {
24
+ const waitUntil = (globalThis as any).__cfWaitUntil__;
25
+ if (typeof waitUntil === 'function') {
26
+ waitUntil(promise.catch((err: any) => console.error('[createEvent]', type, err?.message || err)));
27
+ }
28
+ } else {
29
+ const pending = (globalThis as any).__cfPendingJobs__;
30
+ if (Array.isArray(pending)) {
31
+ pending.push(promise.catch((err: any) => console.error('[createEvent]', type, err?.message || err)));
32
+ }
33
+ }
34
+ return promise;
35
+ }
36
+
37
+ async function doCreateEvent(scope: string, type: LiteralUnion<EventType, string>, model: any, options: any = {}) {
12
38
  const data: any = {
13
39
  object: model.dataValues,
14
40
  };
15
41
  if (type.endsWith('updated')) {
16
42
  data.previous_attributes = pick(model._previousDataValues, options.fields);
17
43
  }
18
- // console.log('createEvent', scope, type, data, options);
44
+
19
45
  const event = await Event.create({
20
46
  type,
21
47
  api_version: API_VERSION,
@@ -24,13 +50,12 @@ export async function createEvent(scope: string, type: LiteralUnion<EventType, s
24
50
  object_type: scope,
25
51
  data,
26
52
  request: {
27
- // FIXME:
28
53
  id: '',
29
54
  idempotency_key: '',
30
55
  requested_by: options.requestedBy || context.getRequestedBy() || 'system',
31
56
  },
32
57
  metadata: {},
33
- pending_webhooks: 99, // force all events goto the event queue
58
+ pending_webhooks: 99,
34
59
  });
35
60
 
36
61
  events.emit('event.created', { id: event.id });
@@ -57,7 +82,6 @@ export async function createStatusEvent(
57
82
  return;
58
83
  }
59
84
 
60
- // console.log('createStatusEvent', scope, prefix, config, data, options);
61
85
  const suffix = config[data.object.status];
62
86
  const event = await Event.create({
63
87
  type: [prefix, suffix].join('.'),
@@ -67,13 +91,12 @@ export async function createStatusEvent(
67
91
  object_type: scope,
68
92
  data,
69
93
  request: {
70
- // FIXME:
71
94
  id: '',
72
95
  idempotency_key: '',
73
96
  requested_by: options.requestedBy || context.getRequestedBy() || 'system',
74
97
  },
75
98
  metadata: {},
76
- pending_webhooks: 99, // force all events goto the event queue
99
+ pending_webhooks: 99,
77
100
  });
78
101
 
79
102
  events.emit('event.created', { id: event.id });
@@ -97,7 +120,6 @@ export async function createCustomEvent(
97
120
  return;
98
121
  }
99
122
 
100
- // console.log('createCustomEvent', scope, prefix, type, data, options);
101
123
  const event = await Event.create({
102
124
  type: [prefix, suffix].join('.'),
103
125
  api_version: API_VERSION,
@@ -106,13 +128,12 @@ export async function createCustomEvent(
106
128
  object_type: scope,
107
129
  data,
108
130
  request: {
109
- // FIXME:
110
131
  id: '',
111
132
  idempotency_key: '',
112
133
  requested_by: options.requestedBy || context.getRequestedBy() || 'system',
113
134
  },
114
135
  metadata: {},
115
- pending_webhooks: 99, // force all events goto the event queue
136
+ pending_webhooks: 99,
116
137
  });
117
138
 
118
139
  events.emit('event.created', { id: event.id });
@@ -121,11 +142,6 @@ export async function createCustomEvent(
121
142
 
122
143
  /**
123
144
  * 创建自定义事件,无需依赖模型对象
124
- * @param type 完整的事件类型,格式为 prefix.suffix
125
- * @param objectType 对象类型
126
- * @param objectId 对象ID
127
- * @param data 事件数据
128
- * @param options 额外选项
129
145
  */
130
146
  export async function createFlexibleEvent(
131
147
  type: string,
@@ -153,7 +169,7 @@ export async function createFlexibleEvent(
153
169
  requested_by: requestedBy || context.getRequestedBy() || 'system',
154
170
  },
155
171
  metadata,
156
- pending_webhooks: 99, // force all events goto the event queue
172
+ pending_webhooks: 99,
157
173
  });
158
174
 
159
175
  events.emit('event.created', { id: event.id });
@@ -3,7 +3,7 @@ import path from 'path';
3
3
  import AuthStorage from '@arcblock/did-connect-storage-nedb';
4
4
  // @ts-ignore
5
5
  import { BlockletService } from '@blocklet/sdk/service/auth';
6
- import { getWallet } from '@blocklet/sdk/lib/wallet';
6
+ import { getWallet, getAccessWallet } from '@blocklet/sdk/lib/wallet';
7
7
  import { WalletAuthenticator } from '@blocklet/sdk/lib/wallet-authenticator';
8
8
  import { WalletHandlers } from '@blocklet/sdk/lib/wallet-handler';
9
9
  import type { Request } from 'express';
@@ -13,9 +13,56 @@ import type { WalletObject } from '@ocap/wallet';
13
13
  import env from './env';
14
14
  import logger from './logger';
15
15
 
16
+ // Workaround #2: @blocklet/sdk's notification.getSender() uses
17
+ // `getWallet().address` (BLOCKLET_APP_SK derived) as the sender appDid for
18
+ // relay/EventBus broadcasts. On migrated blocklets that address (e.g. zNKti3…)
19
+ // is a rotating session id that the relay does not recognise, surfacing as:
20
+ // "Sender blocklet does not exist: <addr>"
21
+ // "Failed to broadcast event via relay: payment_intent.succeeded"
22
+ // The relay/EventBus calls in notification.js invoke `(0, exports.getSender)()`
23
+ // at send time, so mutating the module's own `exports.getSender` intercepts
24
+ // every call site. We must use `require()` (not `import default`) — the module
25
+ // sets `exports.default` to a subset object that does NOT include getSender,
26
+ // so the default-import alias cannot reach the live `exports.getSender`.
27
+ //
28
+ // Path must be `@blocklet/sdk/service/notification` (no `lib/`) — in Node it
29
+ // is a thin re-export that resolves via the module cache to the same object
30
+ // as `@blocklet/sdk/lib/service/notification`; in CF Workers the esbuild
31
+ // config aliases it to a no-op shim (patching a no-op is harmless). Using
32
+ // the `lib/` path breaks the CF build because it does not have an alias
33
+ // and falls through to `@blocklet/sdk` → `shims/blocklet-sdk/index.ts/lib/...`.
34
+ // Same root cause as the WalletAuthenticator override below — remove once the
35
+ // upstream sdk is patched.
36
+ // eslint-disable-next-line @typescript-eslint/no-var-requires, global-require, import/no-extraneous-dependencies
37
+ const notificationExports = require('@blocklet/sdk/service/notification');
38
+
39
+ notificationExports.getSender = () => ({
40
+ appDid: process.env.BLOCKLET_APP_ID || getWallet(undefined, '', 'sk').address,
41
+ wallet: getAccessWallet(),
42
+ });
43
+ logger.info('[sdk-patch] notification.getSender overridden', {
44
+ appDid: notificationExports.getSender().appDid,
45
+ expectedAppId: process.env.BLOCKLET_APP_ID,
46
+ });
47
+
16
48
  export const wallet: WalletObject = getWallet();
17
49
  export const ethWallet: WalletObject = getWallet('ethereum');
18
- export const authenticator = new WalletAuthenticator();
50
+
51
+ // Workaround for migrated blocklets where `BLOCKLET_APP_SK` is a rotating session
52
+ // key whose derived address ≠ appId. Upstream @blocklet/sdk's WalletAuthenticator
53
+ // passes `wallet: getWallet()` to did-connect-js, so outer.agentDid becomes that
54
+ // rotating address. Meanwhile the federated cert (signed by master) sets
55
+ // agentDid = `did:abt:${verifySite.appId}` (permanent), so the wallet-side strict
56
+ // check `cert.agentDid === outer.agentDid` fails with "Agent did does not match
57
+ // with certificate issuer."
58
+ //
59
+ // Force the authenticator to derive its signing wallet from BLOCKLET_APP_PK
60
+ // (permanent app pk → address = appId), aligning with the fix in
61
+ // @blocklet/sdk PR #12810 that already corrected getDelegatee. Remove once the
62
+ // upstream wallet-authenticator.ts is patched accordingly.
63
+ export const authenticator = new WalletAuthenticator({
64
+ wallet: getWallet(undefined, '', 'sk'),
65
+ });
19
66
  export const handlers = new WalletHandlers({
20
67
  authenticator,
21
68
  tokenStorage: new AuthStorage({
@@ -0,0 +1,31 @@
1
+ import CustomError from './error';
2
+
3
+ export function parseChainError(err: unknown): CustomError {
4
+ const msg = (err as Error)?.message ?? String(err);
5
+
6
+ const gasMatch = msg.match(
7
+ /Insufficient fund to pay for tx cost from (\w+)[\s\S]*?expected\s+([\d.]+)\s+(\w+)[\s\S]*?got\s+([\d.]+)/
8
+ );
9
+ if (gasMatch) {
10
+ return new CustomError('INSUFFICIENT_GAS', 'Main account lacks gas token for tx cost').withDetails({
11
+ account: gasMatch[1],
12
+ required: gasMatch[2],
13
+ available: gasMatch[4],
14
+ token: gasMatch[3],
15
+ });
16
+ }
17
+
18
+ // Chain rejects gas-payer JWT when the signing account has no on-chain state
19
+ // (fresh DID that never broadcast a DeclareTx). The wallet shouldn't attach
20
+ // x-gas-payer-* headers in that case, but surface a clean code/message so the
21
+ // UI doesn't render the raw GraphQL string. See #1356.
22
+ const gasPayerNotOnChain = msg.match(/Gas payer (\w+)[\s\S]*?does not exist on chain/i);
23
+ if (gasPayerNotOnChain) {
24
+ return new CustomError(
25
+ 'GAS_PAYER_NOT_ON_CHAIN',
26
+ 'Wallet account has no on-chain history yet; please fund or transact once before paying'
27
+ ).withDetails({ account: gasPayerNotOnChain[1] });
28
+ }
29
+
30
+ return new CustomError('TX_REJECTED', msg).withDetails({ raw: msg });
31
+ }
@@ -2,6 +2,8 @@
2
2
  export default class CustomError extends Error {
3
3
  code: string;
4
4
 
5
+ details?: Record<string, unknown>;
6
+
5
7
  constructor(code = 'GENERIC', ...params: any[]) {
6
8
  super(...params);
7
9
 
@@ -11,6 +13,19 @@ export default class CustomError extends Error {
11
13
 
12
14
  this.code = code;
13
15
  }
16
+
17
+ withDetails(details: Record<string, unknown>) {
18
+ this.details = details;
19
+ return this;
20
+ }
21
+
22
+ toJSON() {
23
+ return {
24
+ code: this.code,
25
+ message: this.message,
26
+ details: this.details,
27
+ };
28
+ }
14
29
  }
15
30
 
16
31
  /**
@@ -9,7 +9,48 @@ interface MyEventType extends EventEmitter {
9
9
  emit(eventName: LiteralUnion<EventType, string | symbol>, ...args: any[]): boolean;
10
10
  }
11
11
 
12
- export const events = new EventEmitter() as MyEventType;
12
+ // eslint-disable-next-line @typescript-eslint/naming-convention
13
+ const _events = new EventEmitter();
14
+
15
+ // Wrap on/once so async listeners auto-register their Promises in CF Workers.
16
+ // EventEmitter.emit() is synchronous and discards async listener return values.
17
+ // In CF Workers, untracked Promises are abandoned when the request/consumer ends.
18
+ // This wrapper ensures async listener work completes via flushPendingJobs().
19
+ // In Blocklet Server (__cfPendingJobs__ doesn't exist), this is a transparent pass-through.
20
+ function wrapAsyncListener(listener: (...args: any[]) => any): (...args: any[]) => any {
21
+ return function (...args: any[]) {
22
+ const result = listener(...args);
23
+ if (result && typeof result.then === 'function') {
24
+ // Context-aware: HTTP uses waitUntil (non-blocking), queue/cron uses pendingJobs (blocking)
25
+ const isHttp = (globalThis as any).__cfHttpContext__;
26
+ if (isHttp) {
27
+ const waitUntil = (globalThis as any).__cfWaitUntil__;
28
+ if (typeof waitUntil === 'function') {
29
+ waitUntil(result.catch((err: any) => console.error('[event] async listener error:', err?.message || err)));
30
+ }
31
+ } else {
32
+ const pending = (globalThis as any).__cfPendingJobs__;
33
+ if (Array.isArray(pending)) {
34
+ pending.push(result.catch((err: any) => console.error('[event] async listener error:', err?.message || err)));
35
+ }
36
+ }
37
+ }
38
+ return result;
39
+ };
40
+ }
41
+
42
+ // eslint-disable-next-line @typescript-eslint/naming-convention
43
+ const _origOn = _events.on.bind(_events);
44
+ // eslint-disable-next-line @typescript-eslint/naming-convention
45
+ const _origOnce = _events.once.bind(_events);
46
+ _events.on = function (eventName: string | symbol, listener: (...args: any[]) => void) {
47
+ return _origOn(eventName, wrapAsyncListener(listener));
48
+ };
49
+ _events.once = function (eventName: string | symbol, listener: (...args: any[]) => void) {
50
+ return _origOnce(eventName, wrapAsyncListener(listener));
51
+ };
52
+
53
+ export const events = _events as MyEventType;
13
54
 
14
55
  export const emitAsync = (event: string, ...args: any[]) => {
15
56
  return new Promise((resolve, reject) => {
@@ -4,7 +4,8 @@ import { withQuery } from 'ufo';
4
4
 
5
5
  import { BN, fromUnitToToken } from '@ocap/util';
6
6
  import { Op, type WhereOptions } from 'sequelize';
7
- import { cloneDeep, pick } from 'lodash';
7
+ import cloneDeep from 'lodash/cloneDeep';
8
+ import pick from 'lodash/pick';
8
9
  import {
9
10
  Customer,
10
11
  Invoice,
@@ -78,8 +79,10 @@ export async function getOneTimeProductInfo(invoiceId: string, paymentCurrency:
78
79
  throw new Error(`Invoice not found: ${invoiceId}`);
79
80
  }
80
81
  const json = doc.toJSON();
81
- const products = (await Product.findAll()).map((x) => x.toJSON());
82
- const prices = (await Price.findAll()).map((x) => x.toJSON());
82
+ const [products, prices] = await Promise.all([
83
+ Product.findAll().then((xs) => xs.map((x) => x.toJSON())),
84
+ Price.findAll().then((xs) => xs.map((x) => x.toJSON())),
85
+ ]);
83
86
  // @ts-ignore
84
87
  expandLineItems(json.lines, products, prices);
85
88
  const oneTimePaymentInfo: Array<{
@@ -108,12 +111,18 @@ export async function getOneTimeProductInfo(invoiceId: string, paymentCurrency:
108
111
 
109
112
  export async function getInvoiceShouldPayTotal(invoice: Invoice) {
110
113
  try {
111
- const subscription = await Subscription.findByPk(invoice.subscription_id);
114
+ // Parallel fetch: subscription + paymentCurrency are independent;
115
+ // subscriptionItems + invoiceItems are also independent of each other.
116
+ const [subscription, paymentCurrency, subscriptionItems, invoiceItems] = await Promise.all([
117
+ Subscription.findByPk(invoice.subscription_id),
118
+ PaymentCurrency.findByPk(invoice.currency_id),
119
+ SubscriptionItem.findAll({ where: { subscription_id: invoice.subscription_id } }),
120
+ InvoiceItem.findAll({ where: { invoice_id: invoice.id } }),
121
+ ]);
122
+
112
123
  if (!subscription) {
113
124
  throw new Error(`Subscription not found: ${invoice.subscription_id}`);
114
125
  }
115
-
116
- const paymentCurrency = await PaymentCurrency.findByPk(invoice.currency_id);
117
126
  if (!paymentCurrency) {
118
127
  throw new Error(`Payment currency not found: ${invoice.currency_id}`);
119
128
  }
@@ -123,12 +132,6 @@ export async function getInvoiceShouldPayTotal(invoice: Invoice) {
123
132
  throw new Error(`Payment method not found: ${paymentCurrency.payment_method_id}`);
124
133
  }
125
134
 
126
- const subscriptionItems = await SubscriptionItem.findAll({
127
- where: { subscription_id: subscription.id },
128
- });
129
- const invoiceItems = await InvoiceItem.findAll({
130
- where: { invoice_id: invoice.id },
131
- });
132
135
  const invoiceQuoteIds = invoiceItems
133
136
  .map((item) => item.metadata?.quote_id)
134
137
  .filter((id): id is string => typeof id === 'string' && id.length > 0);
@@ -577,25 +580,50 @@ async function createInvoiceWithItems(props: BaseInvoiceProps): Promise<{
577
580
  })
578
581
  : itemsData;
579
582
 
583
+ // Pre-fetch all prices and products in bulk to avoid N+1 queries per item
584
+ // eslint-disable-next-line @typescript-eslint/naming-convention
585
+ const _itemPriceIds = enrichedItemsData.map((item) => item.price_id).filter(Boolean);
586
+ // eslint-disable-next-line @typescript-eslint/naming-convention
587
+ const [_allPrices, _taxRateForCustomer] = await Promise.all([
588
+ Price.findAll({ where: { id: _itemPriceIds }, include: [{ model: Product, as: 'product' }] }),
589
+ // Tax rate matching only depends on customer address — same for all items
590
+ customer.address?.country
591
+ ? TaxRate.findMatchingRate({
592
+ country: customer.address.country,
593
+ state: customer.address.state,
594
+ postalCode: customer.address.postal_code,
595
+ livemode,
596
+ }).catch(() => null)
597
+ : Promise.resolve(null),
598
+ ]);
599
+ // eslint-disable-next-line @typescript-eslint/naming-convention
600
+ const _priceMap = new Map(_allPrices.map((p: any) => [p.id, p]));
601
+
580
602
  // create invoice items
581
603
  const items = await Promise.all(
582
604
  enrichedItemsData.map(async (item) => {
583
- // Match tax rate for this specific item
605
+ // Use pre-fetched price/product for tax rate matching
584
606
  let taxRateId: string | undefined;
585
607
  if (customer.address?.country && item.price_id) {
586
608
  try {
587
- const price = await Price.findByPk(item.price_id);
609
+ const price = _priceMap.get(item.price_id);
588
610
  if (price?.product_id) {
589
- const product = await Product.findByPk(price.product_id);
611
+ const { product } = price as any;
590
612
  if (product) {
591
- const taxRate = await TaxRate.findMatchingRate({
592
- country: customer.address.country,
593
- state: customer.address.state,
594
- postalCode: customer.address.postal_code,
595
- taxCode: product.tax_code,
596
- livemode,
597
- });
598
- taxRateId = taxRate?.id;
613
+ // Use pre-fetched tax rate if no product-specific tax_code,
614
+ // otherwise do a specific lookup
615
+ if (!product.tax_code) {
616
+ taxRateId = (_taxRateForCustomer as any)?.id;
617
+ } else {
618
+ const taxRate = await TaxRate.findMatchingRate({
619
+ country: customer.address.country,
620
+ state: customer.address.state,
621
+ postalCode: customer.address.postal_code,
622
+ taxCode: product.tax_code,
623
+ livemode,
624
+ });
625
+ taxRateId = taxRate?.id;
626
+ }
599
627
  }
600
628
  }
601
629
  } catch (error) {
@@ -785,8 +813,10 @@ export async function cleanupInvoiceAndItems(invoiceId: string) {
785
813
  return;
786
814
  }
787
815
 
788
- const removedItem = await InvoiceItem.destroy({ where: { invoice_id: invoiceId } });
789
- const removedInvoice = await Invoice.destroy({ where: { id: invoiceId } });
816
+ const [removedItem, removedInvoice] = await Promise.all([
817
+ InvoiceItem.destroy({ where: { invoice_id: invoiceId } }),
818
+ Invoice.destroy({ where: { id: invoiceId } }),
819
+ ]);
790
820
  logger.info('cleanup invoice and items', { invoiceId, removedItem, removedInvoice });
791
821
  }
792
822
 
@@ -1345,21 +1375,26 @@ export const migrateSubscriptionPaymentMethodInvoice = async (
1345
1375
  }
1346
1376
  }
1347
1377
 
1348
- // 3. Get old and new payment method/currency
1349
- const oldPaymentCurrency = await PaymentCurrency.findByPk(oldCurrencyId);
1378
+ // 3. Get old and new payment currency in parallel
1379
+ const [oldPaymentCurrency, newPaymentCurrency] = await Promise.all([
1380
+ PaymentCurrency.findByPk(oldCurrencyId),
1381
+ PaymentCurrency.findByPk(newCurrencyId),
1382
+ ]);
1350
1383
  if (!oldPaymentCurrency) {
1351
1384
  throw new Error(`Payment currency ${oldCurrencyId} not found`);
1352
1385
  }
1353
- const oldPaymentMethod = await PaymentMethod.findByPk(oldPaymentCurrency.payment_method_id);
1354
- if (!oldPaymentMethod) {
1355
- throw new Error(`Payment method for currency ${oldCurrencyId} not found`);
1356
- }
1357
-
1358
- const newPaymentCurrency = await PaymentCurrency.findByPk(newCurrencyId);
1359
1386
  if (!newPaymentCurrency) {
1360
1387
  throw new Error(`Payment currency ${newCurrencyId} not found`);
1361
1388
  }
1362
- const newPaymentMethod = await PaymentMethod.findByPk(newPaymentCurrency.payment_method_id);
1389
+
1390
+ // Get old and new payment method in parallel
1391
+ const [oldPaymentMethod, newPaymentMethod] = await Promise.all([
1392
+ PaymentMethod.findByPk(oldPaymentCurrency.payment_method_id),
1393
+ PaymentMethod.findByPk(newPaymentCurrency.payment_method_id),
1394
+ ]);
1395
+ if (!oldPaymentMethod) {
1396
+ throw new Error(`Payment method for currency ${oldCurrencyId} not found`);
1397
+ }
1363
1398
  if (!newPaymentMethod) {
1364
1399
  throw new Error(`Payment method for currency ${newCurrencyId} not found`);
1365
1400
  }
@@ -29,9 +29,7 @@ interface CustomerAutoRechargeDailyLimitExceededEmailTemplateContext {
29
29
  customerIndexUrl: string;
30
30
  }
31
31
 
32
- export class CustomerAutoRechargeDailyLimitExceededEmailTemplate
33
- implements BaseEmailTemplate<CustomerAutoRechargeDailyLimitExceededEmailTemplateContext>
34
- {
32
+ export class CustomerAutoRechargeDailyLimitExceededEmailTemplate implements BaseEmailTemplate<CustomerAutoRechargeDailyLimitExceededEmailTemplateContext> {
35
33
  options: CustomerAutoRechargeDailyLimitExceededEmailTemplateOptions;
36
34
 
37
35
  constructor(options: CustomerAutoRechargeDailyLimitExceededEmailTemplateOptions) {
@@ -46,9 +46,7 @@ interface CustomerAutoRechargeFailedEmailTemplateContext {
46
46
  isSkipped: boolean;
47
47
  }
48
48
 
49
- export class CustomerAutoRechargeFailedEmailTemplate
50
- implements BaseEmailTemplate<CustomerAutoRechargeFailedEmailTemplateContext>
51
- {
49
+ export class CustomerAutoRechargeFailedEmailTemplate implements BaseEmailTemplate<CustomerAutoRechargeFailedEmailTemplateContext> {
52
50
  options: CustomerAutoRechargeFailedEmailTemplateOptions;
53
51
 
54
52
  constructor(options: CustomerAutoRechargeFailedEmailTemplateOptions) {
@@ -21,9 +21,7 @@ interface CustomerCreditGrantGrantedEmailTemplateContext {
21
21
  at: string;
22
22
  }
23
23
 
24
- export class CustomerCreditGrantGrantedEmailTemplate
25
- implements BaseEmailTemplate<CustomerCreditGrantGrantedEmailTemplateContext>
26
- {
24
+ export class CustomerCreditGrantGrantedEmailTemplate implements BaseEmailTemplate<CustomerCreditGrantGrantedEmailTemplateContext> {
27
25
  options: CustomerCreditGrantGrantedEmailTemplateOptions;
28
26
 
29
27
  constructor(options: CustomerCreditGrantGrantedEmailTemplateOptions) {
@@ -37,9 +37,7 @@ interface CustomerCreditInsufficientEmailTemplateContext {
37
37
  at: string;
38
38
  }
39
39
 
40
- export class CustomerCreditInsufficientEmailTemplate
41
- implements BaseEmailTemplate<CustomerCreditInsufficientEmailTemplateContext>
42
- {
40
+ export class CustomerCreditInsufficientEmailTemplate implements BaseEmailTemplate<CustomerCreditInsufficientEmailTemplateContext> {
43
41
  options: CustomerCreditInsufficientEmailTemplateOptions;
44
42
 
45
43
  constructor(options: CustomerCreditInsufficientEmailTemplateOptions) {
@@ -26,9 +26,7 @@ interface CustomerCreditLowBalanceEmailTemplateContext {
26
26
  isCritical: boolean; // true if percentage < 1%
27
27
  rechargeUrl: string | null;
28
28
  }
29
- export class CustomerCreditLowBalanceEmailTemplate
30
- implements BaseEmailTemplate<CustomerCreditLowBalanceEmailTemplateContext>
31
- {
29
+ export class CustomerCreditLowBalanceEmailTemplate implements BaseEmailTemplate<CustomerCreditLowBalanceEmailTemplateContext> {
32
30
  // Notification configuration: 10 minute grace period before sending
33
31
  static readonly delay = 10 * 60; // seconds
34
32
 
@@ -43,9 +43,7 @@ interface CustomerRevenueSucceededEmailTemplateContext {
43
43
  * @class CustomerRevenueSucceededEmailTemplate
44
44
  * @implements {BaseEmailTemplate<CustomerRevenueSucceededEmailTemplateContext>}
45
45
  */
46
- export class CustomerRevenueSucceededEmailTemplate
47
- implements BaseEmailTemplate<CustomerRevenueSucceededEmailTemplateContext>
48
- {
46
+ export class CustomerRevenueSucceededEmailTemplate implements BaseEmailTemplate<CustomerRevenueSucceededEmailTemplateContext> {
49
47
  options: CustomerRevenueSucceededEmailTemplateOptions;
50
48
 
51
49
  constructor(options: CustomerRevenueSucceededEmailTemplateOptions) {
@@ -64,9 +64,7 @@ interface CustomerRewardSucceededEmailTemplateContext {
64
64
  * @class CustomerRewardSucceededEmailTemplate
65
65
  * @implements {BaseEmailTemplate<CustomerRewardSucceededEmailTemplateContext>}
66
66
  */
67
- export class CustomerRewardSucceededEmailTemplate
68
- implements BaseEmailTemplate<CustomerRewardSucceededEmailTemplateContext>
69
- {
67
+ export class CustomerRewardSucceededEmailTemplate implements BaseEmailTemplate<CustomerRewardSucceededEmailTemplateContext> {
70
68
  options: CustomerRewardSucceededEmailTemplateOptions;
71
69
 
72
70
  constructor(options: CustomerRewardSucceededEmailTemplateOptions) {
@@ -29,9 +29,7 @@ interface OneTimePaymentRefundSucceededEmailTemplateContext {
29
29
  invoiceNumber?: string;
30
30
  }
31
31
 
32
- export class OneTimePaymentRefundSucceededEmailTemplate
33
- implements BaseEmailTemplate<OneTimePaymentRefundSucceededEmailTemplateContext>
34
- {
32
+ export class OneTimePaymentRefundSucceededEmailTemplate implements BaseEmailTemplate<OneTimePaymentRefundSucceededEmailTemplateContext> {
35
33
  options: OneTimePaymentRefundSucceededEmailTemplateOptions;
36
34
 
37
35
  constructor(options: OneTimePaymentRefundSucceededEmailTemplateOptions) {
@@ -37,9 +37,7 @@ interface OneTimePaymentSucceededEmailTemplateContext {
37
37
  * @class OneTimePaymentSucceededEmailTemplate
38
38
  * @implements {BaseEmailTemplate<OneTimePaymentSucceededEmailTemplateContext>}
39
39
  */
40
- export class OneTimePaymentSucceededEmailTemplate
41
- implements BaseEmailTemplate<OneTimePaymentSucceededEmailTemplateContext>
42
- {
40
+ export class OneTimePaymentSucceededEmailTemplate implements BaseEmailTemplate<OneTimePaymentSucceededEmailTemplateContext> {
43
41
  options: OneTimePaymentSucceededEmailTemplateOptions;
44
42
 
45
43
  constructor(options: OneTimePaymentSucceededEmailTemplateOptions) {
@@ -50,9 +50,7 @@ interface SubscriptionRenewFailedEmailTemplateContext {
50
50
  payer: string;
51
51
  }
52
52
 
53
- export class SubscriptionRenewFailedEmailTemplate
54
- implements BaseEmailTemplate<SubscriptionRenewFailedEmailTemplateContext>
55
- {
53
+ export class SubscriptionRenewFailedEmailTemplate implements BaseEmailTemplate<SubscriptionRenewFailedEmailTemplateContext> {
56
54
  options: SubscriptionRenewFailedEmailTemplateOptions;
57
55
 
58
56
  constructor(options: SubscriptionRenewFailedEmailTemplateOptions) {
@@ -27,9 +27,7 @@ interface SubscriptionSlippageExceededEmailTemplateContext {
27
27
  customActions: any[];
28
28
  }
29
29
 
30
- export class SubscriptionSlippageExceededEmailTemplate
31
- implements BaseEmailTemplate<SubscriptionSlippageExceededEmailTemplateContext>
32
- {
30
+ export class SubscriptionSlippageExceededEmailTemplate implements BaseEmailTemplate<SubscriptionSlippageExceededEmailTemplateContext> {
33
31
  options: SubscriptionSlippageExceededEmailTemplateOptions;
34
32
 
35
33
  constructor(options: SubscriptionSlippageExceededEmailTemplateOptions) {
@@ -29,9 +29,7 @@ interface SubscriptionSlippageWarningEmailTemplateContext {
29
29
  customActions: any[];
30
30
  }
31
31
 
32
- export class SubscriptionSlippageWarningEmailTemplate
33
- implements BaseEmailTemplate<SubscriptionSlippageWarningEmailTemplateContext>
34
- {
32
+ export class SubscriptionSlippageWarningEmailTemplate implements BaseEmailTemplate<SubscriptionSlippageWarningEmailTemplateContext> {
35
33
  options: SubscriptionSlippageWarningEmailTemplateOptions;
36
34
 
37
35
  constructor(options: SubscriptionSlippageWarningEmailTemplateOptions) {
@@ -68,7 +68,7 @@ export class SubscriptionSucceededEmailTemplate extends BaseSubscriptionEmailTem
68
68
 
69
69
  return Boolean(
70
70
  ['disabled', 'minted', 'sent', 'error'].includes(checkoutSession?.nft_mint_status as string) &&
71
- (invoice?.payment_intent_id || (invoice && +invoice.amount_remaining === 0))
71
+ (invoice?.payment_intent_id || (invoice && +invoice.amount_remaining === 0))
72
72
  );
73
73
  },
74
74
  { timeout: 1000 * 10, interval: 1000 }