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