payment-kit 1.29.0 → 1.29.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/dev.ts +41 -2
- package/api/hono.d.ts +42 -0
- package/api/node-sqlite.d.ts +12 -0
- package/api/src/bootstrap.ts +36 -0
- package/api/src/crons/base.ts +3 -3
- package/api/src/crons/currency.ts +1 -1
- package/api/src/crons/index.ts +27 -24
- package/api/src/crons/metering-subscription-detection.ts +1 -1
- package/api/src/crons/overdue-detection.ts +2 -2
- package/api/src/crons/retry-pending-events.ts +6 -0
- package/api/src/index.ts +22 -161
- package/api/src/integrations/app-store/client.ts +3 -4
- package/api/src/integrations/app-store/handlers/subscription.ts +7 -7
- package/api/src/integrations/app-store/signed-data-verifier.ts +3 -2
- package/api/src/integrations/arcblock/token.ts +21 -7
- package/api/src/integrations/google-play/handlers/subscription.ts +6 -6
- package/api/src/integrations/google-play/handlers/voided.ts +2 -2
- package/api/src/integrations/google-play/verify.ts +3 -2
- package/api/src/integrations/iap-reconcile.ts +3 -5
- package/api/src/integrations/stripe/handlers/invoice.ts +2 -2
- package/api/src/integrations/stripe/handlers/subscription.ts +3 -3
- package/api/src/libs/archive/query.ts +19 -0
- package/api/src/libs/audit.ts +61 -4
- package/api/src/libs/auth.ts +99 -38
- package/api/src/libs/context.ts +78 -1
- package/api/src/libs/currency.ts +2 -2
- package/api/src/libs/dayjs.ts +8 -2
- package/api/src/libs/drivers/auth-storage.ts +118 -0
- package/api/src/libs/drivers/cron.ts +264 -0
- package/api/src/libs/drivers/db.ts +170 -0
- package/api/src/libs/drivers/identity.ts +81 -0
- package/api/src/libs/drivers/index.ts +40 -0
- package/api/src/libs/drivers/locks.ts +226 -0
- package/api/src/libs/drivers/migrate-runner.ts +70 -0
- package/api/src/libs/drivers/queue.ts +104 -0
- package/api/src/libs/drivers/secrets.ts +194 -0
- package/api/src/libs/env.ts +170 -54
- package/api/src/libs/exchange-rate/service.ts +7 -6
- package/api/src/libs/http-fetch-adapter.ts +50 -0
- package/api/src/libs/invoice.ts +1 -1
- package/api/src/libs/lock.ts +51 -47
- package/api/src/libs/logger.ts +48 -8
- package/api/src/libs/notification/index.ts +1 -1
- package/api/src/libs/notification/template/customer-credit-low-balance.ts +2 -1
- package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -1
- package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -1
- package/api/src/libs/overdraft-protection.ts +1 -1
- package/api/src/libs/payout.ts +1 -1
- package/api/src/libs/queue/index.ts +259 -52
- package/api/src/libs/queue/runtime.ts +175 -0
- package/api/src/libs/resource.ts +3 -3
- package/api/src/libs/secrets.ts +38 -0
- package/api/src/libs/session.ts +3 -2
- package/api/src/libs/subscription.ts +5 -5
- package/api/src/libs/tenant.ts +92 -0
- package/api/src/libs/url.ts +3 -3
- package/api/src/libs/util.ts +21 -13
- package/api/src/middlewares/hono/cdn.ts +63 -0
- package/api/src/middlewares/hono/context.ts +73 -0
- package/api/src/middlewares/hono/csrf.ts +72 -0
- package/api/src/middlewares/hono/fallback.ts +194 -0
- package/api/src/middlewares/hono/pipeline.ts +73 -0
- package/api/src/middlewares/hono/resource-mount.ts +42 -0
- package/api/src/middlewares/hono/resource.ts +63 -0
- package/api/src/middlewares/hono/security.ts +214 -0
- package/api/src/middlewares/hono/session.ts +114 -0
- package/api/src/middlewares/hono/xss.ts +61 -0
- package/api/src/queues/auto-recharge.ts +12 -10
- package/api/src/queues/checkout-session.ts +17 -12
- package/api/src/queues/credit-consume.ts +40 -36
- package/api/src/queues/credit-grant.ts +25 -18
- package/api/src/queues/credit-reconciliation.ts +7 -5
- package/api/src/queues/discount-status.ts +9 -6
- package/api/src/queues/event.ts +12 -4
- package/api/src/queues/exchange-rate-health.ts +49 -30
- package/api/src/queues/invoice.ts +18 -15
- package/api/src/queues/notification.ts +14 -7
- package/api/src/queues/payment.ts +41 -28
- package/api/src/queues/payout.ts +9 -5
- package/api/src/queues/refund.ts +18 -12
- package/api/src/queues/subscription.ts +83 -53
- package/api/src/queues/token-transfer.ts +15 -10
- package/api/src/queues/usage-record.ts +8 -5
- package/api/src/queues/vendors/commission.ts +7 -5
- package/api/src/queues/vendors/fulfillment-coordinator.ts +17 -13
- package/api/src/queues/vendors/fulfillment.ts +4 -2
- package/api/src/queues/vendors/return-processor.ts +5 -3
- package/api/src/queues/vendors/return-scanner.ts +5 -4
- package/api/src/queues/vendors/status-check.ts +10 -7
- package/api/src/queues/webhook.ts +60 -32
- package/api/src/routes/connect/shared.ts +1 -2
- package/api/src/routes/connect/subscribe.ts +3 -3
- package/api/src/routes/{archive.ts → hono/archive.ts} +69 -64
- package/api/src/routes/{auto-recharge-configs.ts → hono/auto-recharge-configs.ts} +39 -28
- package/api/src/routes/{checkout-sessions.ts → hono/checkout-sessions.ts} +790 -923
- package/api/src/routes/{coupons.ts → hono/coupons.ts} +93 -76
- package/api/src/routes/{credit-grants.ts → hono/credit-grants.ts} +140 -126
- package/api/src/routes/hono/credit-tokens.ts +43 -0
- package/api/src/routes/{credit-transactions.ts → hono/credit-transactions.ts} +37 -29
- package/api/src/routes/{customers.ts → hono/customers.ts} +193 -223
- package/api/src/routes/{donations.ts → hono/donations.ts} +41 -32
- package/api/src/routes/{entitlements.ts → hono/entitlements.ts} +28 -25
- package/api/src/routes/{events.ts → hono/events.ts} +107 -71
- package/api/src/routes/{exchange-rate-providers.ts → hono/exchange-rate-providers.ts} +138 -126
- package/api/src/routes/hono/exchange-rates.ts +77 -0
- package/api/src/routes/hono/index.ts +115 -0
- package/api/src/routes/{integrations → hono/integrations}/app-store.ts +68 -48
- package/api/src/routes/{integrations → hono/integrations}/google-play.ts +78 -58
- package/api/src/routes/hono/integrations/stripe.ts +74 -0
- package/api/src/routes/{invoices.ts → hono/invoices.ts} +253 -244
- package/api/src/routes/{meter-events.ts → hono/meter-events.ts} +120 -110
- package/api/src/routes/hono/meters.ts +288 -0
- package/api/src/routes/hono/passports.ts +73 -0
- package/api/src/routes/{payment-currencies.ts → hono/payment-currencies.ts} +219 -197
- package/api/src/routes/{payment-intents.ts → hono/payment-intents.ts} +136 -132
- package/api/src/routes/{payment-links.ts → hono/payment-links.ts} +145 -128
- package/api/src/routes/{payment-methods.ts → hono/payment-methods.ts} +125 -93
- package/api/src/routes/{payment-stats.ts → hono/payment-stats.ts} +30 -25
- package/api/src/routes/{payouts.ts → hono/payouts.ts} +55 -47
- package/api/src/routes/{prices.ts → hono/prices.ts} +265 -242
- package/api/src/routes/{pricing-table.ts → hono/pricing-table.ts} +94 -87
- package/api/src/routes/{products.ts → hono/products.ts} +172 -159
- package/api/src/routes/{promotion-codes.ts → hono/promotion-codes.ts} +207 -185
- package/api/src/routes/hono/redirect.ts +24 -0
- package/api/src/routes/{refunds.ts → hono/refunds.ts} +96 -80
- package/api/src/routes/{settings.ts → hono/settings.ts} +64 -55
- package/api/src/routes/{subscription-items.ts → hono/subscription-items.ts} +64 -57
- package/api/src/routes/{subscriptions.ts → hono/subscriptions.ts} +475 -528
- package/api/src/routes/{tax-rates.ts → hono/tax-rates.ts} +71 -70
- package/api/src/routes/hono/tool.ts +69 -0
- package/api/src/routes/{usage-records.ts → hono/usage-records.ts} +47 -42
- package/api/src/routes/{vendor.ts → hono/vendor.ts} +315 -167
- package/api/src/routes/{webhook-attempts.ts → hono/webhook-attempts.ts} +17 -13
- package/api/src/routes/hono/webhook-endpoints.ts +126 -0
- package/api/src/service.ts +667 -0
- package/api/src/store/migrations/20230911-seeding.ts +2 -1
- package/api/src/store/migrations/20260609-remove-did-space-jobs.ts +23 -0
- package/api/src/store/migrations/20260610-tenant-columns.ts +40 -0
- package/api/src/store/migrations/20260611-tenant-backfill.ts +33 -0
- package/api/src/store/models/auto-recharge-config.ts +22 -10
- package/api/src/store/models/checkout-session.ts +15 -14
- package/api/src/store/models/coupon.ts +29 -20
- package/api/src/store/models/credit-grant.ts +38 -29
- package/api/src/store/models/credit-transaction.ts +32 -21
- package/api/src/store/models/customer.ts +19 -17
- package/api/src/store/models/discount.ts +11 -2
- package/api/src/store/models/entitlement-grant.ts +21 -9
- package/api/src/store/models/entitlement-product.ts +21 -9
- package/api/src/store/models/entitlement.ts +19 -10
- package/api/src/store/models/event.ts +18 -9
- package/api/src/store/models/exchange-rate-provider.ts +17 -4
- package/api/src/store/models/invoice-item.ts +18 -9
- package/api/src/store/models/invoice.ts +16 -8
- package/api/src/store/models/meter-event.ts +27 -9
- package/api/src/store/models/meter.ts +31 -22
- package/api/src/store/models/payment-currency.ts +25 -8
- package/api/src/store/models/payment-intent.ts +15 -6
- package/api/src/store/models/payment-link.ts +15 -6
- package/api/src/store/models/payment-method.ts +38 -22
- package/api/src/store/models/payment-stat.ts +18 -9
- package/api/src/store/models/payout.ts +15 -6
- package/api/src/store/models/price-quote.ts +17 -8
- package/api/src/store/models/price.ts +24 -12
- package/api/src/store/models/pricing-table.ts +29 -20
- package/api/src/store/models/product-vendor.ts +20 -10
- package/api/src/store/models/product.ts +15 -6
- package/api/src/store/models/promotion-code.ts +14 -6
- package/api/src/store/models/refund.ts +15 -6
- package/api/src/store/models/revenue-snapshot.ts +21 -9
- package/api/src/store/models/setting.ts +18 -9
- package/api/src/store/models/setup-intent.ts +36 -27
- package/api/src/store/models/subscription-item.ts +21 -9
- package/api/src/store/models/subscription-schedule.ts +21 -9
- package/api/src/store/models/subscription.ts +21 -10
- package/api/src/store/models/tax-rate.ts +29 -21
- package/api/src/store/models/usage-record.ts +11 -2
- package/api/src/store/models/webhook-attempt.ts +18 -9
- package/api/src/store/models/webhook-endpoint.ts +18 -9
- package/api/src/store/scoped-core.ts +55 -0
- package/api/src/store/scoped.ts +247 -0
- package/api/src/store/sequelize.ts +66 -22
- package/api/src/store/sql-migrations.ts +20 -0
- package/api/src/store/tenant-backfill.ts +260 -0
- package/api/src/store/tenant-model.ts +124 -0
- package/api/src/store/tenant-tables.ts +50 -0
- package/api/tests/embedded/embedded-multi-mode-d3.spec.ts +257 -0
- package/api/tests/fixtures/bare-query-violation.ts +13 -0
- package/api/tests/fixtures/core-env-violation.ts +10 -0
- package/api/tests/fixtures/host-read-violation.ts +19 -0
- package/api/tests/fixtures/tenants.ts +4 -0
- package/api/tests/integrations/iap-tenant.spec.ts +284 -0
- package/api/tests/libs/archive-query.spec.ts +26 -0
- package/api/tests/libs/audit-tenant.spec.ts +153 -0
- package/api/tests/libs/context.spec.ts +204 -0
- package/api/tests/libs/core-config.spec.ts +115 -0
- package/api/tests/libs/cron-driver-d2.spec.ts +237 -0
- package/api/tests/libs/crons-conservation-d2.spec.ts +52 -0
- package/api/tests/libs/lock-tenant.spec.ts +66 -0
- package/api/tests/libs/scoped.spec.ts +222 -0
- package/api/tests/libs/secrets-facade.spec.ts +52 -0
- package/api/tests/libs/tenancy-slot-authority.spec.ts +209 -0
- package/api/tests/libs/tenant-middleware.spec.ts +42 -0
- package/api/tests/libs/tenant-scanner.spec.ts +120 -0
- package/api/tests/middlewares/hono/cdn.spec.ts +70 -0
- package/api/tests/middlewares/hono/context.spec.ts +113 -0
- package/api/tests/middlewares/hono/csrf.spec.ts +136 -0
- package/api/tests/middlewares/hono/fallback.spec.ts +67 -0
- package/api/tests/middlewares/hono/pipeline.spec.ts +47 -0
- package/api/tests/middlewares/hono/security.spec.ts +181 -0
- package/api/tests/middlewares/hono/session.spec.ts +42 -0
- package/api/tests/middlewares/hono/xss.spec.ts +81 -0
- package/api/tests/models/tenant-backfill.spec.ts +287 -0
- package/api/tests/models/tenant-columns-model.spec.ts +46 -0
- package/api/tests/models/tenant-columns.spec.ts +161 -0
- package/api/tests/queues/credit-consume-batch.spec.ts +8 -1
- package/api/tests/queues/credit-consume.spec.ts +8 -1
- package/api/tests/queues/event-tenant.spec.ts +236 -0
- package/api/tests/queues/exchange-rate-health-tenant-d6.spec.ts +62 -0
- package/api/tests/queues/queue-parity.spec.ts +249 -0
- package/api/tests/queues/queue-runtime-surface.spec.ts +277 -0
- package/api/tests/queues/queue-teardown-d2.spec.ts +127 -0
- package/api/tests/queues/tenant-matrix-a.spec.ts +245 -0
- package/api/tests/queues/tenant-matrix-b.spec.ts +168 -0
- package/api/tests/routes/connect/hono-attach.spec.ts +107 -0
- package/api/tests/service/collapse.spec.ts +96 -0
- package/api/tests/store/tenant-crosscut.spec.ts +202 -0
- package/api/tests/store/tenant-model-spike.spec.ts +177 -0
- package/api/tests/store/tenant-model.spec.ts +162 -0
- package/api/tests/store/tenant-residual.spec.ts +196 -0
- package/api/third.d.ts +4 -0
- package/blocklet.yml +1 -1
- package/cloudflare/README.md +26 -6
- package/cloudflare/build.ts +28 -13
- package/cloudflare/did-connect-auth.ts +0 -217
- package/cloudflare/docs/2026-06-10-bundle-size-analysis.md +288 -0
- package/cloudflare/migrations/0006_tenant_columns.sql +46 -0
- package/cloudflare/migrations/0007_tenant_backfill_indexes.sql +65 -0
- package/cloudflare/migrations/0008_schema_parity.sql +16 -0
- package/cloudflare/migrations/0009_remove_did_space_jobs.sql +5 -0
- package/cloudflare/queue-runtime-mode.ts +13 -0
- package/cloudflare/run-build.js +31 -56
- package/cloudflare/shims/blocklet-sdk/asset-host-transformer.ts +20 -0
- package/cloudflare/shims/blocklet-sdk/config.ts +8 -1
- package/cloudflare/shims/blocklet-sdk/login.ts +12 -0
- package/cloudflare/shims/blocklet-sdk/service-api.ts +14 -0
- package/cloudflare/shims/blocklet-sdk/session.ts +4 -2
- package/cloudflare/shims/blocklet-sdk/util-constants.ts +8 -0
- package/cloudflare/shims/blocklet-sdk/util-csrf.ts +13 -0
- package/cloudflare/shims/blocklet-sdk/util-wallet.ts +8 -0
- package/cloudflare/shims/cron.ts +38 -158
- package/cloudflare/shims/events.ts +124 -0
- package/cloudflare/shims/fastq.ts +15 -1
- package/cloudflare/shims/nedb-storage.ts +16 -8
- package/cloudflare/shims/node-fetch.ts +35 -0
- package/cloudflare/shims/xss.ts +8 -0
- package/cloudflare/tenant-middleware.ts +36 -0
- package/cloudflare/tests/tenant-middleware.spec.ts +160 -0
- package/cloudflare/tests/worker-handler-gate.spec.ts +44 -0
- package/cloudflare/worker.ts +204 -433
- package/cloudflare/wrangler.local-e2e.jsonc +26 -0
- package/jest.config.js +3 -1
- package/package.json +33 -38
- package/scripts/core-env-whitelist.json +1 -0
- package/scripts/e2e-12b-runtime.ts +149 -0
- package/scripts/e2e-core-config.ts +125 -0
- package/scripts/e2e-d1-tenancy.ts +116 -0
- package/scripts/e2e-d2-cron-queue.ts +139 -0
- package/scripts/e2e-d3-embedded-multi.ts +171 -0
- package/scripts/e2e-hono-s2.ts +125 -0
- package/scripts/e2e-hono-s3e.ts +135 -0
- package/scripts/e2e-hono-s4.ts +114 -0
- package/scripts/e2e-migration-contract.ts +100 -0
- package/scripts/e2e-s0.ts +61 -0
- package/scripts/e2e-s1.ts +107 -0
- package/scripts/e2e-s2.ts +178 -0
- package/scripts/e2e-s3.ts +110 -0
- package/scripts/e2e-s4.ts +191 -0
- package/scripts/e2e-s5.ts +139 -0
- package/scripts/e2e-s6.ts +127 -0
- package/scripts/e2e-tenant-model.ts +119 -0
- package/scripts/e2e-tenant-worker.ts +199 -0
- package/scripts/gen-sql-migrations.js +46 -0
- package/scripts/phase8-codemod.js +219 -0
- package/scripts/phase9a-env-getters-codemod.js +82 -0
- package/scripts/scan-core-env.js +109 -0
- package/scripts/scan-tenant-queries.js +235 -0
- package/scripts/schema-drift-guard.ts +210 -0
- package/scripts/tenant-scan-whitelist.json +1 -0
- package/src/env.d.ts +13 -1
- package/tsconfig.json +1 -1
- package/api/src/libs/did-space.ts +0 -235
- package/api/src/libs/middleware.ts +0 -50
- package/api/src/libs/security.ts +0 -192
- package/api/src/queues/space.ts +0 -662
- package/api/src/routes/credit-tokens.ts +0 -38
- package/api/src/routes/exchange-rates.ts +0 -87
- package/api/src/routes/index.ts +0 -142
- package/api/src/routes/integrations/stripe.ts +0 -61
- package/api/src/routes/meters.ts +0 -274
- package/api/src/routes/passports.ts +0 -68
- package/api/src/routes/redirect.ts +0 -20
- package/api/src/routes/tool.ts +0 -65
- package/api/src/routes/webhook-endpoints.ts +0 -126
- package/api/tests/routes/credit-grants.spec.ts +0 -1261
- package/cloudflare/shims/did-space-js.ts +0 -17
- package/cloudflare/shims/did-space.ts +0 -11
- package/cloudflare/shims/express-compat/index.ts +0 -80
- package/cloudflare/shims/express-compat/types.ts +0 -41
- package/cloudflare/shims/lock.ts +0 -115
- package/cloudflare/shims/queue.ts +0 -611
- package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +0 -87
- package/cloudflare/tests/shims/queue-scheduled.spec.ts +0 -186
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
// Phase 3 (express→hono) — hono fork of routes/checkout-sessions.ts. Sub-app with
|
|
2
|
+
// routes relative to /api/checkout-sessions (mounted via mountResourceGroup). The
|
|
3
|
+
// business logic is unchanged; only the express plumbing becomes hono:
|
|
4
|
+
// req.body → c.get('sanitizedBody') ?? {}; res.status(n).json(x) → c.json(x, n).
|
|
1
5
|
/* eslint-disable consistent-return */
|
|
2
6
|
import { isValid } from '@arcblock/did';
|
|
3
7
|
import { getUrl } from '@blocklet/sdk/lib/component';
|
|
4
|
-
import { sessionMiddleware } from '@blocklet/sdk/lib/middlewares/session';
|
|
5
8
|
import { BN, fromTokenToUnit, fromUnitToToken } from '@ocap/util';
|
|
6
|
-
import { NextFunction, Request, Response, Router } from 'express';
|
|
7
9
|
import Joi from 'joi';
|
|
8
10
|
import cloneDeep from 'lodash/cloneDeep';
|
|
9
11
|
import merge from 'lodash/merge';
|
|
@@ -13,22 +15,25 @@ import sortBy from 'lodash/sortBy';
|
|
|
13
15
|
import uniq from 'lodash/uniq';
|
|
14
16
|
import type { WhereOptions } from 'sequelize';
|
|
15
17
|
import { Op } from 'sequelize';
|
|
18
|
+
import type { MiddlewareHandler } from 'hono';
|
|
19
|
+
import { Hono } from 'hono';
|
|
16
20
|
|
|
17
21
|
import { CustomError, formatError, getStatusFromError } from '@blocklet/error';
|
|
18
22
|
import pAll from 'p-all';
|
|
19
23
|
import { withQuery } from 'ufo';
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import
|
|
23
|
-
import
|
|
24
|
-
import
|
|
25
|
-
import {
|
|
24
|
+
import { paymentRateVolatilityThreshold, updateDataConcurrency, stopAcceptingOrders } from '../../libs/env';
|
|
25
|
+
import { MetadataSchema } from '../../libs/api';
|
|
26
|
+
import { checkPassportForPaymentLink } from '../../integrations/blocklet/passport';
|
|
27
|
+
import dayjs from '../../libs/dayjs';
|
|
28
|
+
import logger from '../../libs/logger';
|
|
29
|
+
import { trimDecimals } from '../../libs/math-utils';
|
|
30
|
+
import { authenticate } from '../../middlewares/hono/security';
|
|
26
31
|
import {
|
|
27
32
|
buildSlippageSnapshot,
|
|
28
33
|
DEFAULT_SLIPPAGE_PERCENT,
|
|
29
34
|
isRateBelowMinAcceptableRate,
|
|
30
35
|
normalizeSlippageConfigFromMetadata,
|
|
31
|
-
} from '
|
|
36
|
+
} from '../../libs/slippage';
|
|
32
37
|
import {
|
|
33
38
|
canPayWithDelegation,
|
|
34
39
|
canUpsell,
|
|
@@ -54,8 +59,8 @@ import {
|
|
|
54
59
|
processCheckoutSessionDiscounts,
|
|
55
60
|
getCheckoutSessionAmounts,
|
|
56
61
|
enrichCheckoutSessionWithQuotes,
|
|
57
|
-
} from '
|
|
58
|
-
import { getDaysUntilCancel, getDaysUntilDue, getSubscriptionTrialSetup } from '
|
|
62
|
+
} from '../../libs/session';
|
|
63
|
+
import { getDaysUntilCancel, getDaysUntilDue, getSubscriptionTrialSetup } from '../../libs/subscription';
|
|
59
64
|
import {
|
|
60
65
|
CHECKOUT_SESSION_TTL,
|
|
61
66
|
formatAmountPrecisionLimit,
|
|
@@ -65,7 +70,7 @@ import {
|
|
|
65
70
|
getUserOrAppInfo,
|
|
66
71
|
hasObjectChanged,
|
|
67
72
|
isUserInBlocklist,
|
|
68
|
-
} from '
|
|
73
|
+
} from '../../libs/util';
|
|
69
74
|
import {
|
|
70
75
|
PaymentBeneficiary,
|
|
71
76
|
SetupIntent,
|
|
@@ -80,54 +85,54 @@ import {
|
|
|
80
85
|
Coupon,
|
|
81
86
|
PromotionCode,
|
|
82
87
|
Discount,
|
|
83
|
-
} from '
|
|
84
|
-
import type { ChainType } from '
|
|
85
|
-
import { CheckoutSession } from '
|
|
86
|
-
import { Customer } from '
|
|
87
|
-
import { PaymentCurrency } from '
|
|
88
|
-
import { PaymentIntent } from '
|
|
89
|
-
import { PaymentLink } from '
|
|
90
|
-
import { PaymentMethod } from '
|
|
91
|
-
import { Price } from '
|
|
92
|
-
import { PriceQuote } from '
|
|
93
|
-
import { Product } from '
|
|
88
|
+
} from '../../store/models';
|
|
89
|
+
import type { ChainType } from '../../store/models/types';
|
|
90
|
+
import { CheckoutSession } from '../../store/models/checkout-session';
|
|
91
|
+
import { Customer } from '../../store/models/customer';
|
|
92
|
+
import { PaymentCurrency } from '../../store/models/payment-currency';
|
|
93
|
+
import { PaymentIntent } from '../../store/models/payment-intent';
|
|
94
|
+
import { PaymentLink } from '../../store/models/payment-link';
|
|
95
|
+
import { PaymentMethod } from '../../store/models/payment-method';
|
|
96
|
+
import { Price } from '../../store/models/price';
|
|
97
|
+
import { PriceQuote } from '../../store/models/price-quote';
|
|
98
|
+
import { Product } from '../../store/models/product';
|
|
94
99
|
import {
|
|
95
100
|
ensureStripePaymentIntent,
|
|
96
101
|
ensureStripeSetupIntentForCheckoutSession,
|
|
97
102
|
ensureStripeSubscription,
|
|
98
|
-
} from '
|
|
99
|
-
import { handleStripePaymentSucceed } from '
|
|
100
|
-
import { paymentQueue } from '
|
|
101
|
-
import { invoiceQueue } from '
|
|
102
|
-
import { ensureInvoiceForCheckout, ensureInvoicesForSubscriptions } from '
|
|
103
|
+
} from '../../integrations/stripe/resource';
|
|
104
|
+
import { handleStripePaymentSucceed } from '../../integrations/stripe/handlers/payment-intent';
|
|
105
|
+
import { paymentQueue } from '../../queues/payment';
|
|
106
|
+
import { invoiceQueue } from '../../queues/invoice';
|
|
107
|
+
import { ensureInvoiceForCheckout, ensureInvoicesForSubscriptions } from '../connect/shared';
|
|
103
108
|
import {
|
|
104
109
|
isCreditGrantSufficientForPayment,
|
|
105
110
|
isCreditSufficientForPayment,
|
|
106
111
|
isDelegationSufficientForPayment,
|
|
107
112
|
SufficientForPaymentResult,
|
|
108
|
-
} from '
|
|
109
|
-
import { handleStripeSubscriptionSucceed } from '
|
|
110
|
-
import { CHARGE_SUPPORTED_CHAIN_TYPES } from '
|
|
111
|
-
import { blocklet } from '
|
|
112
|
-
import { addSubscriptionJob } from '
|
|
113
|
-
import { updateDataConcurrency, stopAcceptingOrders } from '../libs/env';
|
|
113
|
+
} from '../../libs/payment';
|
|
114
|
+
import { handleStripeSubscriptionSucceed } from '../../integrations/stripe/handlers/subscription';
|
|
115
|
+
import { CHARGE_SUPPORTED_CHAIN_TYPES } from '../../libs/constants';
|
|
116
|
+
import { blocklet } from '../../libs/auth';
|
|
117
|
+
import { addSubscriptionJob } from '../../queues/subscription';
|
|
114
118
|
import {
|
|
115
119
|
expandLineItemsWithCouponInfo,
|
|
116
120
|
expandDiscountsWithDetails,
|
|
117
121
|
checkPromotionCodeEligibility,
|
|
118
122
|
createDiscountRecordsForCheckout,
|
|
119
123
|
updateSubscriptionDiscountReferences,
|
|
120
|
-
} from '
|
|
121
|
-
import { rollbackDiscountUsageForCheckoutSession, applyDiscountsToLineItems } from '
|
|
122
|
-
import { formatToShortUrl } from '
|
|
123
|
-
import { destroyExistingInvoice } from '
|
|
124
|
-
import { getApproveFunction } from '
|
|
125
|
-
import { getQuoteService } from '
|
|
126
|
-
import { getExchangeRateService } from '
|
|
127
|
-
import { getExchangeRateSymbol } from '
|
|
128
|
-
import { sequelize } from '
|
|
129
|
-
|
|
130
|
-
|
|
124
|
+
} from '../../libs/discount/coupon';
|
|
125
|
+
import { rollbackDiscountUsageForCheckoutSession, applyDiscountsToLineItems } from '../../libs/discount/discount';
|
|
126
|
+
import { formatToShortUrl } from '../../libs/url';
|
|
127
|
+
import { destroyExistingInvoice } from '../../libs/invoice';
|
|
128
|
+
import { getApproveFunction } from '../../integrations/ethereum/contract';
|
|
129
|
+
import { getQuoteService } from '../../libs/quote-service';
|
|
130
|
+
import { getExchangeRateService } from '../../libs/exchange-rate/service';
|
|
131
|
+
import { getExchangeRateSymbol } from '../../libs/exchange-rate/token-address-mapping';
|
|
132
|
+
import { sequelize } from '../../store/sequelize';
|
|
133
|
+
import { sessionMiddleware } from '../../middlewares/hono/session';
|
|
134
|
+
|
|
135
|
+
const app = new Hono();
|
|
131
136
|
|
|
132
137
|
const user = sessionMiddleware({ accessKey: true });
|
|
133
138
|
const auth = authenticate<CheckoutSession>({ component: true, roles: ['owner', 'admin'] });
|
|
@@ -137,7 +142,7 @@ const exchangeRateService = getExchangeRateService();
|
|
|
137
142
|
const DEFAULT_RATE_VOLATILITY_THRESHOLD = 0.1;
|
|
138
143
|
|
|
139
144
|
const getRateVolatilityThreshold = () => {
|
|
140
|
-
const raw = Number(
|
|
145
|
+
const raw = Number(paymentRateVolatilityThreshold() || DEFAULT_RATE_VOLATILITY_THRESHOLD);
|
|
141
146
|
if (!Number.isFinite(raw) || raw <= 0) {
|
|
142
147
|
return DEFAULT_RATE_VOLATILITY_THRESHOLD;
|
|
143
148
|
}
|
|
@@ -249,7 +254,7 @@ export async function validateInventory(line_items: LineItem[], includePendingQu
|
|
|
249
254
|
throw new Error(`Can not exceed available quantity for price: ${priceId}`);
|
|
250
255
|
}
|
|
251
256
|
});
|
|
252
|
-
await pAll(checks, { concurrency: updateDataConcurrency });
|
|
257
|
+
await pAll(checks, { concurrency: updateDataConcurrency() });
|
|
253
258
|
}
|
|
254
259
|
|
|
255
260
|
export async function validatePaymentSettings(paymentMethodId: string, paymentCurrencyId: string) {
|
|
@@ -875,22 +880,22 @@ export const formatCheckoutSession = async (payload: any, throwOnEmptyItems = tr
|
|
|
875
880
|
});
|
|
876
881
|
};
|
|
877
882
|
|
|
878
|
-
|
|
879
|
-
|
|
883
|
+
// Hono MiddlewareHandler version of ensureCheckoutSessionOpen
|
|
884
|
+
export const ensureCheckoutSessionOpen: MiddlewareHandler = async (c, next) => {
|
|
885
|
+
const id = c.req.param('id');
|
|
886
|
+
const doc = await CheckoutSession.findByPk(id);
|
|
880
887
|
if (!doc) {
|
|
881
|
-
return
|
|
888
|
+
return c.json({ code: 'CHECKOUT_NOT_FOUND', error: 'Checkout session not found' }, 404);
|
|
882
889
|
}
|
|
883
890
|
if (doc.status === 'complete') {
|
|
884
|
-
return
|
|
891
|
+
return c.json({ code: 'CHECKOUT_COMPLETED', error: 'Checkout session completed' }, 403);
|
|
885
892
|
}
|
|
886
893
|
if (doc.status === 'expired') {
|
|
887
|
-
return
|
|
894
|
+
return c.json({ code: 'CHECKOUT_EXPIRED', error: 'Checkout session already expired' }, 403);
|
|
888
895
|
}
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
next();
|
|
893
|
-
}
|
|
896
|
+
c.set('doc', doc);
|
|
897
|
+
await next();
|
|
898
|
+
};
|
|
894
899
|
|
|
895
900
|
const getBeneficiaryName = async (beneficiary: PaymentBeneficiary) => {
|
|
896
901
|
if (!beneficiary) return '';
|
|
@@ -1008,7 +1013,7 @@ async function processSubscriptionFastCheckout({
|
|
|
1008
1013
|
});
|
|
1009
1014
|
return amount;
|
|
1010
1015
|
}),
|
|
1011
|
-
{ concurrency: updateDataConcurrency }
|
|
1016
|
+
{ concurrency: updateDataConcurrency() }
|
|
1012
1017
|
);
|
|
1013
1018
|
const totalAmount = subscriptionAmounts
|
|
1014
1019
|
.reduce((sum: BN, amt: string) => sum.add(new BN(amt)), new BN('0'))
|
|
@@ -1056,7 +1061,7 @@ async function processSubscriptionFastCheckout({
|
|
|
1056
1061
|
payment_details: { [paymentMethod.type]: { payer: customer.did } },
|
|
1057
1062
|
});
|
|
1058
1063
|
}),
|
|
1059
|
-
{ concurrency: updateDataConcurrency }
|
|
1064
|
+
{ concurrency: updateDataConcurrency() }
|
|
1060
1065
|
);
|
|
1061
1066
|
if (paymentCurrency.isCredit()) {
|
|
1062
1067
|
// skip invoice creation for credit subscriptions
|
|
@@ -1079,7 +1084,7 @@ async function processSubscriptionFastCheckout({
|
|
|
1079
1084
|
});
|
|
1080
1085
|
await addSubscriptionJob(sub, 'cycle', false, sub.trial_end);
|
|
1081
1086
|
}),
|
|
1082
|
-
{ concurrency: updateDataConcurrency }
|
|
1087
|
+
{ concurrency: updateDataConcurrency() }
|
|
1083
1088
|
);
|
|
1084
1089
|
return {
|
|
1085
1090
|
success: true,
|
|
@@ -1102,14 +1107,14 @@ async function processSubscriptionFastCheckout({
|
|
|
1102
1107
|
return invoiceQueue.push({ id: invoice.id, job: { invoiceId: invoice.id, retryOnError: false } });
|
|
1103
1108
|
}
|
|
1104
1109
|
}),
|
|
1105
|
-
{ concurrency: updateDataConcurrency }
|
|
1110
|
+
{ concurrency: updateDataConcurrency() }
|
|
1106
1111
|
);
|
|
1107
1112
|
// Add subscription cycle jobs
|
|
1108
1113
|
await pAll(
|
|
1109
1114
|
subscriptions.map((sub) => async () => {
|
|
1110
1115
|
await addSubscriptionJob(sub, 'cycle', false, sub.trial_end);
|
|
1111
1116
|
}),
|
|
1112
|
-
{ concurrency: updateDataConcurrency }
|
|
1117
|
+
{ concurrency: updateDataConcurrency() }
|
|
1113
1118
|
);
|
|
1114
1119
|
|
|
1115
1120
|
logger.info('Created and queued invoices for fast checkout with subscriptions', {
|
|
@@ -1146,21 +1151,22 @@ async function processSubscriptionFastCheckout({
|
|
|
1146
1151
|
}
|
|
1147
1152
|
|
|
1148
1153
|
// create checkout session
|
|
1149
|
-
|
|
1154
|
+
app.post('/', authLogin, async (c) => {
|
|
1150
1155
|
try {
|
|
1151
|
-
const
|
|
1152
|
-
raw
|
|
1153
|
-
raw.
|
|
1156
|
+
const body = c.get('sanitizedBody') ?? {};
|
|
1157
|
+
const raw: Partial<CheckoutSession> = await formatCheckoutSession(body);
|
|
1158
|
+
raw.livemode = !!c.get('livemode');
|
|
1159
|
+
raw.created_via = c.get('user')?.via as string;
|
|
1154
1160
|
|
|
1155
1161
|
// Customer permission validation and createMine handling
|
|
1156
|
-
const { create_mine: createMine } =
|
|
1157
|
-
const currentUserDid =
|
|
1162
|
+
const { create_mine: createMine } = body;
|
|
1163
|
+
const currentUserDid = c.get('user')?.did;
|
|
1158
1164
|
|
|
1159
|
-
const isAdmin = ['owner', 'admin'].includes(
|
|
1165
|
+
const isAdmin = ['owner', 'admin'].includes(c.get('user')?.role as string);
|
|
1160
1166
|
// Handle createMine parameter
|
|
1161
1167
|
if (createMine === true) {
|
|
1162
1168
|
if (!currentUserDid) {
|
|
1163
|
-
return
|
|
1169
|
+
return c.json({ error: 'User not authenticated, cannot create checkout session for self' }, 400);
|
|
1164
1170
|
}
|
|
1165
1171
|
|
|
1166
1172
|
const currentCustomer = await Customer.findOne({ where: { did: currentUserDid } });
|
|
@@ -1173,7 +1179,7 @@ router.post('/', authLogin, async (req, res) => {
|
|
|
1173
1179
|
createdBy: currentUserDid,
|
|
1174
1180
|
};
|
|
1175
1181
|
} else if (!isAdmin) {
|
|
1176
|
-
return
|
|
1182
|
+
return c.json({ error: 'Not authorized to perform this action' }, 403);
|
|
1177
1183
|
}
|
|
1178
1184
|
|
|
1179
1185
|
if (raw.line_items) {
|
|
@@ -1181,24 +1187,24 @@ router.post('/', authLogin, async (req, res) => {
|
|
|
1181
1187
|
await validateInventory(raw.line_items, true);
|
|
1182
1188
|
} catch (err) {
|
|
1183
1189
|
logger.error('validateInventory failed', { error: err, line_items: raw.line_items });
|
|
1184
|
-
return
|
|
1190
|
+
return c.json({ error: err.message }, 400);
|
|
1185
1191
|
}
|
|
1186
1192
|
}
|
|
1187
1193
|
|
|
1188
1194
|
// Process discounts before creating checkout session
|
|
1189
1195
|
let processedDiscounts: any[] = [];
|
|
1190
|
-
if (
|
|
1196
|
+
if (body.discounts && Array.isArray(body.discounts)) {
|
|
1191
1197
|
if (!isAdmin) {
|
|
1192
|
-
return
|
|
1198
|
+
return c.json({ error: 'Not allowed to apply discounts' }, 403);
|
|
1193
1199
|
}
|
|
1194
1200
|
try {
|
|
1195
|
-
processedDiscounts = await processCheckoutSessionDiscounts(raw as any,
|
|
1201
|
+
processedDiscounts = await processCheckoutSessionDiscounts(raw as any, body.discounts);
|
|
1196
1202
|
} catch (discountError) {
|
|
1197
1203
|
logger.error('Discount processing failed during checkout session creation', {
|
|
1198
1204
|
error: discountError.message,
|
|
1199
|
-
discounts:
|
|
1205
|
+
discounts: body.discounts,
|
|
1200
1206
|
});
|
|
1201
|
-
return
|
|
1207
|
+
return c.json({ error: discountError.message }, 400);
|
|
1202
1208
|
}
|
|
1203
1209
|
}
|
|
1204
1210
|
|
|
@@ -1212,7 +1218,7 @@ router.post('/', authLogin, async (req, res) => {
|
|
|
1212
1218
|
try {
|
|
1213
1219
|
await prefetchQuotesForCheckoutSession(doc as CheckoutSession);
|
|
1214
1220
|
} catch (error: any) {
|
|
1215
|
-
return
|
|
1221
|
+
return c.json({ error: error.message }, 400);
|
|
1216
1222
|
}
|
|
1217
1223
|
|
|
1218
1224
|
let url = getUrl(`/checkout/${doc.submit_type}/${doc.id}`);
|
|
@@ -1220,24 +1226,23 @@ router.post('/', authLogin, async (req, res) => {
|
|
|
1220
1226
|
url = withQuery(url, getConnectQueryParam({ userDid: currentUserDid }));
|
|
1221
1227
|
}
|
|
1222
1228
|
|
|
1223
|
-
|
|
1229
|
+
return c.json({ ...doc.toJSON(), url });
|
|
1224
1230
|
} catch (error) {
|
|
1225
|
-
logger.error('Create checkout session failed', { error: error.message, body:
|
|
1226
|
-
|
|
1231
|
+
logger.error('Create checkout session failed', { error: error.message, body: c.get('sanitizedBody') ?? {} });
|
|
1232
|
+
return c.json({ error: error.message }, 500);
|
|
1227
1233
|
}
|
|
1228
1234
|
});
|
|
1229
1235
|
|
|
1230
|
-
export async function startCheckoutSessionFromPaymentLink(id: string,
|
|
1231
|
-
const
|
|
1236
|
+
export async function startCheckoutSessionFromPaymentLink(id: string, c: any) {
|
|
1237
|
+
const body = c.get('sanitizedBody') ?? {};
|
|
1238
|
+
const { metadata, needShortUrl = false } = body;
|
|
1232
1239
|
try {
|
|
1233
1240
|
const link = await PaymentLink.findByPk(id);
|
|
1234
1241
|
if (!link) {
|
|
1235
|
-
|
|
1236
|
-
return;
|
|
1242
|
+
return c.json({ error: 'Payment link not found, please contact the source of the payment link.' }, 400);
|
|
1237
1243
|
}
|
|
1238
1244
|
if (!link.active) {
|
|
1239
|
-
|
|
1240
|
-
return;
|
|
1245
|
+
return c.json({ error: 'Payment link archived, we can not create new checkout session.' }, 400);
|
|
1241
1246
|
}
|
|
1242
1247
|
|
|
1243
1248
|
const items = await Price.expand(link.line_items, { upsell: true });
|
|
@@ -1249,10 +1254,9 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
|
|
|
1249
1254
|
raw.livemode = link.livemode;
|
|
1250
1255
|
raw.created_via = 'portal';
|
|
1251
1256
|
raw.submit_type = link.submit_type;
|
|
1252
|
-
raw.currency_id = link.currency_id ||
|
|
1257
|
+
raw.currency_id = link.currency_id || c.get('baseCurrency').id;
|
|
1253
1258
|
if (!raw.currency_id) {
|
|
1254
|
-
|
|
1255
|
-
return;
|
|
1259
|
+
return c.json({ error: 'Currency not found in payment link' }, 400);
|
|
1256
1260
|
}
|
|
1257
1261
|
raw.payment_link_id = link.id;
|
|
1258
1262
|
|
|
@@ -1271,11 +1275,7 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
|
|
|
1271
1275
|
if (link.subscription_data?.billing_threshold_amount) {
|
|
1272
1276
|
protectedSettings.billing_threshold_amount = getBillingThreshold(link.subscription_data);
|
|
1273
1277
|
}
|
|
1274
|
-
raw.subscription_data = merge(
|
|
1275
|
-
link.subscription_data,
|
|
1276
|
-
getDataObjectFromQuery(req.query, 'subscription_data'),
|
|
1277
|
-
protectedSettings
|
|
1278
|
-
);
|
|
1278
|
+
raw.subscription_data = merge(link.subscription_data, getDataObjectFromQuery(c.req.query()), protectedSettings);
|
|
1279
1279
|
|
|
1280
1280
|
if (link.after_completion?.hosted_confirmation?.custom_message) {
|
|
1281
1281
|
raw.payment_intent_data = {
|
|
@@ -1317,17 +1317,17 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
|
|
|
1317
1317
|
raw.success_url = link.after_completion?.redirect?.url;
|
|
1318
1318
|
}
|
|
1319
1319
|
|
|
1320
|
-
if (req.query
|
|
1321
|
-
raw.success_url = req.query
|
|
1322
|
-
raw.cancel_url = req.query
|
|
1320
|
+
if (c.req.query('redirect')) {
|
|
1321
|
+
raw.success_url = c.req.query('redirect') as string;
|
|
1322
|
+
raw.cancel_url = c.req.query('redirect') as string;
|
|
1323
1323
|
}
|
|
1324
1324
|
|
|
1325
1325
|
try {
|
|
1326
|
-
if (req.query
|
|
1326
|
+
if (c.req.query('nft_mint_factory') && isValid(c.req.query('nft_mint_factory') as string)) {
|
|
1327
1327
|
raw.nft_mint_settings = {
|
|
1328
1328
|
enabled: true,
|
|
1329
1329
|
behavior: 'per_checkout_session',
|
|
1330
|
-
factory: req.query
|
|
1330
|
+
factory: c.req.query('nft_mint_factory') as string,
|
|
1331
1331
|
};
|
|
1332
1332
|
raw.nft_mint_status = 'pending';
|
|
1333
1333
|
logger.info('use nft_mint_settings from query when checkout from payment link', { v: raw.nft_mint_settings });
|
|
@@ -1339,17 +1339,17 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
|
|
|
1339
1339
|
}
|
|
1340
1340
|
|
|
1341
1341
|
let doc;
|
|
1342
|
-
if (req.query
|
|
1342
|
+
if (c.req.query('preview') === '1') {
|
|
1343
1343
|
doc = await CheckoutSession.findOne({ where: { payment_link_id: link.id, metadata: { preview: '1' } } });
|
|
1344
1344
|
if (doc) {
|
|
1345
1345
|
await doc.update(omit(raw, ['metadata']));
|
|
1346
1346
|
} else {
|
|
1347
1347
|
raw.metadata = {
|
|
1348
1348
|
...link.metadata,
|
|
1349
|
-
...getDataObjectFromQuery(req.query),
|
|
1349
|
+
...getDataObjectFromQuery(c.req.query()),
|
|
1350
1350
|
...metadata,
|
|
1351
|
-
days_until_due: getDaysUntilDue(req.query),
|
|
1352
|
-
days_until_cancel: getDaysUntilCancel(req.query),
|
|
1351
|
+
days_until_due: getDaysUntilDue(c.req.query()),
|
|
1352
|
+
days_until_cancel: getDaysUntilCancel(c.req.query()),
|
|
1353
1353
|
passport: await checkPassportForPaymentLink(link),
|
|
1354
1354
|
preview: '1',
|
|
1355
1355
|
};
|
|
@@ -1357,10 +1357,10 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
|
|
|
1357
1357
|
} else {
|
|
1358
1358
|
raw.metadata = {
|
|
1359
1359
|
...link.metadata,
|
|
1360
|
-
...getDataObjectFromQuery(req.query),
|
|
1360
|
+
...getDataObjectFromQuery(c.req.query()),
|
|
1361
1361
|
...metadata,
|
|
1362
|
-
days_until_due: getDaysUntilDue(req.query),
|
|
1363
|
-
days_until_cancel: getDaysUntilCancel(req.query),
|
|
1362
|
+
days_until_due: getDaysUntilDue(c.req.query()),
|
|
1363
|
+
days_until_cancel: getDaysUntilCancel(c.req.query()),
|
|
1364
1364
|
passport: await checkPassportForPaymentLink(link),
|
|
1365
1365
|
};
|
|
1366
1366
|
}
|
|
@@ -1371,7 +1371,7 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
|
|
|
1371
1371
|
|
|
1372
1372
|
doc.line_items = items;
|
|
1373
1373
|
|
|
1374
|
-
if (req.query
|
|
1374
|
+
if (c.req.query('upsell') === '1') {
|
|
1375
1375
|
// add upsell to line items
|
|
1376
1376
|
const updatedItems = cloneDeep(doc.line_items);
|
|
1377
1377
|
updatedItems.forEach((item: any) => {
|
|
@@ -1422,7 +1422,7 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
|
|
|
1422
1422
|
paymentUrl = await formatToShortUrl({ url: paymentUrl, validUntil, maxVisits });
|
|
1423
1423
|
}
|
|
1424
1424
|
|
|
1425
|
-
|
|
1425
|
+
return c.json({
|
|
1426
1426
|
paymentUrl,
|
|
1427
1427
|
checkoutSession: doc.toJSON(),
|
|
1428
1428
|
quotes, // Include quotes information for frontend
|
|
@@ -1431,201 +1431,36 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
|
|
|
1431
1431
|
paymentMethods: await getPaymentMethods(doc),
|
|
1432
1432
|
paymentLink: link,
|
|
1433
1433
|
paymentIntent: null,
|
|
1434
|
-
customer:
|
|
1434
|
+
customer: c.get('user') ? await Customer.findOne({ where: { did: c.get('user').did } }) : null,
|
|
1435
1435
|
});
|
|
1436
1436
|
} catch (err) {
|
|
1437
1437
|
logger.error(err);
|
|
1438
1438
|
if (err instanceof CustomError) {
|
|
1439
|
-
|
|
1440
|
-
} else {
|
|
1441
|
-
res.status(500).json({ error: err.message });
|
|
1439
|
+
return c.json({ error: formatError(err) }, getStatusFromError(err));
|
|
1442
1440
|
}
|
|
1441
|
+
return c.json({ error: err.message }, 500);
|
|
1443
1442
|
}
|
|
1444
1443
|
}
|
|
1445
1444
|
|
|
1446
1445
|
// start checkout session from payment link
|
|
1447
|
-
|
|
1446
|
+
app.post('/start/:id', user, (c) => {
|
|
1448
1447
|
logger.info('Starting checkout session from payment link', {
|
|
1449
|
-
paymentLinkId: req.
|
|
1450
|
-
userId:
|
|
1448
|
+
paymentLinkId: c.req.param('id'),
|
|
1449
|
+
userId: c.get('user')?.did,
|
|
1451
1450
|
});
|
|
1452
|
-
|
|
1453
|
-
});
|
|
1454
|
-
|
|
1455
|
-
// for Node.js SDK
|
|
1456
|
-
router.get('/:id', auth, async (req, res) => {
|
|
1457
|
-
const doc = await CheckoutSession.findByPk(req.params.id);
|
|
1458
|
-
|
|
1459
|
-
if (doc) {
|
|
1460
|
-
// @ts-ignore
|
|
1461
|
-
doc.line_items = await Price.expand(doc.line_items, { upsell: true });
|
|
1462
|
-
doc.url = getUrl(`/checkout/${doc.submit_type}/${doc.id}`);
|
|
1463
|
-
}
|
|
1464
|
-
|
|
1465
|
-
if (doc) {
|
|
1466
|
-
res.json(doc?.toJSON());
|
|
1467
|
-
} else {
|
|
1468
|
-
res.status(404).json(null);
|
|
1469
|
-
}
|
|
1470
|
-
});
|
|
1471
|
-
|
|
1472
|
-
// abort stripe subscription(s) created during an incomplete checkout session
|
|
1473
|
-
router.post('/:id/abort-stripe', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
1474
|
-
try {
|
|
1475
|
-
const checkoutSession = req.doc as CheckoutSession;
|
|
1476
|
-
|
|
1477
|
-
if (checkoutSession.status === 'complete') {
|
|
1478
|
-
return res.status(400).json({ error: 'Checkout session already completed' });
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
|
-
// cancel stripe subscriptions if any
|
|
1482
|
-
const canceledSubscriptions: string[] = [];
|
|
1483
|
-
if (['subscription', 'setup'].includes(checkoutSession.mode)) {
|
|
1484
|
-
const subscriptionIds = getCheckoutSessionSubscriptionIds(checkoutSession);
|
|
1485
|
-
const subscriptions = await Subscription.findAll({ where: { id: subscriptionIds } });
|
|
1486
|
-
|
|
1487
|
-
const cancelOps = subscriptions.map(async (sub) => {
|
|
1488
|
-
const stripeSubId = sub.payment_details?.stripe?.subscription_id;
|
|
1489
|
-
if (!stripeSubId) {
|
|
1490
|
-
return null;
|
|
1491
|
-
}
|
|
1492
|
-
const method = await PaymentMethod.findByPk(sub.default_payment_method_id);
|
|
1493
|
-
if (!method || method.type !== 'stripe') {
|
|
1494
|
-
return null;
|
|
1495
|
-
}
|
|
1496
|
-
const client = method.getStripeClient();
|
|
1497
|
-
try {
|
|
1498
|
-
await client.subscriptions.cancel(stripeSubId);
|
|
1499
|
-
await sub.update({
|
|
1500
|
-
payment_details: omit(sub.payment_details || {}, 'stripe'),
|
|
1501
|
-
payment_settings: {
|
|
1502
|
-
payment_method_options: omit(sub.payment_settings?.payment_method_options || {}, 'stripe'),
|
|
1503
|
-
payment_method_types: sub.payment_settings?.payment_method_types || [],
|
|
1504
|
-
},
|
|
1505
|
-
});
|
|
1506
|
-
canceledSubscriptions.push(sub.id);
|
|
1507
|
-
} catch (err) {
|
|
1508
|
-
logger.error('Failed to cancel stripe subscription for checkout abort', {
|
|
1509
|
-
checkoutSessionId: checkoutSession.id,
|
|
1510
|
-
subscriptionId: sub.id,
|
|
1511
|
-
error: err.message,
|
|
1512
|
-
});
|
|
1513
|
-
}
|
|
1514
|
-
return sub.id;
|
|
1515
|
-
});
|
|
1516
|
-
await Promise.all(cancelOps);
|
|
1517
|
-
}
|
|
1518
|
-
|
|
1519
|
-
// remove related invoice if created
|
|
1520
|
-
try {
|
|
1521
|
-
const existInvoice = await Invoice.findOne({ where: { checkout_session_id: checkoutSession.id } });
|
|
1522
|
-
if (existInvoice) {
|
|
1523
|
-
await destroyExistingInvoice(existInvoice);
|
|
1524
|
-
}
|
|
1525
|
-
} catch (error: any) {
|
|
1526
|
-
logger.error('Failed to destroy invoice on checkout abort', {
|
|
1527
|
-
checkoutSessionId: checkoutSession.id,
|
|
1528
|
-
error: error.message,
|
|
1529
|
-
});
|
|
1530
|
-
}
|
|
1531
|
-
res.json({ checkoutSessionId: checkoutSession.id, canceledSubscriptions });
|
|
1532
|
-
} catch (err: any) {
|
|
1533
|
-
logger.error('Error aborting stripe for checkout session', {
|
|
1534
|
-
sessionId: req.params.id,
|
|
1535
|
-
error: err.message,
|
|
1536
|
-
stack: err.stack,
|
|
1537
|
-
});
|
|
1538
|
-
res.status(500).json({ error: err.message });
|
|
1539
|
-
}
|
|
1540
|
-
});
|
|
1541
|
-
|
|
1542
|
-
// Skip payment method for $0 subscription — user chose "Skip, bind later"
|
|
1543
|
-
// Keeps the subscription but sets cancel_at_period_end so it won't renew without a payment method
|
|
1544
|
-
router.post('/:id/skip-payment-method', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
1545
|
-
try {
|
|
1546
|
-
if (!req.user) {
|
|
1547
|
-
return res.status(403).json({ code: 'REQUIRE_LOGIN', error: 'Please login to continue' });
|
|
1548
|
-
}
|
|
1549
|
-
|
|
1550
|
-
const checkoutSession = req.doc as CheckoutSession;
|
|
1551
|
-
|
|
1552
|
-
if (!['subscription', 'setup'].includes(checkoutSession.mode)) {
|
|
1553
|
-
return res.status(400).json({ error: 'Skip payment method is only supported for subscriptions' });
|
|
1554
|
-
}
|
|
1555
|
-
|
|
1556
|
-
const subscriptionIds = getCheckoutSessionSubscriptionIds(checkoutSession);
|
|
1557
|
-
const subscriptions = await Subscription.findAll({ where: { id: subscriptionIds } });
|
|
1558
|
-
|
|
1559
|
-
if (!subscriptions.length) {
|
|
1560
|
-
return res.status(400).json({ error: 'No subscriptions found for this checkout session' });
|
|
1561
|
-
}
|
|
1562
|
-
|
|
1563
|
-
// Cancel Stripe setup intents and activate subscriptions concurrently
|
|
1564
|
-
await Promise.all(
|
|
1565
|
-
subscriptions.map(async (sub) => {
|
|
1566
|
-
const stripeSubId = sub.payment_details?.stripe?.subscription_id;
|
|
1567
|
-
if (stripeSubId) {
|
|
1568
|
-
const method = await PaymentMethod.findByPk(sub.default_payment_method_id);
|
|
1569
|
-
if (method?.type === 'stripe') {
|
|
1570
|
-
const client = method.getStripeClient();
|
|
1571
|
-
try {
|
|
1572
|
-
const stripeSub = await client.subscriptions.retrieve(stripeSubId, {
|
|
1573
|
-
expand: ['pending_setup_intent'],
|
|
1574
|
-
});
|
|
1575
|
-
if (stripeSub.pending_setup_intent && typeof stripeSub.pending_setup_intent !== 'string') {
|
|
1576
|
-
await client.setupIntents.cancel(stripeSub.pending_setup_intent.id);
|
|
1577
|
-
}
|
|
1578
|
-
await client.subscriptions.update(stripeSubId, { cancel_at_period_end: true });
|
|
1579
|
-
} catch (err: any) {
|
|
1580
|
-
logger.error('Failed to update Stripe subscription for skip-payment-method', {
|
|
1581
|
-
checkoutSessionId: checkoutSession.id,
|
|
1582
|
-
subscriptionId: sub.id,
|
|
1583
|
-
stripeSubId,
|
|
1584
|
-
error: err.message,
|
|
1585
|
-
});
|
|
1586
|
-
}
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1590
|
-
// Activate the local subscription with cancel_at_period_end
|
|
1591
|
-
await sub.update({
|
|
1592
|
-
status: sub.trial_end && sub.trial_end > Date.now() / 1000 ? 'trialing' : 'active',
|
|
1593
|
-
cancel_at_period_end: true,
|
|
1594
|
-
});
|
|
1595
|
-
await addSubscriptionJob(sub, 'cycle', false, sub.trial_end);
|
|
1596
|
-
})
|
|
1597
|
-
);
|
|
1598
|
-
|
|
1599
|
-
// Complete the checkout session
|
|
1600
|
-
await checkoutSession.update({
|
|
1601
|
-
status: 'complete',
|
|
1602
|
-
payment_status: 'no_payment_required',
|
|
1603
|
-
});
|
|
1604
|
-
|
|
1605
|
-
return res.json({
|
|
1606
|
-
checkoutSession: { id: checkoutSession.id, status: 'complete' },
|
|
1607
|
-
skipped: true,
|
|
1608
|
-
});
|
|
1609
|
-
} catch (err: any) {
|
|
1610
|
-
logger.error('Error in skip-payment-method', {
|
|
1611
|
-
sessionId: req.params.id,
|
|
1612
|
-
error: err.message,
|
|
1613
|
-
stack: err.stack,
|
|
1614
|
-
});
|
|
1615
|
-
res.status(500).json({ error: err.message });
|
|
1616
|
-
}
|
|
1451
|
+
return startCheckoutSessionFromPaymentLink(c.req.param('id') as string, c);
|
|
1617
1452
|
});
|
|
1618
1453
|
|
|
1619
|
-
// for checkout page
|
|
1620
1454
|
// Lightweight status-only endpoint for polling.
|
|
1621
1455
|
// Returns only status fields — no includes, no joins, single D1 query.
|
|
1622
1456
|
// Used by waitForCheckoutComplete to minimize D1 load during payment processing.
|
|
1623
|
-
|
|
1624
|
-
|
|
1457
|
+
// Static prefix /status/:id registered before /:id to avoid shadowing.
|
|
1458
|
+
app.get('/status/:id', user, async (c) => {
|
|
1459
|
+
const doc = await CheckoutSession.findByPk(c.req.param('id'), {
|
|
1625
1460
|
attributes: ['id', 'status', 'payment_status', 'payment_intent_id'],
|
|
1626
1461
|
});
|
|
1627
1462
|
if (!doc) {
|
|
1628
|
-
return
|
|
1463
|
+
return c.json({ error: 'Checkout session not found' }, 404);
|
|
1629
1464
|
}
|
|
1630
1465
|
|
|
1631
1466
|
const piStatus = doc.payment_intent_id
|
|
@@ -1634,18 +1469,18 @@ router.get('/status/:id', user, async (req, res) => {
|
|
|
1634
1469
|
}).then((pi) => (pi ? { status: pi.status, last_payment_error: pi.last_payment_error } : null))
|
|
1635
1470
|
: null;
|
|
1636
1471
|
|
|
1637
|
-
return
|
|
1472
|
+
return c.json({
|
|
1638
1473
|
checkoutSession: { status: doc.status, payment_status: doc.payment_status },
|
|
1639
1474
|
paymentIntent: piStatus,
|
|
1640
1475
|
});
|
|
1641
1476
|
});
|
|
1642
1477
|
|
|
1643
|
-
|
|
1644
|
-
|
|
1478
|
+
// Static prefix /retrieve/:id registered before /:id to avoid shadowing.
|
|
1479
|
+
app.get('/retrieve/:id', user, async (c) => {
|
|
1480
|
+
const doc = await CheckoutSession.findByPk(c.req.param('id'));
|
|
1645
1481
|
|
|
1646
1482
|
if (!doc) {
|
|
1647
|
-
|
|
1648
|
-
return;
|
|
1483
|
+
return c.json({ error: 'Checkout session not found, you may have incorrectly copied the link.' }, 404);
|
|
1649
1484
|
}
|
|
1650
1485
|
|
|
1651
1486
|
// Check if subscription status needs updating (conditional write — must happen before reads)
|
|
@@ -1705,7 +1540,7 @@ router.get('/retrieve/:id', user, async (req, res) => {
|
|
|
1705
1540
|
|
|
1706
1541
|
// Handle dynamic pricing: create or reuse quotes
|
|
1707
1542
|
const isCheckoutConfirmed = doc.payment_status === 'paid' || doc.status === 'complete' || !!doc.subscription_id;
|
|
1708
|
-
const forceRefresh = req.query
|
|
1543
|
+
const forceRefresh = c.req.query('forceRefresh') === '1' || c.req.query('forceRefresh') === 'true';
|
|
1709
1544
|
|
|
1710
1545
|
// Start PaymentIntent fetch early — runs in parallel with quote processing (save ~85ms D1 RTT)
|
|
1711
1546
|
const paymentIntentPromise = doc.payment_intent_id
|
|
@@ -1796,10 +1631,10 @@ router.get('/retrieve/:id', user, async (req, res) => {
|
|
|
1796
1631
|
const [paymentMethods, paymentLink, customer] = await Promise.all([
|
|
1797
1632
|
getPaymentMethods(doc),
|
|
1798
1633
|
doc.payment_link_id ? PaymentLink.findByPk(doc.payment_link_id) : null,
|
|
1799
|
-
|
|
1634
|
+
c.get('user') ? Customer.findOne({ where: { did: c.get('user').did } }) : null,
|
|
1800
1635
|
]);
|
|
1801
1636
|
|
|
1802
|
-
|
|
1637
|
+
return c.json({
|
|
1803
1638
|
checkoutSession: {
|
|
1804
1639
|
...doc.toJSON(),
|
|
1805
1640
|
discounts: enhancedDiscounts,
|
|
@@ -1812,12 +1647,69 @@ router.get('/retrieve/:id', user, async (req, res) => {
|
|
|
1812
1647
|
paymentLink,
|
|
1813
1648
|
paymentIntent,
|
|
1814
1649
|
customer,
|
|
1815
|
-
...(stopAcceptingOrders ? { stopAcceptingOrders: true } : {}),
|
|
1650
|
+
...(stopAcceptingOrders() ? { stopAcceptingOrders: true } : {}),
|
|
1816
1651
|
});
|
|
1817
1652
|
});
|
|
1818
1653
|
|
|
1819
|
-
//
|
|
1820
|
-
//
|
|
1654
|
+
// for Node.js SDK
|
|
1655
|
+
// Static prefix /broker-status/:id registered before /:id to avoid shadowing.
|
|
1656
|
+
app.get('/broker-status/:id', user, async (c) => {
|
|
1657
|
+
const { needShortUrl = false } = c.req.query();
|
|
1658
|
+
const doc = await CheckoutSession.findByPk(c.req.param('id'));
|
|
1659
|
+
|
|
1660
|
+
if (!doc) {
|
|
1661
|
+
return c.json({
|
|
1662
|
+
checkoutSession: {},
|
|
1663
|
+
paymentLink: null,
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
// @ts-ignore
|
|
1668
|
+
doc.line_items = await Price.expand(doc.line_items, { upsell: true });
|
|
1669
|
+
|
|
1670
|
+
const hasVendorConfig = doc.line_items?.some((item: any) => !!item?.price?.product?.vendor_config?.length);
|
|
1671
|
+
|
|
1672
|
+
if (!hasVendorConfig || doc.payment_status === 'unpaid' || doc.fulfillment_status === 'cancelled') {
|
|
1673
|
+
return c.json({
|
|
1674
|
+
checkoutSession: {},
|
|
1675
|
+
paymentLink: null,
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
const paymentUrl = getUrl(`/checkout/pay/${doc.id}`);
|
|
1680
|
+
const paymentLink = needShortUrl
|
|
1681
|
+
? await formatToShortUrl({
|
|
1682
|
+
url: paymentUrl,
|
|
1683
|
+
validUntil: dayjs().add(20, 'minutes').format('YYYY-MM-DDTHH:mm:ss+00:00'),
|
|
1684
|
+
maxVisits: 5,
|
|
1685
|
+
})
|
|
1686
|
+
: paymentUrl;
|
|
1687
|
+
|
|
1688
|
+
return c.json({
|
|
1689
|
+
checkoutSession: {
|
|
1690
|
+
...doc.toJSON(),
|
|
1691
|
+
line_items: doc.line_items, // Override with expanded line_items (includes price object)
|
|
1692
|
+
},
|
|
1693
|
+
paymentLink,
|
|
1694
|
+
});
|
|
1695
|
+
});
|
|
1696
|
+
|
|
1697
|
+
// for Node.js SDK
|
|
1698
|
+
app.get('/:id', auth, async (c) => {
|
|
1699
|
+
const doc = await CheckoutSession.findByPk(c.req.param('id'));
|
|
1700
|
+
|
|
1701
|
+
if (doc) {
|
|
1702
|
+
// @ts-ignore
|
|
1703
|
+
doc.line_items = await Price.expand(doc.line_items, { upsell: true });
|
|
1704
|
+
doc.url = getUrl(`/checkout/${doc.submit_type}/${doc.id}`);
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
if (doc) {
|
|
1708
|
+
return c.json(doc?.toJSON());
|
|
1709
|
+
}
|
|
1710
|
+
return c.json(null, 404);
|
|
1711
|
+
});
|
|
1712
|
+
|
|
1821
1713
|
/**
|
|
1822
1714
|
* Exchange Rate Endpoint (Final Freeze Architecture)
|
|
1823
1715
|
*
|
|
@@ -1826,23 +1718,25 @@ router.get('/retrieve/:id', user, async (req, res) => {
|
|
|
1826
1718
|
*
|
|
1827
1719
|
* @see Intent: blocklets/core/ai/intent/20260112-dynamic-price.md
|
|
1828
1720
|
*/
|
|
1829
|
-
|
|
1830
|
-
|
|
1721
|
+
// Fetch latest exchange rate for checkout session (for UI display/monitoring only)
|
|
1722
|
+
// Also renews active Quote's expires_at if it's about to expire
|
|
1723
|
+
app.get('/:id/exchange-rate', user, async (c) => {
|
|
1724
|
+
const doc = await CheckoutSession.findByPk(c.req.param('id'));
|
|
1831
1725
|
if (!doc) {
|
|
1832
|
-
return
|
|
1726
|
+
return c.json({ error: 'Checkout session not found, you may have incorrectly copied the link.' }, 404);
|
|
1833
1727
|
}
|
|
1834
1728
|
|
|
1835
|
-
const currencyId = (req.query
|
|
1729
|
+
const currencyId = (c.req.query('currency_id') as string) || doc.currency_id;
|
|
1836
1730
|
const paymentCurrency = (await PaymentCurrency.findByPk(currencyId, {
|
|
1837
1731
|
include: [{ model: PaymentMethod, as: 'payment_method' }],
|
|
1838
1732
|
})) as (PaymentCurrency & { payment_method: PaymentMethod }) | null;
|
|
1839
1733
|
|
|
1840
1734
|
if (!paymentCurrency) {
|
|
1841
|
-
return
|
|
1735
|
+
return c.json({ error: 'Payment currency not found' }, 400);
|
|
1842
1736
|
}
|
|
1843
1737
|
|
|
1844
1738
|
if (paymentCurrency.payment_method?.type === 'stripe') {
|
|
1845
|
-
return
|
|
1739
|
+
return c.json({ error: 'Exchange rate is not supported for stripe payments' }, 400);
|
|
1846
1740
|
}
|
|
1847
1741
|
|
|
1848
1742
|
const serverNow = Math.floor(Date.now() / 1000);
|
|
@@ -1851,7 +1745,7 @@ router.get('/:id/exchange-rate', user, async (req, res) => {
|
|
|
1851
1745
|
const rateResult = await fetchCurrentExchangeRate(paymentCurrency, paymentCurrency.payment_method);
|
|
1852
1746
|
|
|
1853
1747
|
// Final Freeze: Only return rate snapshot, no quote data
|
|
1854
|
-
return
|
|
1748
|
+
return c.json({
|
|
1855
1749
|
server_now: serverNow,
|
|
1856
1750
|
rate: rateResult.rate,
|
|
1857
1751
|
timestamp_ms: rateResult.timestamp_ms,
|
|
@@ -1876,55 +1770,14 @@ router.get('/:id/exchange-rate', user, async (req, res) => {
|
|
|
1876
1770
|
});
|
|
1877
1771
|
|
|
1878
1772
|
// Return 503 with RATE_UNAVAILABLE code
|
|
1879
|
-
return
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
// for checkout page
|
|
1887
|
-
router.get('/broker-status/:id', user, async (req, res) => {
|
|
1888
|
-
const { needShortUrl = false } = req.query;
|
|
1889
|
-
const doc = await CheckoutSession.findByPk(req.params.id);
|
|
1890
|
-
|
|
1891
|
-
if (!doc) {
|
|
1892
|
-
res.json({
|
|
1893
|
-
checkoutSession: {},
|
|
1894
|
-
paymentLink: null,
|
|
1895
|
-
});
|
|
1896
|
-
return;
|
|
1897
|
-
}
|
|
1898
|
-
|
|
1899
|
-
// @ts-ignore
|
|
1900
|
-
doc.line_items = await Price.expand(doc.line_items, { upsell: true });
|
|
1901
|
-
|
|
1902
|
-
const hasVendorConfig = doc.line_items?.some((item: any) => !!item?.price?.product?.vendor_config?.length);
|
|
1903
|
-
|
|
1904
|
-
if (!hasVendorConfig || doc.payment_status === 'unpaid' || doc.fulfillment_status === 'cancelled') {
|
|
1905
|
-
res.json({
|
|
1906
|
-
checkoutSession: {},
|
|
1907
|
-
paymentLink: null,
|
|
1908
|
-
});
|
|
1909
|
-
return;
|
|
1773
|
+
return c.json(
|
|
1774
|
+
{
|
|
1775
|
+
code: 'RATE_UNAVAILABLE',
|
|
1776
|
+
error: error?.message || 'Exchange rate unavailable',
|
|
1777
|
+
},
|
|
1778
|
+
503
|
|
1779
|
+
);
|
|
1910
1780
|
}
|
|
1911
|
-
|
|
1912
|
-
const paymentUrl = getUrl(`/checkout/pay/${doc.id}`);
|
|
1913
|
-
const paymentLink = needShortUrl
|
|
1914
|
-
? await formatToShortUrl({
|
|
1915
|
-
url: paymentUrl,
|
|
1916
|
-
validUntil: dayjs().add(20, 'minutes').format('YYYY-MM-DDTHH:mm:ss+00:00'),
|
|
1917
|
-
maxVisits: 5,
|
|
1918
|
-
})
|
|
1919
|
-
: paymentUrl;
|
|
1920
|
-
|
|
1921
|
-
res.json({
|
|
1922
|
-
checkoutSession: {
|
|
1923
|
-
...doc.toJSON(),
|
|
1924
|
-
line_items: doc.line_items, // Override with expanded line_items (includes price object)
|
|
1925
|
-
},
|
|
1926
|
-
paymentLink,
|
|
1927
|
-
});
|
|
1928
1781
|
});
|
|
1929
1782
|
|
|
1930
1783
|
async function checkVendorConfig(items: TLineItemExpanded[]) {
|
|
@@ -2666,20 +2519,24 @@ async function prefetchQuotesForCheckoutSession(checkoutSession: CheckoutSession
|
|
|
2666
2519
|
}
|
|
2667
2520
|
|
|
2668
2521
|
// submit order
|
|
2669
|
-
|
|
2522
|
+
app.put('/:id/submit', user, ensureCheckoutSessionOpen, async (c) => {
|
|
2670
2523
|
let consumedQuotes: PriceQuote[] = [];
|
|
2671
|
-
const checkoutSession =
|
|
2524
|
+
const checkoutSession = c.get('doc') as CheckoutSession;
|
|
2525
|
+
const body = c.get('sanitizedBody') ?? {};
|
|
2672
2526
|
try {
|
|
2673
|
-
if (!
|
|
2674
|
-
return
|
|
2527
|
+
if (!c.get('user')) {
|
|
2528
|
+
return c.json({ code: 'REQUIRE_LOGIN', error: 'Please login to continue' }, 403);
|
|
2675
2529
|
}
|
|
2676
2530
|
|
|
2677
2531
|
// Check if system is accepting new orders
|
|
2678
|
-
if (stopAcceptingOrders) {
|
|
2679
|
-
return
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2532
|
+
if (stopAcceptingOrders()) {
|
|
2533
|
+
return c.json(
|
|
2534
|
+
{
|
|
2535
|
+
code: 'STOP_ACCEPTING_ORDERS',
|
|
2536
|
+
error: 'We are not accepting new orders at this time. Please try again later.',
|
|
2537
|
+
},
|
|
2538
|
+
422
|
|
2539
|
+
);
|
|
2683
2540
|
}
|
|
2684
2541
|
|
|
2685
2542
|
// Idempotency check: If already complete/paid, return existing state
|
|
@@ -2695,28 +2552,34 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
2695
2552
|
const quoteIds = (existingPaymentIntent?.metadata as any)?.quoteIds || [];
|
|
2696
2553
|
const quotes = quoteIds.length > 0 ? await PriceQuote.findAll({ where: { id: quoteIds } }) : [];
|
|
2697
2554
|
|
|
2698
|
-
return
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2555
|
+
return c.json(
|
|
2556
|
+
{
|
|
2557
|
+
checkoutSession,
|
|
2558
|
+
paymentIntent: existingPaymentIntent,
|
|
2559
|
+
invoice: existingInvoice,
|
|
2560
|
+
quotes: quotes.map((q) => ({
|
|
2561
|
+
id: q.id,
|
|
2562
|
+
status: q.status,
|
|
2563
|
+
quoted_amount: q.quoted_amount,
|
|
2564
|
+
expires_at: q.expires_at,
|
|
2565
|
+
})),
|
|
2566
|
+
message: 'Checkout session already completed',
|
|
2567
|
+
},
|
|
2568
|
+
200
|
|
2569
|
+
);
|
|
2710
2570
|
}
|
|
2711
2571
|
|
|
2712
|
-
const hasVendorConfig = await checkVendorConfig(
|
|
2572
|
+
const hasVendorConfig = await checkVendorConfig((c.get('doc') as CheckoutSession).line_items as any);
|
|
2713
2573
|
if (hasVendorConfig) {
|
|
2714
|
-
const { user: userDetail } = await blocklet.getUser(
|
|
2574
|
+
const { user: userDetail } = await blocklet.getUser(c.get('user').did, { enableConnectedAccount: true });
|
|
2715
2575
|
if (!userDetail?.sourceAppPid) {
|
|
2716
|
-
return
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2576
|
+
return c.json(
|
|
2577
|
+
{
|
|
2578
|
+
code: 'UNIFIED_APP_REQUIRED',
|
|
2579
|
+
error: 'This action requires a unified account. Please switch accounts and try again.',
|
|
2580
|
+
},
|
|
2581
|
+
403
|
|
2582
|
+
);
|
|
2720
2583
|
}
|
|
2721
2584
|
}
|
|
2722
2585
|
|
|
@@ -2729,7 +2592,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
2729
2592
|
line_items: checkoutSession.line_items,
|
|
2730
2593
|
checkoutSessionId: checkoutSession.id,
|
|
2731
2594
|
});
|
|
2732
|
-
return
|
|
2595
|
+
return c.json({ error: (err as any).message }, 400);
|
|
2733
2596
|
}
|
|
2734
2597
|
}
|
|
2735
2598
|
// validate cross sell
|
|
@@ -2738,20 +2601,18 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
2738
2601
|
const result = await getCrossSellItem(checkoutSession);
|
|
2739
2602
|
// @ts-ignore
|
|
2740
2603
|
if (result.id) {
|
|
2741
|
-
return
|
|
2742
|
-
.status(400)
|
|
2743
|
-
.json({ code: 'REQUIRE_CROSS_SELL', error: 'Please select cross sell product to continue' });
|
|
2604
|
+
return c.json({ code: 'REQUIRE_CROSS_SELL', error: 'Please select cross sell product to continue' }, 400);
|
|
2744
2605
|
}
|
|
2745
2606
|
}
|
|
2746
2607
|
}
|
|
2747
2608
|
|
|
2748
2609
|
const { paymentMethod, paymentCurrency } = await validatePaymentSettings(
|
|
2749
|
-
|
|
2750
|
-
|
|
2610
|
+
(body as any).payment_method,
|
|
2611
|
+
(body as any).payment_currency
|
|
2751
2612
|
);
|
|
2752
2613
|
await checkoutSession.update({
|
|
2753
2614
|
currency_id: paymentCurrency.id,
|
|
2754
|
-
metadata: { ...checkoutSession.metadata, preferred_locale:
|
|
2615
|
+
metadata: { ...checkoutSession.metadata, preferred_locale: c.get('locale') },
|
|
2755
2616
|
});
|
|
2756
2617
|
|
|
2757
2618
|
const expandedLineItems = await Price.expand(checkoutSession.line_items, { product: true, upsell: true });
|
|
@@ -2763,13 +2624,13 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
2763
2624
|
const isTrialing = trialSetup.trialInDays > 0 || trialSetup.trialEnd > submitNow;
|
|
2764
2625
|
|
|
2765
2626
|
// Check if using Final Freeze flow (idempotency_key provided)
|
|
2766
|
-
const useFinalFreezeFlow = !!
|
|
2627
|
+
const useFinalFreezeFlow = !!(body as any).idempotency_key;
|
|
2767
2628
|
|
|
2768
2629
|
if (useFinalFreezeFlow) {
|
|
2769
2630
|
// Final Freeze: Create quotes at submit time with dual-layer validation
|
|
2770
2631
|
logger.info('Using Final Freeze quote creation flow', {
|
|
2771
2632
|
sessionId: checkoutSession.id,
|
|
2772
|
-
idempotencyKey:
|
|
2633
|
+
idempotencyKey: (body as any).idempotency_key,
|
|
2773
2634
|
isTrialing,
|
|
2774
2635
|
});
|
|
2775
2636
|
|
|
@@ -2777,38 +2638,34 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
2777
2638
|
checkoutSession,
|
|
2778
2639
|
lineItems: expandedLineItems,
|
|
2779
2640
|
paymentCurrencyId: paymentCurrency.id,
|
|
2780
|
-
idempotencyKey:
|
|
2781
|
-
previewRate:
|
|
2782
|
-
priceConfirmed:
|
|
2641
|
+
idempotencyKey: (body as any).idempotency_key,
|
|
2642
|
+
previewRate: (body as any).preview_rate,
|
|
2643
|
+
priceConfirmed: (body as any).price_confirmed === true,
|
|
2783
2644
|
trialing: isTrialing,
|
|
2784
2645
|
});
|
|
2785
2646
|
|
|
2786
2647
|
// Handle validation errors (PRICE_UNAVAILABLE, PRICE_UNSTABLE, PRICE_CHANGED, RATE_BELOW_SLIPPAGE_LIMIT)
|
|
2787
2648
|
if (quoteResult.validationError) {
|
|
2788
2649
|
const { code, message, changePercent, currentRate, minAcceptableRate } = quoteResult.validationError;
|
|
2789
|
-
// RATE_BELOW_SLIPPAGE_LIMIT: 400 - user needs to update slippage settings
|
|
2790
|
-
// PRICE_CHANGED: 409 - price changed, needs confirmation
|
|
2791
|
-
// PRICE_UNAVAILABLE/PRICE_UNSTABLE: 503 - service temporarily unavailable
|
|
2792
2650
|
let statusCode = 503;
|
|
2793
2651
|
if (code === 'RATE_BELOW_SLIPPAGE_LIMIT') {
|
|
2794
2652
|
statusCode = 400;
|
|
2795
2653
|
} else if (code === 'PRICE_CHANGED') {
|
|
2796
2654
|
statusCode = 409;
|
|
2797
2655
|
}
|
|
2798
|
-
return
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
});
|
|
2656
|
+
return c.json(
|
|
2657
|
+
{
|
|
2658
|
+
code,
|
|
2659
|
+
error: message,
|
|
2660
|
+
change_percent: changePercent,
|
|
2661
|
+
...(code === 'PRICE_CHANGED' && { requires_confirmation: true }),
|
|
2662
|
+
...(code === 'RATE_BELOW_SLIPPAGE_LIMIT' && {
|
|
2663
|
+
current_rate: currentRate,
|
|
2664
|
+
min_acceptable_rate: minAcceptableRate,
|
|
2665
|
+
}),
|
|
2666
|
+
},
|
|
2667
|
+
statusCode as any
|
|
2668
|
+
);
|
|
2812
2669
|
}
|
|
2813
2670
|
|
|
2814
2671
|
consumedQuotes = quoteResult.consumedQuotes;
|
|
@@ -2824,7 +2681,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
2824
2681
|
checkoutSession,
|
|
2825
2682
|
lineItems: expandedLineItems,
|
|
2826
2683
|
paymentCurrencyId: paymentCurrency.id,
|
|
2827
|
-
quotesInput:
|
|
2684
|
+
quotesInput: (body as any).quotes,
|
|
2828
2685
|
strict: true,
|
|
2829
2686
|
});
|
|
2830
2687
|
consumedQuotes = quoteResult.consumedQuotes;
|
|
@@ -2834,7 +2691,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
2834
2691
|
}
|
|
2835
2692
|
} catch (err) {
|
|
2836
2693
|
if (err instanceof CustomError) {
|
|
2837
|
-
const errCode = getQuoteErrorCode(err) || err.code || 'QUOTE_INVALID';
|
|
2694
|
+
const errCode = getQuoteErrorCode(err) || (err as any).code || 'QUOTE_INVALID';
|
|
2838
2695
|
if (
|
|
2839
2696
|
[
|
|
2840
2697
|
'QUOTE_AMOUNT_MISMATCH',
|
|
@@ -2844,41 +2701,34 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
2844
2701
|
'QUOTE_LOCK_EXPIRED',
|
|
2845
2702
|
].includes(String(errCode))
|
|
2846
2703
|
) {
|
|
2847
|
-
return
|
|
2704
|
+
return c.json({ code: String(errCode), error: (err as any).message }, 409);
|
|
2848
2705
|
}
|
|
2849
|
-
return
|
|
2706
|
+
return c.json({ code: String(errCode), error: (err as any).message }, 400);
|
|
2850
2707
|
}
|
|
2851
2708
|
throw err;
|
|
2852
2709
|
}
|
|
2853
2710
|
}
|
|
2854
2711
|
|
|
2855
|
-
// Save quote_id and quote_currency_id to CheckoutSession.line_items for future reference
|
|
2856
|
-
// Preserve all existing fields in line_items, but update quote-related fields
|
|
2857
|
-
// Quote data is the single source of truth in PriceQuote table
|
|
2858
|
-
// Also update metadata.slippage with min_acceptable_rate for subscription creation
|
|
2859
2712
|
const quoteWithMinRate = consumedQuotes.find((q) => q.min_acceptable_rate);
|
|
2860
2713
|
let updatedMetadata = checkoutSession.metadata;
|
|
2861
2714
|
|
|
2862
2715
|
if (quoteWithMinRate) {
|
|
2863
|
-
// Use min_acceptable_rate from quote
|
|
2864
2716
|
updatedMetadata = {
|
|
2865
2717
|
...(checkoutSession.metadata || {}),
|
|
2866
2718
|
slippage: {
|
|
2867
2719
|
...((checkoutSession.metadata as any)?.slippage || {}),
|
|
2868
|
-
mode: 'rate',
|
|
2720
|
+
mode: 'rate',
|
|
2869
2721
|
min_acceptable_rate: quoteWithMinRate.min_acceptable_rate,
|
|
2870
2722
|
percent: quoteWithMinRate.slippage_percent ?? 0.5,
|
|
2871
2723
|
updated_at_ms: Date.now(),
|
|
2872
2724
|
},
|
|
2873
2725
|
};
|
|
2874
2726
|
} else {
|
|
2875
|
-
// No quote (e.g., trial period) - calculate min_acceptable_rate from current rate if needed
|
|
2876
2727
|
const existingSlippage = (checkoutSession.metadata as any)?.slippage;
|
|
2877
2728
|
const hasDynamicPricing = expandedLineItems.some(
|
|
2878
2729
|
(item) => ((item.upsell_price || item.price) as any)?.pricing_type === 'dynamic'
|
|
2879
2730
|
);
|
|
2880
2731
|
|
|
2881
|
-
// If dynamic pricing and no min_acceptable_rate yet, calculate from current rate
|
|
2882
2732
|
if (hasDynamicPricing && existingSlippage && !existingSlippage.min_acceptable_rate) {
|
|
2883
2733
|
try {
|
|
2884
2734
|
const rateResult = await fetchCurrentExchangeRate(paymentCurrency, paymentMethod);
|
|
@@ -2886,14 +2736,13 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
2886
2736
|
const rate = Number(rateResult.rate);
|
|
2887
2737
|
const percent = existingSlippage.percent ?? DEFAULT_SLIPPAGE_PERCENT;
|
|
2888
2738
|
if (Number.isFinite(rate) && rate > 0) {
|
|
2889
|
-
// min_rate = rate / (1 + percent/100)
|
|
2890
2739
|
const minRate = rate / (1 + percent / 100);
|
|
2891
2740
|
updatedMetadata = {
|
|
2892
2741
|
...(checkoutSession.metadata || {}),
|
|
2893
2742
|
slippage: {
|
|
2894
2743
|
...existingSlippage,
|
|
2895
2744
|
min_acceptable_rate: minRate.toFixed(8).replace(/\.?0+$/, ''),
|
|
2896
|
-
base_currency: 'USD',
|
|
2745
|
+
base_currency: 'USD',
|
|
2897
2746
|
updated_at_ms: Date.now(),
|
|
2898
2747
|
},
|
|
2899
2748
|
};
|
|
@@ -2910,7 +2759,6 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
2910
2759
|
sessionId: checkoutSession.id,
|
|
2911
2760
|
error: error.message,
|
|
2912
2761
|
});
|
|
2913
|
-
// Continue without min_acceptable_rate - not a fatal error
|
|
2914
2762
|
}
|
|
2915
2763
|
}
|
|
2916
2764
|
}
|
|
@@ -2921,16 +2769,12 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
2921
2769
|
const baseItem = checkoutSession.line_items.find((li: any) => li.price_id === item.price_id);
|
|
2922
2770
|
const quoteId = (item as any).quote_id || (baseItem as any)?.quote_id;
|
|
2923
2771
|
const quoteCurrencyId = (item as any).quote_currency_id || (baseItem as any)?.quote_currency_id;
|
|
2924
|
-
const result: any = {
|
|
2925
|
-
...baseItem, // Preserve all original fields
|
|
2926
|
-
};
|
|
2772
|
+
const result: any = { ...baseItem };
|
|
2927
2773
|
if (quoteId) {
|
|
2928
|
-
result.quote_id = String(quoteId);
|
|
2929
|
-
// Also save quote_currency_id so backend can validate custom_amount against currency
|
|
2774
|
+
result.quote_id = String(quoteId);
|
|
2930
2775
|
if (quoteCurrencyId) {
|
|
2931
2776
|
result.quote_currency_id = quoteCurrencyId;
|
|
2932
2777
|
}
|
|
2933
|
-
// Update custom_amount from the new quote if available
|
|
2934
2778
|
if ((item as any).custom_amount) {
|
|
2935
2779
|
result.custom_amount = (item as any).custom_amount;
|
|
2936
2780
|
}
|
|
@@ -2946,16 +2790,16 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
2946
2790
|
savedMinAcceptableRate: quoteWithMinRate?.min_acceptable_rate,
|
|
2947
2791
|
});
|
|
2948
2792
|
|
|
2949
|
-
let customer = await Customer.findOne({ where: { did:
|
|
2793
|
+
let customer = await Customer.findOne({ where: { did: c.get('user').did } });
|
|
2950
2794
|
if (!customer) {
|
|
2951
|
-
const { user: userInfo } = await blocklet.getUser(
|
|
2795
|
+
const { user: userInfo } = await blocklet.getUser(c.get('user').did);
|
|
2952
2796
|
customer = await Customer.create({
|
|
2953
2797
|
livemode: !!checkoutSession.livemode,
|
|
2954
|
-
did:
|
|
2955
|
-
name:
|
|
2956
|
-
email:
|
|
2957
|
-
phone:
|
|
2958
|
-
address:
|
|
2798
|
+
did: c.get('user').did,
|
|
2799
|
+
name: (body as any).customer_name,
|
|
2800
|
+
email: (body as any).customer_email || userInfo?.email || '',
|
|
2801
|
+
phone: (body as any).customer_phone || userInfo?.phone || '',
|
|
2802
|
+
address: (body as any).billing_address || Customer.formatAddressFromUser(userInfo),
|
|
2959
2803
|
description: userInfo?.remark || '',
|
|
2960
2804
|
metadata: {},
|
|
2961
2805
|
balance: '0',
|
|
@@ -2963,7 +2807,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
2963
2807
|
delinquent: false,
|
|
2964
2808
|
invoice_prefix: Customer.getInvoicePrefix(),
|
|
2965
2809
|
});
|
|
2966
|
-
logger.info('customer created on checkout session submit', { did:
|
|
2810
|
+
logger.info('customer created on checkout session submit', { did: c.get('user').did, id: customer.id });
|
|
2967
2811
|
try {
|
|
2968
2812
|
await blocklet.updateUserAddress(
|
|
2969
2813
|
{
|
|
@@ -2972,48 +2816,39 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
2972
2816
|
},
|
|
2973
2817
|
{
|
|
2974
2818
|
headers: {
|
|
2975
|
-
cookie: req.
|
|
2819
|
+
cookie: c.req.header('cookie') || '',
|
|
2976
2820
|
},
|
|
2977
2821
|
}
|
|
2978
2822
|
);
|
|
2979
|
-
logger.info('updateUserAddress success', {
|
|
2980
|
-
did: customer.did,
|
|
2981
|
-
});
|
|
2823
|
+
logger.info('updateUserAddress success', { did: customer.did });
|
|
2982
2824
|
} catch (err) {
|
|
2983
|
-
logger.error('updateUserAddress failed', {
|
|
2984
|
-
error: err,
|
|
2985
|
-
customerId: customer.id,
|
|
2986
|
-
});
|
|
2825
|
+
logger.error('updateUserAddress failed', { error: err, customerId: customer.id });
|
|
2987
2826
|
}
|
|
2988
2827
|
} else {
|
|
2989
2828
|
const updates: Record<string, any> = {};
|
|
2990
2829
|
if (checkoutSession.customer_update?.name) {
|
|
2991
|
-
if (
|
|
2992
|
-
updates.name =
|
|
2830
|
+
if ((body as any).customer_name) {
|
|
2831
|
+
updates.name = (body as any).customer_name;
|
|
2993
2832
|
}
|
|
2994
|
-
if (
|
|
2995
|
-
updates.email =
|
|
2833
|
+
if ((body as any).customer_email) {
|
|
2834
|
+
updates.email = (body as any).customer_email;
|
|
2996
2835
|
}
|
|
2997
|
-
if (
|
|
2998
|
-
updates.phone =
|
|
2836
|
+
if ((body as any).customer_phone) {
|
|
2837
|
+
updates.phone = (body as any).customer_phone;
|
|
2999
2838
|
}
|
|
3000
2839
|
}
|
|
3001
2840
|
if (checkoutSession.customer_update?.address) {
|
|
3002
|
-
updates.address = Customer.formatUpdateAddress(
|
|
2841
|
+
updates.address = Customer.formatUpdateAddress((body as any).billing_address, customer);
|
|
3003
2842
|
}
|
|
3004
2843
|
if (!customer.invoice_prefix) {
|
|
3005
2844
|
updates.invoice_prefix = Customer.getInvoicePrefix();
|
|
3006
2845
|
}
|
|
3007
2846
|
|
|
3008
2847
|
if (!hasObjectChanged(updates, customer, { deepCompare: ['address'] })) {
|
|
3009
|
-
logger.info('customer update skipped (no changes)', {
|
|
3010
|
-
did: customer.did,
|
|
3011
|
-
});
|
|
2848
|
+
logger.info('customer update skipped (no changes)', { did: customer.did });
|
|
3012
2849
|
} else {
|
|
3013
2850
|
await customer.update(updates);
|
|
3014
|
-
logger.info('customer updated', {
|
|
3015
|
-
did: customer.did,
|
|
3016
|
-
});
|
|
2851
|
+
logger.info('customer updated', { did: customer.did });
|
|
3017
2852
|
|
|
3018
2853
|
try {
|
|
3019
2854
|
await blocklet.updateUserAddress(
|
|
@@ -3025,18 +2860,13 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
3025
2860
|
},
|
|
3026
2861
|
{
|
|
3027
2862
|
headers: {
|
|
3028
|
-
cookie: req.
|
|
2863
|
+
cookie: c.req.header('cookie') || '',
|
|
3029
2864
|
},
|
|
3030
2865
|
}
|
|
3031
2866
|
);
|
|
3032
|
-
logger.info('updateUserAddress success', {
|
|
3033
|
-
did: customer.did,
|
|
3034
|
-
});
|
|
2867
|
+
logger.info('updateUserAddress success', { did: customer.did });
|
|
3035
2868
|
} catch (err) {
|
|
3036
|
-
logger.error('updateUserAddress failed', {
|
|
3037
|
-
error: err,
|
|
3038
|
-
customerId: customer.id,
|
|
3039
|
-
});
|
|
2869
|
+
logger.error('updateUserAddress failed', { error: err, customerId: customer.id });
|
|
3040
2870
|
}
|
|
3041
2871
|
}
|
|
3042
2872
|
}
|
|
@@ -3044,57 +2874,55 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
3044
2874
|
// check if customer can make new purchase
|
|
3045
2875
|
const canMakeNewPurchase = await customer.canMakeNewPurchase(checkoutSession.invoice_id);
|
|
3046
2876
|
if (!canMakeNewPurchase) {
|
|
3047
|
-
return
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
2877
|
+
return c.json(
|
|
2878
|
+
{
|
|
2879
|
+
code: 'CUSTOMER_LIMITED',
|
|
2880
|
+
error: 'Customer can not make new purchase, maybe you have unpaid invoices from previous purchases',
|
|
2881
|
+
},
|
|
2882
|
+
403
|
|
2883
|
+
);
|
|
3051
2884
|
}
|
|
3052
2885
|
|
|
3053
2886
|
// check if user is in blocklist
|
|
3054
2887
|
if (CHARGE_SUPPORTED_CHAIN_TYPES.includes(paymentMethod.type)) {
|
|
3055
|
-
const inBlock = await isUserInBlocklist(
|
|
2888
|
+
const inBlock = await isUserInBlocklist(c.get('user').did, paymentMethod);
|
|
3056
2889
|
if (inBlock) {
|
|
3057
|
-
return
|
|
3058
|
-
.status(403)
|
|
3059
|
-
.json({ code: 'PAYMENT_RESTRICTED', error: 'Unable to process your purchase at this time' });
|
|
2890
|
+
return c.json({ code: 'PAYMENT_RESTRICTED', error: 'Unable to process your purchase at this time' }, 403);
|
|
3060
2891
|
}
|
|
3061
2892
|
}
|
|
3062
2893
|
|
|
3063
|
-
await checkoutSession.update({ customer_id: customer.id, customer_did:
|
|
2894
|
+
await checkoutSession.update({ customer_id: customer.id, customer_did: c.get('user').did });
|
|
3064
2895
|
|
|
3065
|
-
// Verify and recalculate amount with current discounts
|
|
3066
2896
|
const { lineItems, trialInDays, trialEnd, now } = await calculateAndUpdateAmount(
|
|
3067
2897
|
checkoutSession,
|
|
3068
2898
|
paymentCurrency.id,
|
|
3069
2899
|
true,
|
|
3070
|
-
{
|
|
3071
|
-
lineItemsOverride: lineItemsWithQuotes,
|
|
3072
|
-
}
|
|
2900
|
+
{ lineItemsOverride: lineItemsWithQuotes }
|
|
3073
2901
|
);
|
|
3074
2902
|
|
|
3075
|
-
// Validate payment amounts meet minimum requirements
|
|
3076
2903
|
if (paymentMethod.type === 'stripe') {
|
|
3077
2904
|
const result = await validatePaymentAmounts(lineItems, paymentCurrency, checkoutSession);
|
|
3078
2905
|
if (!result.valid) {
|
|
3079
|
-
return
|
|
2906
|
+
return c.json({ error: result.error }, 400);
|
|
3080
2907
|
}
|
|
3081
2908
|
}
|
|
3082
2909
|
|
|
3083
|
-
// Validate checkout session ownership if it was created for a specific customer
|
|
3084
2910
|
if (checkoutSession.customer_did && checkoutSession.metadata?.createdBy) {
|
|
3085
2911
|
const createdByDid = checkoutSession.metadata.createdBy;
|
|
3086
|
-
if (createdByDid !==
|
|
3087
|
-
return
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
2912
|
+
if (createdByDid !== c.get('user').did) {
|
|
2913
|
+
return c.json(
|
|
2914
|
+
{
|
|
2915
|
+
error:
|
|
2916
|
+
"It's not allowed to submit checkout sessions created by other users, please create your own checkout session",
|
|
2917
|
+
},
|
|
2918
|
+
403
|
|
2919
|
+
);
|
|
3091
2920
|
}
|
|
3092
2921
|
}
|
|
3093
2922
|
|
|
3094
|
-
// Validate promotion codes are still valid during submission
|
|
3095
2923
|
await validatePromotionCodesOnSubmit(checkoutSession, {
|
|
3096
2924
|
lineItems,
|
|
3097
|
-
customerId:
|
|
2925
|
+
customerId: c.get('user').did,
|
|
3098
2926
|
amount: checkoutSession.amount_subtotal,
|
|
3099
2927
|
});
|
|
3100
2928
|
|
|
@@ -3106,12 +2934,11 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
3106
2934
|
}
|
|
3107
2935
|
} catch (err) {
|
|
3108
2936
|
logger.error('Failed to destroy exist invoice', {
|
|
3109
|
-
error: err.message,
|
|
2937
|
+
error: (err as any).message,
|
|
3110
2938
|
checkoutSessionId: checkoutSession.id,
|
|
3111
2939
|
});
|
|
3112
2940
|
}
|
|
3113
2941
|
|
|
3114
|
-
// create or update payment intent
|
|
3115
2942
|
let paymentIntent: PaymentIntent | null = null;
|
|
3116
2943
|
if (checkoutSession.mode === 'payment') {
|
|
3117
2944
|
const result = await createOrUpdatePaymentIntent(
|
|
@@ -3126,13 +2953,12 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
3126
2953
|
);
|
|
3127
2954
|
paymentIntent = result.paymentIntent;
|
|
3128
2955
|
|
|
3129
|
-
// Create discount records for payment mode immediately after payment intent creation
|
|
3130
2956
|
if (checkoutSession.discounts?.length) {
|
|
3131
2957
|
try {
|
|
3132
2958
|
const discountResult = await createDiscountRecordsForCheckout({
|
|
3133
2959
|
checkoutSession,
|
|
3134
2960
|
customerId: customer.id,
|
|
3135
|
-
subscriptionIds: [],
|
|
2961
|
+
subscriptionIds: [],
|
|
3136
2962
|
});
|
|
3137
2963
|
|
|
3138
2964
|
logger.info('Created discount records for checkout session payment', {
|
|
@@ -3145,7 +2971,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
3145
2971
|
logger.error('Failed to create discount records for checkout session payment', {
|
|
3146
2972
|
checkoutSessionId: checkoutSession.id,
|
|
3147
2973
|
paymentIntentId: paymentIntent?.id,
|
|
3148
|
-
error: discountError.message,
|
|
2974
|
+
error: (discountError as any).message,
|
|
3149
2975
|
});
|
|
3150
2976
|
}
|
|
3151
2977
|
}
|
|
@@ -3156,7 +2982,6 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
3156
2982
|
}
|
|
3157
2983
|
}
|
|
3158
2984
|
|
|
3159
|
-
// SetupIntent processing
|
|
3160
2985
|
let setupIntent: SetupIntent | null = null;
|
|
3161
2986
|
if (checkoutSession.mode === 'setup' && paymentMethod.type !== 'stripe') {
|
|
3162
2987
|
if (checkoutSession.setup_intent_id) {
|
|
@@ -3165,10 +2990,10 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
3165
2990
|
|
|
3166
2991
|
if (setupIntent) {
|
|
3167
2992
|
if (setupIntent.isImmutable()) {
|
|
3168
|
-
return
|
|
2993
|
+
return c.json({ code: 'SETUP_SUCCEEDED', error: 'Checkout session setup completed' }, 403);
|
|
3169
2994
|
}
|
|
3170
2995
|
if (setupIntent.status === 'processing') {
|
|
3171
|
-
return
|
|
2996
|
+
return c.json({ code: 'SETUP_PROCESSING', error: 'Checkout session setup processing' }, 403);
|
|
3172
2997
|
}
|
|
3173
2998
|
await setupIntent.update({
|
|
3174
2999
|
status: 'requires_capture',
|
|
@@ -3197,7 +3022,6 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
3197
3022
|
metadata: checkoutSession.metadata,
|
|
3198
3023
|
});
|
|
3199
3024
|
|
|
3200
|
-
// persist setup intent id
|
|
3201
3025
|
await checkoutSession.update({ setup_intent_id: setupIntent.id });
|
|
3202
3026
|
logger.info('setupIntent created on checkout session submit', {
|
|
3203
3027
|
session: checkoutSession.id,
|
|
@@ -3206,7 +3030,6 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
3206
3030
|
}
|
|
3207
3031
|
}
|
|
3208
3032
|
|
|
3209
|
-
// subscription processing
|
|
3210
3033
|
let subscription: Subscription | null = null;
|
|
3211
3034
|
let subscriptions: Subscription[] = [];
|
|
3212
3035
|
|
|
@@ -3245,7 +3068,6 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
3245
3068
|
enableSubscriptionGrouping: checkoutSession.enable_subscription_grouping,
|
|
3246
3069
|
});
|
|
3247
3070
|
|
|
3248
|
-
// Create discount records immediately after subscriptions are created
|
|
3249
3071
|
if (checkoutSession.discounts?.length) {
|
|
3250
3072
|
try {
|
|
3251
3073
|
const discountResult = await createDiscountRecordsForCheckout({
|
|
@@ -3254,7 +3076,6 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
3254
3076
|
subscriptionIds: subscriptions.map((s) => s.id),
|
|
3255
3077
|
});
|
|
3256
3078
|
|
|
3257
|
-
// Update subscription discount_id references
|
|
3258
3079
|
if (discountResult.subscriptionsUpdated.length > 0) {
|
|
3259
3080
|
try {
|
|
3260
3081
|
const subscriptionUpdateResult = await updateSubscriptionDiscountReferences({
|
|
@@ -3270,7 +3091,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
3270
3091
|
logger.error('Failed to update subscription discount_id references', {
|
|
3271
3092
|
checkoutSessionId: checkoutSession.id,
|
|
3272
3093
|
subscriptionsUpdated: discountResult.subscriptionsUpdated,
|
|
3273
|
-
error: subscriptionUpdateError.message,
|
|
3094
|
+
error: (subscriptionUpdateError as any).message,
|
|
3274
3095
|
});
|
|
3275
3096
|
throw subscriptionUpdateError;
|
|
3276
3097
|
}
|
|
@@ -3285,16 +3106,16 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
3285
3106
|
} catch (discountError) {
|
|
3286
3107
|
logger.error('Failed to create discount records for checkout session', {
|
|
3287
3108
|
checkoutSessionId: checkoutSession.id,
|
|
3288
|
-
error: discountError.message,
|
|
3109
|
+
error: (discountError as any).message,
|
|
3289
3110
|
});
|
|
3290
|
-
return
|
|
3291
|
-
|
|
3292
|
-
|
|
3111
|
+
return c.json(
|
|
3112
|
+
{ error: 'Failed to create discount records for checkout session, please try again later' },
|
|
3113
|
+
400
|
|
3114
|
+
);
|
|
3293
3115
|
}
|
|
3294
3116
|
}
|
|
3295
3117
|
}
|
|
3296
3118
|
|
|
3297
|
-
// Prepare discount configuration
|
|
3298
3119
|
let discountConfig;
|
|
3299
3120
|
if (checkoutSession.discounts && checkoutSession.discounts.length > 0) {
|
|
3300
3121
|
const firstDiscount = checkoutSession.discounts[0];
|
|
@@ -3307,7 +3128,6 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
3307
3128
|
}
|
|
3308
3129
|
}
|
|
3309
3130
|
|
|
3310
|
-
// Calculate actual current payment amount (excludes future renewals)
|
|
3311
3131
|
const trialing = trialInDays > 0 || trialEnd > now;
|
|
3312
3132
|
const { total: currentPaymentTotal } = await getCheckoutAmount(
|
|
3313
3133
|
lineItems,
|
|
@@ -3332,7 +3152,6 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
3332
3152
|
},
|
|
3333
3153
|
};
|
|
3334
3154
|
|
|
3335
|
-
// if we can complete purchase with customer balance
|
|
3336
3155
|
const balance = isCreditSufficientForPayment({
|
|
3337
3156
|
paymentMethod,
|
|
3338
3157
|
paymentCurrency,
|
|
@@ -3350,7 +3169,6 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
3350
3169
|
paymentCurrency.isCredit() ||
|
|
3351
3170
|
(checkoutSession.mode === 'subscription' && checkoutSession.subscription_data?.no_stake);
|
|
3352
3171
|
if (isPayment && paymentIntent && canFastPay) {
|
|
3353
|
-
// if we can complete purchase without any wallet interaction
|
|
3354
3172
|
delegation = await isDelegationSufficientForPayment({
|
|
3355
3173
|
paymentMethod,
|
|
3356
3174
|
paymentCurrency,
|
|
@@ -3365,7 +3183,6 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
3365
3183
|
};
|
|
3366
3184
|
}
|
|
3367
3185
|
} else if (paymentMethod.type === 'arcblock' && canFastPayForSubscription) {
|
|
3368
|
-
// if we can complete purchase without any wallet interaction
|
|
3369
3186
|
const result = await processSubscriptionFastCheckout({
|
|
3370
3187
|
checkoutSession,
|
|
3371
3188
|
customer,
|
|
@@ -3383,12 +3200,9 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
3383
3200
|
logger.warn(`Fast checkout processing failed: ${result.message}`, {
|
|
3384
3201
|
checkoutSessionId: checkoutSession.id,
|
|
3385
3202
|
});
|
|
3386
|
-
|
|
3387
3203
|
creditSufficient = false;
|
|
3388
3204
|
} else {
|
|
3389
|
-
delegation = {
|
|
3390
|
-
sufficient: true,
|
|
3391
|
-
};
|
|
3205
|
+
delegation = { sufficient: true };
|
|
3392
3206
|
canFastPay = true;
|
|
3393
3207
|
fastPayInfo = {
|
|
3394
3208
|
type: 'delegation',
|
|
@@ -3428,10 +3242,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
3428
3242
|
const primarySubscription = (subscriptions.find((x) => x.metadata.is_primary_subscription) ||
|
|
3429
3243
|
subscriptions[0]) as Subscription;
|
|
3430
3244
|
if (!stripeContext) {
|
|
3431
|
-
stripeContext = {
|
|
3432
|
-
type: 'subscription',
|
|
3433
|
-
stripe_subscriptions: '',
|
|
3434
|
-
};
|
|
3245
|
+
stripeContext = { type: 'subscription', stripe_subscriptions: '' };
|
|
3435
3246
|
}
|
|
3436
3247
|
stripeContext.stripe_subscriptions = '';
|
|
3437
3248
|
await pAll(
|
|
@@ -3486,12 +3297,9 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
3486
3297
|
status: stripeSubscription.status,
|
|
3487
3298
|
};
|
|
3488
3299
|
}
|
|
3489
|
-
return {
|
|
3490
|
-
stripeSubscription,
|
|
3491
|
-
subscription: sub,
|
|
3492
|
-
};
|
|
3300
|
+
return { stripeSubscription, subscription: sub };
|
|
3493
3301
|
}),
|
|
3494
|
-
{ concurrency: updateDataConcurrency }
|
|
3302
|
+
{ concurrency: updateDataConcurrency() }
|
|
3495
3303
|
);
|
|
3496
3304
|
if (subscriptions.length > 1) {
|
|
3497
3305
|
stripeContext.has_multiple_subscriptions = true;
|
|
@@ -3514,14 +3322,14 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
3514
3322
|
}
|
|
3515
3323
|
|
|
3516
3324
|
logger.info('Checkout session submitted successfully', {
|
|
3517
|
-
sessionId: req.
|
|
3325
|
+
sessionId: c.req.param('id'),
|
|
3518
3326
|
paymentIntentId: paymentIntent?.id,
|
|
3519
3327
|
setupIntentId: setupIntent?.id,
|
|
3520
3328
|
subscriptionId: subscription?.id,
|
|
3521
3329
|
customerId: customer?.id,
|
|
3522
3330
|
});
|
|
3523
3331
|
|
|
3524
|
-
return
|
|
3332
|
+
return c.json({
|
|
3525
3333
|
paymentIntent,
|
|
3526
3334
|
setupIntent,
|
|
3527
3335
|
stripeContext,
|
|
@@ -3543,50 +3351,44 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
3543
3351
|
quoteService.markAsPaymentFailed(q.id).catch((error) =>
|
|
3544
3352
|
logger.error('Failed to mark quote as payment failed', {
|
|
3545
3353
|
quoteId: q.id,
|
|
3546
|
-
sessionId: req.
|
|
3547
|
-
error: error.message,
|
|
3354
|
+
sessionId: c.req.param('id'),
|
|
3355
|
+
error: (error as any).message,
|
|
3548
3356
|
})
|
|
3549
3357
|
)
|
|
3550
3358
|
)
|
|
3551
3359
|
);
|
|
3552
3360
|
}
|
|
3553
3361
|
if (err instanceof CustomError) {
|
|
3554
|
-
if (err.code === 'RATE_UNAVAILABLE') {
|
|
3555
|
-
return
|
|
3362
|
+
if ((err as any).code === 'RATE_UNAVAILABLE') {
|
|
3363
|
+
return c.json({ code: (err as any).code, error: (err as any).message }, 503);
|
|
3556
3364
|
}
|
|
3557
|
-
if (['QUOTE_LOCK_EXPIRED'].includes(String(err.code))) {
|
|
3558
|
-
return
|
|
3365
|
+
if (['QUOTE_LOCK_EXPIRED'].includes(String((err as any).code))) {
|
|
3366
|
+
return c.json({ code: String((err as any).code), error: (err as any).message }, 409);
|
|
3559
3367
|
}
|
|
3560
3368
|
}
|
|
3561
3369
|
logger.error('Error submitting checkout session', {
|
|
3562
|
-
sessionId: req.
|
|
3370
|
+
sessionId: c.req.param('id'),
|
|
3563
3371
|
error: err,
|
|
3564
|
-
stack: err.stack,
|
|
3372
|
+
stack: (err as any).stack,
|
|
3565
3373
|
});
|
|
3566
|
-
|
|
3374
|
+
return c.json({ code: (err as any).code, error: (err as any).message }, 500);
|
|
3567
3375
|
}
|
|
3568
3376
|
});
|
|
3569
3377
|
|
|
3570
3378
|
// 打赏(不强制登录)
|
|
3571
|
-
|
|
3379
|
+
app.put('/:id/donate-submit', ensureCheckoutSessionOpen, async (c) => {
|
|
3572
3380
|
let consumedQuotes: PriceQuote[] = [];
|
|
3381
|
+
const body = c.get('sanitizedBody') ?? {};
|
|
3573
3382
|
try {
|
|
3574
|
-
const checkoutSession =
|
|
3383
|
+
const checkoutSession = c.get('doc') as CheckoutSession;
|
|
3575
3384
|
if (!isDonationCheckoutSession(checkoutSession)) {
|
|
3576
|
-
return
|
|
3577
|
-
code: 'INVALID_DONATION',
|
|
3578
|
-
error: 'This endpoint is only for donations',
|
|
3579
|
-
});
|
|
3385
|
+
return c.json({ code: 'INVALID_DONATION', error: 'This endpoint is only for donations' }, 400);
|
|
3580
3386
|
}
|
|
3581
3387
|
|
|
3582
3388
|
if (checkoutSession.mode !== 'payment') {
|
|
3583
|
-
return
|
|
3584
|
-
code: 'INVALID_MODE',
|
|
3585
|
-
error: 'This endpoint is only for payment mode donations',
|
|
3586
|
-
});
|
|
3389
|
+
return c.json({ code: 'INVALID_MODE', error: 'This endpoint is only for payment mode donations' }, 400);
|
|
3587
3390
|
}
|
|
3588
3391
|
|
|
3589
|
-
// validate inventory
|
|
3590
3392
|
if (checkoutSession.line_items) {
|
|
3591
3393
|
try {
|
|
3592
3394
|
await validateInventory(checkoutSession.line_items);
|
|
@@ -3596,14 +3398,13 @@ router.put('/:id/donate-submit', ensureCheckoutSessionOpen, async (req, res) =>
|
|
|
3596
3398
|
line_items: checkoutSession.line_items,
|
|
3597
3399
|
checkoutSessionId: checkoutSession.id,
|
|
3598
3400
|
});
|
|
3599
|
-
return
|
|
3401
|
+
return c.json({ error: (err as any).message }, 400);
|
|
3600
3402
|
}
|
|
3601
3403
|
}
|
|
3602
3404
|
|
|
3603
|
-
// validate payment settings
|
|
3604
3405
|
const { paymentMethod, paymentCurrency } = await validatePaymentSettings(
|
|
3605
|
-
|
|
3606
|
-
|
|
3406
|
+
(body as any).payment_method,
|
|
3407
|
+
(body as any).payment_currency
|
|
3607
3408
|
);
|
|
3608
3409
|
await checkoutSession.update({ currency_id: paymentCurrency.id });
|
|
3609
3410
|
|
|
@@ -3614,7 +3415,7 @@ router.put('/:id/donate-submit', ensureCheckoutSessionOpen, async (req, res) =>
|
|
|
3614
3415
|
checkoutSession,
|
|
3615
3416
|
lineItems: expandedLineItems,
|
|
3616
3417
|
paymentCurrencyId: paymentCurrency.id,
|
|
3617
|
-
quotesInput:
|
|
3418
|
+
quotesInput: (body as any).quotes,
|
|
3618
3419
|
strict: true,
|
|
3619
3420
|
});
|
|
3620
3421
|
consumedQuotes = quoteResult.consumedQuotes;
|
|
@@ -3624,7 +3425,7 @@ router.put('/:id/donate-submit', ensureCheckoutSessionOpen, async (req, res) =>
|
|
|
3624
3425
|
}
|
|
3625
3426
|
} catch (err) {
|
|
3626
3427
|
if (err instanceof CustomError) {
|
|
3627
|
-
const errCode = String(getQuoteErrorCode(err) || err.code || 'QUOTE_INVALID');
|
|
3428
|
+
const errCode = String(getQuoteErrorCode(err) || (err as any).code || 'QUOTE_INVALID');
|
|
3628
3429
|
if (
|
|
3629
3430
|
[
|
|
3630
3431
|
'QUOTE_AMOUNT_MISMATCH',
|
|
@@ -3634,14 +3435,13 @@ router.put('/:id/donate-submit', ensureCheckoutSessionOpen, async (req, res) =>
|
|
|
3634
3435
|
'QUOTE_LOCK_EXPIRED',
|
|
3635
3436
|
].includes(errCode)
|
|
3636
3437
|
) {
|
|
3637
|
-
return
|
|
3438
|
+
return c.json({ code: errCode, error: (err as any).message }, 409);
|
|
3638
3439
|
}
|
|
3639
|
-
return
|
|
3440
|
+
return c.json({ code: errCode, error: (err as any).message }, 400);
|
|
3640
3441
|
}
|
|
3641
3442
|
throw err;
|
|
3642
3443
|
}
|
|
3643
3444
|
|
|
3644
|
-
// calculate amount and update checkout session
|
|
3645
3445
|
const { lineItems } = await calculateAndUpdateAmount(checkoutSession, paymentCurrency.id, false, {
|
|
3646
3446
|
lineItemsOverride: lineItemsWithQuotes,
|
|
3647
3447
|
});
|
|
@@ -3661,12 +3461,12 @@ router.put('/:id/donate-submit', ensureCheckoutSessionOpen, async (req, res) =>
|
|
|
3661
3461
|
await quoteService.markQuotesAsPaidByInvoice(paymentIntent.invoice_id);
|
|
3662
3462
|
}
|
|
3663
3463
|
|
|
3664
|
-
return
|
|
3464
|
+
return c.json({
|
|
3665
3465
|
paymentIntent,
|
|
3666
3466
|
checkoutSession,
|
|
3667
3467
|
paymentMethod,
|
|
3668
3468
|
paymentCurrency,
|
|
3669
|
-
formData:
|
|
3469
|
+
formData: body,
|
|
3670
3470
|
});
|
|
3671
3471
|
} catch (err) {
|
|
3672
3472
|
if (consumedQuotes.length) {
|
|
@@ -3676,45 +3476,49 @@ router.put('/:id/donate-submit', ensureCheckoutSessionOpen, async (req, res) =>
|
|
|
3676
3476
|
quoteService.markAsPaymentFailed(q.id).catch((error) =>
|
|
3677
3477
|
logger.error('Failed to mark quote as payment failed', {
|
|
3678
3478
|
quoteId: q.id,
|
|
3679
|
-
sessionId: req.
|
|
3680
|
-
error: error.message,
|
|
3479
|
+
sessionId: c.req.param('id'),
|
|
3480
|
+
error: (error as any).message,
|
|
3681
3481
|
})
|
|
3682
3482
|
)
|
|
3683
3483
|
)
|
|
3684
3484
|
);
|
|
3685
3485
|
}
|
|
3686
3486
|
if (err instanceof CustomError) {
|
|
3687
|
-
if (err.code === 'RATE_UNAVAILABLE') {
|
|
3688
|
-
return
|
|
3487
|
+
if ((err as any).code === 'RATE_UNAVAILABLE') {
|
|
3488
|
+
return c.json({ code: (err as any).code, error: (err as any).message }, 503);
|
|
3689
3489
|
}
|
|
3690
|
-
if (['QUOTE_LOCK_EXPIRED'].includes(String(err.code))) {
|
|
3691
|
-
return
|
|
3490
|
+
if (['QUOTE_LOCK_EXPIRED'].includes(String((err as any).code))) {
|
|
3491
|
+
return c.json({ code: String((err as any).code), error: (err as any).message }, 409);
|
|
3692
3492
|
}
|
|
3693
3493
|
}
|
|
3694
3494
|
logger.error('Error processing donation submission', {
|
|
3695
|
-
sessionId: req.
|
|
3696
|
-
error: err.message,
|
|
3697
|
-
stack: err.stack,
|
|
3495
|
+
sessionId: c.req.param('id'),
|
|
3496
|
+
error: (err as any).message,
|
|
3497
|
+
stack: (err as any).stack,
|
|
3698
3498
|
});
|
|
3699
|
-
|
|
3499
|
+
return c.json({ code: (err as any).code, error: (err as any).message }, 400);
|
|
3700
3500
|
}
|
|
3701
3501
|
});
|
|
3702
3502
|
|
|
3703
|
-
|
|
3503
|
+
app.post('/:id/fast-checkout-confirm', user, ensureCheckoutSessionOpen, async (c) => {
|
|
3704
3504
|
let consumedQuotes: PriceQuote[] = [];
|
|
3505
|
+
const body = c.get('sanitizedBody') ?? {};
|
|
3705
3506
|
try {
|
|
3706
|
-
if (!
|
|
3707
|
-
return
|
|
3507
|
+
if (!c.get('user')) {
|
|
3508
|
+
return c.json({ code: 'REQUIRE_LOGIN', error: 'Please login to continue' }, 403);
|
|
3708
3509
|
}
|
|
3709
3510
|
|
|
3710
|
-
if (stopAcceptingOrders) {
|
|
3711
|
-
return
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3511
|
+
if (stopAcceptingOrders()) {
|
|
3512
|
+
return c.json(
|
|
3513
|
+
{
|
|
3514
|
+
code: 'STOP_ACCEPTING_ORDERS',
|
|
3515
|
+
error: 'We are not accepting new orders at this time. Please try again later.',
|
|
3516
|
+
},
|
|
3517
|
+
422
|
|
3518
|
+
);
|
|
3715
3519
|
}
|
|
3716
3520
|
|
|
3717
|
-
const checkoutSession =
|
|
3521
|
+
const checkoutSession = c.get('doc') as CheckoutSession;
|
|
3718
3522
|
|
|
3719
3523
|
if (checkoutSession.line_items) {
|
|
3720
3524
|
try {
|
|
@@ -3725,56 +3529,57 @@ router.post('/:id/fast-checkout-confirm', user, ensureCheckoutSessionOpen, async
|
|
|
3725
3529
|
line_items: checkoutSession.line_items,
|
|
3726
3530
|
checkoutSessionId: checkoutSession.id,
|
|
3727
3531
|
});
|
|
3728
|
-
return
|
|
3532
|
+
return c.json({ error: (err as any).message }, 400);
|
|
3729
3533
|
}
|
|
3730
3534
|
}
|
|
3731
|
-
// validate cross sell
|
|
3732
3535
|
if (checkoutSession.cross_sell_behavior === 'required') {
|
|
3733
3536
|
if (checkoutSession.line_items.some((x) => x.cross_sell) === false) {
|
|
3734
3537
|
const result = await getCrossSellItem(checkoutSession);
|
|
3735
3538
|
// @ts-ignore
|
|
3736
3539
|
if (result.id) {
|
|
3737
|
-
return
|
|
3738
|
-
.status(400)
|
|
3739
|
-
.json({ code: 'REQUIRE_CROSS_SELL', error: 'Please select cross sell product to continue' });
|
|
3540
|
+
return c.json({ code: 'REQUIRE_CROSS_SELL', error: 'Please select cross sell product to continue' }, 400);
|
|
3740
3541
|
}
|
|
3741
3542
|
}
|
|
3742
3543
|
}
|
|
3743
3544
|
|
|
3744
|
-
// Parallel: load currency + customer (independent lookups)
|
|
3745
3545
|
const [paymentCurrency, customer] = await Promise.all([
|
|
3746
3546
|
PaymentCurrency.findByPk(checkoutSession.currency_id),
|
|
3747
|
-
Customer.findByPkOrDid(
|
|
3547
|
+
Customer.findByPkOrDid(c.get('user').did),
|
|
3748
3548
|
]);
|
|
3749
3549
|
if (!paymentCurrency) {
|
|
3750
|
-
return
|
|
3550
|
+
return c.json({ error: 'Payment currency not found' }, 400);
|
|
3751
3551
|
}
|
|
3752
3552
|
if (!customer) {
|
|
3753
|
-
return
|
|
3553
|
+
return c.json({ error: '' }, 400);
|
|
3754
3554
|
}
|
|
3755
3555
|
|
|
3756
3556
|
const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
|
|
3757
3557
|
if (!paymentMethod) {
|
|
3758
|
-
return
|
|
3558
|
+
return c.json({ error: 'Payment method not found' }, 400);
|
|
3759
3559
|
}
|
|
3760
3560
|
|
|
3761
|
-
// Validate checkout session ownership if it was created for a specific customer
|
|
3762
3561
|
if (checkoutSession.customer_id && checkoutSession.metadata?.createdBy) {
|
|
3763
3562
|
const createdByDid = checkoutSession.metadata.createdBy;
|
|
3764
|
-
if (createdByDid !==
|
|
3765
|
-
return
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3563
|
+
if (createdByDid !== c.get('user').did) {
|
|
3564
|
+
return c.json(
|
|
3565
|
+
{
|
|
3566
|
+
error:
|
|
3567
|
+
"It's not allowed to submit checkout sessions created by other users, please create your own checkout session",
|
|
3568
|
+
},
|
|
3569
|
+
403
|
|
3570
|
+
);
|
|
3769
3571
|
}
|
|
3770
3572
|
}
|
|
3771
3573
|
|
|
3772
3574
|
const canMakeNewPurchase = await customer.canMakeNewPurchase(checkoutSession.invoice_id);
|
|
3773
3575
|
if (!canMakeNewPurchase) {
|
|
3774
|
-
return
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3576
|
+
return c.json(
|
|
3577
|
+
{
|
|
3578
|
+
code: 'CUSTOMER_LIMITED',
|
|
3579
|
+
error: 'Customer can not make new purchase, maybe you have unpaid invoices from previous purchases',
|
|
3580
|
+
},
|
|
3581
|
+
403
|
|
3582
|
+
);
|
|
3778
3583
|
}
|
|
3779
3584
|
|
|
3780
3585
|
const expandedLineItems = await Price.expand(checkoutSession.line_items, { product: true, upsell: true });
|
|
@@ -3784,7 +3589,7 @@ router.post('/:id/fast-checkout-confirm', user, ensureCheckoutSessionOpen, async
|
|
|
3784
3589
|
checkoutSession,
|
|
3785
3590
|
lineItems: expandedLineItems,
|
|
3786
3591
|
paymentCurrencyId: paymentCurrency.id,
|
|
3787
|
-
quotesInput:
|
|
3592
|
+
quotesInput: (body as any).quotes,
|
|
3788
3593
|
strict: true,
|
|
3789
3594
|
});
|
|
3790
3595
|
consumedQuotes = quoteResult.consumedQuotes;
|
|
@@ -3794,7 +3599,7 @@ router.post('/:id/fast-checkout-confirm', user, ensureCheckoutSessionOpen, async
|
|
|
3794
3599
|
}
|
|
3795
3600
|
} catch (err) {
|
|
3796
3601
|
if (err instanceof CustomError) {
|
|
3797
|
-
const errCode = String(getQuoteErrorCode(err) || err.code || 'QUOTE_INVALID');
|
|
3602
|
+
const errCode = String(getQuoteErrorCode(err) || (err as any).code || 'QUOTE_INVALID');
|
|
3798
3603
|
if (
|
|
3799
3604
|
[
|
|
3800
3605
|
'QUOTE_AMOUNT_MISMATCH',
|
|
@@ -3804,9 +3609,9 @@ router.post('/:id/fast-checkout-confirm', user, ensureCheckoutSessionOpen, async
|
|
|
3804
3609
|
'QUOTE_LOCK_EXPIRED',
|
|
3805
3610
|
].includes(errCode)
|
|
3806
3611
|
) {
|
|
3807
|
-
return
|
|
3612
|
+
return c.json({ code: errCode, error: (err as any).message }, 409);
|
|
3808
3613
|
}
|
|
3809
|
-
return
|
|
3614
|
+
return c.json({ code: errCode, error: (err as any).message }, 400);
|
|
3810
3615
|
}
|
|
3811
3616
|
throw err;
|
|
3812
3617
|
}
|
|
@@ -3815,9 +3620,7 @@ router.post('/:id/fast-checkout-confirm', user, ensureCheckoutSessionOpen, async
|
|
|
3815
3620
|
checkoutSession,
|
|
3816
3621
|
paymentCurrency.id,
|
|
3817
3622
|
true,
|
|
3818
|
-
{
|
|
3819
|
-
lineItemsOverride: lineItemsWithQuotes,
|
|
3820
|
-
}
|
|
3623
|
+
{ lineItemsOverride: lineItemsWithQuotes }
|
|
3821
3624
|
);
|
|
3822
3625
|
|
|
3823
3626
|
let paymentIntent: PaymentIntent | null = null;
|
|
@@ -3862,15 +3665,12 @@ router.post('/:id/fast-checkout-confirm', user, ensureCheckoutSessionOpen, async
|
|
|
3862
3665
|
customer,
|
|
3863
3666
|
amount: fastCheckoutAmount,
|
|
3864
3667
|
});
|
|
3865
|
-
// Check if this is a credit payment
|
|
3866
3668
|
const isCredit = paymentCurrency.isCredit();
|
|
3867
|
-
|
|
3868
3669
|
const isPayment = checkoutSession.mode === 'payment';
|
|
3869
3670
|
let fastPaid = false;
|
|
3870
3671
|
let canFastPay = isPayment && (await canPayWithDelegation(paymentIntent?.beneficiaries || [], paymentMethod));
|
|
3871
3672
|
let delegation: SufficientForPaymentResult | null = null;
|
|
3872
3673
|
|
|
3873
|
-
// Handle credit payment directly
|
|
3874
3674
|
if (isCredit) {
|
|
3875
3675
|
const result = await isCreditGrantSufficientForPayment({
|
|
3876
3676
|
paymentMethod,
|
|
@@ -3879,15 +3679,16 @@ router.post('/:id/fast-checkout-confirm', user, ensureCheckoutSessionOpen, async
|
|
|
3879
3679
|
amount: fastCheckoutAmount,
|
|
3880
3680
|
});
|
|
3881
3681
|
if (!result.sufficient) {
|
|
3882
|
-
return
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3682
|
+
return c.json(
|
|
3683
|
+
{
|
|
3684
|
+
code: 'CREDIT_INSUFFICIENT',
|
|
3685
|
+
error: result.reason,
|
|
3686
|
+
sufficient: result.sufficient,
|
|
3687
|
+
},
|
|
3688
|
+
400
|
|
3689
|
+
);
|
|
3887
3690
|
}
|
|
3888
3691
|
fastPaid = true;
|
|
3889
|
-
// For credit payments, we use the existing subscription fast checkout flow
|
|
3890
|
-
// but skip the actual payment processing
|
|
3891
3692
|
if (['setup', 'subscription'].includes(checkoutSession.mode)) {
|
|
3892
3693
|
const subscriptionIds = getCheckoutSessionSubscriptionIds(checkoutSession);
|
|
3893
3694
|
const subscriptions = await Subscription.findAll({ where: { id: subscriptionIds } });
|
|
@@ -3928,11 +3729,9 @@ router.post('/:id/fast-checkout-confirm', user, ensureCheckoutSessionOpen, async
|
|
|
3928
3729
|
});
|
|
3929
3730
|
await addSubscriptionJob(sub, 'cycle', false, sub.trial_end);
|
|
3930
3731
|
}),
|
|
3931
|
-
{ concurrency: updateDataConcurrency }
|
|
3732
|
+
{ concurrency: updateDataConcurrency() }
|
|
3932
3733
|
);
|
|
3933
|
-
delegation = {
|
|
3934
|
-
sufficient: true,
|
|
3935
|
-
};
|
|
3734
|
+
delegation = { sufficient: true };
|
|
3936
3735
|
canFastPay = true;
|
|
3937
3736
|
}
|
|
3938
3737
|
} else if (isPayment && paymentIntent && canFastPay) {
|
|
@@ -3969,7 +3768,6 @@ router.post('/:id/fast-checkout-confirm', user, ensureCheckoutSessionOpen, async
|
|
|
3969
3768
|
) {
|
|
3970
3769
|
const subscriptionIds = getCheckoutSessionSubscriptionIds(checkoutSession);
|
|
3971
3770
|
const subscriptions = await Subscription.findAll({ where: { id: subscriptionIds } });
|
|
3972
|
-
// if we can complete purchase without any wallet interaction
|
|
3973
3771
|
const result = await processSubscriptionFastCheckout({
|
|
3974
3772
|
checkoutSession,
|
|
3975
3773
|
customer,
|
|
@@ -3987,13 +3785,11 @@ router.post('/:id/fast-checkout-confirm', user, ensureCheckoutSessionOpen, async
|
|
|
3987
3785
|
});
|
|
3988
3786
|
} else {
|
|
3989
3787
|
fastPaid = true;
|
|
3990
|
-
delegation = {
|
|
3991
|
-
sufficient: true,
|
|
3992
|
-
};
|
|
3788
|
+
delegation = { sufficient: true };
|
|
3993
3789
|
canFastPay = true;
|
|
3994
3790
|
}
|
|
3995
3791
|
} else if (paymentMethod.type !== 'arcblock') {
|
|
3996
|
-
return
|
|
3792
|
+
return c.json({ error: 'Payment method not supported for fast checkout' }, 400);
|
|
3997
3793
|
}
|
|
3998
3794
|
|
|
3999
3795
|
if (paymentIntent?.status === 'succeeded' && paymentIntent?.invoice_id) {
|
|
@@ -4001,12 +3797,7 @@ router.post('/:id/fast-checkout-confirm', user, ensureCheckoutSessionOpen, async
|
|
|
4001
3797
|
await quoteService.markQuotesAsPaidByInvoice(paymentIntent.invoice_id);
|
|
4002
3798
|
}
|
|
4003
3799
|
|
|
4004
|
-
return
|
|
4005
|
-
paymentIntent,
|
|
4006
|
-
checkoutSession,
|
|
4007
|
-
customer,
|
|
4008
|
-
fastPaid,
|
|
4009
|
-
});
|
|
3800
|
+
return c.json({ paymentIntent, checkoutSession, customer, fastPaid });
|
|
4010
3801
|
} catch (err) {
|
|
4011
3802
|
if (consumedQuotes.length) {
|
|
4012
3803
|
const quoteService = getQuoteService();
|
|
@@ -4015,139 +3806,132 @@ router.post('/:id/fast-checkout-confirm', user, ensureCheckoutSessionOpen, async
|
|
|
4015
3806
|
quoteService.markAsPaymentFailed(q.id).catch((error) =>
|
|
4016
3807
|
logger.error('Failed to mark quote as payment failed', {
|
|
4017
3808
|
quoteId: q.id,
|
|
4018
|
-
sessionId: req.
|
|
4019
|
-
error: error.message,
|
|
3809
|
+
sessionId: c.req.param('id'),
|
|
3810
|
+
error: (error as any).message,
|
|
4020
3811
|
})
|
|
4021
3812
|
)
|
|
4022
3813
|
)
|
|
4023
3814
|
);
|
|
4024
3815
|
}
|
|
4025
3816
|
if (err instanceof CustomError) {
|
|
4026
|
-
if (err.code === 'RATE_UNAVAILABLE') {
|
|
4027
|
-
return
|
|
3817
|
+
if ((err as any).code === 'RATE_UNAVAILABLE') {
|
|
3818
|
+
return c.json({ code: (err as any).code, error: (err as any).message }, 503);
|
|
4028
3819
|
}
|
|
4029
|
-
if (['QUOTE_LOCK_EXPIRED'].includes(String(err.code))) {
|
|
4030
|
-
return
|
|
3820
|
+
if (['QUOTE_LOCK_EXPIRED'].includes(String((err as any).code))) {
|
|
3821
|
+
return c.json({ code: String((err as any).code), error: (err as any).message }, 409);
|
|
4031
3822
|
}
|
|
4032
3823
|
}
|
|
4033
3824
|
logger.error('Error confirming fast checkout', {
|
|
4034
|
-
sessionId: req.
|
|
4035
|
-
error: err.message,
|
|
4036
|
-
stack: err.stack,
|
|
3825
|
+
sessionId: c.req.param('id'),
|
|
3826
|
+
error: (err as any).message,
|
|
3827
|
+
stack: (err as any).stack,
|
|
4037
3828
|
});
|
|
4038
|
-
|
|
3829
|
+
return c.json({ code: (err as any).code, error: (err as any).message }, 500);
|
|
4039
3830
|
}
|
|
4040
3831
|
});
|
|
4041
3832
|
|
|
4042
3833
|
// upsell
|
|
4043
|
-
|
|
3834
|
+
app.put('/:id/upsell', user, ensureCheckoutSessionOpen, async (c) => {
|
|
3835
|
+
const body = c.get('sanitizedBody') ?? {};
|
|
4044
3836
|
try {
|
|
4045
|
-
|
|
4046
|
-
const checkoutSession = req.doc as CheckoutSession;
|
|
3837
|
+
const checkoutSession = c.get('doc') as CheckoutSession;
|
|
4047
3838
|
|
|
4048
3839
|
if (checkoutSession.line_items) {
|
|
4049
|
-
// validate line items
|
|
4050
3840
|
if (checkoutSession.line_items.length > 1) {
|
|
4051
|
-
return
|
|
3841
|
+
return c.json({ error: 'Upsell not supported for checkoutSession with multiple line items' }, 400);
|
|
4052
3842
|
}
|
|
4053
3843
|
|
|
4054
|
-
|
|
4055
|
-
const [from, to] = await Promise.all([Price.findByPk(req.body.from), Price.findByPk(req.body.to)]);
|
|
3844
|
+
const [from, to] = await Promise.all([Price.findByPk((body as any).from), Price.findByPk((body as any).to)]);
|
|
4056
3845
|
if (!from) {
|
|
4057
|
-
return
|
|
3846
|
+
return c.json({ error: 'Upsell from price not found' }, 400);
|
|
4058
3847
|
}
|
|
4059
3848
|
if (!to) {
|
|
4060
|
-
return
|
|
3849
|
+
return c.json({ error: 'Upsell to price not found' }, 400);
|
|
4061
3850
|
}
|
|
4062
3851
|
|
|
4063
3852
|
if (canUpsell(from, to) === false) {
|
|
4064
|
-
return
|
|
3853
|
+
return c.json({ error: `Upsell not possible from ${from} to ${to}` }, 400);
|
|
4065
3854
|
}
|
|
4066
3855
|
|
|
4067
3856
|
const index = checkoutSession.line_items.findIndex((x) => x.price_id === from.id);
|
|
4068
3857
|
if (index === -1) {
|
|
4069
|
-
return
|
|
3858
|
+
return c.json({ error: 'Upsell from not exist in checkoutSession line items' }, 400);
|
|
4070
3859
|
}
|
|
4071
3860
|
|
|
4072
3861
|
const items = cloneDeep(checkoutSession.line_items);
|
|
4073
3862
|
items[index] = merge(items[index], { upsell_price_id: to.id });
|
|
4074
3863
|
await checkoutSession.update({ line_items: items });
|
|
4075
|
-
logger.info('CheckoutSession updated on upsell', { id: req.
|
|
3864
|
+
logger.info('CheckoutSession updated on upsell', { id: c.req.param('id'), from: from.id, to: to.id });
|
|
4076
3865
|
|
|
4077
|
-
// recalculate amount
|
|
4078
3866
|
await checkoutSession.update(await getCheckoutSessionAmounts(checkoutSession));
|
|
4079
3867
|
}
|
|
4080
3868
|
|
|
4081
3869
|
const items = await Price.expand(checkoutSession.line_items, { upsell: true });
|
|
4082
|
-
|
|
3870
|
+
return c.json({ ...checkoutSession.toJSON(), line_items: items });
|
|
4083
3871
|
} catch (err) {
|
|
4084
3872
|
logger.error(err);
|
|
4085
|
-
|
|
3873
|
+
return c.json({ error: (err as any).message }, 500);
|
|
4086
3874
|
}
|
|
4087
3875
|
});
|
|
4088
3876
|
|
|
4089
|
-
|
|
3877
|
+
app.put('/:id/downsell', user, ensureCheckoutSessionOpen, async (c) => {
|
|
3878
|
+
const body = c.get('sanitizedBody') ?? {};
|
|
4090
3879
|
try {
|
|
4091
|
-
|
|
4092
|
-
const checkoutSession = req.doc as CheckoutSession;
|
|
3880
|
+
const checkoutSession = c.get('doc') as CheckoutSession;
|
|
4093
3881
|
|
|
4094
|
-
|
|
4095
|
-
const from = await Price.findByPk(req.body.from);
|
|
3882
|
+
const from = await Price.findByPk((body as any).from);
|
|
4096
3883
|
if (!from) {
|
|
4097
|
-
return
|
|
3884
|
+
return c.json({ error: 'Upsell from price not found' }, 400);
|
|
4098
3885
|
}
|
|
4099
3886
|
|
|
4100
3887
|
if (checkoutSession.line_items) {
|
|
4101
3888
|
const index = checkoutSession.line_items.findIndex((x) => x.upsell_price_id === from.id);
|
|
4102
3889
|
if (index === -1) {
|
|
4103
|
-
return
|
|
3890
|
+
return c.json({ error: 'Upsell not configured for checkout session' }, 400);
|
|
4104
3891
|
}
|
|
4105
3892
|
|
|
4106
3893
|
const items = cloneDeep(checkoutSession.line_items);
|
|
4107
3894
|
items[index] = merge(items[index], { upsell_price_id: '' });
|
|
4108
3895
|
await checkoutSession.update({ line_items: items });
|
|
4109
|
-
logger.info('CheckoutSession updated on downsell', { id: req.
|
|
3896
|
+
logger.info('CheckoutSession updated on downsell', { id: c.req.param('id'), from: from.id });
|
|
4110
3897
|
|
|
4111
|
-
// recalculate amount
|
|
4112
3898
|
await checkoutSession.update(await getCheckoutSessionAmounts(checkoutSession));
|
|
4113
3899
|
}
|
|
4114
3900
|
|
|
4115
3901
|
const items = await Price.expand(checkoutSession.line_items, { upsell: true });
|
|
4116
3902
|
logger.info('Checkout session updated after downsell', {
|
|
4117
|
-
sessionId: req.
|
|
3903
|
+
sessionId: c.req.param('id'),
|
|
4118
3904
|
fromPriceId: from.id,
|
|
4119
3905
|
newAmount: checkoutSession.amount_total,
|
|
4120
3906
|
});
|
|
4121
|
-
|
|
3907
|
+
return c.json({ ...checkoutSession.toJSON(), line_items: items });
|
|
4122
3908
|
} catch (err) {
|
|
4123
3909
|
logger.error('Error processing downsell', {
|
|
4124
|
-
sessionId: req.
|
|
4125
|
-
error: err.message,
|
|
4126
|
-
stack: err.stack,
|
|
3910
|
+
sessionId: c.req.param('id'),
|
|
3911
|
+
error: (err as any).message,
|
|
3912
|
+
stack: (err as any).stack,
|
|
4127
3913
|
});
|
|
4128
|
-
|
|
3914
|
+
return c.json({ error: (err as any).message }, 500);
|
|
4129
3915
|
}
|
|
4130
3916
|
});
|
|
4131
3917
|
|
|
4132
3918
|
// adjust quantity
|
|
4133
|
-
|
|
3919
|
+
app.put('/:id/adjust-quantity', user, ensureCheckoutSessionOpen, async (c) => {
|
|
3920
|
+
const body = c.get('sanitizedBody') ?? {};
|
|
4134
3921
|
try {
|
|
4135
|
-
const checkoutSession =
|
|
4136
|
-
const { itemId, quantity, currency_id: currencyIdInput } =
|
|
3922
|
+
const checkoutSession = c.get('doc') as CheckoutSession;
|
|
3923
|
+
const { itemId, quantity, currency_id: currencyIdInput } = body as any;
|
|
4137
3924
|
if (!checkoutSession.line_items) {
|
|
4138
|
-
return
|
|
3925
|
+
return c.json({ error: 'Line items not found' }, 400);
|
|
4139
3926
|
}
|
|
4140
3927
|
|
|
4141
3928
|
const item = checkoutSession.line_items.find((x) => x.price_id === itemId);
|
|
4142
3929
|
if (!item) {
|
|
4143
|
-
return
|
|
3930
|
+
return c.json({ error: 'Item not found' }, 400);
|
|
4144
3931
|
}
|
|
4145
3932
|
|
|
4146
|
-
// Final Freeze: When adjusting quantity, clear all quote-related fields
|
|
4147
|
-
// Quote will be created only at Submit time
|
|
4148
3933
|
const items = cloneDeep(checkoutSession.line_items).map((lineItem: any) => {
|
|
4149
3934
|
const cleaned = { ...lineItem };
|
|
4150
|
-
// Clear quote-related fields - price will be calculated by frontend using live rate
|
|
4151
3935
|
delete cleaned.quote_id;
|
|
4152
3936
|
delete cleaned.quoted_amount;
|
|
4153
3937
|
delete cleaned.custom_amount;
|
|
@@ -4166,18 +3950,15 @@ router.put('/:id/adjust-quantity', user, ensureCheckoutSessionOpen, async (req,
|
|
|
4166
3950
|
}
|
|
4167
3951
|
await validateInventory(items, true);
|
|
4168
3952
|
|
|
4169
|
-
// Build update payload - clear quote_locked_at since we're in preview mode
|
|
4170
3953
|
const updatePayload: { line_items: any; currency_id?: string; metadata?: any } = { line_items: items };
|
|
4171
3954
|
if (currencyIdInput && currencyIdInput !== checkoutSession.currency_id) {
|
|
4172
3955
|
updatePayload.currency_id = currencyIdInput;
|
|
4173
3956
|
}
|
|
4174
|
-
// Clear quote lock since quantity changed
|
|
4175
3957
|
if (checkoutSession.metadata?.quote_locked_at) {
|
|
4176
3958
|
updatePayload.metadata = { ...checkoutSession.metadata, quote_locked_at: undefined };
|
|
4177
3959
|
}
|
|
4178
3960
|
await checkoutSession.update(updatePayload);
|
|
4179
3961
|
|
|
4180
|
-
// Also clear quote_locked_at on payment intent if exists
|
|
4181
3962
|
if (checkoutSession.payment_intent_id) {
|
|
4182
3963
|
const paymentIntent = await PaymentIntent.findByPk(checkoutSession.payment_intent_id);
|
|
4183
3964
|
if (paymentIntent?.quote_locked_at) {
|
|
@@ -4185,7 +3966,6 @@ router.put('/:id/adjust-quantity', user, ensureCheckoutSessionOpen, async (req,
|
|
|
4185
3966
|
}
|
|
4186
3967
|
}
|
|
4187
3968
|
|
|
4188
|
-
// Expand line items for response (no quote enrichment needed - frontend uses live rate)
|
|
4189
3969
|
const lineItems = await Price.expand(checkoutSession.line_items, { upsell: true });
|
|
4190
3970
|
|
|
4191
3971
|
logger.info('Adjusted quantity and cleared quote data', {
|
|
@@ -4194,76 +3974,77 @@ router.put('/:id/adjust-quantity', user, ensureCheckoutSessionOpen, async (req,
|
|
|
4194
3974
|
quantity,
|
|
4195
3975
|
});
|
|
4196
3976
|
|
|
4197
|
-
|
|
3977
|
+
return c.json({ ...checkoutSession.toJSON(), line_items: lineItems, quotes: {}, rateUnavailable: false });
|
|
4198
3978
|
} catch (err) {
|
|
4199
3979
|
logger.error(err);
|
|
4200
|
-
|
|
3980
|
+
return c.json({ error: (err as any).message }, 400);
|
|
4201
3981
|
}
|
|
4202
3982
|
});
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
const doc =
|
|
3983
|
+
|
|
3984
|
+
app.put('/:id/expire', auth, ensureCheckoutSessionOpen, async (c) => {
|
|
3985
|
+
const doc = c.get('doc') as CheckoutSession;
|
|
4206
3986
|
|
|
4207
3987
|
if (doc.status === 'complete') {
|
|
4208
|
-
return
|
|
3988
|
+
return c.json({ error: 'Cannot expire checkout session that is already completed' }, 400);
|
|
4209
3989
|
}
|
|
4210
3990
|
if (doc.status === 'expired') {
|
|
4211
|
-
return
|
|
3991
|
+
return c.json({ error: 'Cannot expire checkout session that is already expired' }, 400);
|
|
4212
3992
|
}
|
|
4213
3993
|
if (doc.payment_status === 'paid') {
|
|
4214
|
-
return
|
|
3994
|
+
return c.json({ error: 'Cannot expire checkout session that is already paid' }, 400);
|
|
4215
3995
|
}
|
|
4216
3996
|
|
|
4217
3997
|
await doc.update({ status: 'expired', expires_at: dayjs().unix() });
|
|
4218
3998
|
logger.info('Checkout session expired', {
|
|
4219
|
-
sessionId: req.
|
|
4220
|
-
userId:
|
|
3999
|
+
sessionId: c.req.param('id'),
|
|
4000
|
+
userId: c.get('user')?.did,
|
|
4221
4001
|
expiresAt: doc.expires_at,
|
|
4222
4002
|
});
|
|
4223
4003
|
|
|
4224
|
-
|
|
4004
|
+
return c.json(doc);
|
|
4225
4005
|
});
|
|
4226
4006
|
|
|
4227
4007
|
// Return the expanded price to cross-sell-to for the checkout session
|
|
4228
|
-
|
|
4008
|
+
app.get('/:id/cross-sell', user, ensureCheckoutSessionOpen, async (c) => {
|
|
4229
4009
|
try {
|
|
4230
|
-
const checkoutSession =
|
|
4231
|
-
const skipError = req.query
|
|
4010
|
+
const checkoutSession = c.get('doc') as CheckoutSession;
|
|
4011
|
+
const skipError = c.req.query('skipError') === 'true';
|
|
4232
4012
|
const result = await getCrossSellItem(checkoutSession);
|
|
4233
4013
|
|
|
4234
|
-
if (skipError && result.error) {
|
|
4235
|
-
return
|
|
4014
|
+
if (skipError && (result as any).error) {
|
|
4015
|
+
return c.json(result, 200);
|
|
4236
4016
|
}
|
|
4237
|
-
return
|
|
4017
|
+
return c.json(result, (result as any).error ? 400 : 200);
|
|
4238
4018
|
} catch (err) {
|
|
4239
4019
|
logger.error(err);
|
|
4240
|
-
|
|
4020
|
+
return c.json({ error: (err as any).message }, 500);
|
|
4241
4021
|
}
|
|
4242
4022
|
});
|
|
4243
4023
|
|
|
4244
|
-
|
|
4024
|
+
app.put('/:id/cross-sell', user, ensureCheckoutSessionOpen, async (c) => {
|
|
4025
|
+
const body = c.get('sanitizedBody') ?? {};
|
|
4245
4026
|
try {
|
|
4246
|
-
const checkoutSession =
|
|
4247
|
-
if (!
|
|
4248
|
-
return
|
|
4027
|
+
const checkoutSession = c.get('doc') as CheckoutSession;
|
|
4028
|
+
if (!(body as any).to) {
|
|
4029
|
+
return c.json({ error: 'Cross sell item is required' }, 400);
|
|
4249
4030
|
}
|
|
4250
4031
|
|
|
4251
4032
|
const result = await getCrossSellItem(checkoutSession);
|
|
4252
4033
|
// @ts-ignore
|
|
4253
4034
|
if (result.error) {
|
|
4254
|
-
return
|
|
4035
|
+
return c.json(result, 400);
|
|
4255
4036
|
}
|
|
4256
4037
|
|
|
4257
4038
|
// @ts-ignore
|
|
4258
4039
|
const to = result as TPriceExpanded;
|
|
4259
|
-
if (to.id !==
|
|
4260
|
-
return
|
|
4040
|
+
if (to.id !== (body as any).to) {
|
|
4041
|
+
return c.json({ error: 'Cross sell item does not match' }, 400);
|
|
4261
4042
|
}
|
|
4262
4043
|
|
|
4263
4044
|
if (checkoutSession.line_items) {
|
|
4264
4045
|
const index = checkoutSession.line_items.findIndex((x) => x.upsell_price_id === to.id);
|
|
4265
4046
|
if (index > -1) {
|
|
4266
|
-
return
|
|
4047
|
+
return c.json({ error: 'Cross sell item already exist' }, 400);
|
|
4267
4048
|
}
|
|
4268
4049
|
|
|
4269
4050
|
const items = cloneDeep(checkoutSession.line_items);
|
|
@@ -4273,74 +4054,70 @@ router.put('/:id/cross-sell', user, ensureCheckoutSessionOpen, async (req, res)
|
|
|
4273
4054
|
cross_sell: true,
|
|
4274
4055
|
});
|
|
4275
4056
|
await checkoutSession.update({ line_items: items });
|
|
4276
|
-
logger.info('CheckoutSession updated on add cross-sell', { id: req.
|
|
4057
|
+
logger.info('CheckoutSession updated on add cross-sell', { id: c.req.param('id'), crossSell: to.id });
|
|
4277
4058
|
|
|
4278
|
-
// recalculate amount
|
|
4279
4059
|
await checkoutSession.update(await getCheckoutSessionAmounts(checkoutSession));
|
|
4280
4060
|
}
|
|
4281
4061
|
|
|
4282
4062
|
const items = await Price.expand(checkoutSession.line_items, { upsell: true });
|
|
4283
|
-
|
|
4063
|
+
return c.json({ ...checkoutSession.toJSON(), line_items: items });
|
|
4284
4064
|
} catch (err) {
|
|
4285
4065
|
logger.error(err);
|
|
4286
|
-
|
|
4066
|
+
return c.json({ error: (err as any).message }, 500);
|
|
4287
4067
|
}
|
|
4288
4068
|
});
|
|
4289
4069
|
|
|
4290
|
-
|
|
4070
|
+
app.delete('/:id/cross-sell', user, ensureCheckoutSessionOpen, async (c) => {
|
|
4291
4071
|
try {
|
|
4292
|
-
const checkoutSession =
|
|
4072
|
+
const checkoutSession = c.get('doc') as CheckoutSession;
|
|
4293
4073
|
if (checkoutSession.line_items) {
|
|
4294
4074
|
const index = checkoutSession.line_items.findIndex((x) => x.cross_sell);
|
|
4295
4075
|
if (index === -1) {
|
|
4296
|
-
return
|
|
4076
|
+
return c.json({ error: 'Cross sell item not exist' }, 400);
|
|
4297
4077
|
}
|
|
4298
4078
|
|
|
4299
4079
|
const items = cloneDeep(checkoutSession.line_items);
|
|
4300
4080
|
items.splice(index, 1);
|
|
4301
4081
|
await checkoutSession.update({ line_items: items });
|
|
4302
|
-
logger.info('CheckoutSession updated on remove cross-sell', { id: req.
|
|
4082
|
+
logger.info('CheckoutSession updated on remove cross-sell', { id: c.req.param('id') });
|
|
4303
4083
|
|
|
4304
|
-
// recalculate amount
|
|
4305
4084
|
await checkoutSession.update(await getCheckoutSessionAmounts(checkoutSession));
|
|
4306
4085
|
}
|
|
4307
4086
|
|
|
4308
4087
|
const items = await Price.expand(checkoutSession.line_items, { upsell: true });
|
|
4309
|
-
|
|
4088
|
+
return c.json({ ...checkoutSession.toJSON(), line_items: items });
|
|
4310
4089
|
} catch (err) {
|
|
4311
4090
|
logger.error(err);
|
|
4312
|
-
|
|
4091
|
+
return c.json({ error: (err as any).message }, 500);
|
|
4313
4092
|
}
|
|
4314
4093
|
});
|
|
4315
4094
|
|
|
4316
4095
|
// Apply promotion code discount (preview only - doesn't create actual discount record)
|
|
4317
|
-
|
|
4096
|
+
app.post('/:id/apply-promotion', user, ensureCheckoutSessionOpen, async (c) => {
|
|
4097
|
+
const body = c.get('sanitizedBody') ?? {};
|
|
4318
4098
|
try {
|
|
4319
|
-
const checkoutSession =
|
|
4320
|
-
const { promotion_code: promotionCode, currency_id: currencyId } =
|
|
4099
|
+
const checkoutSession = c.get('doc') as CheckoutSession;
|
|
4100
|
+
const { promotion_code: promotionCode, currency_id: currencyId } = body as any;
|
|
4321
4101
|
|
|
4322
4102
|
if (!promotionCode) {
|
|
4323
|
-
return
|
|
4103
|
+
return c.json({ error: 'Promotion code is required' }, 400);
|
|
4324
4104
|
}
|
|
4325
4105
|
|
|
4326
|
-
if (!
|
|
4327
|
-
return
|
|
4106
|
+
if (!c.get('user')) {
|
|
4107
|
+
return c.json({ error: 'Authentication required' }, 403);
|
|
4328
4108
|
}
|
|
4329
4109
|
|
|
4330
|
-
|
|
4331
|
-
const
|
|
4332
|
-
// Use customer.id if exists, otherwise fall back to DID for promo validation
|
|
4333
|
-
const customerId = customer?.id || req.user.did;
|
|
4110
|
+
const customer = await Customer.findOne({ where: { did: c.get('user').did } });
|
|
4111
|
+
const customerId = customer?.id || c.get('user').did;
|
|
4334
4112
|
|
|
4335
|
-
// Get currency
|
|
4336
4113
|
const curCurrencyId = currencyId || checkoutSession.currency_id;
|
|
4337
4114
|
if (!curCurrencyId) {
|
|
4338
|
-
return
|
|
4115
|
+
return c.json({ error: 'Currency not found in checkout session' }, 400);
|
|
4339
4116
|
}
|
|
4340
4117
|
|
|
4341
4118
|
const currency = await PaymentCurrency.findByPk(curCurrencyId);
|
|
4342
4119
|
if (!currency) {
|
|
4343
|
-
return
|
|
4120
|
+
return c.json({ error: 'Currency not found' }, 400);
|
|
4344
4121
|
}
|
|
4345
4122
|
|
|
4346
4123
|
const foundPromotionCode = (await PromotionCode.findOne({
|
|
@@ -4355,57 +4132,46 @@ router.post('/:id/apply-promotion', user, ensureCheckoutSessionOpen, async (req,
|
|
|
4355
4132
|
})) as PromotionCode & { coupon: Coupon };
|
|
4356
4133
|
|
|
4357
4134
|
if (!foundPromotionCode || !foundPromotionCode.active) {
|
|
4358
|
-
return
|
|
4135
|
+
return c.json({ error: 'Promotion code not found or inactive' }, 400);
|
|
4359
4136
|
}
|
|
4360
4137
|
|
|
4361
|
-
// Validate subscription grouping + repeating coupon combination
|
|
4362
4138
|
if (checkoutSession.enable_subscription_grouping) {
|
|
4363
4139
|
if (foundPromotionCode?.coupon?.duration === 'repeating') {
|
|
4364
|
-
return
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4140
|
+
return c.json(
|
|
4141
|
+
{
|
|
4142
|
+
error:
|
|
4143
|
+
'Repeating coupons cannot be used with subscription grouping. This limitation ensures proper discount management across multiple subscriptions in a single purchase.',
|
|
4144
|
+
},
|
|
4145
|
+
400
|
|
4146
|
+
);
|
|
4368
4147
|
}
|
|
4369
4148
|
}
|
|
4370
4149
|
|
|
4371
4150
|
const checkoutItems = checkoutSession.line_items || [];
|
|
4372
|
-
// Get line items with quote data for accurate discount calculation
|
|
4373
4151
|
const expandedItems = await Price.expand(checkoutSession.line_items || [], { product: true, upsell: true });
|
|
4374
4152
|
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
checkoutSession,
|
|
4379
|
-
expandedItems,
|
|
4380
|
-
curCurrencyId,
|
|
4381
|
-
{ skipGeneration: true } // Don't generate new quotes, just use existing ones
|
|
4382
|
-
);
|
|
4153
|
+
const quoteResult = await enrichCheckoutSessionWithQuotes(checkoutSession, expandedItems, curCurrencyId, {
|
|
4154
|
+
skipGeneration: true,
|
|
4155
|
+
});
|
|
4383
4156
|
const itemsWithQuotes = quoteResult.lineItems;
|
|
4384
4157
|
|
|
4385
|
-
// Calculate trial status for discount application (same logic as in calculateAndUpdateAmount)
|
|
4386
4158
|
const now = dayjs().unix();
|
|
4387
4159
|
const trialSetup = getSubscriptionTrialSetup(checkoutSession.subscription_data as any, currency.id);
|
|
4388
4160
|
const isTrialing = trialSetup.trialInDays > 0 || trialSetup.trialEnd > now;
|
|
4389
4161
|
|
|
4390
|
-
// Apply discount using our new function
|
|
4391
4162
|
const discountResult = await applyDiscountsToLineItems({
|
|
4392
4163
|
lineItems: itemsWithQuotes,
|
|
4393
4164
|
promotionCodeId: foundPromotionCode.id,
|
|
4394
4165
|
couponId: foundPromotionCode.coupon_id,
|
|
4395
4166
|
customerId,
|
|
4396
4167
|
currency,
|
|
4397
|
-
billingContext: {
|
|
4398
|
-
trialing: isTrialing,
|
|
4399
|
-
},
|
|
4168
|
+
billingContext: { trialing: isTrialing },
|
|
4400
4169
|
});
|
|
4401
4170
|
|
|
4402
4171
|
if (!discountResult.discountSummary.appliedCoupon) {
|
|
4403
|
-
return
|
|
4404
|
-
.status(400)
|
|
4405
|
-
.json({ error: discountResult.notValidReason || 'Promotion code cannot be applied to this order' });
|
|
4172
|
+
return c.json({ error: discountResult.notValidReason || 'Promotion code cannot be applied to this order' }, 400);
|
|
4406
4173
|
}
|
|
4407
4174
|
|
|
4408
|
-
// Create discount configuration for checkout session
|
|
4409
4175
|
const coupon = await Coupon.findByPk(foundPromotionCode.coupon_id);
|
|
4410
4176
|
const discountConfig = {
|
|
4411
4177
|
promotion_code: foundPromotionCode.id,
|
|
@@ -4415,7 +4181,6 @@ router.post('/:id/apply-promotion', user, ensureCheckoutSessionOpen, async (req,
|
|
|
4415
4181
|
verification_data: { code: promotionCode },
|
|
4416
4182
|
};
|
|
4417
4183
|
|
|
4418
|
-
// Update checkout session with discount preview (without detailed coupon info in line_items)
|
|
4419
4184
|
await checkoutSession.update({
|
|
4420
4185
|
discounts: [discountConfig],
|
|
4421
4186
|
currency_id: curCurrencyId,
|
|
@@ -4427,8 +4192,8 @@ router.post('/:id/apply-promotion', user, ensureCheckoutSessionOpen, async (req,
|
|
|
4427
4192
|
discount_amounts: item.discount_amounts,
|
|
4428
4193
|
};
|
|
4429
4194
|
}),
|
|
4430
|
-
amount_subtotal: checkoutSession.amount_total,
|
|
4431
|
-
amount_total: discountResult.discountSummary.finalTotal,
|
|
4195
|
+
amount_subtotal: checkoutSession.amount_total,
|
|
4196
|
+
amount_total: discountResult.discountSummary.finalTotal,
|
|
4432
4197
|
total_details: {
|
|
4433
4198
|
...checkoutSession.total_details,
|
|
4434
4199
|
amount_discount: discountResult.discountSummary.totalDiscountAmount,
|
|
@@ -4439,7 +4204,6 @@ router.post('/:id/apply-promotion', user, ensureCheckoutSessionOpen, async (req,
|
|
|
4439
4204
|
},
|
|
4440
4205
|
});
|
|
4441
4206
|
|
|
4442
|
-
// Create enhanced line items with complete coupon information for response
|
|
4443
4207
|
const enhancedLineItemsWithCoupon = await expandLineItemsWithCouponInfo(
|
|
4444
4208
|
discountResult.enhancedLineItems,
|
|
4445
4209
|
[discountConfig],
|
|
@@ -4454,10 +4218,9 @@ router.post('/:id/apply-promotion', user, ensureCheckoutSessionOpen, async (req,
|
|
|
4454
4218
|
finalAmount: discountResult.discountSummary.finalTotal,
|
|
4455
4219
|
});
|
|
4456
4220
|
|
|
4457
|
-
// Expand discounts with complete details for response
|
|
4458
4221
|
const enhancedDiscounts = await expandDiscountsWithDetails([discountConfig]);
|
|
4459
4222
|
|
|
4460
|
-
|
|
4223
|
+
return c.json({
|
|
4461
4224
|
...checkoutSession.toJSON(),
|
|
4462
4225
|
line_items: enhancedLineItemsWithCoupon,
|
|
4463
4226
|
discounts: enhancedDiscounts,
|
|
@@ -4465,36 +4228,35 @@ router.post('/:id/apply-promotion', user, ensureCheckoutSessionOpen, async (req,
|
|
|
4465
4228
|
});
|
|
4466
4229
|
} catch (err) {
|
|
4467
4230
|
logger.error('Error applying promotion code', {
|
|
4468
|
-
sessionId: req.
|
|
4469
|
-
error: err.message,
|
|
4470
|
-
stack: err.stack,
|
|
4231
|
+
sessionId: c.req.param('id'),
|
|
4232
|
+
error: (err as any).message,
|
|
4233
|
+
stack: (err as any).stack,
|
|
4471
4234
|
});
|
|
4472
|
-
|
|
4235
|
+
return c.json({ error: (err as any).message }, 400);
|
|
4473
4236
|
}
|
|
4474
4237
|
});
|
|
4475
4238
|
|
|
4476
4239
|
// Recalculate promotion code discount when currency changes
|
|
4477
|
-
|
|
4240
|
+
app.post('/:id/recalculate-promotion', user, ensureCheckoutSessionOpen, async (c) => {
|
|
4241
|
+
const body = c.get('sanitizedBody') ?? {};
|
|
4478
4242
|
try {
|
|
4479
|
-
const checkoutSession =
|
|
4480
|
-
const { currency_id: newCurrencyId } =
|
|
4243
|
+
const checkoutSession = c.get('doc') as CheckoutSession;
|
|
4244
|
+
const { currency_id: newCurrencyId } = body as any;
|
|
4481
4245
|
|
|
4482
4246
|
if (!newCurrencyId) {
|
|
4483
|
-
return
|
|
4247
|
+
return c.json({ error: 'Currency ID is required' }, 400);
|
|
4484
4248
|
}
|
|
4485
4249
|
|
|
4486
|
-
if (!
|
|
4487
|
-
return
|
|
4250
|
+
if (!c.get('user')) {
|
|
4251
|
+
return c.json({ error: 'Authentication required' }, 403);
|
|
4488
4252
|
}
|
|
4489
4253
|
|
|
4490
|
-
// Try recovering discounts from confirmed records before deciding there is nothing to recalculate.
|
|
4491
4254
|
if (!checkoutSession.discounts?.length) {
|
|
4492
4255
|
await recoverDiscountConfigFromRecords(checkoutSession);
|
|
4493
4256
|
}
|
|
4494
4257
|
|
|
4495
|
-
// Check if there are existing discounts to recalculate
|
|
4496
4258
|
if (!checkoutSession.discounts?.length) {
|
|
4497
|
-
return
|
|
4259
|
+
return c.json({
|
|
4498
4260
|
...checkoutSession.toJSON(),
|
|
4499
4261
|
discounts: [],
|
|
4500
4262
|
discount_applied: false,
|
|
@@ -4502,49 +4264,42 @@ router.post('/:id/recalculate-promotion', user, ensureCheckoutSessionOpen, async
|
|
|
4502
4264
|
});
|
|
4503
4265
|
}
|
|
4504
4266
|
|
|
4505
|
-
// Get new currency
|
|
4506
4267
|
const currency = await PaymentCurrency.findByPk(newCurrencyId);
|
|
4507
4268
|
if (!currency) {
|
|
4508
|
-
return
|
|
4269
|
+
return c.json({ error: 'Currency not found' }, 400);
|
|
4509
4270
|
}
|
|
4510
4271
|
|
|
4511
4272
|
const checkoutItems = checkoutSession.line_items || [];
|
|
4512
|
-
// Get line items - no need for quote data since frontend calculates discount amounts
|
|
4513
4273
|
const expandedItems = await Price.expand(checkoutSession.line_items || [], { product: true, upsell: true });
|
|
4514
4274
|
const supportedCurrencyIds = getSupportedPaymentCurrencies(expandedItems as any[]);
|
|
4515
4275
|
if (!supportedCurrencyIds.includes(newCurrencyId)) {
|
|
4516
|
-
return
|
|
4276
|
+
return c.json({ error: 'Currency not supported for this checkout session' }, 400);
|
|
4517
4277
|
}
|
|
4518
4278
|
|
|
4519
|
-
// Get the first discount (assuming only one promotion code at a time)
|
|
4520
4279
|
const existingDiscount = checkoutSession.discounts[0];
|
|
4521
4280
|
if (!existingDiscount) {
|
|
4522
|
-
return
|
|
4281
|
+
return c.json({ error: 'No discount found' }, 400);
|
|
4523
4282
|
}
|
|
4524
4283
|
|
|
4525
4284
|
const promotionCodeId = existingDiscount.promotion_code;
|
|
4526
4285
|
const couponId = existingDiscount.coupon;
|
|
4527
4286
|
|
|
4528
4287
|
if (!promotionCodeId || !couponId) {
|
|
4529
|
-
return
|
|
4288
|
+
return c.json({ error: 'Invalid discount configuration' }, 400);
|
|
4530
4289
|
}
|
|
4531
4290
|
|
|
4532
|
-
// Validate promotion code and coupon still exist and are active
|
|
4533
4291
|
const [foundPromotionCode, coupon] = await Promise.all([
|
|
4534
4292
|
PromotionCode.findByPk(promotionCodeId),
|
|
4535
4293
|
Coupon.findByPk(couponId),
|
|
4536
4294
|
]);
|
|
4537
4295
|
|
|
4538
4296
|
if (!foundPromotionCode) {
|
|
4539
|
-
return
|
|
4297
|
+
return c.json({ error: 'Promotion code not found' }, 400);
|
|
4540
4298
|
}
|
|
4541
4299
|
if (!coupon) {
|
|
4542
|
-
return
|
|
4300
|
+
return c.json({ error: 'Coupon not found' }, 400);
|
|
4543
4301
|
}
|
|
4544
4302
|
|
|
4545
|
-
// Re-submit scenario: discount-status queue may have deactivated the promo/coupon
|
|
4546
|
-
// because this session's own usage pushed it past max_redemptions.
|
|
4547
|
-
// Exclude current session's confirmed discount records before checking active/valid.
|
|
4548
4303
|
const currentSessionUsage = await Discount.count({
|
|
4549
4304
|
where: {
|
|
4550
4305
|
checkout_session_id: checkoutSession.id,
|
|
@@ -4554,25 +4309,21 @@ router.post('/:id/recalculate-promotion', user, ensureCheckoutSessionOpen, async
|
|
|
4554
4309
|
});
|
|
4555
4310
|
|
|
4556
4311
|
if (!foundPromotionCode.active && currentSessionUsage === 0) {
|
|
4557
|
-
return
|
|
4312
|
+
return c.json({ error: 'Promotion code no longer active' }, 400);
|
|
4558
4313
|
}
|
|
4559
4314
|
|
|
4560
4315
|
if (!coupon.valid && currentSessionUsage === 0) {
|
|
4561
|
-
return
|
|
4316
|
+
return c.json({ error: 'Coupon no longer valid' }, 400);
|
|
4562
4317
|
}
|
|
4563
4318
|
|
|
4564
|
-
// Check if coupon can be applied with the new currency (for fixed amount coupons)
|
|
4565
4319
|
const canApplyWithCurrency =
|
|
4566
|
-
coupon.percent_off > 0 ||
|
|
4567
|
-
coupon.currency_id === currency.id ||
|
|
4568
|
-
coupon.currency_options?.[currency.id]?.amount_off;
|
|
4320
|
+
coupon.percent_off > 0 ||
|
|
4321
|
+
coupon.currency_id === currency.id ||
|
|
4322
|
+
coupon.currency_options?.[currency.id]?.amount_off;
|
|
4569
4323
|
|
|
4570
4324
|
if (!canApplyWithCurrency) {
|
|
4571
|
-
// Rollback previously confirmed discount usage/records for this open checkout session.
|
|
4572
|
-
// This resets coupon/promotion counters when discount becomes inapplicable.
|
|
4573
4325
|
await rollbackDiscountUsageForCheckoutSession(checkoutSession.id);
|
|
4574
4326
|
|
|
4575
|
-
// Remove discount if it can't be applied with new currency
|
|
4576
4327
|
await checkoutSession.update({
|
|
4577
4328
|
discounts: [],
|
|
4578
4329
|
line_items: expandedItems.map((item) => {
|
|
@@ -4596,7 +4347,7 @@ router.post('/:id/recalculate-promotion', user, ensureCheckoutSessionOpen, async
|
|
|
4596
4347
|
promotionCode: foundPromotionCode.code,
|
|
4597
4348
|
});
|
|
4598
4349
|
|
|
4599
|
-
return
|
|
4350
|
+
return c.json({
|
|
4600
4351
|
...checkoutSession.toJSON(),
|
|
4601
4352
|
line_items: expandedItems,
|
|
4602
4353
|
discounts: [],
|
|
@@ -4605,7 +4356,6 @@ router.post('/:id/recalculate-promotion', user, ensureCheckoutSessionOpen, async
|
|
|
4605
4356
|
});
|
|
4606
4357
|
}
|
|
4607
4358
|
|
|
4608
|
-
// Mark which items are discountable based on coupon product restrictions
|
|
4609
4359
|
const enhancedLineItems = expandedItems.map((item) => {
|
|
4610
4360
|
const isDiscountable =
|
|
4611
4361
|
!coupon.applies_to?.products?.length ||
|
|
@@ -4615,20 +4365,17 @@ router.post('/:id/recalculate-promotion', user, ensureCheckoutSessionOpen, async
|
|
|
4615
4365
|
return {
|
|
4616
4366
|
...cur,
|
|
4617
4367
|
discountable: isDiscountable,
|
|
4618
|
-
discount_amounts: [],
|
|
4368
|
+
discount_amounts: [],
|
|
4619
4369
|
};
|
|
4620
4370
|
});
|
|
4621
4371
|
|
|
4622
|
-
// Create updated discount configuration - NO discount_amount, frontend calculates it
|
|
4623
4372
|
const discountConfig = {
|
|
4624
4373
|
promotion_code: promotionCodeId,
|
|
4625
4374
|
coupon: couponId,
|
|
4626
|
-
// discount_amount removed - frontend calculates based on live exchange rate
|
|
4627
4375
|
verification_method: existingDiscount.verification_method,
|
|
4628
4376
|
verification_data: existingDiscount.verification_data,
|
|
4629
4377
|
};
|
|
4630
4378
|
|
|
4631
|
-
// Update checkout session with discountable flags only
|
|
4632
4379
|
await checkoutSession.update({
|
|
4633
4380
|
discounts: [discountConfig],
|
|
4634
4381
|
line_items: enhancedLineItems,
|
|
@@ -4642,13 +4389,10 @@ router.post('/:id/recalculate-promotion', user, ensureCheckoutSessionOpen, async
|
|
|
4642
4389
|
promotionCode: foundPromotionCode.code,
|
|
4643
4390
|
});
|
|
4644
4391
|
|
|
4645
|
-
// Expand discounts with complete details for response (coupon_details for frontend calculation)
|
|
4646
4392
|
const enhancedDiscounts = await expandDiscountsWithDetails([discountConfig]);
|
|
4647
|
-
|
|
4648
|
-
// Expand line items for response
|
|
4649
4393
|
const responseItems = await Price.expand(enhancedLineItems, { product: true, upsell: true });
|
|
4650
4394
|
|
|
4651
|
-
|
|
4395
|
+
return c.json({
|
|
4652
4396
|
...checkoutSession.toJSON(),
|
|
4653
4397
|
line_items: responseItems,
|
|
4654
4398
|
discounts: enhancedDiscounts,
|
|
@@ -4657,50 +4401,43 @@ router.post('/:id/recalculate-promotion', user, ensureCheckoutSessionOpen, async
|
|
|
4657
4401
|
});
|
|
4658
4402
|
} catch (err) {
|
|
4659
4403
|
logger.error('Error recalculating promotion code', {
|
|
4660
|
-
sessionId: req.
|
|
4661
|
-
error: err.message,
|
|
4662
|
-
stack: err.stack,
|
|
4404
|
+
sessionId: c.req.param('id'),
|
|
4405
|
+
error: (err as any).message,
|
|
4406
|
+
stack: (err as any).stack,
|
|
4663
4407
|
});
|
|
4664
|
-
|
|
4408
|
+
return c.json({ error: (err as any).message }, 400);
|
|
4665
4409
|
}
|
|
4666
4410
|
});
|
|
4667
4411
|
|
|
4668
4412
|
// Remove promotion code discount (remove preview)
|
|
4669
|
-
|
|
4413
|
+
app.delete('/:id/remove-promotion', user, ensureCheckoutSessionOpen, async (c) => {
|
|
4670
4414
|
try {
|
|
4671
|
-
const checkoutSession =
|
|
4415
|
+
const checkoutSession = c.get('doc') as CheckoutSession;
|
|
4672
4416
|
|
|
4673
4417
|
if (checkoutSession.status !== 'open') {
|
|
4674
|
-
return
|
|
4418
|
+
return c.json({ error: 'Checkout session is not open' }, 400);
|
|
4675
4419
|
}
|
|
4676
4420
|
|
|
4677
|
-
// Get currency
|
|
4678
4421
|
const currency = await PaymentCurrency.findByPk(checkoutSession.currency_id);
|
|
4679
4422
|
if (!currency) {
|
|
4680
|
-
return
|
|
4423
|
+
return c.json({ error: 'Currency not found' }, 400);
|
|
4681
4424
|
}
|
|
4682
4425
|
|
|
4683
|
-
// Get original line items without discount information
|
|
4684
4426
|
const originalItems = await Price.expand(checkoutSession.line_items || [], { product: true, upsell: true });
|
|
4685
4427
|
|
|
4686
|
-
// Calculate trial status for accurate original amount calculation
|
|
4687
4428
|
const now = dayjs().unix();
|
|
4688
4429
|
const trialSetup = getSubscriptionTrialSetup(checkoutSession.subscription_data as any, currency.id);
|
|
4689
4430
|
const isTrialing = trialSetup.trialInDays > 0 || trialSetup.trialEnd > now;
|
|
4690
4431
|
|
|
4691
4432
|
await rollbackDiscountUsageForCheckoutSession(checkoutSession.id);
|
|
4692
|
-
// Calculate original amounts without any discounts
|
|
4693
4433
|
const originalResult = await applyDiscountsToLineItems({
|
|
4694
4434
|
lineItems: originalItems,
|
|
4695
|
-
couponId: 'dummy',
|
|
4435
|
+
couponId: 'dummy',
|
|
4696
4436
|
customerId: 'dummy',
|
|
4697
4437
|
currency,
|
|
4698
|
-
billingContext: {
|
|
4699
|
-
trialing: isTrialing,
|
|
4700
|
-
},
|
|
4438
|
+
billingContext: { trialing: isTrialing },
|
|
4701
4439
|
});
|
|
4702
4440
|
|
|
4703
|
-
// Update checkout session to remove discount
|
|
4704
4441
|
await checkoutSession.update({
|
|
4705
4442
|
discounts: [],
|
|
4706
4443
|
line_items: originalResult.enhancedLineItems.map((item: TLineItemExpanded) => ({
|
|
@@ -4722,22 +4459,20 @@ router.delete('/:id/remove-promotion', user, ensureCheckoutSessionOpen, async (r
|
|
|
4722
4459
|
},
|
|
4723
4460
|
});
|
|
4724
4461
|
|
|
4725
|
-
logger.info('Promotion code removed from checkout session', {
|
|
4726
|
-
sessionId: checkoutSession.id,
|
|
4727
|
-
});
|
|
4462
|
+
logger.info('Promotion code removed from checkout session', { sessionId: checkoutSession.id });
|
|
4728
4463
|
|
|
4729
|
-
|
|
4464
|
+
return c.json({
|
|
4730
4465
|
...checkoutSession.toJSON(),
|
|
4731
4466
|
line_items: originalResult.enhancedLineItems,
|
|
4732
4467
|
discount_applied: false,
|
|
4733
4468
|
});
|
|
4734
4469
|
} catch (err) {
|
|
4735
4470
|
logger.error('Error removing promotion code', {
|
|
4736
|
-
sessionId: req.
|
|
4737
|
-
error: err.message,
|
|
4738
|
-
stack: err.stack,
|
|
4471
|
+
sessionId: c.req.param('id'),
|
|
4472
|
+
error: (err as any).message,
|
|
4473
|
+
stack: (err as any).stack,
|
|
4739
4474
|
});
|
|
4740
|
-
|
|
4475
|
+
return c.json({ error: (err as any).message }, 500);
|
|
4741
4476
|
}
|
|
4742
4477
|
});
|
|
4743
4478
|
|
|
@@ -4751,46 +4486,45 @@ const amountSchema = Joi.object({
|
|
|
4751
4486
|
}),
|
|
4752
4487
|
priceId: Joi.string().required(),
|
|
4753
4488
|
});
|
|
4489
|
+
|
|
4754
4490
|
// change payment amount
|
|
4755
|
-
|
|
4491
|
+
app.put('/:id/amount', ensureCheckoutSessionOpen, async (c) => {
|
|
4492
|
+
const body = c.get('sanitizedBody') ?? {};
|
|
4756
4493
|
try {
|
|
4757
|
-
const { error, value } = amountSchema.validate(
|
|
4758
|
-
stripUnknown: true,
|
|
4759
|
-
});
|
|
4494
|
+
const { error, value } = amountSchema.validate(body, { stripUnknown: true });
|
|
4760
4495
|
if (error) {
|
|
4761
|
-
return
|
|
4496
|
+
return c.json({ error: error.message }, 400);
|
|
4762
4497
|
}
|
|
4763
4498
|
|
|
4764
4499
|
const { amount, priceId } = value;
|
|
4765
|
-
const checkoutSession =
|
|
4500
|
+
const checkoutSession = c.get('doc') as CheckoutSession;
|
|
4766
4501
|
const items = await Price.expand(checkoutSession.line_items);
|
|
4767
4502
|
const item = items.find((x) => x.price_id === priceId);
|
|
4768
4503
|
if (!item) {
|
|
4769
|
-
return
|
|
4504
|
+
return c.json({ error: 'LineItem not in checkout session' }, 400);
|
|
4770
4505
|
}
|
|
4771
4506
|
if (!item.price.custom_unit_amount) {
|
|
4772
|
-
return
|
|
4507
|
+
return c.json({ error: 'PriceItem not customizable for checkout session' }, 400);
|
|
4773
4508
|
}
|
|
4774
4509
|
|
|
4775
|
-
// validate amount on donation settings
|
|
4776
4510
|
if (checkoutSession.payment_link_id) {
|
|
4777
4511
|
const link = await PaymentLink.findByPk(checkoutSession.payment_link_id);
|
|
4778
4512
|
if (!checkoutSession.currency_id) {
|
|
4779
|
-
return
|
|
4513
|
+
return c.json({ error: 'Currency not found in checkout session' }, 400);
|
|
4780
4514
|
}
|
|
4781
4515
|
const currency = await PaymentCurrency.findByPk(checkoutSession.currency_id);
|
|
4782
4516
|
if (!currency) {
|
|
4783
|
-
return
|
|
4517
|
+
return c.json({ error: 'Currency not found' }, 404);
|
|
4784
4518
|
}
|
|
4785
4519
|
if (link?.donation_settings?.amount && currency) {
|
|
4786
4520
|
const input = Number(fromUnitToToken(amount, currency.decimal));
|
|
4787
4521
|
const { minimum, maximum, presets, custom } = link.donation_settings.amount;
|
|
4788
4522
|
if (custom) {
|
|
4789
4523
|
if (input < Number(minimum)) {
|
|
4790
|
-
return
|
|
4524
|
+
return c.json({ error: 'Custom amount should not be smaller than minimum' }, 400);
|
|
4791
4525
|
}
|
|
4792
4526
|
if (input > Number(maximum)) {
|
|
4793
|
-
return
|
|
4527
|
+
return c.json({ error: 'Custom amount should not be smaller than maximum' }, 400);
|
|
4794
4528
|
}
|
|
4795
4529
|
const precisionError = formatAmountPrecisionLimit(
|
|
4796
4530
|
input.toString(),
|
|
@@ -4798,45 +4532,43 @@ router.put('/:id/amount', ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
4798
4532
|
'Custom amount'
|
|
4799
4533
|
);
|
|
4800
4534
|
if (precisionError) {
|
|
4801
|
-
return
|
|
4535
|
+
return c.json({ error: precisionError }, 400);
|
|
4802
4536
|
}
|
|
4803
|
-
} else if (presets?.some((x) => Number(x) === input) === false) {
|
|
4804
|
-
return
|
|
4537
|
+
} else if (presets?.some((x: any) => Number(x) === input) === false) {
|
|
4538
|
+
return c.json({ error: 'Custom amount must be one of the presets' }, 400);
|
|
4805
4539
|
}
|
|
4806
4540
|
}
|
|
4807
4541
|
} else {
|
|
4808
4542
|
const { minimum, maximum } = item.price.custom_unit_amount;
|
|
4809
4543
|
if (new BN(amount).lt(new BN(minimum))) {
|
|
4810
|
-
return
|
|
4544
|
+
return c.json({ error: 'Custom amount should not be smaller than minimum' }, 400);
|
|
4811
4545
|
}
|
|
4812
4546
|
if (new BN(amount).gt(new BN(maximum))) {
|
|
4813
|
-
return
|
|
4547
|
+
return c.json({ error: 'Custom amount should not be smaller than maximum' }, 400);
|
|
4814
4548
|
}
|
|
4815
4549
|
}
|
|
4816
4550
|
|
|
4817
|
-
// update line items
|
|
4818
4551
|
const newItems = cloneDeep(checkoutSession.line_items);
|
|
4819
4552
|
const newItem = newItems.find((x) => x.price_id === priceId);
|
|
4820
4553
|
if (newItem) {
|
|
4821
4554
|
newItem.custom_amount = amount;
|
|
4822
4555
|
}
|
|
4823
4556
|
await checkoutSession.update({ line_items: newItems.map((x) => omit(x, ['price'])) as LineItem[] });
|
|
4824
|
-
logger.info('CheckoutSession updated on amount', { id: req.
|
|
4557
|
+
logger.info('CheckoutSession updated on amount', { id: c.req.param('id'), amount, priceId });
|
|
4825
4558
|
|
|
4826
|
-
// recalculate amount
|
|
4827
4559
|
await checkoutSession.update(await getCheckoutSessionAmounts(checkoutSession));
|
|
4828
4560
|
|
|
4829
|
-
return
|
|
4561
|
+
return c.json({ ...checkoutSession.toJSON(), line_items: await Price.expand(newItems) });
|
|
4830
4562
|
} catch (err) {
|
|
4831
4563
|
logger.error(err);
|
|
4832
4564
|
if (err instanceof CustomError) {
|
|
4833
|
-
return
|
|
4565
|
+
return c.json({ error: formatError(err) }, getStatusFromError(err) as any);
|
|
4834
4566
|
}
|
|
4835
|
-
return
|
|
4567
|
+
return c.json({ error: (err as any).message }, 500);
|
|
4836
4568
|
}
|
|
4837
4569
|
});
|
|
4838
4570
|
|
|
4839
|
-
const
|
|
4571
|
+
const listSchema = Joi.object<{
|
|
4840
4572
|
page: number;
|
|
4841
4573
|
pageSize: number;
|
|
4842
4574
|
status?: string;
|
|
@@ -4861,8 +4593,10 @@ const schema = Joi.object<{
|
|
|
4861
4593
|
subscription_id: Joi.string().empty(''),
|
|
4862
4594
|
livemode: Joi.boolean().empty(''),
|
|
4863
4595
|
});
|
|
4864
|
-
|
|
4865
|
-
|
|
4596
|
+
|
|
4597
|
+
app.get('/', auth, async (c) => {
|
|
4598
|
+
const query = c.req.query();
|
|
4599
|
+
const { page, pageSize, livemode, ...rest } = await listSchema.validateAsync(query, {
|
|
4866
4600
|
stripUnknown: false,
|
|
4867
4601
|
allowUnknown: true,
|
|
4868
4602
|
});
|
|
@@ -4870,41 +4604,40 @@ router.get('/', auth, async (req, res) => {
|
|
|
4870
4604
|
|
|
4871
4605
|
['status', 'payment_status', 'nft_mint_status'].forEach((key) => {
|
|
4872
4606
|
// @ts-ignore
|
|
4873
|
-
if (
|
|
4607
|
+
if (rest[key]) {
|
|
4874
4608
|
// @ts-ignore
|
|
4875
|
-
where[key] =
|
|
4609
|
+
where[key] = rest[key].split(',').map((x: string) => x.trim()).filter(Boolean); // prettier-ignore
|
|
4876
4610
|
}
|
|
4877
4611
|
});
|
|
4878
|
-
if (
|
|
4879
|
-
where.customer_id =
|
|
4612
|
+
if (rest.customer_id) {
|
|
4613
|
+
where.customer_id = rest.customer_id;
|
|
4880
4614
|
}
|
|
4881
|
-
if (
|
|
4882
|
-
where.payment_intent_id =
|
|
4615
|
+
if (rest.payment_intent_id) {
|
|
4616
|
+
where.payment_intent_id = rest.payment_intent_id;
|
|
4883
4617
|
}
|
|
4884
|
-
if (
|
|
4885
|
-
where.payment_link_id =
|
|
4618
|
+
if (rest.payment_link_id) {
|
|
4619
|
+
where.payment_link_id = rest.payment_link_id;
|
|
4886
4620
|
}
|
|
4887
|
-
if (
|
|
4888
|
-
where.subscription_id =
|
|
4621
|
+
if (rest.subscription_id) {
|
|
4622
|
+
where.subscription_id = rest.subscription_id;
|
|
4889
4623
|
}
|
|
4890
|
-
if (
|
|
4891
|
-
const customer = await Customer.findOne({ where: { did:
|
|
4624
|
+
if (rest.customer_did && isValid(rest.customer_did)) {
|
|
4625
|
+
const customer = await Customer.findOne({ where: { did: rest.customer_did } });
|
|
4892
4626
|
if (customer) {
|
|
4893
4627
|
where.customer_id = customer.id;
|
|
4894
4628
|
} else {
|
|
4895
|
-
|
|
4896
|
-
return;
|
|
4629
|
+
return c.json({ count: 0, list: [] });
|
|
4897
4630
|
}
|
|
4898
4631
|
}
|
|
4899
4632
|
if (typeof livemode === 'boolean') {
|
|
4900
4633
|
where.livemode = livemode;
|
|
4901
4634
|
}
|
|
4902
4635
|
|
|
4903
|
-
Object.keys(
|
|
4636
|
+
Object.keys(rest)
|
|
4904
4637
|
.filter((x) => x.startsWith('metadata.'))
|
|
4905
4638
|
.forEach((key: string) => {
|
|
4906
4639
|
// @ts-ignore
|
|
4907
|
-
where[key] =
|
|
4640
|
+
where[key] = rest[key];
|
|
4908
4641
|
});
|
|
4909
4642
|
|
|
4910
4643
|
try {
|
|
@@ -4931,7 +4664,7 @@ router.get('/', auth, async (req, res) => {
|
|
|
4931
4664
|
});
|
|
4932
4665
|
});
|
|
4933
4666
|
|
|
4934
|
-
const condition = { where: { livemode: !!
|
|
4667
|
+
const condition = { where: { livemode: !!c.get('livemode') } };
|
|
4935
4668
|
const products =
|
|
4936
4669
|
productIds.size > 0
|
|
4937
4670
|
? (await Product.findAll({ ...condition, where: { ...condition.where, id: Array.from(productIds) } })).map(
|
|
@@ -4951,25 +4684,26 @@ router.get('/', auth, async (req, res) => {
|
|
|
4951
4684
|
x.url = getUrl(`/checkout/${x.submit_type}/${x.id}`);
|
|
4952
4685
|
});
|
|
4953
4686
|
|
|
4954
|
-
|
|
4687
|
+
return c.json({ count, list: docs });
|
|
4955
4688
|
} catch (err) {
|
|
4956
4689
|
logger.error(err);
|
|
4957
|
-
|
|
4690
|
+
return c.json({ count: 0, list: [] });
|
|
4958
4691
|
}
|
|
4959
4692
|
});
|
|
4960
4693
|
|
|
4961
|
-
|
|
4962
|
-
const
|
|
4694
|
+
app.put('/:id', auth, async (c) => {
|
|
4695
|
+
const body = c.get('sanitizedBody') ?? {};
|
|
4696
|
+
const doc = await CheckoutSession.findByPk(c.req.param('id'));
|
|
4963
4697
|
|
|
4964
4698
|
if (!doc) {
|
|
4965
|
-
return
|
|
4699
|
+
return c.json({ error: 'CheckoutSession not found' }, 404);
|
|
4966
4700
|
}
|
|
4967
4701
|
|
|
4968
|
-
const raw = pick(
|
|
4702
|
+
const raw = pick(body as any, ['metadata']);
|
|
4969
4703
|
if (raw.metadata) {
|
|
4970
4704
|
const { error: metadataError } = MetadataSchema.validate(raw.metadata);
|
|
4971
4705
|
if (metadataError) {
|
|
4972
|
-
return
|
|
4706
|
+
return c.json({ error: metadataError }, 400);
|
|
4973
4707
|
}
|
|
4974
4708
|
raw.metadata = formatMetadata(raw.metadata);
|
|
4975
4709
|
}
|
|
@@ -4979,18 +4713,18 @@ router.put('/:id', auth, async (req, res) => {
|
|
|
4979
4713
|
sessionId: doc.id,
|
|
4980
4714
|
updatedFields: Object.keys(raw),
|
|
4981
4715
|
});
|
|
4982
|
-
|
|
4716
|
+
return c.json(doc);
|
|
4983
4717
|
});
|
|
4984
4718
|
|
|
4985
|
-
|
|
4719
|
+
app.put('/:id/slippage', user, ensureCheckoutSessionOpen, async (c) => {
|
|
4720
|
+
const body = c.get('sanitizedBody') ?? {};
|
|
4986
4721
|
try {
|
|
4987
|
-
const checkoutSession =
|
|
4988
|
-
const { slippage_percent: slippagePercent } =
|
|
4989
|
-
const rawConfig =
|
|
4722
|
+
const checkoutSession = c.get('doc') as CheckoutSession;
|
|
4723
|
+
const { slippage_percent: slippagePercent } = body as any;
|
|
4724
|
+
const rawConfig = (body as any)?.slippage_config || (body as any)?.slippage || null;
|
|
4990
4725
|
|
|
4991
4726
|
const normalizePercent = (value: any) => {
|
|
4992
4727
|
const normalized = typeof value === 'string' ? Number(value) : value;
|
|
4993
|
-
// Only validate that it's a non-negative finite number, no upper limit
|
|
4994
4728
|
if (!Number.isFinite(normalized) || normalized < 0) {
|
|
4995
4729
|
return null;
|
|
4996
4730
|
}
|
|
@@ -5002,12 +4736,10 @@ router.put('/:id/slippage', user, ensureCheckoutSessionOpen, async (req, res) =>
|
|
|
5002
4736
|
const mode = rawConfig.mode === 'rate' ? 'rate' : 'percent';
|
|
5003
4737
|
const minRate = rawConfig.min_acceptable_rate ?? rawConfig.minAcceptableRate;
|
|
5004
4738
|
|
|
5005
|
-
// For rate mode, min_acceptable_rate is required; percent is derived
|
|
5006
4739
|
if (mode === 'rate') {
|
|
5007
4740
|
if (minRate === undefined || minRate === null || minRate === '') {
|
|
5008
|
-
return
|
|
4741
|
+
return c.json({ error: 'min_acceptable_rate is required for rate mode' }, 400);
|
|
5009
4742
|
}
|
|
5010
|
-
// Accept any non-negative percent value for rate mode (calculated from rate)
|
|
5011
4743
|
const percent = normalizePercent(rawConfig.percent);
|
|
5012
4744
|
const baseCurrency = rawConfig.base_currency ?? rawConfig.baseCurrency;
|
|
5013
4745
|
config = {
|
|
@@ -5018,16 +4750,14 @@ router.put('/:id/slippage', user, ensureCheckoutSessionOpen, async (req, res) =>
|
|
|
5018
4750
|
updated_at_ms: Date.now(),
|
|
5019
4751
|
};
|
|
5020
4752
|
} else {
|
|
5021
|
-
// Percent mode: validate percent
|
|
5022
4753
|
const percent = normalizePercent(rawConfig.percent);
|
|
5023
4754
|
if (percent === null) {
|
|
5024
|
-
return
|
|
4755
|
+
return c.json({ error: 'slippage_percent must be a non-negative number' }, 400);
|
|
5025
4756
|
}
|
|
5026
4757
|
const baseCurrency = rawConfig.base_currency ?? rawConfig.baseCurrency;
|
|
5027
4758
|
config = {
|
|
5028
4759
|
mode,
|
|
5029
4760
|
percent,
|
|
5030
|
-
// Also save min_acceptable_rate if provided (calculated by frontend from current rate)
|
|
5031
4761
|
...(minRate ? { min_acceptable_rate: String(minRate) } : {}),
|
|
5032
4762
|
...(baseCurrency ? { base_currency: String(baseCurrency) } : {}),
|
|
5033
4763
|
updated_at_ms: Date.now(),
|
|
@@ -5036,7 +4766,7 @@ router.put('/:id/slippage', user, ensureCheckoutSessionOpen, async (req, res) =>
|
|
|
5036
4766
|
} else if (slippagePercent !== undefined && slippagePercent !== null) {
|
|
5037
4767
|
const slippageValue = normalizePercent(slippagePercent);
|
|
5038
4768
|
if (slippageValue === null) {
|
|
5039
|
-
return
|
|
4769
|
+
return c.json({ error: 'slippage_percent must be a non-negative number' }, 400);
|
|
5040
4770
|
}
|
|
5041
4771
|
config = {
|
|
5042
4772
|
mode: 'percent',
|
|
@@ -5044,7 +4774,7 @@ router.put('/:id/slippage', user, ensureCheckoutSessionOpen, async (req, res) =>
|
|
|
5044
4774
|
updated_at_ms: Date.now(),
|
|
5045
4775
|
};
|
|
5046
4776
|
} else {
|
|
5047
|
-
return
|
|
4777
|
+
return c.json({ error: 'slippage config is required' }, 400);
|
|
5048
4778
|
}
|
|
5049
4779
|
|
|
5050
4780
|
const nextMetadata = {
|
|
@@ -5057,14 +4787,12 @@ router.put('/:id/slippage', user, ensureCheckoutSessionOpen, async (req, res) =>
|
|
|
5057
4787
|
slippageConfig: config,
|
|
5058
4788
|
});
|
|
5059
4789
|
|
|
5060
|
-
// Final Freeze: Don't create Quotes during Preview (slippage change)
|
|
5061
|
-
// Slippage is applied at Submit time when Quote is created
|
|
5062
4790
|
const items = await Price.expand(checkoutSession.line_items, { upsell: true });
|
|
5063
4791
|
const enriched = await enrichCheckoutSessionWithQuotes(checkoutSession, items, checkoutSession.currency_id, {
|
|
5064
|
-
skipGeneration: true,
|
|
4792
|
+
skipGeneration: true,
|
|
5065
4793
|
});
|
|
5066
4794
|
|
|
5067
|
-
|
|
4795
|
+
return c.json({
|
|
5068
4796
|
...checkoutSession.toJSON(),
|
|
5069
4797
|
line_items: enriched.lineItems,
|
|
5070
4798
|
quotes: enriched.quotes,
|
|
@@ -5073,13 +4801,13 @@ router.put('/:id/slippage', user, ensureCheckoutSessionOpen, async (req, res) =>
|
|
|
5073
4801
|
});
|
|
5074
4802
|
} catch (err) {
|
|
5075
4803
|
logger.error('Error updating checkout session slippage', {
|
|
5076
|
-
sessionId: req.
|
|
4804
|
+
sessionId: c.req.param('id'),
|
|
5077
4805
|
error: err,
|
|
5078
4806
|
});
|
|
5079
4807
|
if (err instanceof CustomError) {
|
|
5080
|
-
return
|
|
4808
|
+
return c.json({ code: (err as any).code, error: (err as any).message }, getStatusFromError(err) as any);
|
|
5081
4809
|
}
|
|
5082
|
-
|
|
4810
|
+
return c.json({ error: (err as any).message }, 500);
|
|
5083
4811
|
}
|
|
5084
4812
|
});
|
|
5085
4813
|
|
|
@@ -5089,19 +4817,19 @@ router.put('/:id/slippage', user, ensureCheckoutSessionOpen, async (req, res) =>
|
|
|
5089
4817
|
* because quotes are currency-specific (e.g., TBA quote amount with 18 decimals
|
|
5090
4818
|
* cannot be used for USD with 2 decimals)
|
|
5091
4819
|
*/
|
|
5092
|
-
|
|
4820
|
+
app.put('/:id/switch-currency', user, ensureCheckoutSessionOpen, async (c) => {
|
|
4821
|
+
const body = c.get('sanitizedBody') ?? {};
|
|
5093
4822
|
try {
|
|
5094
|
-
const checkoutSession =
|
|
5095
|
-
const { currency_id: newCurrencyId } =
|
|
4823
|
+
const checkoutSession = c.get('doc') as CheckoutSession;
|
|
4824
|
+
const { currency_id: newCurrencyId } = body as any;
|
|
5096
4825
|
|
|
5097
4826
|
if (!newCurrencyId) {
|
|
5098
|
-
return
|
|
4827
|
+
return c.json({ error: 'currency_id is required' }, 400);
|
|
5099
4828
|
}
|
|
5100
4829
|
|
|
5101
|
-
// Validate the new currency exists
|
|
5102
4830
|
const newCurrency = await PaymentCurrency.findByPk(newCurrencyId);
|
|
5103
4831
|
if (!newCurrency) {
|
|
5104
|
-
return
|
|
4832
|
+
return c.json({ error: 'Currency not found' }, 400);
|
|
5105
4833
|
}
|
|
5106
4834
|
|
|
5107
4835
|
const expandedItemsForSupportCheck = await Price.expand(checkoutSession.line_items || [], {
|
|
@@ -5110,14 +4838,13 @@ router.put('/:id/switch-currency', user, ensureCheckoutSessionOpen, async (req,
|
|
|
5110
4838
|
});
|
|
5111
4839
|
const supportedCurrencyIds = getSupportedPaymentCurrencies(expandedItemsForSupportCheck as any[]);
|
|
5112
4840
|
if (!supportedCurrencyIds.includes(newCurrencyId)) {
|
|
5113
|
-
return
|
|
4841
|
+
return c.json({ error: 'Currency not supported for this checkout session' }, 400);
|
|
5114
4842
|
}
|
|
5115
4843
|
|
|
5116
4844
|
const oldCurrencyId = checkoutSession.currency_id;
|
|
5117
4845
|
const currencyChanged = oldCurrencyId && oldCurrencyId !== newCurrencyId;
|
|
5118
4846
|
|
|
5119
4847
|
if (currencyChanged) {
|
|
5120
|
-
// Clear quote-related fields from line_items
|
|
5121
4848
|
const cleanedLineItems = checkoutSession.line_items.map((item: any) => {
|
|
5122
4849
|
const {
|
|
5123
4850
|
quote_id: quoteId,
|
|
@@ -5129,11 +4856,9 @@ router.put('/:id/switch-currency', user, ensureCheckoutSessionOpen, async (req,
|
|
|
5129
4856
|
return rest;
|
|
5130
4857
|
});
|
|
5131
4858
|
|
|
5132
|
-
// Clear discount amounts - they will be recalculated by recalculate-promotion
|
|
5133
|
-
// This prevents showing stale values (e.g., ABT units when switching to USD)
|
|
5134
4859
|
const cleanedDiscounts = checkoutSession.discounts?.map((discount: any) => ({
|
|
5135
4860
|
...discount,
|
|
5136
|
-
discount_amount: null,
|
|
4861
|
+
discount_amount: null,
|
|
5137
4862
|
}));
|
|
5138
4863
|
|
|
5139
4864
|
await checkoutSession.update({
|
|
@@ -5142,7 +4867,7 @@ router.put('/:id/switch-currency', user, ensureCheckoutSessionOpen, async (req,
|
|
|
5142
4867
|
discounts: cleanedDiscounts,
|
|
5143
4868
|
total_details: {
|
|
5144
4869
|
...checkoutSession.total_details,
|
|
5145
|
-
amount_discount: '0',
|
|
4870
|
+
amount_discount: '0',
|
|
5146
4871
|
},
|
|
5147
4872
|
});
|
|
5148
4873
|
|
|
@@ -5152,31 +4877,173 @@ router.put('/:id/switch-currency', user, ensureCheckoutSessionOpen, async (req,
|
|
|
5152
4877
|
newCurrencyId,
|
|
5153
4878
|
});
|
|
5154
4879
|
} else {
|
|
5155
|
-
|
|
5156
|
-
await checkoutSession.update({
|
|
5157
|
-
currency_id: newCurrencyId,
|
|
5158
|
-
});
|
|
4880
|
+
await checkoutSession.update({ currency_id: newCurrencyId });
|
|
5159
4881
|
}
|
|
5160
4882
|
|
|
5161
|
-
// Reload and expand line items
|
|
5162
4883
|
await checkoutSession.reload();
|
|
5163
4884
|
const expandedLineItems = await Price.expand(checkoutSession.line_items, { product: true, upsell: true });
|
|
5164
4885
|
|
|
5165
|
-
|
|
4886
|
+
return c.json({
|
|
5166
4887
|
...checkoutSession.toJSON(),
|
|
5167
4888
|
line_items: expandedLineItems,
|
|
5168
4889
|
currency_changed: currencyChanged,
|
|
5169
4890
|
});
|
|
5170
4891
|
} catch (err) {
|
|
5171
4892
|
logger.error('Error switching checkout session currency', {
|
|
5172
|
-
sessionId: req.
|
|
4893
|
+
sessionId: c.req.param('id'),
|
|
5173
4894
|
error: err,
|
|
5174
4895
|
});
|
|
5175
4896
|
if (err instanceof CustomError) {
|
|
5176
|
-
return
|
|
4897
|
+
return c.json({ code: (err as any).code, error: (err as any).message }, getStatusFromError(err) as any);
|
|
4898
|
+
}
|
|
4899
|
+
return c.json({ error: (err as any).message }, 500);
|
|
4900
|
+
}
|
|
4901
|
+
});
|
|
4902
|
+
|
|
4903
|
+
// [restored — missed in the bulk conversion, caught by the route-count check]
|
|
4904
|
+
app.post('/:id/abort-stripe', user, ensureCheckoutSessionOpen, async (c) => {
|
|
4905
|
+
try {
|
|
4906
|
+
const checkoutSession = c.get('doc') as CheckoutSession;
|
|
4907
|
+
|
|
4908
|
+
if (checkoutSession.status === 'complete') {
|
|
4909
|
+
return c.json({ error: 'Checkout session already completed' }, 400);
|
|
4910
|
+
}
|
|
4911
|
+
|
|
4912
|
+
// cancel stripe subscriptions if any
|
|
4913
|
+
const canceledSubscriptions: string[] = [];
|
|
4914
|
+
if (['subscription', 'setup'].includes(checkoutSession.mode)) {
|
|
4915
|
+
const subscriptionIds = getCheckoutSessionSubscriptionIds(checkoutSession);
|
|
4916
|
+
const subscriptions = await Subscription.findAll({ where: { id: subscriptionIds } });
|
|
4917
|
+
|
|
4918
|
+
const cancelOps = subscriptions.map(async (sub) => {
|
|
4919
|
+
const stripeSubId = sub.payment_details?.stripe?.subscription_id;
|
|
4920
|
+
if (!stripeSubId) {
|
|
4921
|
+
return null;
|
|
4922
|
+
}
|
|
4923
|
+
const method = await PaymentMethod.findByPk(sub.default_payment_method_id);
|
|
4924
|
+
if (!method || method.type !== 'stripe') {
|
|
4925
|
+
return null;
|
|
4926
|
+
}
|
|
4927
|
+
const client = method.getStripeClient();
|
|
4928
|
+
try {
|
|
4929
|
+
await client.subscriptions.cancel(stripeSubId);
|
|
4930
|
+
await sub.update({
|
|
4931
|
+
payment_details: omit(sub.payment_details || {}, 'stripe'),
|
|
4932
|
+
payment_settings: {
|
|
4933
|
+
payment_method_options: omit(sub.payment_settings?.payment_method_options || {}, 'stripe'),
|
|
4934
|
+
payment_method_types: sub.payment_settings?.payment_method_types || [],
|
|
4935
|
+
},
|
|
4936
|
+
});
|
|
4937
|
+
canceledSubscriptions.push(sub.id);
|
|
4938
|
+
} catch (err: any) {
|
|
4939
|
+
logger.error('Failed to cancel stripe subscription for checkout abort', {
|
|
4940
|
+
checkoutSessionId: checkoutSession.id,
|
|
4941
|
+
subscriptionId: sub.id,
|
|
4942
|
+
error: err.message,
|
|
4943
|
+
});
|
|
4944
|
+
}
|
|
4945
|
+
return sub.id;
|
|
4946
|
+
});
|
|
4947
|
+
await Promise.all(cancelOps);
|
|
4948
|
+
}
|
|
4949
|
+
|
|
4950
|
+
// remove related invoice if created
|
|
4951
|
+
try {
|
|
4952
|
+
const existInvoice = await Invoice.findOne({ where: { checkout_session_id: checkoutSession.id } });
|
|
4953
|
+
if (existInvoice) {
|
|
4954
|
+
await destroyExistingInvoice(existInvoice);
|
|
4955
|
+
}
|
|
4956
|
+
} catch (error: any) {
|
|
4957
|
+
logger.error('Failed to destroy invoice on checkout abort', {
|
|
4958
|
+
checkoutSessionId: checkoutSession.id,
|
|
4959
|
+
error: error.message,
|
|
4960
|
+
});
|
|
5177
4961
|
}
|
|
5178
|
-
|
|
4962
|
+
return c.json({ checkoutSessionId: checkoutSession.id, canceledSubscriptions });
|
|
4963
|
+
} catch (err: any) {
|
|
4964
|
+
logger.error('Error aborting stripe for checkout session', {
|
|
4965
|
+
sessionId: c.req.param('id'),
|
|
4966
|
+
error: err.message,
|
|
4967
|
+
stack: err.stack,
|
|
4968
|
+
});
|
|
4969
|
+
return c.json({ error: err.message }, 500);
|
|
4970
|
+
}
|
|
4971
|
+
});
|
|
4972
|
+
|
|
4973
|
+
// Skip payment method for $0 subscription — user chose "Skip, bind later"
|
|
4974
|
+
app.post('/:id/skip-payment-method', user, ensureCheckoutSessionOpen, async (c) => {
|
|
4975
|
+
try {
|
|
4976
|
+
if (!c.get('user')) {
|
|
4977
|
+
return c.json({ code: 'REQUIRE_LOGIN', error: 'Please login to continue' }, 403);
|
|
4978
|
+
}
|
|
4979
|
+
|
|
4980
|
+
const checkoutSession = c.get('doc') as CheckoutSession;
|
|
4981
|
+
|
|
4982
|
+
if (!['subscription', 'setup'].includes(checkoutSession.mode)) {
|
|
4983
|
+
return c.json({ error: 'Skip payment method is only supported for subscriptions' }, 400);
|
|
4984
|
+
}
|
|
4985
|
+
|
|
4986
|
+
const subscriptionIds = getCheckoutSessionSubscriptionIds(checkoutSession);
|
|
4987
|
+
const subscriptions = await Subscription.findAll({ where: { id: subscriptionIds } });
|
|
4988
|
+
|
|
4989
|
+
if (!subscriptions.length) {
|
|
4990
|
+
return c.json({ error: 'No subscriptions found for this checkout session' }, 400);
|
|
4991
|
+
}
|
|
4992
|
+
|
|
4993
|
+
// Cancel Stripe setup intents and activate subscriptions concurrently
|
|
4994
|
+
await Promise.all(
|
|
4995
|
+
subscriptions.map(async (sub) => {
|
|
4996
|
+
const stripeSubId = sub.payment_details?.stripe?.subscription_id;
|
|
4997
|
+
if (stripeSubId) {
|
|
4998
|
+
const method = await PaymentMethod.findByPk(sub.default_payment_method_id);
|
|
4999
|
+
if (method?.type === 'stripe') {
|
|
5000
|
+
const client = method.getStripeClient();
|
|
5001
|
+
try {
|
|
5002
|
+
const stripeSub = await client.subscriptions.retrieve(stripeSubId, {
|
|
5003
|
+
expand: ['pending_setup_intent'],
|
|
5004
|
+
});
|
|
5005
|
+
if (stripeSub.pending_setup_intent && typeof stripeSub.pending_setup_intent !== 'string') {
|
|
5006
|
+
await client.setupIntents.cancel(stripeSub.pending_setup_intent.id);
|
|
5007
|
+
}
|
|
5008
|
+
await client.subscriptions.update(stripeSubId, { cancel_at_period_end: true });
|
|
5009
|
+
} catch (err: any) {
|
|
5010
|
+
logger.error('Failed to update Stripe subscription for skip-payment-method', {
|
|
5011
|
+
checkoutSessionId: checkoutSession.id,
|
|
5012
|
+
subscriptionId: sub.id,
|
|
5013
|
+
stripeSubId,
|
|
5014
|
+
error: err.message,
|
|
5015
|
+
});
|
|
5016
|
+
}
|
|
5017
|
+
}
|
|
5018
|
+
}
|
|
5019
|
+
|
|
5020
|
+
// Activate the local subscription with cancel_at_period_end
|
|
5021
|
+
await sub.update({
|
|
5022
|
+
status: sub.trial_end && sub.trial_end > Date.now() / 1000 ? 'trialing' : 'active',
|
|
5023
|
+
cancel_at_period_end: true,
|
|
5024
|
+
});
|
|
5025
|
+
await addSubscriptionJob(sub, 'cycle', false, sub.trial_end);
|
|
5026
|
+
})
|
|
5027
|
+
);
|
|
5028
|
+
|
|
5029
|
+
// Complete the checkout session
|
|
5030
|
+
await checkoutSession.update({
|
|
5031
|
+
status: 'complete',
|
|
5032
|
+
payment_status: 'no_payment_required',
|
|
5033
|
+
});
|
|
5034
|
+
|
|
5035
|
+
return c.json({
|
|
5036
|
+
checkoutSession: { id: checkoutSession.id, status: 'complete' },
|
|
5037
|
+
skipped: true,
|
|
5038
|
+
});
|
|
5039
|
+
} catch (err: any) {
|
|
5040
|
+
logger.error('Error in skip-payment-method', {
|
|
5041
|
+
sessionId: c.req.param('id'),
|
|
5042
|
+
error: err.message,
|
|
5043
|
+
stack: err.stack,
|
|
5044
|
+
});
|
|
5045
|
+
return c.json({ error: err.message }, 500);
|
|
5179
5046
|
}
|
|
5180
5047
|
});
|
|
5181
5048
|
|
|
5182
|
-
export default
|
|
5049
|
+
export default app;
|