payment-kit 1.27.2 → 1.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/__blocklet__.js +37 -0
- package/api/ocap-1.30-subpath-shims.d.ts +35 -0
- package/api/src/crons/index.ts +32 -0
- package/api/src/crons/metering-subscription-detection.ts +12 -14
- package/api/src/crons/overdue-detection.ts +51 -74
- package/api/src/crons/retry-pending-events.ts +58 -0
- package/api/src/integrations/app-store/apple-root-certs.ts +26 -0
- package/api/src/integrations/app-store/client.ts +369 -0
- package/api/src/integrations/app-store/handlers/index.ts +46 -0
- package/api/src/integrations/app-store/handlers/subscription.ts +635 -0
- package/api/src/integrations/app-store/node-apple-receipt-verify.d.ts +17 -0
- package/api/src/integrations/app-store/notification-routing.ts +18 -0
- package/api/src/integrations/app-store/signed-data-verifier.ts +150 -0
- package/api/src/integrations/arcblock/nft.ts +6 -2
- package/api/src/integrations/arcblock/stake.ts +3 -2
- package/api/src/integrations/arcblock/token.ts +4 -4
- package/api/src/integrations/blocklet/notification.ts +1 -1
- package/api/src/integrations/ethereum/tx.ts +29 -0
- package/api/src/integrations/google-play/client.ts +276 -0
- package/api/src/integrations/google-play/handlers/index.ts +69 -0
- package/api/src/integrations/google-play/handlers/subscription.ts +565 -0
- package/api/src/integrations/google-play/handlers/voided.ts +106 -0
- package/api/src/integrations/google-play/setup.ts +43 -0
- package/api/src/integrations/google-play/verify.ts +251 -0
- package/api/src/integrations/iap-reconcile.ts +415 -0
- package/api/src/integrations/stripe/handlers/invoice.ts +70 -53
- package/api/src/integrations/stripe/handlers/payment-intent.ts +8 -1
- package/api/src/integrations/stripe/resource.ts +8 -0
- package/api/src/libs/audit.ts +70 -24
- package/api/src/libs/auth.ts +49 -2
- package/api/src/libs/chain-error.ts +31 -0
- package/api/src/libs/entitlement.ts +399 -0
- package/api/src/libs/env.ts +2 -0
- package/api/src/libs/error.ts +15 -0
- package/api/src/libs/event.ts +42 -1
- package/api/src/libs/invoice.ts +69 -34
- package/api/src/libs/notification/template/customer-auto-recharge-daily-limit-exceeded.ts +1 -3
- package/api/src/libs/notification/template/customer-auto-recharge-failed.ts +1 -3
- package/api/src/libs/notification/template/customer-credit-grant-granted.ts +1 -3
- package/api/src/libs/notification/template/customer-credit-insufficient.ts +1 -3
- package/api/src/libs/notification/template/customer-credit-low-balance.ts +1 -3
- package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -3
- package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -3
- package/api/src/libs/notification/template/one-time-payment-refund-succeeded.ts +1 -3
- package/api/src/libs/notification/template/one-time-payment-succeeded.ts +1 -3
- package/api/src/libs/notification/template/subscription-renew-failed.ts +1 -3
- package/api/src/libs/notification/template/subscription-slippage-exceeded.ts +1 -3
- package/api/src/libs/notification/template/subscription-slippage-warning.ts +1 -3
- package/api/src/libs/notification/template/subscription-succeeded.ts +1 -1
- package/api/src/libs/pagination.ts +14 -9
- package/api/src/libs/payment.ts +25 -10
- package/api/src/libs/security.ts +51 -0
- package/api/src/libs/session.ts +1 -1
- package/api/src/libs/subscription.ts +13 -1
- package/api/src/libs/timing.ts +35 -0
- package/api/src/libs/util.ts +29 -15
- package/api/src/libs/wallet-migration.ts +72 -53
- package/api/src/queues/auto-recharge.ts +1 -1
- package/api/src/queues/credit-consume.ts +94 -12
- package/api/src/queues/credit-grant.ts +4 -0
- package/api/src/queues/event.ts +39 -21
- package/api/src/queues/invoice.ts +1 -0
- package/api/src/queues/payment.ts +83 -15
- package/api/src/queues/refund.ts +84 -71
- package/api/src/queues/subscription.ts +1 -0
- package/api/src/queues/webhook.ts +12 -2
- package/api/src/routes/checkout-sessions.ts +82 -43
- package/api/src/routes/connect/change-payment.ts +2 -0
- package/api/src/routes/connect/change-plan.ts +2 -0
- package/api/src/routes/connect/pay.ts +12 -3
- package/api/src/routes/connect/setup.ts +3 -1
- package/api/src/routes/connect/shared.ts +52 -39
- package/api/src/routes/connect/subscribe.ts +4 -1
- package/api/src/routes/credit-grants.ts +25 -17
- package/api/src/routes/donations.ts +2 -2
- package/api/src/routes/entitlements.ts +105 -0
- package/api/src/routes/events.ts +2 -2
- package/api/src/routes/index.ts +12 -2
- package/api/src/routes/integrations/app-store.ts +267 -0
- package/api/src/routes/integrations/google-play.ts +324 -0
- package/api/src/routes/meter-events.ts +16 -6
- package/api/src/routes/payment-links.ts +1 -1
- package/api/src/routes/payment-methods.ts +131 -1
- package/api/src/routes/settings.ts +1 -1
- package/api/src/routes/tax-rates.ts +1 -1
- package/api/src/store/migrations/20260526-iap-foundation.ts +105 -0
- package/api/src/store/models/customer.ts +37 -1
- package/api/src/store/models/entitlement-grant.ts +118 -0
- package/api/src/store/models/entitlement-product.ts +48 -0
- package/api/src/store/models/entitlement.ts +86 -0
- package/api/src/store/models/index.ts +9 -0
- package/api/src/store/models/invoice.ts +20 -0
- package/api/src/store/models/payment-method.ts +66 -1
- package/api/src/store/models/price.ts +23 -14
- package/api/src/store/models/refund.ts +10 -0
- package/api/src/store/models/subscription.ts +14 -0
- package/api/src/store/models/types.ts +32 -0
- package/api/tests/integrations/app-store/client.spec.ts +335 -0
- package/api/tests/integrations/app-store/handlers.spec.ts +480 -0
- package/api/tests/integrations/app-store/notifications.spec.ts +381 -0
- package/api/tests/integrations/app-store/signed-data-verifier.spec.ts +72 -0
- package/api/tests/integrations/app-store/webhook-routing.spec.ts +27 -0
- package/api/tests/integrations/google-play/handlers.spec.ts +341 -0
- package/api/tests/integrations/google-play/verify.spec.ts +215 -0
- package/api/tests/integrations/iap-reconcile.spec.ts +237 -0
- package/api/tests/libs/entitlement.spec.ts +347 -0
- package/api/tests/libs/wallet-migration.spec.ts +4 -4
- package/api/tests/queues/credit-consume-batch.spec.ts +5 -2
- package/api/tests/queues/credit-consume.spec.ts +8 -4
- package/api/tests/routes/credit-grants.spec.ts +1 -0
- package/blocklet.yml +1 -1
- package/cloudflare/MIGRATION-CHALLENGES.md +676 -0
- package/cloudflare/MIGRATION-RUNBOOK.md +777 -0
- package/cloudflare/README.md +499 -0
- package/cloudflare/STAGING-MIGRATION-GUIDE.md +602 -0
- package/cloudflare/build.ts +151 -0
- package/cloudflare/did-connect-auth.ts +527 -0
- package/cloudflare/docs/2026-04-22-sdk-1.30.9-upgrade-retro.md +324 -0
- package/cloudflare/docs/2026-04-24-queue-ops-followup.md +218 -0
- package/cloudflare/docs/cf-queues-ops-alert-analysis.md +663 -0
- package/cloudflare/docs/cf-workers-local-dev-and-fixes.md +284 -0
- package/cloudflare/docs/cleanup-tasks-2026-05.md +62 -0
- package/cloudflare/docs/payment-kit-platform-analysis-2026-04-20.md +354 -0
- package/cloudflare/frontend-shims/buffer-polyfill.ts +9 -0
- package/cloudflare/frontend-shims/js-sdk.ts +43 -0
- package/cloudflare/frontend-shims/mime-types.ts +46 -0
- package/cloudflare/frontend-shims/session.ts +24 -0
- package/cloudflare/frontend-shims/vite-plugin-noop.ts +6 -0
- package/cloudflare/index.html +40 -0
- package/cloudflare/migrate-to-d1.js +252 -0
- package/cloudflare/migrations/0001_initial_schema.sql +82 -0
- package/cloudflare/migrations/0002_indexes.sql +75 -0
- package/cloudflare/migrations/0003_locks_and_constraints.sql +18 -0
- package/cloudflare/migrations/0004_iap_foundation.sql +72 -0
- package/cloudflare/migrations/0005_iap_tenant_backfill.sql +112 -0
- package/cloudflare/run-build.js +391 -0
- package/cloudflare/scripts/test-decrypt.js +102 -0
- package/cloudflare/shims/arcblock-ws.ts +20 -0
- package/cloudflare/shims/axios-http-adapter.ts +4 -0
- package/cloudflare/shims/axios-lite.ts +117 -0
- package/cloudflare/shims/blocklet-sdk/auth-service.ts +33 -0
- package/cloudflare/shims/blocklet-sdk/cdn.ts +3 -0
- package/cloudflare/shims/blocklet-sdk/component-api.ts +35 -0
- package/cloudflare/shims/blocklet-sdk/component.ts +18 -0
- package/cloudflare/shims/blocklet-sdk/config.ts +8 -0
- package/cloudflare/shims/blocklet-sdk/did.ts +14 -0
- package/cloudflare/shims/blocklet-sdk/env.ts +12 -0
- package/cloudflare/shims/blocklet-sdk/eventbus.ts +3 -0
- package/cloudflare/shims/blocklet-sdk/fallback.ts +3 -0
- package/cloudflare/shims/blocklet-sdk/index.ts +11 -0
- package/cloudflare/shims/blocklet-sdk/logger.ts +11 -0
- package/cloudflare/shims/blocklet-sdk/middlewares.ts +15 -0
- package/cloudflare/shims/blocklet-sdk/notification.ts +11 -0
- package/cloudflare/shims/blocklet-sdk/security.ts +53 -0
- package/cloudflare/shims/blocklet-sdk/session.ts +8 -0
- package/cloudflare/shims/blocklet-sdk/verify-session.ts +44 -0
- package/cloudflare/shims/blocklet-sdk/verify-sign.ts +38 -0
- package/cloudflare/shims/blocklet-sdk/wallet-authenticator.ts +3 -0
- package/cloudflare/shims/blocklet-sdk/wallet-handler.ts +6 -0
- package/cloudflare/shims/blocklet-sdk/wallet.ts +103 -0
- package/cloudflare/shims/cookie-parser.ts +3 -0
- package/cloudflare/shims/cors.ts +21 -0
- package/cloudflare/shims/cron.ts +189 -0
- package/cloudflare/shims/crypto-js-warn.ts +7 -0
- package/cloudflare/shims/did-space-js.ts +17 -0
- package/cloudflare/shims/did-space.ts +11 -0
- package/cloudflare/shims/error.ts +18 -0
- package/cloudflare/shims/express-compat/index.ts +80 -0
- package/cloudflare/shims/express-compat/types.ts +41 -0
- package/cloudflare/shims/fastq.ts +105 -0
- package/cloudflare/shims/lock.ts +115 -0
- package/cloudflare/shims/mime-types.ts +56 -0
- package/cloudflare/shims/nedb-storage.ts +9 -0
- package/cloudflare/shims/node-child-process.ts +9 -0
- package/cloudflare/shims/node-fs.ts +20 -0
- package/cloudflare/shims/node-http.ts +13 -0
- package/cloudflare/shims/node-https.ts +4 -0
- package/cloudflare/shims/node-misc.ts +15 -0
- package/cloudflare/shims/node-net.ts +8 -0
- package/cloudflare/shims/node-os.ts +14 -0
- package/cloudflare/shims/node-tty.ts +8 -0
- package/cloudflare/shims/node-zlib.ts +17 -0
- package/cloudflare/shims/noop.ts +26 -0
- package/cloudflare/shims/payment-vendor.ts +14 -0
- package/cloudflare/shims/querystring.ts +12 -0
- package/cloudflare/shims/queue.ts +611 -0
- package/cloudflare/shims/rolldown-runtime.ts +43 -0
- package/cloudflare/shims/sequelize-d1/datatypes.ts +24 -0
- package/cloudflare/shims/sequelize-d1/helpers.ts +46 -0
- package/cloudflare/shims/sequelize-d1/index.ts +34 -0
- package/cloudflare/shims/sequelize-d1/model.ts +1176 -0
- package/cloudflare/shims/sequelize-d1/operators.ts +306 -0
- package/cloudflare/shims/sequelize-d1/retry.ts +85 -0
- package/cloudflare/shims/sequelize-d1/sequelize-class.ts +119 -0
- package/cloudflare/shims/sequelize-d1/timing.ts +81 -0
- package/cloudflare/shims/sequelize-d1/types.ts +35 -0
- package/cloudflare/shims/stripe-cf.ts +29 -0
- package/cloudflare/shims/ws-lite.ts +103 -0
- package/cloudflare/shims/xss.ts +3 -0
- package/cloudflare/tests/shims/cron.spec.ts +210 -0
- package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +87 -0
- package/cloudflare/tests/shims/queue-scheduled.spec.ts +186 -0
- package/cloudflare/vite.config.ts +162 -0
- package/cloudflare/worker.ts +1608 -0
- package/cloudflare/wrangler.json +63 -0
- package/cloudflare/wrangler.jsonc +75 -0
- package/cloudflare/wrangler.staging.json +67 -0
- package/cloudflare/wrangler.toml +28 -0
- package/jest.config.js +4 -12
- package/package.json +30 -22
- package/scripts/seed-google-play.ts +79 -0
- package/src/app.tsx +62 -4
- package/src/components/customer/link.tsx +9 -13
- package/src/components/customer/notification-preference.tsx +3 -2
- package/src/components/filter-toolbar.tsx +4 -0
- package/src/components/invoice/list.tsx +9 -1
- package/src/components/invoice-pdf/utils.ts +2 -1
- package/src/components/layout/admin.tsx +39 -5
- package/src/components/layout/user-cf.tsx +77 -0
- package/src/components/payment-intent/actions.tsx +23 -3
- package/src/components/payment-method/app-store.tsx +103 -0
- package/src/components/payment-method/form.tsx +7 -1
- package/src/components/payment-method/google-play.tsx +85 -0
- package/src/components/safe-did-address.tsx +75 -0
- package/src/components/subscription/list.tsx +20 -0
- package/src/libs/patch-user-card.ts +25 -0
- package/src/libs/util.ts +5 -7
- package/src/locales/en.tsx +63 -0
- package/src/locales/zh.tsx +63 -0
- package/src/pages/admin/billing/meter-events/index.tsx +4 -0
- package/src/pages/admin/billing/subscriptions/detail.tsx +80 -0
- package/src/pages/admin/customers/customers/detail.tsx +8 -2
- package/src/pages/admin/customers/customers/index.tsx +2 -2
- package/src/pages/admin/overview.tsx +3 -1
- package/src/pages/admin/settings/payment-methods/create.tsx +12 -0
- package/src/pages/admin/settings/payment-methods/index.tsx +1 -1
- package/src/pages/customer/subscription/detail.tsx +4 -4
- package/tsconfig.api.json +1 -6
- package/tsconfig.json +3 -4
- package/tsconfig.types.json +2 -1
- package/vite.config.ts +6 -1
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
/* eslint-disable no-await-in-loop, no-continue --
|
|
2
|
+
Sequential per-subscription processing is intentional: each iteration
|
|
3
|
+
issues 1-2 D1 writes + an Apple/Google API call, and parallelizing would
|
|
4
|
+
blast D1 with N concurrent transactions and trip per-method rate limits
|
|
5
|
+
on Apple's App Store Server API. The `continue` guards bail early on
|
|
6
|
+
rows that can't be processed (missing tenant, missing original txn,
|
|
7
|
+
etc.) which is cleaner than nested if-else for a long per-row pipeline. */
|
|
8
|
+
// IAP reconcile cron — webhook backup.
|
|
9
|
+
//
|
|
10
|
+
// Webhooks are the fast path for App Store Server Notifications V2 and Google
|
|
11
|
+
// Play RTDN. They occasionally fail to deliver (Apple/Google outages, our 5xx
|
|
12
|
+
// during deploy, Pub/Sub backlog dropping old messages). When that happens the
|
|
13
|
+
// local Subscription row falls out of sync with the real subscription state at
|
|
14
|
+
// Apple/Google, and the user either keeps Pro after a refund or loses Pro at
|
|
15
|
+
// renewal.
|
|
16
|
+
//
|
|
17
|
+
// This module periodically pulls each active `app_store` / `google_play`
|
|
18
|
+
// subscription's authoritative state from the App Store Server API + Google
|
|
19
|
+
// Play Developer API and applies drift to the local row. Cron schedule is
|
|
20
|
+
// every 5 minutes by default; tuneable via `IAP_RECONCILE_CRON_TIME`.
|
|
21
|
+
|
|
22
|
+
import { Op } from 'sequelize';
|
|
23
|
+
|
|
24
|
+
import { createEvent } from '../libs/audit';
|
|
25
|
+
import logger from '../libs/logger';
|
|
26
|
+
import { PaymentMethod, Price, Subscription, SubscriptionItem } from '../store/models';
|
|
27
|
+
import { AppStoreClient, AppStoreTransactionPayload } from './app-store/client';
|
|
28
|
+
import { GooglePlayClient, GooglePlaySubscriptionPurchase } from './google-play/client';
|
|
29
|
+
|
|
30
|
+
/** Don't re-check subs that were updated by a webhook within the last 5 minutes. */
|
|
31
|
+
const RECENT_UPDATE_GUARD_MS = 5 * 60 * 1000;
|
|
32
|
+
|
|
33
|
+
/** Per-channel batch cap so a single cron tick can't stall on Apple/Google rate limits. */
|
|
34
|
+
const DEFAULT_BATCH_SIZE = Number(process.env.IAP_RECONCILE_BATCH_SIZE ?? '100');
|
|
35
|
+
|
|
36
|
+
type ReconcileStats = { checked: number; updated: number; errors: number };
|
|
37
|
+
|
|
38
|
+
const emptyStats = (): ReconcileStats => ({ checked: 0, updated: 0, errors: 0 });
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Single entry point used by the cron registry. Catches per-channel errors so
|
|
42
|
+
* one channel's outage doesn't take down the other.
|
|
43
|
+
*/
|
|
44
|
+
export async function runIapReconcile(): Promise<{ app_store: ReconcileStats; google_play: ReconcileStats }> {
|
|
45
|
+
const [appStore, googlePlay] = await Promise.all([
|
|
46
|
+
reconcileAppStore().catch((err) => {
|
|
47
|
+
logger.error('iap-reconcile: app_store channel failed', { error: err?.message });
|
|
48
|
+
return emptyStats();
|
|
49
|
+
}),
|
|
50
|
+
reconcileGooglePlay().catch((err) => {
|
|
51
|
+
logger.error('iap-reconcile: google_play channel failed', { error: err?.message });
|
|
52
|
+
return emptyStats();
|
|
53
|
+
}),
|
|
54
|
+
]);
|
|
55
|
+
// Self-heal SubscriptionItem rows for any IAP sub whose Product mapping
|
|
56
|
+
// became available AFTER purchase (typical: customer bought before admin
|
|
57
|
+
// bound the SKU to a local Product, so ingest skipped SubscriptionItem
|
|
58
|
+
// creation; once the binding exists, admin UI's product-name column and
|
|
59
|
+
// anything else that walks subscription.items would otherwise stay blank
|
|
60
|
+
// forever).
|
|
61
|
+
await backfillMissingSubscriptionItems().catch((err) => {
|
|
62
|
+
logger.error('iap-reconcile: subscription_items backfill failed', { error: err?.message });
|
|
63
|
+
});
|
|
64
|
+
logger.info('iap-reconcile: pass complete', { app_store: appStore, google_play: googlePlay });
|
|
65
|
+
return { app_store: appStore, google_play: googlePlay };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Backfill SubscriptionItem rows for IAP Subscriptions that lost out on the
|
|
70
|
+
* happy-path create (Price was unmapped at ingest time, or the create call
|
|
71
|
+
* crashed mid-flow). Idempotent — only inserts when both:
|
|
72
|
+
* - The sub has zero SubscriptionItem rows
|
|
73
|
+
* - We can resolve a Price via channel SKU → Price.metadata mapping
|
|
74
|
+
*
|
|
75
|
+
* Stripe-style schema: SKU binding lives on Price.metadata, not
|
|
76
|
+
* Product.metadata, so we point the SubscriptionItem at the exact Price the
|
|
77
|
+
* customer paid for (not the Product's default_price). That preserves the
|
|
78
|
+
* monthly-vs-yearly distinction in the SubscriptionItem record.
|
|
79
|
+
*/
|
|
80
|
+
async function backfillMissingSubscriptionItems(): Promise<void> {
|
|
81
|
+
const candidates = await Subscription.findAll({
|
|
82
|
+
where: { channel: { [Op.in]: ['google_play', 'app_store'] } as any },
|
|
83
|
+
limit: 500,
|
|
84
|
+
});
|
|
85
|
+
// Cache decoded PaymentMethod tenant identifiers (bundle_id / package_name)
|
|
86
|
+
// so we don't re-decrypt settings for every sub. Most installations have a
|
|
87
|
+
// small number of IAP PaymentMethods so this fits in a Map.
|
|
88
|
+
const tenantCache = new Map<string, string | null>();
|
|
89
|
+
const tenantFor = async (sub: Subscription): Promise<string | null> => {
|
|
90
|
+
const pmId = (sub as any).default_payment_method_id as string | undefined;
|
|
91
|
+
if (!pmId) return null;
|
|
92
|
+
if (tenantCache.has(pmId)) return tenantCache.get(pmId)!;
|
|
93
|
+
const pm = await PaymentMethod.findByPk(pmId);
|
|
94
|
+
const settings = pm ? PaymentMethod.decryptSettings(pm.settings) : null;
|
|
95
|
+
const tenant =
|
|
96
|
+
sub.channel === 'google_play'
|
|
97
|
+
? (settings?.google_play?.package_name ?? null)
|
|
98
|
+
: (settings?.app_store?.bundle_id ?? null);
|
|
99
|
+
tenantCache.set(pmId, tenant);
|
|
100
|
+
return tenant;
|
|
101
|
+
};
|
|
102
|
+
let inserted = 0;
|
|
103
|
+
for (const sub of candidates) {
|
|
104
|
+
const existing = await SubscriptionItem.count({ where: { subscription_id: sub.id } });
|
|
105
|
+
if (existing > 0) continue;
|
|
106
|
+
|
|
107
|
+
const pd: any = (sub as any).payment_details;
|
|
108
|
+
const sku = sub.channel === 'google_play' ? pd?.google_play?.product_id : pd?.app_store?.product_id;
|
|
109
|
+
if (!sku) continue;
|
|
110
|
+
|
|
111
|
+
// Multi-tenant scoping: filter Price lookup by the originating app's
|
|
112
|
+
// bundle_id (iOS) / package_name (Android). Without this, a sub from
|
|
113
|
+
// App A with SKU "pro_monthly" could be backfilled with App B's Price
|
|
114
|
+
// if App B also has a SKU "pro_monthly" — same SKU string lives in
|
|
115
|
+
// independent App Store / Play Console namespaces.
|
|
116
|
+
const tenant = await tenantFor(sub);
|
|
117
|
+
if (!tenant) {
|
|
118
|
+
logger.warn('iap-reconcile: no tenant id (bundleId/packageName) for sub, skipping backfill', {
|
|
119
|
+
subscriptionId: sub.id,
|
|
120
|
+
channel: sub.channel,
|
|
121
|
+
});
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const skuKey = sub.channel === 'google_play' ? 'google_play_product_id' : 'app_store_product_id';
|
|
125
|
+
const tenantKey = sub.channel === 'google_play' ? 'package_name' : 'bundle_id';
|
|
126
|
+
const price = await Price.findOne({
|
|
127
|
+
where: { [`metadata.${skuKey}`]: sku, [`metadata.${tenantKey}`]: tenant } as any,
|
|
128
|
+
});
|
|
129
|
+
if (!price) continue;
|
|
130
|
+
|
|
131
|
+
await SubscriptionItem.create({
|
|
132
|
+
subscription_id: sub.id,
|
|
133
|
+
price_id: price.id,
|
|
134
|
+
quantity: 1,
|
|
135
|
+
livemode: sub.livemode,
|
|
136
|
+
metadata: { backfilled_at: Math.floor(Date.now() / 1000), reason: 'reconcile_missing_item' },
|
|
137
|
+
} as any);
|
|
138
|
+
inserted += 1;
|
|
139
|
+
logger.info('iap-reconcile: backfilled SubscriptionItem', {
|
|
140
|
+
subscriptionId: sub.id,
|
|
141
|
+
productId: price.product_id,
|
|
142
|
+
priceId: price.id,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
if (inserted > 0) logger.info(`iap-reconcile: backfilled ${inserted} subscription_items`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// App Store
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
export async function reconcileAppStore(batchSize = DEFAULT_BATCH_SIZE): Promise<ReconcileStats> {
|
|
153
|
+
const stats = emptyStats();
|
|
154
|
+
const methods = await PaymentMethod.findAll({ where: { type: 'app_store' } });
|
|
155
|
+
if (methods.length === 0) return stats;
|
|
156
|
+
|
|
157
|
+
const subs = await Subscription.findAll({
|
|
158
|
+
where: {
|
|
159
|
+
status: { [Op.in]: ['active', 'past_due', 'trialing'] },
|
|
160
|
+
'payment_details.app_store.original_transaction_id': { [Op.ne]: null } as any,
|
|
161
|
+
updated_at: { [Op.lt]: new Date(Date.now() - RECENT_UPDATE_GUARD_MS) },
|
|
162
|
+
} as any,
|
|
163
|
+
order: [['updated_at', 'ASC']],
|
|
164
|
+
limit: batchSize,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
for (const sub of subs) {
|
|
168
|
+
stats.checked += 1;
|
|
169
|
+
try {
|
|
170
|
+
const originalTransactionId = sub.payment_details?.app_store?.original_transaction_id as string | undefined;
|
|
171
|
+
if (!originalTransactionId) continue;
|
|
172
|
+
|
|
173
|
+
const method = pickIapMethodForSub(methods, sub);
|
|
174
|
+
if (!method) {
|
|
175
|
+
logger.warn('iap-reconcile: no matching app_store PaymentMethod', { subscriptionId: sub.id });
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Apple SDK throws when issuer/key creds aren't present. Skip cleanly.
|
|
180
|
+
let client: AppStoreClient;
|
|
181
|
+
try {
|
|
182
|
+
client = method.getAppStoreClient();
|
|
183
|
+
} catch (err: any) {
|
|
184
|
+
logger.warn('iap-reconcile: cannot build app_store client', { subscriptionId: sub.id, error: err?.message });
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const transaction = await client.getSubscriptionStatus(originalTransactionId);
|
|
189
|
+
if (!transaction) {
|
|
190
|
+
logger.warn('iap-reconcile: app_store getSubscriptionStatus returned null', { originalTransactionId });
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const drifted = await applyAppStoreTransactionDrift(sub, transaction);
|
|
194
|
+
if (drifted) stats.updated += 1;
|
|
195
|
+
} catch (err: any) {
|
|
196
|
+
stats.errors += 1;
|
|
197
|
+
logger.error('iap-reconcile: app_store sub failed', { subscriptionId: sub.id, error: err?.message });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return stats;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Select the IAP PaymentMethod that actually funds this subscription. Match by
|
|
205
|
+
* the bound `default_payment_method_id` — NOT the non-existent `payment_method_id`
|
|
206
|
+
* (the previous bug, which always fell through to methods[0] and queried the
|
|
207
|
+
* wrong app/credentials in multi-app/multi-env deployments — PR #1381 review P1).
|
|
208
|
+
* Only default to the sole method when there is exactly one; never pick an
|
|
209
|
+
* arbitrary method.
|
|
210
|
+
*/
|
|
211
|
+
export function pickIapMethodForSub(methods: PaymentMethod[], sub: Subscription): PaymentMethod | undefined {
|
|
212
|
+
const bound = methods.find((m) => m.id === sub.default_payment_method_id);
|
|
213
|
+
if (bound) return bound;
|
|
214
|
+
return methods.length === 1 ? methods[0] : undefined;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Compare Apple's authoritative `transaction` payload against our local row
|
|
219
|
+
* and apply only the drifts that matter — period_end forward jump (renewed),
|
|
220
|
+
* revocation (refunded), explicit expiry.
|
|
221
|
+
*
|
|
222
|
+
* Returns true if we wrote to the row.
|
|
223
|
+
*/
|
|
224
|
+
export async function applyAppStoreTransactionDrift(
|
|
225
|
+
sub: Subscription,
|
|
226
|
+
transaction: AppStoreTransactionPayload
|
|
227
|
+
): Promise<boolean> {
|
|
228
|
+
const appleExpiresSec = transaction.expiresDate ? Math.floor(transaction.expiresDate / 1000) : undefined;
|
|
229
|
+
const revoked = Boolean(transaction.revocationDate);
|
|
230
|
+
|
|
231
|
+
if (revoked) {
|
|
232
|
+
if (sub.status === 'canceled') return false;
|
|
233
|
+
await sub.update({
|
|
234
|
+
status: 'canceled',
|
|
235
|
+
canceled_at: Math.floor((transaction.revocationDate ?? Date.now()) / 1000),
|
|
236
|
+
cancelation_details: {
|
|
237
|
+
reason: 'app_store_revoked',
|
|
238
|
+
feedback: `reconcile-cron: detected revocationDate=${transaction.revocationDate}`,
|
|
239
|
+
} as any,
|
|
240
|
+
metadata: {
|
|
241
|
+
...(sub.metadata || {}),
|
|
242
|
+
app_store_reconciled_at: Math.floor(Date.now() / 1000),
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
createEvent('Subscription', 'customer.subscription.deleted', sub).catch(() => {});
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Renewal drift — Apple says we have more time than we record.
|
|
250
|
+
if (appleExpiresSec && appleExpiresSec > sub.current_period_end + 60) {
|
|
251
|
+
await sub.update({
|
|
252
|
+
status: 'active',
|
|
253
|
+
current_period_end: appleExpiresSec,
|
|
254
|
+
payment_details: {
|
|
255
|
+
...(sub.payment_details || {}),
|
|
256
|
+
app_store: {
|
|
257
|
+
...(sub.payment_details?.app_store || ({} as any)),
|
|
258
|
+
expires_at: transaction.expiresDate ? Math.floor(transaction.expiresDate / 1000) : undefined,
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
metadata: {
|
|
262
|
+
...(sub.metadata || {}),
|
|
263
|
+
app_store_reconciled_at: Math.floor(Date.now() / 1000),
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
createEvent('Subscription', 'customer.subscription.updated', sub).catch(() => {});
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Past expiry — Apple says we're past period end but we still say active.
|
|
271
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
272
|
+
if (appleExpiresSec && appleExpiresSec < nowSec - 60 && (sub.status === 'active' || sub.status === 'trialing')) {
|
|
273
|
+
await sub.update({
|
|
274
|
+
status: 'canceled',
|
|
275
|
+
canceled_at: nowSec,
|
|
276
|
+
cancelation_details: {
|
|
277
|
+
reason: 'app_store_expired',
|
|
278
|
+
feedback: `reconcile-cron: appleExpires=${transaction.expiresDate} < now`,
|
|
279
|
+
} as any,
|
|
280
|
+
metadata: {
|
|
281
|
+
...(sub.metadata || {}),
|
|
282
|
+
app_store_reconciled_at: nowSec,
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
createEvent('Subscription', 'customer.subscription.deleted', sub).catch(() => {});
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
// Google Play
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
export async function reconcileGooglePlay(batchSize = DEFAULT_BATCH_SIZE): Promise<ReconcileStats> {
|
|
297
|
+
const stats = emptyStats();
|
|
298
|
+
const methods = await PaymentMethod.findAll({ where: { type: 'google_play' } });
|
|
299
|
+
if (methods.length === 0) return stats;
|
|
300
|
+
|
|
301
|
+
const subs = await Subscription.findAll({
|
|
302
|
+
where: {
|
|
303
|
+
status: { [Op.in]: ['active', 'past_due', 'trialing'] },
|
|
304
|
+
'payment_details.google_play.purchase_token': { [Op.ne]: null } as any,
|
|
305
|
+
updated_at: { [Op.lt]: new Date(Date.now() - RECENT_UPDATE_GUARD_MS) },
|
|
306
|
+
} as any,
|
|
307
|
+
order: [['updated_at', 'ASC']],
|
|
308
|
+
limit: batchSize,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
for (const sub of subs) {
|
|
312
|
+
stats.checked += 1;
|
|
313
|
+
try {
|
|
314
|
+
const purchaseToken = sub.payment_details?.google_play?.purchase_token as string | undefined;
|
|
315
|
+
const productId = sub.payment_details?.google_play?.product_id as string | undefined;
|
|
316
|
+
if (!purchaseToken || !productId) continue;
|
|
317
|
+
|
|
318
|
+
const method = pickIapMethodForSub(methods, sub);
|
|
319
|
+
if (!method) {
|
|
320
|
+
logger.warn('iap-reconcile: no matching google_play PaymentMethod', { subscriptionId: sub.id });
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
let client: GooglePlayClient;
|
|
324
|
+
try {
|
|
325
|
+
client = method.getGooglePlayClient();
|
|
326
|
+
} catch (err: any) {
|
|
327
|
+
logger.warn('iap-reconcile: cannot build google_play client', { subscriptionId: sub.id, error: err?.message });
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const purchase = await client.getSubscription(productId, purchaseToken);
|
|
332
|
+
const drifted = await applyGooglePlayPurchaseDrift(sub, purchase);
|
|
333
|
+
if (drifted) stats.updated += 1;
|
|
334
|
+
} catch (err: any) {
|
|
335
|
+
stats.errors += 1;
|
|
336
|
+
logger.error('iap-reconcile: google_play sub failed', { subscriptionId: sub.id, error: err?.message });
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return stats;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Compare Google's authoritative `purchase` payload against our local row
|
|
344
|
+
* and apply only meaningful drifts.
|
|
345
|
+
*
|
|
346
|
+
* Returns true if we wrote to the row.
|
|
347
|
+
*/
|
|
348
|
+
export async function applyGooglePlayPurchaseDrift(
|
|
349
|
+
sub: Subscription,
|
|
350
|
+
purchase: GooglePlaySubscriptionPurchase
|
|
351
|
+
): Promise<boolean> {
|
|
352
|
+
const expirySec = purchase.expiryTimeMillis ? Math.floor(Number(purchase.expiryTimeMillis) / 1000) : undefined;
|
|
353
|
+
// cancelReason: 0=user, 1=system, 2=replaced, 3=developer
|
|
354
|
+
const revoked =
|
|
355
|
+
typeof purchase.cancelReason === 'number' && purchase.cancelReason >= 0 && purchase.autoRenewing === false;
|
|
356
|
+
|
|
357
|
+
// Renewal drift forward
|
|
358
|
+
if (expirySec && expirySec > sub.current_period_end + 60) {
|
|
359
|
+
await sub.update({
|
|
360
|
+
status: 'active',
|
|
361
|
+
current_period_end: expirySec,
|
|
362
|
+
payment_details: {
|
|
363
|
+
...(sub.payment_details || {}),
|
|
364
|
+
google_play: {
|
|
365
|
+
...((sub.payment_details?.google_play || {}) as any),
|
|
366
|
+
expiry_time_millis: purchase.expiryTimeMillis,
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
metadata: {
|
|
370
|
+
...(sub.metadata || {}),
|
|
371
|
+
google_play_reconciled_at: Math.floor(Date.now() / 1000),
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
createEvent('Subscription', 'customer.subscription.updated', sub).catch(() => {});
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Auto-renew off (user canceled but still has paid period) → schedule cancel
|
|
379
|
+
if (revoked && !sub.cancel_at_period_end) {
|
|
380
|
+
await sub.update({
|
|
381
|
+
cancel_at_period_end: true,
|
|
382
|
+
cancelation_details: {
|
|
383
|
+
reason: 'google_play_auto_renew_off',
|
|
384
|
+
feedback: `reconcile-cron: cancelReason=${purchase.cancelReason} autoRenewing=false`,
|
|
385
|
+
} as any,
|
|
386
|
+
metadata: {
|
|
387
|
+
...(sub.metadata || {}),
|
|
388
|
+
google_play_reconciled_at: Math.floor(Date.now() / 1000),
|
|
389
|
+
},
|
|
390
|
+
});
|
|
391
|
+
createEvent('Subscription', 'customer.subscription.updated', sub).catch(() => {});
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Past expiry — Google says we're expired but local still active.
|
|
396
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
397
|
+
if (expirySec && expirySec < nowSec - 60 && (sub.status === 'active' || sub.status === 'trialing')) {
|
|
398
|
+
await sub.update({
|
|
399
|
+
status: 'canceled',
|
|
400
|
+
canceled_at: nowSec,
|
|
401
|
+
cancelation_details: {
|
|
402
|
+
reason: 'google_play_expired',
|
|
403
|
+
feedback: `reconcile-cron: googleExpires=${purchase.expiryTimeMillis} < now`,
|
|
404
|
+
} as any,
|
|
405
|
+
metadata: {
|
|
406
|
+
...(sub.metadata || {}),
|
|
407
|
+
google_play_reconciled_at: nowSec,
|
|
408
|
+
},
|
|
409
|
+
});
|
|
410
|
+
createEvent('Subscription', 'customer.subscription.deleted', sub).catch(() => {});
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
@@ -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({
|