payment-kit 1.27.2 → 1.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (241) 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 +32 -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/crons/retry-pending-events.ts +58 -0
  7. package/api/src/integrations/app-store/apple-root-certs.ts +26 -0
  8. package/api/src/integrations/app-store/client.ts +369 -0
  9. package/api/src/integrations/app-store/handlers/index.ts +46 -0
  10. package/api/src/integrations/app-store/handlers/subscription.ts +635 -0
  11. package/api/src/integrations/app-store/node-apple-receipt-verify.d.ts +17 -0
  12. package/api/src/integrations/app-store/notification-routing.ts +18 -0
  13. package/api/src/integrations/app-store/signed-data-verifier.ts +150 -0
  14. package/api/src/integrations/arcblock/nft.ts +6 -2
  15. package/api/src/integrations/arcblock/stake.ts +3 -2
  16. package/api/src/integrations/arcblock/token.ts +4 -4
  17. package/api/src/integrations/blocklet/notification.ts +1 -1
  18. package/api/src/integrations/ethereum/tx.ts +29 -0
  19. package/api/src/integrations/google-play/client.ts +276 -0
  20. package/api/src/integrations/google-play/handlers/index.ts +69 -0
  21. package/api/src/integrations/google-play/handlers/subscription.ts +565 -0
  22. package/api/src/integrations/google-play/handlers/voided.ts +106 -0
  23. package/api/src/integrations/google-play/setup.ts +43 -0
  24. package/api/src/integrations/google-play/verify.ts +251 -0
  25. package/api/src/integrations/iap-reconcile.ts +415 -0
  26. package/api/src/integrations/stripe/handlers/invoice.ts +70 -53
  27. package/api/src/integrations/stripe/handlers/payment-intent.ts +8 -1
  28. package/api/src/integrations/stripe/resource.ts +8 -0
  29. package/api/src/libs/audit.ts +70 -24
  30. package/api/src/libs/auth.ts +49 -2
  31. package/api/src/libs/chain-error.ts +31 -0
  32. package/api/src/libs/entitlement.ts +399 -0
  33. package/api/src/libs/env.ts +2 -0
  34. package/api/src/libs/error.ts +15 -0
  35. package/api/src/libs/event.ts +42 -1
  36. package/api/src/libs/invoice.ts +69 -34
  37. package/api/src/libs/notification/template/customer-auto-recharge-daily-limit-exceeded.ts +1 -3
  38. package/api/src/libs/notification/template/customer-auto-recharge-failed.ts +1 -3
  39. package/api/src/libs/notification/template/customer-credit-grant-granted.ts +1 -3
  40. package/api/src/libs/notification/template/customer-credit-insufficient.ts +1 -3
  41. package/api/src/libs/notification/template/customer-credit-low-balance.ts +1 -3
  42. package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -3
  43. package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -3
  44. package/api/src/libs/notification/template/one-time-payment-refund-succeeded.ts +1 -3
  45. package/api/src/libs/notification/template/one-time-payment-succeeded.ts +1 -3
  46. package/api/src/libs/notification/template/subscription-renew-failed.ts +1 -3
  47. package/api/src/libs/notification/template/subscription-slippage-exceeded.ts +1 -3
  48. package/api/src/libs/notification/template/subscription-slippage-warning.ts +1 -3
  49. package/api/src/libs/notification/template/subscription-succeeded.ts +1 -1
  50. package/api/src/libs/pagination.ts +14 -9
  51. package/api/src/libs/payment.ts +25 -10
  52. package/api/src/libs/security.ts +51 -0
  53. package/api/src/libs/session.ts +1 -1
  54. package/api/src/libs/subscription.ts +13 -1
  55. package/api/src/libs/timing.ts +35 -0
  56. package/api/src/libs/util.ts +29 -15
  57. package/api/src/libs/wallet-migration.ts +72 -53
  58. package/api/src/queues/auto-recharge.ts +1 -1
  59. package/api/src/queues/credit-consume.ts +94 -12
  60. package/api/src/queues/credit-grant.ts +4 -0
  61. package/api/src/queues/event.ts +39 -21
  62. package/api/src/queues/invoice.ts +1 -0
  63. package/api/src/queues/payment.ts +83 -15
  64. package/api/src/queues/refund.ts +84 -71
  65. package/api/src/queues/subscription.ts +1 -0
  66. package/api/src/queues/webhook.ts +12 -2
  67. package/api/src/routes/checkout-sessions.ts +82 -43
  68. package/api/src/routes/connect/change-payment.ts +2 -0
  69. package/api/src/routes/connect/change-plan.ts +2 -0
  70. package/api/src/routes/connect/pay.ts +12 -3
  71. package/api/src/routes/connect/setup.ts +3 -1
  72. package/api/src/routes/connect/shared.ts +52 -39
  73. package/api/src/routes/connect/subscribe.ts +4 -1
  74. package/api/src/routes/credit-grants.ts +25 -17
  75. package/api/src/routes/donations.ts +2 -2
  76. package/api/src/routes/entitlements.ts +105 -0
  77. package/api/src/routes/events.ts +2 -2
  78. package/api/src/routes/index.ts +12 -2
  79. package/api/src/routes/integrations/app-store.ts +267 -0
  80. package/api/src/routes/integrations/google-play.ts +324 -0
  81. package/api/src/routes/meter-events.ts +16 -6
  82. package/api/src/routes/payment-links.ts +1 -1
  83. package/api/src/routes/payment-methods.ts +131 -1
  84. package/api/src/routes/settings.ts +1 -1
  85. package/api/src/routes/tax-rates.ts +1 -1
  86. package/api/src/store/migrations/20260526-iap-foundation.ts +105 -0
  87. package/api/src/store/models/customer.ts +37 -1
  88. package/api/src/store/models/entitlement-grant.ts +118 -0
  89. package/api/src/store/models/entitlement-product.ts +48 -0
  90. package/api/src/store/models/entitlement.ts +86 -0
  91. package/api/src/store/models/index.ts +9 -0
  92. package/api/src/store/models/invoice.ts +20 -0
  93. package/api/src/store/models/payment-method.ts +66 -1
  94. package/api/src/store/models/price.ts +23 -14
  95. package/api/src/store/models/refund.ts +10 -0
  96. package/api/src/store/models/subscription.ts +14 -0
  97. package/api/src/store/models/types.ts +32 -0
  98. package/api/tests/integrations/app-store/client.spec.ts +335 -0
  99. package/api/tests/integrations/app-store/handlers.spec.ts +480 -0
  100. package/api/tests/integrations/app-store/notifications.spec.ts +381 -0
  101. package/api/tests/integrations/app-store/signed-data-verifier.spec.ts +72 -0
  102. package/api/tests/integrations/app-store/webhook-routing.spec.ts +27 -0
  103. package/api/tests/integrations/google-play/handlers.spec.ts +341 -0
  104. package/api/tests/integrations/google-play/verify.spec.ts +215 -0
  105. package/api/tests/integrations/iap-reconcile.spec.ts +237 -0
  106. package/api/tests/libs/entitlement.spec.ts +347 -0
  107. package/api/tests/libs/wallet-migration.spec.ts +4 -4
  108. package/api/tests/queues/credit-consume-batch.spec.ts +5 -2
  109. package/api/tests/queues/credit-consume.spec.ts +8 -4
  110. package/api/tests/routes/credit-grants.spec.ts +1 -0
  111. package/blocklet.yml +1 -1
  112. package/cloudflare/MIGRATION-CHALLENGES.md +676 -0
  113. package/cloudflare/MIGRATION-RUNBOOK.md +777 -0
  114. package/cloudflare/README.md +499 -0
  115. package/cloudflare/STAGING-MIGRATION-GUIDE.md +602 -0
  116. package/cloudflare/build.ts +151 -0
  117. package/cloudflare/did-connect-auth.ts +527 -0
  118. package/cloudflare/docs/2026-04-22-sdk-1.30.9-upgrade-retro.md +324 -0
  119. package/cloudflare/docs/2026-04-24-queue-ops-followup.md +218 -0
  120. package/cloudflare/docs/cf-queues-ops-alert-analysis.md +663 -0
  121. package/cloudflare/docs/cf-workers-local-dev-and-fixes.md +284 -0
  122. package/cloudflare/docs/cleanup-tasks-2026-05.md +62 -0
  123. package/cloudflare/docs/payment-kit-platform-analysis-2026-04-20.md +354 -0
  124. package/cloudflare/frontend-shims/buffer-polyfill.ts +9 -0
  125. package/cloudflare/frontend-shims/js-sdk.ts +43 -0
  126. package/cloudflare/frontend-shims/mime-types.ts +46 -0
  127. package/cloudflare/frontend-shims/session.ts +24 -0
  128. package/cloudflare/frontend-shims/vite-plugin-noop.ts +6 -0
  129. package/cloudflare/index.html +40 -0
  130. package/cloudflare/migrate-to-d1.js +252 -0
  131. package/cloudflare/migrations/0001_initial_schema.sql +82 -0
  132. package/cloudflare/migrations/0002_indexes.sql +75 -0
  133. package/cloudflare/migrations/0003_locks_and_constraints.sql +18 -0
  134. package/cloudflare/migrations/0004_iap_foundation.sql +72 -0
  135. package/cloudflare/migrations/0005_iap_tenant_backfill.sql +112 -0
  136. package/cloudflare/run-build.js +391 -0
  137. package/cloudflare/scripts/test-decrypt.js +102 -0
  138. package/cloudflare/shims/arcblock-ws.ts +20 -0
  139. package/cloudflare/shims/axios-http-adapter.ts +4 -0
  140. package/cloudflare/shims/axios-lite.ts +117 -0
  141. package/cloudflare/shims/blocklet-sdk/auth-service.ts +33 -0
  142. package/cloudflare/shims/blocklet-sdk/cdn.ts +3 -0
  143. package/cloudflare/shims/blocklet-sdk/component-api.ts +35 -0
  144. package/cloudflare/shims/blocklet-sdk/component.ts +18 -0
  145. package/cloudflare/shims/blocklet-sdk/config.ts +8 -0
  146. package/cloudflare/shims/blocklet-sdk/did.ts +14 -0
  147. package/cloudflare/shims/blocklet-sdk/env.ts +12 -0
  148. package/cloudflare/shims/blocklet-sdk/eventbus.ts +3 -0
  149. package/cloudflare/shims/blocklet-sdk/fallback.ts +3 -0
  150. package/cloudflare/shims/blocklet-sdk/index.ts +11 -0
  151. package/cloudflare/shims/blocklet-sdk/logger.ts +11 -0
  152. package/cloudflare/shims/blocklet-sdk/middlewares.ts +15 -0
  153. package/cloudflare/shims/blocklet-sdk/notification.ts +11 -0
  154. package/cloudflare/shims/blocklet-sdk/security.ts +53 -0
  155. package/cloudflare/shims/blocklet-sdk/session.ts +8 -0
  156. package/cloudflare/shims/blocklet-sdk/verify-session.ts +44 -0
  157. package/cloudflare/shims/blocklet-sdk/verify-sign.ts +38 -0
  158. package/cloudflare/shims/blocklet-sdk/wallet-authenticator.ts +3 -0
  159. package/cloudflare/shims/blocklet-sdk/wallet-handler.ts +6 -0
  160. package/cloudflare/shims/blocklet-sdk/wallet.ts +103 -0
  161. package/cloudflare/shims/cookie-parser.ts +3 -0
  162. package/cloudflare/shims/cors.ts +21 -0
  163. package/cloudflare/shims/cron.ts +189 -0
  164. package/cloudflare/shims/crypto-js-warn.ts +7 -0
  165. package/cloudflare/shims/did-space-js.ts +17 -0
  166. package/cloudflare/shims/did-space.ts +11 -0
  167. package/cloudflare/shims/error.ts +18 -0
  168. package/cloudflare/shims/express-compat/index.ts +80 -0
  169. package/cloudflare/shims/express-compat/types.ts +41 -0
  170. package/cloudflare/shims/fastq.ts +105 -0
  171. package/cloudflare/shims/lock.ts +115 -0
  172. package/cloudflare/shims/mime-types.ts +56 -0
  173. package/cloudflare/shims/nedb-storage.ts +9 -0
  174. package/cloudflare/shims/node-child-process.ts +9 -0
  175. package/cloudflare/shims/node-fs.ts +20 -0
  176. package/cloudflare/shims/node-http.ts +13 -0
  177. package/cloudflare/shims/node-https.ts +4 -0
  178. package/cloudflare/shims/node-misc.ts +15 -0
  179. package/cloudflare/shims/node-net.ts +8 -0
  180. package/cloudflare/shims/node-os.ts +14 -0
  181. package/cloudflare/shims/node-tty.ts +8 -0
  182. package/cloudflare/shims/node-zlib.ts +17 -0
  183. package/cloudflare/shims/noop.ts +26 -0
  184. package/cloudflare/shims/payment-vendor.ts +14 -0
  185. package/cloudflare/shims/querystring.ts +12 -0
  186. package/cloudflare/shims/queue.ts +611 -0
  187. package/cloudflare/shims/rolldown-runtime.ts +43 -0
  188. package/cloudflare/shims/sequelize-d1/datatypes.ts +24 -0
  189. package/cloudflare/shims/sequelize-d1/helpers.ts +46 -0
  190. package/cloudflare/shims/sequelize-d1/index.ts +34 -0
  191. package/cloudflare/shims/sequelize-d1/model.ts +1176 -0
  192. package/cloudflare/shims/sequelize-d1/operators.ts +306 -0
  193. package/cloudflare/shims/sequelize-d1/retry.ts +85 -0
  194. package/cloudflare/shims/sequelize-d1/sequelize-class.ts +119 -0
  195. package/cloudflare/shims/sequelize-d1/timing.ts +81 -0
  196. package/cloudflare/shims/sequelize-d1/types.ts +35 -0
  197. package/cloudflare/shims/stripe-cf.ts +29 -0
  198. package/cloudflare/shims/ws-lite.ts +103 -0
  199. package/cloudflare/shims/xss.ts +3 -0
  200. package/cloudflare/tests/shims/cron.spec.ts +210 -0
  201. package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +87 -0
  202. package/cloudflare/tests/shims/queue-scheduled.spec.ts +186 -0
  203. package/cloudflare/vite.config.ts +162 -0
  204. package/cloudflare/worker.ts +1608 -0
  205. package/cloudflare/wrangler.json +63 -0
  206. package/cloudflare/wrangler.jsonc +75 -0
  207. package/cloudflare/wrangler.staging.json +67 -0
  208. package/cloudflare/wrangler.toml +28 -0
  209. package/jest.config.js +4 -12
  210. package/package.json +30 -22
  211. package/scripts/seed-google-play.ts +79 -0
  212. package/src/app.tsx +62 -4
  213. package/src/components/customer/link.tsx +9 -13
  214. package/src/components/customer/notification-preference.tsx +3 -2
  215. package/src/components/filter-toolbar.tsx +4 -0
  216. package/src/components/invoice/list.tsx +9 -1
  217. package/src/components/invoice-pdf/utils.ts +2 -1
  218. package/src/components/layout/admin.tsx +39 -5
  219. package/src/components/layout/user-cf.tsx +77 -0
  220. package/src/components/payment-intent/actions.tsx +23 -3
  221. package/src/components/payment-method/app-store.tsx +103 -0
  222. package/src/components/payment-method/form.tsx +7 -1
  223. package/src/components/payment-method/google-play.tsx +85 -0
  224. package/src/components/safe-did-address.tsx +75 -0
  225. package/src/components/subscription/list.tsx +20 -0
  226. package/src/libs/patch-user-card.ts +25 -0
  227. package/src/libs/util.ts +5 -7
  228. package/src/locales/en.tsx +63 -0
  229. package/src/locales/zh.tsx +63 -0
  230. package/src/pages/admin/billing/meter-events/index.tsx +4 -0
  231. package/src/pages/admin/billing/subscriptions/detail.tsx +80 -0
  232. package/src/pages/admin/customers/customers/detail.tsx +8 -2
  233. package/src/pages/admin/customers/customers/index.tsx +2 -2
  234. package/src/pages/admin/overview.tsx +3 -1
  235. package/src/pages/admin/settings/payment-methods/create.tsx +12 -0
  236. package/src/pages/admin/settings/payment-methods/index.tsx +1 -1
  237. package/src/pages/customer/subscription/detail.tsx +4 -4
  238. package/tsconfig.api.json +1 -6
  239. package/tsconfig.json +3 -4
  240. package/tsconfig.types.json +2 -1
  241. package/vite.config.ts +6 -1
@@ -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
+ }
@@ -1,6 +1,7 @@
1
1
  import Cron from '@abtnode/cron';
2
2
 
3
3
  import { checkStakeRevokeTx } from '../integrations/arcblock/stake';
4
+ import { runIapReconcile } from '../integrations/iap-reconcile';
4
5
  import {
5
6
  batchHandleStripeInvoices,
6
7
  batchHandleStripePayments,
@@ -9,7 +10,9 @@ import {
9
10
  import {
10
11
  creditConsumptionCronTime,
11
12
  depositVaultCronTime,
13
+ eventRetryCronTime,
12
14
  expiredSessionCleanupCronTime,
15
+ iapReconcileCronTime,
13
16
  notificationCronTime,
14
17
  overdueDetectionCronTime,
15
18
  paymentStatCronTime,
@@ -24,11 +27,13 @@ import {
24
27
  import logger from '../libs/logger';
25
28
  import { startCreditConsumeQueue } from '../queues/credit-consume';
26
29
  import { startDepositVaultQueue } from '../queues/payment';
30
+ import { startRefundQueue } from '../queues/refund';
27
31
  import { startSubscriptionQueue } from '../queues/subscription';
28
32
  import { startVendorStatusCheckSchedule } from '../queues/vendors/status-check';
29
33
  import { CheckoutSession } from '../store/models';
30
34
  import { createOverdueDetection } from './overdue-detection';
31
35
  import { createPaymentStat } from './payment-stat';
36
+ import { retryPendingEvents } from './retry-pending-events';
32
37
  import { SubscriptionTrialWillEndSchedule } from './subscription-trial-will-end';
33
38
  import { SubscriptionWillCanceledSchedule } from './subscription-will-canceled';
34
39
  import { SubscriptionWillRenewSchedule } from './subscription-will-renew';
@@ -85,6 +90,15 @@ function init() {
85
90
  fn: startSubscriptionQueue,
86
91
  options: { runOnInit: false },
87
92
  },
93
+ {
94
+ // Recover pending refunds that were missed (e.g. after deploy or failed dispatch).
95
+ // Previously invoked unconditionally on every CF scheduled tick; now throttled
96
+ // to every 5 minutes to stay within CF Queue free-tier ops budget.
97
+ name: 'refund.recovery',
98
+ time: '0 */5 * * * *',
99
+ fn: startRefundQueue,
100
+ options: { runOnInit: false },
101
+ },
88
102
  {
89
103
  name: 'checkoutSession.cleanup.expired',
90
104
  time: expiredSessionCleanupCronTime,
@@ -118,6 +132,24 @@ function init() {
118
132
  fn: checkStakeRevokeTx,
119
133
  options: { runOnInit: false },
120
134
  },
135
+ {
136
+ // Backup for App Store / Google Play webhooks — pulls authoritative
137
+ // subscription state and patches local drift (refunds/renewals the
138
+ // webhook missed). See blocklets/core/api/src/integrations/iap-reconcile.ts.
139
+ name: 'iap.reconcile',
140
+ time: iapReconcileCronTime,
141
+ fn: () => runIapReconcile(),
142
+ options: { runOnInit: false },
143
+ },
144
+ {
145
+ // Backstop for the fire-and-forget event → webhook path. Rescans events
146
+ // with pending_webhooks>0 older than 60s and re-invokes handleEvent.
147
+ // See blocklets/core/api/src/crons/retry-pending-events.ts.
148
+ name: 'event.retry',
149
+ time: eventRetryCronTime,
150
+ fn: () => retryPendingEvents(),
151
+ options: { runOnInit: false },
152
+ },
121
153
  {
122
154
  name: 'payment.stat',
123
155
  time: paymentStatCronTime,
@@ -119,20 +119,18 @@ export class MeteringSubscriptionDetectionTemplate implements BaseEmailTemplate<
119
119
  });
120
120
 
121
121
  const meteringInvoices: any[] = [];
122
- await Promise.all(
123
- invoices.map(async (invoice) => {
124
- const invoiceJson = invoice.toJSON();
125
- const prices = (await Price.findAll()).map((x) => x.toJSON());
126
- // @ts-ignore
127
- (invoiceJson.lines || []).forEach((item) => {
128
- item.price = prices.find((x) => x.id === item.price_id);
129
- });
130
- // @ts-ignore
131
- if ((invoiceJson.lines || []).some((line) => line.price?.recurring?.usage_type === 'metered')) {
132
- meteringInvoices.push(invoice);
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 PaymentCurrency.findAll();
107
- const currencyMap = new Map(allCurrencies.map((c) => [c.id, c]));
108
-
109
- // 2. Fetch all pending meter events in the last 24 hours
110
- const events = await MeterEvent.findAll({
111
- where: {
112
- status: ['pending', 'requires_capture', 'requires_action'],
113
- created_at: {
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 in the last 24 hours
213
- const payments = await PaymentIntent.findAll({
214
- where: {
215
- status: 'succeeded',
216
- created_at: {
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
- // Get all refunds in the last 24 hours
223
- const refunds = await Refund.findAll({
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
- // New subscriptions created in the last 24 hours
286
- const newSubscriptions = await Subscription.count({
287
- where: {
288
- created_at: {
289
- [Op.between]: [startTime, endTime],
290
- },
291
- },
292
- });
293
-
294
- // Subscriptions canceled in the last 24 hours
295
- const canceledSubscriptions = await Subscription.count({
296
- where: {
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 PaymentIntent.findAll({
421
- where: {
422
- status: { [Op.in]: failedStatuses },
423
- created_at: {
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;
@@ -0,0 +1,58 @@
1
+ // Backstop for the event → webhook fire-and-forget path.
2
+ //
3
+ // createEvent emits 'event.created' (sync) → an async listener calls
4
+ // handleEvent → schedules HTTP webhook delivery. On CF Workers the listener's
5
+ // microtask can lose the worker before its waitUntil registers if it fires
6
+ // from the last line of an HTTP handler (e.g. ingestVerifiedGooglePlayPurchase
7
+ // ends with `createEvent('subscription.started', ...).catch(...)` right before
8
+ // returning — the response flushes and runtime may stop draining new
9
+ // waitUntils). Result: events row exists with pending_webhooks>0 but no
10
+ // webhook_attempt is ever written, so downstream apps silently miss the
11
+ // notification.
12
+ //
13
+ // This cron rescans for pending events older than the realtime grace window
14
+ // and re-invokes handleEvent. Stripe et al ship the same belt-and-suspenders.
15
+
16
+ import { Op } from 'sequelize';
17
+
18
+ import logger from '../libs/logger';
19
+ import { eventQueue } from '../queues/event';
20
+ import { Event } from '../store/models/event';
21
+
22
+ const REALTIME_GRACE_SECONDS = 60;
23
+ // Each cron tick only enqueues, does NOT inline await handleEvent. CF Workers
24
+ // scheduled handler has a tight CPU budget (~30s) — inline handling N events
25
+ // blew past it after ~7 iterations and was cut off mid-batch, so the tail
26
+ // never got processed. Pushing to eventQueue lets the consumer (which has a
27
+ // looser per-message budget) work through them asynchronously.
28
+ //
29
+ // BATCH_LIMIT must stay small: even push-only, the scheduled handler awaits
30
+ // flushPendingJobs() at the end which await's each push's enqueue promise
31
+ // (D1 addJob + CF Queue send). 50 was too many — pushes never finished
32
+ // sending, jobs table never got rows, handleEvent never ran. 5 leaves
33
+ // generous headroom and we still drain the backlog over a few minutes.
34
+ const BATCH_LIMIT = 5;
35
+
36
+ export async function retryPendingEvents(): Promise<void> {
37
+ const threshold = new Date(Date.now() - REALTIME_GRACE_SECONDS * 1000);
38
+ const docs = await Event.findAll({
39
+ where: {
40
+ pending_webhooks: { [Op.gt]: 0 },
41
+ created_at: { [Op.lt]: threshold },
42
+ },
43
+ attributes: ['id'],
44
+ order: [['created_at', 'ASC']],
45
+ limit: BATCH_LIMIT,
46
+ });
47
+
48
+ if (docs.length === 0) return;
49
+ logger.info(`event.retry: enqueuing ${docs.length} pending events older than ${REALTIME_GRACE_SECONDS}s`);
50
+
51
+ for (const doc of docs) {
52
+ try {
53
+ eventQueue.push({ id: doc.id, job: { eventId: doc.id }, persist: false });
54
+ } catch (err: any) {
55
+ logger.error('event.retry enqueue failed', { eventId: doc.id, error: err?.message });
56
+ }
57
+ }
58
+ }
@@ -0,0 +1,26 @@
1
+ // Apple Root Certificates for App Store JWS verification.
2
+ //
3
+ // Vendored as base64 constants so `tsc` compiles cleanly without copying
4
+ // non-ts assets into dist/. Source files (DER-encoded `.cer`) live under
5
+ // `./certs/` and were downloaded from https://www.apple.com/certificateauthority/
6
+ //
7
+ // Apple's IAP JWS signing chain currently terminates at one of these roots.
8
+ // Apple rotates roots roughly every 25 years — refresh when they do.
9
+ // - Apple Inc. Root Certificate (RSA 2048, valid 2006-04 → 2035-02)
10
+ // - Apple Root CA - G2 (RSA 4096, valid 2014-04 → 2039-04)
11
+ // - Apple Root CA - G3 (ECC P-384, valid 2014-04 → 2039-04)
12
+
13
+ const APPLE_INC_ROOT_B64 =
14
+ 'MIIEuzCCA6OgAwIBAgIBAjANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQGEwJVUzETMBEGA1UEChMKQXBwbGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxFjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwHhcNMDYwNDI1MjE0MDM2WhcNMzUwMjA5MjE0MDM2WjBiMQswCQYDVQQGEwJVUzETMBEGA1UEChMKQXBwbGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxFjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkkakJH5HbHkdQ6wXtXnmELes2oldMVeyLGYne+Uts9QerIjAC6Bg++FAJ039BqJj50cpmnCRrEdCju+QbKsMflZ56DKRHi1vUFjczy8QPTc4UadHJGXL1XQ7Vf1+b8iUDulWPTV0N8WQ1IxVLFVkds5T39pyez1C6wVhQZ48ItCD3y6wsIG9wtj8BMIy3Q88PnT3zK0koGsj+zrW5DtleHNbLPbU6rfQPDgCSC7EhFi501TwN22IWq6NxkkdTVcGvL0Gz+PvjcM3mo0xFfh9Ma1CWQYnEdGILEINBhzOKgbEwWOxaBDKMaLOPHd5lc/9nXmW8Sdh2nzMUZaF3lMktAgMBAAGjggF6MIIBdjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUK9BpR5R2Cf70a40uQKb3R01/CF4wHwYDVR0jBBgwFoAUK9BpR5R2Cf70a40uQKb3R01/CF4wggERBgNVHSAEggEIMIIBBDCCAQAGCSqGSIb3Y2QFATCB8jAqBggrBgEFBQcCARYeaHR0cHM6Ly93d3cuYXBwbGUuY29tL2FwcGxlY2EvMIHDBggrBgEFBQcCAjCBthqBs1JlbGlhbmNlIG9uIHRoaXMgY2VydGlmaWNhdGUgYnkgYW55IHBhcnR5IGFzc3VtZXMgYWNjZXB0YW5jZSBvZiB0aGUgdGhlbiBhcHBsaWNhYmxlIHN0YW5kYXJkIHRlcm1zIGFuZCBjb25kaXRpb25zIG9mIHVzZSwgY2VydGlmaWNhdGUgcG9saWN5IGFuZCBjZXJ0aWZpY2F0aW9uIHByYWN0aWNlIHN0YXRlbWVudHMuMA0GCSqGSIb3DQEBBQUAA4IBAQBcNplMLXi37Yyb3PN3m/J20ncwT8EfhYOFG5k9RzfyqZtAjizUsZAS2L70c5vu0mQPy3lPNNiiPvl4/2vIB+x9OYOLUyDTOMSxv5pPCmv/K/xZpwUJfBdAVhEedNO3iyM7R6PVbyTi69G3cN8PReEnyvFteO3ntRcXqNx+IjXKJdXZD9Zr1KIkIxH3oayPc4FgxhtbCS+SsvhESPBgOJ4V9T0mZyCKM2r3DYLP3uujL/lTaltkwGMzd/c6ByxW69oPIQ7aunMZT7XZNn/Bh1XZp5m5MkL72NVxnn6hUrcbvZNCJBIqxw8dtk2cXmPIS4AXUKqK1drk/NAJBzewdXUh';
15
+
16
+ const APPLE_ROOT_CA_G2_B64 =
17
+ 'MIIFkjCCA3qgAwIBAgIIAeDltYNno+AwDQYJKoZIhvcNAQEMBQAwZzEbMBkGA1UEAwwSQXBwbGUgUm9vdCBDQSAtIEcyMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UEBhMCVVMwHhcNMTQwNDMwMTgxMDA5WhcNMzkwNDMwMTgxMDA5WjBnMRswGQYDVQQDDBJBcHBsZSBSb290IENBIC0gRzIxJjAkBgNVBAsMHUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRMwEQYDVQQKDApBcHBsZSBJbmMuMQswCQYDVQQGEwJVUzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANgREkhI2imKScUcx+xuM23+TfvgHN6sXuI2pyT5f1BrTM65MFQn5bPW7SXmMLYFN14UIhHF6Kob0vuy0gmVOKTvKkmMXT5xZgM4+xb1hYjkWpIMBDLyyED7Ul+f9sDx47pFoFDVEovy3d6RhiPw9bZyLgHaC/YuOQhfGaFjQQscp5TBhsRTL3b2CtcM0YM/GlMZ81fVJ3/8E7j4ko380yhDPLVoACVdJ2LT3VXdRCCQgzWTxb+4Gftr49wIQuavbfqeQMpOhYV4SbHXw8EwOTKrfl+q04tvny0aIWhwZ7Oj8ZhBbZF8+NfbqOdfIRqMM78xdLe40fTgIvS/cjTf94FNcX1RoeKz8NMoFnNvzcytN31O661A4T+B/fc9Cj6i8b0xlilZ3MIZgIxbdMYs0xBTJh0UT8TUgWY8h2czJxQI6bR3hDRSj4n4aJgXv8O7qhOTH11UL6jHfPsNFL4VPSQ08prcdUFmIrQB1guvkJ4M6mL4m1k8COKWNORj3rw31OsMiANDC1CvoDTdUE0V+1ok2Az6DGOeHwOx4e7hqkP0ZmUoNwIx7wHHHtHMn23KVDpA287PT0aLSmWaasZobNfMmRtHsHLDd4/E92GcdB/O/WuhwpyUgquUoue9G7q5cDmVF8Up8zlYNPXEpMZ7YLlmQ1A/bmH8DvmGqmAMQ0uVAgMBAAGjQjBAMB0GA1UdDgQWBBTEmRNsGAPCe8CjoA1/coB6HHcmjTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQwFAAOCAgEAUabz4vS4PZO/Lc4Pu1vhVRROTtHlznldgX/+tvCHM/jvlOV+3Gp5pxy+8JS3ptEwnMgNCnWefZKVfhidfsJxaXwU6s+DDuQUQp50DhDNqxq6EWGBeNjxtUVAeKuowM77fWM3aPbn+6/Gw0vsHzYmE1SGlHKy6gLti23kDKaQwFd1z4xCfVzmMX3zybKSaUYOiPjjLUKyOKimGY3xn83uamW8GrAlvacp/fQ+onVJv57byfenHmOZ4VxG/5IFjPoeIPmGlFYl5bRXOJ3riGQUIUkhOb9iZqmxospvPyFgxYnURTbImHy99v6ZSYA7LNKmp4gDBDEZt7Y6YUX6yfIjyGNzv1aJMbDZfGKnexWoiIqrOEDCzBL/FePwN983csvMmOa/orz6JopxVtfnJBtIRD6e/J/JzBrsQzwBvDR4yGn1xuZW7AYJNpDrFEobXsmII9oDMJELuDY++ee1KG++P+w8j2Ud5cAeh6Squpj9kuNsJnfdBrRkBof0Tta6SqoWqPQFZ2aWuuJVecMsXUmPgEkrihLHdoBR37q9ZV0+N0djMenl9MU/S60EinpxLK8JQzcPqOMyT/RFtm2XNuyE9QoB6he7hY1Ck3DDUOUUi78/w0EP3SIEIwiKum1xRKtzCTrJ+VKACd+66eYWyi4uTLLT3OUEVLLUNIAytbwPF+E=';
18
+
19
+ const APPLE_ROOT_CA_G3_B64 =
20
+ 'MIICQzCCAcmgAwIBAgIILcX8iNLFS5UwCgYIKoZIzj0EAwMwZzEbMBkGA1UEAwwSQXBwbGUgUm9vdCBDQSAtIEczMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UEBhMCVVMwHhcNMTQwNDMwMTgxOTA2WhcNMzkwNDMwMTgxOTA2WjBnMRswGQYDVQQDDBJBcHBsZSBSb290IENBIC0gRzMxJjAkBgNVBAsMHUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRMwEQYDVQQKDApBcHBsZSBJbmMuMQswCQYDVQQGEwJVUzB2MBAGByqGSM49AgEGBSuBBAAiA2IABJjpLz1AcqTtkyJygRMc3RCV8cWjTnHcFBbZDuWmBSp3ZHtfTjjTuxxEtX/1H7YyYl3J6YRbTzBPEVoA/VhYDKX1DyxNB0cTddqXl5dvMVztK517IDvYuVTZXpmkOlEKMaNCMEAwHQYDVR0OBBYEFLuw3qFYM4iapIqZ3r6966/ayySrMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gAMGUCMQCD6cHEFl4aXTQY2e3v9GwOAEZLuN+yRhHFD/3meoyhpmvOwgPUnPWTxnS4at+qIxUCMG1mihDK1A3UT82NQz60imOlM27jbdoXt2QfyFMm+YhidDkLF1vLUagM6BgD56KyKA==';
21
+
22
+ export const APPLE_ROOT_CERTS: Buffer[] = [
23
+ Buffer.from(APPLE_INC_ROOT_B64, 'base64'),
24
+ Buffer.from(APPLE_ROOT_CA_G2_B64, 'base64'),
25
+ Buffer.from(APPLE_ROOT_CA_G3_B64, 'base64'),
26
+ ];