payment-kit 1.13.15
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/.eslintrc.js +15 -0
- package/README.md +3 -0
- package/api/dev.ts +6 -0
- package/api/hooks/pre-start.js +12 -0
- package/api/src/hooks/pre-start.ts +21 -0
- package/api/src/index.ts +92 -0
- package/api/src/jobs/event.ts +72 -0
- package/api/src/jobs/invoice.ts +148 -0
- package/api/src/jobs/payment.ts +208 -0
- package/api/src/jobs/subscription.ts +301 -0
- package/api/src/jobs/webhook.ts +113 -0
- package/api/src/libs/audit.ts +73 -0
- package/api/src/libs/auth.ts +40 -0
- package/api/src/libs/chain/arcblock.ts +13 -0
- package/api/src/libs/dayjs.ts +17 -0
- package/api/src/libs/env.ts +5 -0
- package/api/src/libs/hooks.ts +42 -0
- package/api/src/libs/logger.ts +27 -0
- package/api/src/libs/middleware.ts +12 -0
- package/api/src/libs/payment.ts +53 -0
- package/api/src/libs/queue/index.ts +263 -0
- package/api/src/libs/queue/store.ts +47 -0
- package/api/src/libs/security.ts +95 -0
- package/api/src/libs/session.ts +164 -0
- package/api/src/libs/util.ts +93 -0
- package/api/src/locales/en.ts +3 -0
- package/api/src/locales/index.ts +37 -0
- package/api/src/locales/zh.ts +3 -0
- package/api/src/routes/checkout-sessions.ts +536 -0
- package/api/src/routes/connect/collect.ts +109 -0
- package/api/src/routes/connect/pay.ts +116 -0
- package/api/src/routes/connect/setup.ts +121 -0
- package/api/src/routes/connect/shared.ts +410 -0
- package/api/src/routes/connect/subscribe.ts +128 -0
- package/api/src/routes/customers.ts +70 -0
- package/api/src/routes/events.ts +76 -0
- package/api/src/routes/index.ts +59 -0
- package/api/src/routes/invoices.ts +126 -0
- package/api/src/routes/payment-currencies.ts +38 -0
- package/api/src/routes/payment-intents.ts +122 -0
- package/api/src/routes/payment-links.ts +221 -0
- package/api/src/routes/payment-methods.ts +39 -0
- package/api/src/routes/prices.ts +134 -0
- package/api/src/routes/products.ts +191 -0
- package/api/src/routes/settings.ts +33 -0
- package/api/src/routes/subscription-items.ts +148 -0
- package/api/src/routes/subscriptions.ts +254 -0
- package/api/src/routes/usage-records.ts +120 -0
- package/api/src/routes/webhook-attempts.ts +57 -0
- package/api/src/routes/webhook-endpoints.ts +105 -0
- package/api/src/store/migrate.ts +16 -0
- package/api/src/store/migrations/20230905-genesis.ts +52 -0
- package/api/src/store/migrations/20230911-seeding.ts +145 -0
- package/api/src/store/models/checkout-session.ts +395 -0
- package/api/src/store/models/coupon.ts +137 -0
- package/api/src/store/models/customer.ts +199 -0
- package/api/src/store/models/discount.ts +116 -0
- package/api/src/store/models/event.ts +111 -0
- package/api/src/store/models/index.ts +165 -0
- package/api/src/store/models/invoice-item.ts +185 -0
- package/api/src/store/models/invoice.ts +492 -0
- package/api/src/store/models/job.ts +75 -0
- package/api/src/store/models/payment-currency.ts +139 -0
- package/api/src/store/models/payment-intent.ts +282 -0
- package/api/src/store/models/payment-link.ts +219 -0
- package/api/src/store/models/payment-method.ts +169 -0
- package/api/src/store/models/price.ts +266 -0
- package/api/src/store/models/product.ts +162 -0
- package/api/src/store/models/promotion-code.ts +112 -0
- package/api/src/store/models/setup-intent.ts +206 -0
- package/api/src/store/models/subscription-item.ts +103 -0
- package/api/src/store/models/subscription-schedule.ts +157 -0
- package/api/src/store/models/subscription.ts +307 -0
- package/api/src/store/models/types.ts +406 -0
- package/api/src/store/models/usage-record.ts +132 -0
- package/api/src/store/models/webhook-attempt.ts +96 -0
- package/api/src/store/models/webhook-endpoint.ts +96 -0
- package/api/src/store/sequelize.ts +15 -0
- package/api/third.d.ts +28 -0
- package/blocklet.md +3 -0
- package/blocklet.yml +89 -0
- package/index.html +14 -0
- package/logo.png +0 -0
- package/package.json +133 -0
- package/public/.gitkeep +0 -0
- package/screenshots/.gitkeep +0 -0
- package/screenshots/1-subscription.png +0 -0
- package/screenshots/2-customer-1.png +0 -0
- package/screenshots/3-customer-2.png +0 -0
- package/screenshots/4-admin-3.png +0 -0
- package/screenshots/5-admin-4.png +0 -0
- package/scripts/build-clean.js +6 -0
- package/scripts/bump-version.mjs +35 -0
- package/src/app.tsx +68 -0
- package/src/components/actions.tsx +85 -0
- package/src/components/blockchain/tx.tsx +29 -0
- package/src/components/checkout/amount.tsx +24 -0
- package/src/components/checkout/error.tsx +30 -0
- package/src/components/checkout/footer.tsx +12 -0
- package/src/components/checkout/form/address.tsx +38 -0
- package/src/components/checkout/form/index.tsx +295 -0
- package/src/components/checkout/header.tsx +23 -0
- package/src/components/checkout/pay.tsx +222 -0
- package/src/components/checkout/product-card.tsx +56 -0
- package/src/components/checkout/product-item.tsx +37 -0
- package/src/components/checkout/skeleton/overview.tsx +21 -0
- package/src/components/checkout/skeleton/payment.tsx +35 -0
- package/src/components/checkout/success.tsx +183 -0
- package/src/components/checkout/summary.tsx +34 -0
- package/src/components/collapse.tsx +50 -0
- package/src/components/confirm.tsx +55 -0
- package/src/components/copyable.tsx +38 -0
- package/src/components/currency.tsx +15 -0
- package/src/components/customer/actions.tsx +73 -0
- package/src/components/data.tsx +20 -0
- package/src/components/drawer-form.tsx +77 -0
- package/src/components/error-fallback.tsx +7 -0
- package/src/components/error.tsx +39 -0
- package/src/components/event/list.tsx +217 -0
- package/src/components/info-card.tsx +40 -0
- package/src/components/info-metric.tsx +35 -0
- package/src/components/info-row.tsx +28 -0
- package/src/components/input.tsx +40 -0
- package/src/components/invoice/action.tsx +94 -0
- package/src/components/invoice/list.tsx +225 -0
- package/src/components/invoice/table.tsx +110 -0
- package/src/components/layout.tsx +70 -0
- package/src/components/livemode.tsx +23 -0
- package/src/components/metadata/editor.tsx +57 -0
- package/src/components/metadata/form.tsx +45 -0
- package/src/components/payment-intent/actions.tsx +81 -0
- package/src/components/payment-intent/list.tsx +204 -0
- package/src/components/payment-link/actions.tsx +114 -0
- package/src/components/payment-link/after-pay.tsx +87 -0
- package/src/components/payment-link/before-pay.tsx +175 -0
- package/src/components/payment-link/item.tsx +135 -0
- package/src/components/payment-link/product-select.tsx +66 -0
- package/src/components/payment-link/rename.tsx +64 -0
- package/src/components/portal/invoice/list.tsx +110 -0
- package/src/components/portal/subscription/cancel.tsx +83 -0
- package/src/components/portal/subscription/list.tsx +232 -0
- package/src/components/price/actions.tsx +21 -0
- package/src/components/price/form.tsx +292 -0
- package/src/components/product/actions.tsx +125 -0
- package/src/components/product/add-price.tsx +59 -0
- package/src/components/product/create.tsx +97 -0
- package/src/components/product/edit-price.tsx +75 -0
- package/src/components/product/edit.tsx +67 -0
- package/src/components/product/features.tsx +32 -0
- package/src/components/product/form.tsx +76 -0
- package/src/components/relative-time.tsx +41 -0
- package/src/components/section/header.tsx +29 -0
- package/src/components/status.tsx +12 -0
- package/src/components/subscription/actions/cancel.tsx +66 -0
- package/src/components/subscription/actions/index.tsx +172 -0
- package/src/components/subscription/actions/pause.tsx +83 -0
- package/src/components/subscription/items/actions.tsx +31 -0
- package/src/components/subscription/items/index.tsx +107 -0
- package/src/components/subscription/list.tsx +200 -0
- package/src/components/switch.tsx +48 -0
- package/src/components/table.tsx +66 -0
- package/src/components/uploader.tsx +81 -0
- package/src/components/webhook/attempts.tsx +149 -0
- package/src/contexts/products.tsx +42 -0
- package/src/contexts/session.ts +10 -0
- package/src/contexts/settings.tsx +54 -0
- package/src/env.d.ts +17 -0
- package/src/global.css +97 -0
- package/src/hooks/mobile.ts +15 -0
- package/src/index.tsx +6 -0
- package/src/libs/api.ts +19 -0
- package/src/libs/dayjs.ts +17 -0
- package/src/libs/util.ts +474 -0
- package/src/locales/en.tsx +395 -0
- package/src/locales/index.tsx +8 -0
- package/src/locales/zh.tsx +389 -0
- package/src/pages/admin/billing/index.tsx +56 -0
- package/src/pages/admin/billing/invoices/detail.tsx +215 -0
- package/src/pages/admin/billing/invoices/index.tsx +5 -0
- package/src/pages/admin/billing/subscriptions/detail.tsx +237 -0
- package/src/pages/admin/billing/subscriptions/index.tsx +5 -0
- package/src/pages/admin/customers/customers/detail.tsx +209 -0
- package/src/pages/admin/customers/customers/index.tsx +109 -0
- package/src/pages/admin/customers/index.tsx +47 -0
- package/src/pages/admin/developers/events/detail.tsx +77 -0
- package/src/pages/admin/developers/events/index.tsx +5 -0
- package/src/pages/admin/developers/index.tsx +60 -0
- package/src/pages/admin/developers/logs.tsx +3 -0
- package/src/pages/admin/developers/overview.tsx +3 -0
- package/src/pages/admin/developers/webhooks/detail.tsx +109 -0
- package/src/pages/admin/developers/webhooks/index.tsx +102 -0
- package/src/pages/admin/index.tsx +120 -0
- package/src/pages/admin/overview.tsx +3 -0
- package/src/pages/admin/payments/index.tsx +65 -0
- package/src/pages/admin/payments/intents/detail.tsx +205 -0
- package/src/pages/admin/payments/intents/index.tsx +5 -0
- package/src/pages/admin/payments/links/create.tsx +141 -0
- package/src/pages/admin/payments/links/detail.tsx +318 -0
- package/src/pages/admin/payments/links/index.tsx +167 -0
- package/src/pages/admin/products/coupons/index.tsx +3 -0
- package/src/pages/admin/products/index.tsx +81 -0
- package/src/pages/admin/products/prices/actions.tsx +151 -0
- package/src/pages/admin/products/prices/detail.tsx +203 -0
- package/src/pages/admin/products/prices/list.tsx +95 -0
- package/src/pages/admin/products/pricing-tables.tsx +3 -0
- package/src/pages/admin/products/products/create.tsx +105 -0
- package/src/pages/admin/products/products/detail.tsx +246 -0
- package/src/pages/admin/products/products/index.tsx +154 -0
- package/src/pages/admin/settings/branding.tsx +3 -0
- package/src/pages/admin/settings/business.tsx +3 -0
- package/src/pages/admin/settings/index.tsx +47 -0
- package/src/pages/admin/settings/payment-methods.tsx +80 -0
- package/src/pages/checkout/index.tsx +38 -0
- package/src/pages/checkout/pay.tsx +89 -0
- package/src/pages/customer/index.tsx +93 -0
- package/src/pages/customer/invoice.tsx +147 -0
- package/src/pages/home.tsx +9 -0
- package/tsconfig.api.json +9 -0
- package/tsconfig.eslint.json +7 -0
- package/tsconfig.json +99 -0
- package/tsconfig.types.json +11 -0
- package/vite.config.ts +19 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { toDelegateAddress } from '@arcblock/did-util';
|
|
2
|
+
import { getWalletDid } from '@blocklet/sdk/lib/did';
|
|
3
|
+
import type { DelegateState } from '@ocap/client';
|
|
4
|
+
import { BN } from '@ocap/util';
|
|
5
|
+
|
|
6
|
+
import type { TPaymentCurrency } from '../store/models/payment-currency';
|
|
7
|
+
import type { TPaymentMethod } from '../store/models/payment-method';
|
|
8
|
+
import { blocklet, wallet } from './auth';
|
|
9
|
+
import { getClient } from './chain/arcblock';
|
|
10
|
+
|
|
11
|
+
export async function isDelegationSufficientForPayment(args: {
|
|
12
|
+
paymentMethod: TPaymentMethod;
|
|
13
|
+
paymentCurrency: TPaymentCurrency;
|
|
14
|
+
userDid: string;
|
|
15
|
+
amount: string;
|
|
16
|
+
}): Promise<{ sufficient: boolean; reason?: string; delegator?: string; state?: DelegateState }> {
|
|
17
|
+
const { paymentCurrency, paymentMethod, userDid, amount } = args;
|
|
18
|
+
if (paymentMethod.type === 'arcblock') {
|
|
19
|
+
// user have bond wallet did?
|
|
20
|
+
const { user } = await blocklet.getUser(userDid, { enableConnectedAccount: true });
|
|
21
|
+
const delegator = getWalletDid(user);
|
|
22
|
+
if (!delegator) {
|
|
23
|
+
return { sufficient: false, reason: 'NO_DID_WALLET' };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const client = await getClient(paymentMethod.settings?.arcblock?.api_host as string);
|
|
27
|
+
|
|
28
|
+
// have delegated before?
|
|
29
|
+
const address = toDelegateAddress(delegator, wallet.address);
|
|
30
|
+
const { state } = await client.getDelegateState({ address });
|
|
31
|
+
if (!state) {
|
|
32
|
+
return { sufficient: false, reason: 'NO_DELEGATION' };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// balance enough token for payment?
|
|
36
|
+
const { tokens } = await client.getAccountTokens({ address: delegator, token: paymentCurrency.contract as string });
|
|
37
|
+
const [token] = tokens;
|
|
38
|
+
if (!token) {
|
|
39
|
+
return { sufficient: false, reason: 'NO_TOKEN' };
|
|
40
|
+
}
|
|
41
|
+
if (new BN(amount).gt(new BN(token.balance))) {
|
|
42
|
+
return { sufficient: false, reason: 'NO_ENOUGH_TOKEN' };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return { sufficient: true, delegator, state };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (paymentMethod.type === 'stripe') {
|
|
49
|
+
return { sufficient: false, reason: 'NOT_SUPPORTED' };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
throw new Error(`Payment method ${paymentMethod.type} not supported`);
|
|
53
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/* eslint-disable no-await-in-loop */
|
|
2
|
+
/* eslint-disable no-return-assign */
|
|
3
|
+
import EventEmitter from 'events';
|
|
4
|
+
|
|
5
|
+
import fastq from 'fastq';
|
|
6
|
+
import { nanoid } from 'nanoid';
|
|
7
|
+
|
|
8
|
+
import logger from '../logger';
|
|
9
|
+
import { sleep, tryWithTimeout } from '../util';
|
|
10
|
+
import createQueueStore from './store';
|
|
11
|
+
|
|
12
|
+
const CANCELLED = '__CANCELLED__';
|
|
13
|
+
const MIN_DELAY = process.env.NODE_ENV === 'test' ? 2 : 8;
|
|
14
|
+
|
|
15
|
+
type QueueOptions<T> = {
|
|
16
|
+
id?: (job: T) => string;
|
|
17
|
+
concurrency?: number;
|
|
18
|
+
maxRetries?: number;
|
|
19
|
+
maxTimeout?: number;
|
|
20
|
+
retryDelay?: number;
|
|
21
|
+
enableScheduledJob?: boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type QueueParams<T> = {
|
|
25
|
+
name: string;
|
|
26
|
+
onJob: (job: T) => Promise<any>;
|
|
27
|
+
options?: QueueOptions<T>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const defaults: QueueOptions<any> = {
|
|
31
|
+
concurrency: 1,
|
|
32
|
+
maxRetries: 1,
|
|
33
|
+
maxTimeout: 24 * 60 * 60 * 1000,
|
|
34
|
+
retryDelay: 0,
|
|
35
|
+
enableScheduledJob: false,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type PushParams<T> = {
|
|
39
|
+
job: T;
|
|
40
|
+
id?: string;
|
|
41
|
+
persist?: boolean;
|
|
42
|
+
delay?: number; // in seconds
|
|
43
|
+
runAt?: number; // unix timestamp in seconds
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export default function createQueue<T = any>({ name, onJob, options = defaults }: QueueParams<T>) {
|
|
47
|
+
const store = createQueueStore(name);
|
|
48
|
+
const { concurrency, maxRetries, maxTimeout, retryDelay, enableScheduledJob } = Object.assign({}, defaults, options);
|
|
49
|
+
|
|
50
|
+
const getJobId = (id: string | undefined, job: any) => id || (options.id ? options.id(job) : nanoid()) || nanoid();
|
|
51
|
+
|
|
52
|
+
const queueEvents = new EventEmitter();
|
|
53
|
+
const queue = fastq(async ({ job, id, persist }: { job: any; id: string; persist: boolean }, cb: Function) => {
|
|
54
|
+
if (persist) {
|
|
55
|
+
try {
|
|
56
|
+
const cancelled = await store.isCancelled(id);
|
|
57
|
+
if (cancelled) {
|
|
58
|
+
cb(null, CANCELLED);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
} catch (err) {
|
|
62
|
+
cb(err);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const result = await tryWithTimeout(() => onJob(job), maxTimeout);
|
|
69
|
+
cb(null, result);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
cb(err);
|
|
72
|
+
}
|
|
73
|
+
// @ts-ignore
|
|
74
|
+
}, concurrency);
|
|
75
|
+
|
|
76
|
+
const push = ({ job, id, persist, delay, runAt }: PushParams<T>) => {
|
|
77
|
+
const jobEvents = new EventEmitter();
|
|
78
|
+
const emit = (e: string, data: any) => {
|
|
79
|
+
queueEvents.emit(e, data);
|
|
80
|
+
jobEvents.emit(e, data);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
if (!job) {
|
|
84
|
+
throw new Error('Can not queue empty job');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const jobId = getJobId(id, job);
|
|
88
|
+
|
|
89
|
+
if (delay || runAt) {
|
|
90
|
+
if (!enableScheduledJob) {
|
|
91
|
+
throw new Error('Must set options.enableScheduledJob to true to run delay jobs');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 这里不是精确的 delay, 延迟的时间太短没有意义,所以这里限制了最小 delay
|
|
95
|
+
if (delay && delay < MIN_DELAY) {
|
|
96
|
+
throw new Error(`minimum delay is ${MIN_DELAY}s`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const attrs: { delay?: number; will_run_at?: number } = {};
|
|
100
|
+
if (delay) {
|
|
101
|
+
attrs.delay = delay;
|
|
102
|
+
attrs.will_run_at = Date.now() + delay * 1000;
|
|
103
|
+
}
|
|
104
|
+
if (runAt) {
|
|
105
|
+
attrs.delay = 1;
|
|
106
|
+
attrs.will_run_at = runAt * 1000;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
store
|
|
110
|
+
.addJob(jobId, job, attrs)
|
|
111
|
+
.then(() => {
|
|
112
|
+
emit('queued', { id: jobId, job, attrs });
|
|
113
|
+
})
|
|
114
|
+
.catch((err) => {
|
|
115
|
+
logger.error('Can not add scheduled job to store', { error: err });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// @ts-ignore
|
|
119
|
+
jobEvents.id = jobId;
|
|
120
|
+
return jobEvents;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// eslint-disable-next-line @typescript-eslint/no-shadow
|
|
124
|
+
const clearJob = (id: string) => store.deleteJob(id);
|
|
125
|
+
|
|
126
|
+
const onJobComplete = async (err: any, result: any) => {
|
|
127
|
+
if (result === CANCELLED) {
|
|
128
|
+
emit('cancelled', { id: jobId, job, result });
|
|
129
|
+
clearJob(jobId);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!err) {
|
|
134
|
+
emit('finished', { id: jobId, job, result });
|
|
135
|
+
clearJob(jobId);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const doc = await store.getJob(jobId);
|
|
141
|
+
if (!doc) {
|
|
142
|
+
emit('failed', { id: jobId, job, error: err });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// @ts-ignore
|
|
147
|
+
if (doc.retry_count >= maxRetries) {
|
|
148
|
+
logger.info('fail job on max retry exceed', { id: jobId, job });
|
|
149
|
+
await clearJob(jobId);
|
|
150
|
+
emit('failed', { id: jobId, job, error: err });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
await store.updateJob(jobId, { retry_count: doc.retry_count + 1 });
|
|
155
|
+
logger.info('retry job', { id: jobId, count: doc.retry_count + 1 });
|
|
156
|
+
setTimeout(() => {
|
|
157
|
+
emit('retry', { id: jobId, job, doc });
|
|
158
|
+
queue.unshift({ id: jobId, job }, onJobComplete);
|
|
159
|
+
}, retryDelay);
|
|
160
|
+
// eslint-disable-next-line no-shadow, @typescript-eslint/no-shadow
|
|
161
|
+
} catch (err: any) {
|
|
162
|
+
console.error(err);
|
|
163
|
+
await clearJob(jobId);
|
|
164
|
+
emit('failed', { id: jobId, job, error: err });
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const queueJob = () =>
|
|
169
|
+
setImmediate(() => {
|
|
170
|
+
emit('queued', { id: jobId, job, persist });
|
|
171
|
+
logger.info('queue job', { id: jobId, job });
|
|
172
|
+
queue.push({ id: jobId, job, persist }, onJobComplete);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
if (persist) {
|
|
176
|
+
store
|
|
177
|
+
.addJob(jobId, job)
|
|
178
|
+
.then(queueJob)
|
|
179
|
+
.catch((err) => {
|
|
180
|
+
logger.error('Can not add job to store', { error: err });
|
|
181
|
+
});
|
|
182
|
+
} else {
|
|
183
|
+
queueJob();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// @ts-ignore
|
|
187
|
+
jobEvents.id = jobId;
|
|
188
|
+
return jobEvents;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const cancel = async (id: string) => {
|
|
192
|
+
const doc = await store.updateJob(id, { cancelled: true });
|
|
193
|
+
return doc ? doc.job : null;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const getJob = async (id: string) => {
|
|
197
|
+
const doc = await store.getJob(id);
|
|
198
|
+
return doc ? doc.job : null;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// Populate the queue on startup
|
|
202
|
+
process.nextTick(async () => {
|
|
203
|
+
try {
|
|
204
|
+
const jobs = await store.getJobs();
|
|
205
|
+
logger.info(`${name} jobs to populate`, { count: jobs.length });
|
|
206
|
+
jobs.forEach((x) => {
|
|
207
|
+
if (x.job && x.id) {
|
|
208
|
+
push({ job: x.job, id: x.id, persist: false });
|
|
209
|
+
} else {
|
|
210
|
+
logger.info('skip invalid job from db', { job: x });
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
// eslint-disable-next-line no-shadow
|
|
214
|
+
} catch (err) {
|
|
215
|
+
console.error(err);
|
|
216
|
+
logger.error(`Can not load existing ${name} jobs`, { error: err });
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const loop = async () => {
|
|
221
|
+
if (enableScheduledJob === true) {
|
|
222
|
+
// eslint-disable-next-line no-constant-condition
|
|
223
|
+
while (true) {
|
|
224
|
+
await sleep((MIN_DELAY * 1000) / 2);
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
/* eslint-disable no-await-in-loop */
|
|
228
|
+
const jobs = await store.getScheduledJobs();
|
|
229
|
+
for (const x of jobs) {
|
|
230
|
+
if (x.job && x.id) {
|
|
231
|
+
push({ job: x.job, id: x.id, persist: false });
|
|
232
|
+
} else {
|
|
233
|
+
logger.info('skip invalid job from db', { job: x });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
} catch (err) {
|
|
237
|
+
console.error(err);
|
|
238
|
+
logger.error(`Can not load scheduled ${name} jobs`, { error: err });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
loop();
|
|
245
|
+
|
|
246
|
+
return Object.assign(queueEvents, {
|
|
247
|
+
store,
|
|
248
|
+
push,
|
|
249
|
+
drain: (cb: any) => (queue.drain = cb),
|
|
250
|
+
empty: (cb: any) => (queue.empty = cb),
|
|
251
|
+
saturated: (cb: any) => (queue.saturated = cb),
|
|
252
|
+
error: (cb: any) => (queue.error = cb),
|
|
253
|
+
get: getJob,
|
|
254
|
+
cancel,
|
|
255
|
+
options: {
|
|
256
|
+
concurrency,
|
|
257
|
+
maxRetries,
|
|
258
|
+
maxTimeout,
|
|
259
|
+
retryDelay,
|
|
260
|
+
enableScheduledJob,
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Op } from 'sequelize';
|
|
2
|
+
|
|
3
|
+
import { Job, TJob } from '../../store/models/job';
|
|
4
|
+
import { CustomError } from '../util';
|
|
5
|
+
|
|
6
|
+
export default function createQueueStore(queue: string) {
|
|
7
|
+
return {
|
|
8
|
+
async isCancelled(id: string): Promise<boolean> {
|
|
9
|
+
const job = await Job.findOne({ where: { queue, id } });
|
|
10
|
+
return !!job && !!job.cancelled;
|
|
11
|
+
},
|
|
12
|
+
getJob(id: string): Promise<TJob | null> {
|
|
13
|
+
return Job.findOne({ where: { queue, id } });
|
|
14
|
+
},
|
|
15
|
+
getJobs(): Promise<TJob[]> {
|
|
16
|
+
return Job.findAll({ where: { queue, delay: -1 }, order: [['created_at', 'ASC']] });
|
|
17
|
+
},
|
|
18
|
+
getScheduledJobs(): Promise<TJob[]> {
|
|
19
|
+
return Job.findAll({
|
|
20
|
+
where: { queue, delay: { [Op.not]: -1 }, will_run_at: { [Op.lte]: Date.now() } },
|
|
21
|
+
order: [['created_at', 'ASC']],
|
|
22
|
+
});
|
|
23
|
+
},
|
|
24
|
+
async updateJob(id: string, updates: Partial<TJob>): Promise<TJob> {
|
|
25
|
+
const job = await Job.findOne({ where: { queue, id } });
|
|
26
|
+
if (!job) {
|
|
27
|
+
throw new CustomError('JOB_NOT_FOUND', `Job ${id} does not exist`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return job.update(updates);
|
|
31
|
+
},
|
|
32
|
+
async addJob(id: string, job: any, attrs: Partial<TJob> = {}): Promise<TJob> {
|
|
33
|
+
const exist = await Job.findOne({ where: { queue, id } });
|
|
34
|
+
if (exist) {
|
|
35
|
+
throw new CustomError('JOB_DUPLICATE', `Job ${queue}#${id} already exist`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return Job.create({ id, job, queue, retry_count: 1, cancelled: false, ...attrs });
|
|
39
|
+
},
|
|
40
|
+
async deleteJob(id: string): Promise<boolean> {
|
|
41
|
+
const data = await Job.destroy({ where: { queue, id } });
|
|
42
|
+
return data > 0;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type TQueueStore = ReturnType<typeof createQueueStore>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { auth } from '@blocklet/sdk/lib/middlewares';
|
|
2
|
+
import { verify } from '@blocklet/sdk/lib/util/verify-sign';
|
|
3
|
+
import type { NextFunction, Request, Response } from 'express';
|
|
4
|
+
import type { Model } from 'sequelize';
|
|
5
|
+
|
|
6
|
+
import { Customer } from '../store/models/customer';
|
|
7
|
+
|
|
8
|
+
export const ensureAdmin = auth({ roles: ['owner', 'admin'] });
|
|
9
|
+
|
|
10
|
+
type PermissionSpec<T extends Model> = {
|
|
11
|
+
component?: boolean; // allow component calls
|
|
12
|
+
roles?: string[]; // allow current session user with one of the specified roles
|
|
13
|
+
record?: {
|
|
14
|
+
// allow record owner
|
|
15
|
+
model: T;
|
|
16
|
+
field: string;
|
|
17
|
+
};
|
|
18
|
+
mine?: boolean;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* This middleware is used to authenticate request by session or component call.
|
|
23
|
+
* If a request is authenticated, it will set `req.user` to the session user.
|
|
24
|
+
* If a request is authenticated by component call, it will set `req.user` to the component user.
|
|
25
|
+
* If a request is authenticated by record owner, it will set `req.user` to the session user and set `req.doc` to the record.
|
|
26
|
+
*/
|
|
27
|
+
export function authenticate<T extends Model>({ component, roles, record, mine }: PermissionSpec<T>) {
|
|
28
|
+
return async (req: Request, res: Response, next: NextFunction) => {
|
|
29
|
+
// authenticate by component call
|
|
30
|
+
const sig = req.get('x-component-sig');
|
|
31
|
+
if (component && sig) {
|
|
32
|
+
const data = typeof req.body === 'undefined' ? {} : req.body;
|
|
33
|
+
const verified = verify(data, sig);
|
|
34
|
+
if (!verified) {
|
|
35
|
+
return res.status(401).json({ error: 'Invalid signature for component call' });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
req.user = {
|
|
39
|
+
did: <string>req.get('x-component-did'),
|
|
40
|
+
role: 'owner',
|
|
41
|
+
provider: 'wallet',
|
|
42
|
+
fullName: <string>req.get('x-component-did'),
|
|
43
|
+
walletOS: '',
|
|
44
|
+
via: 'api',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return next();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (req.headers['x-user-did']) {
|
|
51
|
+
req.user = {
|
|
52
|
+
did: <string>req.headers['x-user-did'],
|
|
53
|
+
role: <string>req.headers['x-user-role'],
|
|
54
|
+
provider: <string>req.headers['x-user-provider'],
|
|
55
|
+
fullName: decodeURIComponent(<string>req.headers['x-user-fullname']),
|
|
56
|
+
walletOS: <string>req.headers['x-user-wallet-os'],
|
|
57
|
+
via: 'dashboard',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// authenticate by session user
|
|
61
|
+
if (roles) {
|
|
62
|
+
if (roles.includes(req.user?.role)) {
|
|
63
|
+
return next();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (mine) {
|
|
68
|
+
const customer = await Customer.findOne({ where: { did: req.user.did } });
|
|
69
|
+
if (customer) {
|
|
70
|
+
req.customer = customer;
|
|
71
|
+
req.query.customer_id = customer.id;
|
|
72
|
+
return next();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// authenticate by record owner
|
|
77
|
+
if (record) {
|
|
78
|
+
const { model, field = 'customer_id' } = record;
|
|
79
|
+
// @ts-ignore
|
|
80
|
+
const doc: T | null = await model.findByPk(req.params.id);
|
|
81
|
+
if (doc && doc[field as keyof T]) {
|
|
82
|
+
const customer = await Customer.findOne({ where: { did: req.user.did } });
|
|
83
|
+
req.doc = doc;
|
|
84
|
+
req.customer = customer;
|
|
85
|
+
if (customer && customer.id === doc[field as keyof T]) {
|
|
86
|
+
req.user.via = 'portal';
|
|
87
|
+
return next();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return res.status(401).json({ error: 'Not authorized to perform this action' });
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { env } from '@blocklet/sdk/lib/config';
|
|
2
|
+
import { BN } from '@ocap/util';
|
|
3
|
+
|
|
4
|
+
import type { Price, TPrice } from '../store/models/price';
|
|
5
|
+
import type { Product } from '../store/models/product';
|
|
6
|
+
import type { LineItem, PriceRecurring } from '../store/models/types';
|
|
7
|
+
import dayjs from './dayjs';
|
|
8
|
+
|
|
9
|
+
export type TLineItemExpanded = LineItem & { price: TPrice };
|
|
10
|
+
|
|
11
|
+
export function getStatementDescriptor(items: any[]) {
|
|
12
|
+
for (const item of items) {
|
|
13
|
+
if (item.price?.product?.statement_descriptor) {
|
|
14
|
+
return item.price?.product?.statement_descriptor;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return env.appName;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getCheckoutMode(items: TLineItemExpanded[] = []) {
|
|
22
|
+
if (items.length === 0) {
|
|
23
|
+
return 'setup';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// all metered
|
|
27
|
+
if (items.every((x) => x.price.type === 'recurring' && x.price.recurring?.usage_type === 'metered')) {
|
|
28
|
+
return 'setup';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (items.some((x) => x.price.type === 'recurring')) {
|
|
32
|
+
return 'subscription';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return 'payment';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// FIXME: apply coupon for discounts
|
|
39
|
+
export function getCheckoutAmount(items: TLineItemExpanded[] = [], includeFreeTrial = false) {
|
|
40
|
+
const subtotal = items
|
|
41
|
+
.reduce((acc, x) => {
|
|
42
|
+
if (x.price.type === 'recurring') {
|
|
43
|
+
if (includeFreeTrial) {
|
|
44
|
+
return acc;
|
|
45
|
+
}
|
|
46
|
+
if (x.price.recurring?.usage_type === 'metered') {
|
|
47
|
+
return acc;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return acc.add(new BN(x.price.unit_amount).mul(new BN(x.quantity)));
|
|
51
|
+
}, new BN(0))
|
|
52
|
+
.toString();
|
|
53
|
+
|
|
54
|
+
const total = items
|
|
55
|
+
.reduce((acc, x) => {
|
|
56
|
+
if (x.price.type === 'recurring') {
|
|
57
|
+
if (includeFreeTrial) {
|
|
58
|
+
return acc;
|
|
59
|
+
}
|
|
60
|
+
if (x.price.recurring?.usage_type === 'metered') {
|
|
61
|
+
return acc;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return acc.add(new BN(x.price.unit_amount).mul(new BN(x.quantity)));
|
|
65
|
+
}, new BN(0))
|
|
66
|
+
.toString();
|
|
67
|
+
|
|
68
|
+
return { subtotal, total, discount: '0', shipping: '0', tax: '0' };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function getRecurringPeriod(recurring: PriceRecurring) {
|
|
72
|
+
const { interval } = recurring;
|
|
73
|
+
const count = recurring.interval_count || 1;
|
|
74
|
+
const dayInMs = 24 * 60 * 60 * 1000;
|
|
75
|
+
|
|
76
|
+
switch (interval) {
|
|
77
|
+
case 'hour':
|
|
78
|
+
return 60 * 60 * 1000;
|
|
79
|
+
case 'day':
|
|
80
|
+
return count * dayInMs;
|
|
81
|
+
case 'week':
|
|
82
|
+
return count * 7 * dayInMs;
|
|
83
|
+
case 'month':
|
|
84
|
+
return count * 30 * dayInMs;
|
|
85
|
+
case 'year':
|
|
86
|
+
return count * 365 * dayInMs;
|
|
87
|
+
default:
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function getSubscriptionCreateSetup(items: TLineItemExpanded[], trialInDays = 0) {
|
|
93
|
+
let setup = new BN(0);
|
|
94
|
+
let subscription = new BN(0);
|
|
95
|
+
|
|
96
|
+
items.forEach((x) => {
|
|
97
|
+
setup = setup.add(new BN(x.price.unit_amount).mul(new BN(x.quantity)));
|
|
98
|
+
if (x.price.type === 'recurring') {
|
|
99
|
+
if (trialInDays === 0) {
|
|
100
|
+
subscription = setup.add(new BN(x.price.unit_amount).mul(new BN(x.quantity)));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const recurring = items.find((x) => x.price.type === 'recurring')?.price.recurring as PriceRecurring;
|
|
106
|
+
const cycle = getRecurringPeriod(recurring);
|
|
107
|
+
const trial = trialInDays ? trialInDays * 24 * 60 * 60 * 1000 : 0;
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
recurring,
|
|
111
|
+
cycle: {
|
|
112
|
+
duration: cycle,
|
|
113
|
+
anchor: trialInDays ? dayjs().add(trial, 'millisecond').unix() : dayjs().unix(),
|
|
114
|
+
},
|
|
115
|
+
trail: {
|
|
116
|
+
start: trialInDays ? dayjs().unix() : 0,
|
|
117
|
+
end: trialInDays ? dayjs().add(trial, 'millisecond').unix() : 0,
|
|
118
|
+
},
|
|
119
|
+
period: {
|
|
120
|
+
start: dayjs().unix(),
|
|
121
|
+
end: dayjs()
|
|
122
|
+
.add(trialInDays ? trial : cycle, 'millisecond')
|
|
123
|
+
.unix(),
|
|
124
|
+
},
|
|
125
|
+
amount: {
|
|
126
|
+
setup: setup.toString(),
|
|
127
|
+
subscription: subscription.toString(),
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function getSubscriptionCycleSetup(recurring: PriceRecurring, previousPeriodEnd: number) {
|
|
133
|
+
const cycle = getRecurringPeriod(recurring);
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
recurring,
|
|
137
|
+
cycle,
|
|
138
|
+
period: {
|
|
139
|
+
start: previousPeriodEnd,
|
|
140
|
+
end: dayjs.unix(previousPeriodEnd).add(cycle, 'millisecond').unix(),
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function getSubscriptionCycleAmount(items: TLineItemExpanded[]) {
|
|
146
|
+
let amount = new BN(0);
|
|
147
|
+
|
|
148
|
+
items.forEach((x) => {
|
|
149
|
+
amount = amount.add(new BN(x.price.unit_amount).mul(new BN(x.quantity)));
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
total: amount.toString(),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function expandLineItems(items: any[], products: Product[], prices: Price[]) {
|
|
158
|
+
items.forEach((item) => {
|
|
159
|
+
item.price = prices.find((x) => x.id === item.price_id);
|
|
160
|
+
item.price.product = products.find((x) => x.id === item.price.product_id);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return items;
|
|
164
|
+
}
|