payment-kit 1.13.159 → 1.13.161
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/src/hooks/pre-flight.ts +0 -3
- package/api/src/index.ts +4 -2
- package/api/src/integrations/stripe/handlers/invoice.ts +6 -0
- package/api/src/integrations/stripe/handlers/setup-intent.ts +53 -6
- package/api/src/integrations/stripe/handlers/subscription.ts +5 -3
- package/api/src/integrations/stripe/resource.ts +12 -4
- package/api/src/libs/subscription.ts +12 -1
- package/api/src/routes/checkout-sessions.ts +0 -1
- package/api/src/routes/connect/change-payment.ts +106 -0
- package/api/src/routes/connect/{update.ts → change-plan.ts} +1 -1
- package/api/src/routes/connect/setup.ts +5 -3
- package/api/src/routes/connect/shared.ts +50 -11
- package/api/src/routes/invoices.ts +24 -0
- package/api/src/routes/payment-intents.ts +24 -0
- package/api/src/routes/refunds.ts +24 -0
- package/api/src/routes/subscriptions.ts +254 -6
- package/api/src/store/migrate.ts +1 -1
- package/api/src/store/models/setup-intent.ts +2 -5
- package/blocklet.yml +1 -1
- package/package.json +14 -14
- package/src/app.tsx +14 -4
- package/src/components/metadata/list.tsx +25 -0
- package/src/components/price/currency-select.tsx +1 -1
- package/src/components/subscription/portal/actions.tsx +4 -6
- package/src/libs/util.ts +7 -21
- package/src/pages/admin/billing/invoices/detail.tsx +2 -10
- package/src/pages/admin/billing/subscriptions/detail.tsx +2 -9
- package/src/pages/admin/customers/customers/detail.tsx +3 -3
- package/src/pages/admin/payments/intents/detail.tsx +2 -10
- package/src/pages/admin/payments/links/detail.tsx +6 -14
- package/src/pages/admin/payments/refunds/detail.tsx +2 -10
- package/src/pages/admin/products/prices/detail.tsx +6 -13
- package/src/pages/admin/products/pricing-tables/detail.tsx +6 -14
- package/src/pages/admin/products/products/detail.tsx +6 -14
- package/src/pages/customer/invoice/past-due.tsx +49 -15
- package/src/pages/customer/subscription/change-payment.tsx +362 -0
- package/src/pages/customer/subscription/{update.tsx → change-plan.tsx} +26 -37
- package/src/pages/customer/subscription/detail.tsx +19 -7
- package/api/src/libs/hooks.ts +0 -25
|
@@ -2,13 +2,10 @@ import '@blocklet/sdk/lib/error-handler';
|
|
|
2
2
|
|
|
3
3
|
import dotenv from 'dotenv-flow';
|
|
4
4
|
|
|
5
|
-
import { ensureSqliteBinaryFile } from '../libs/hooks';
|
|
6
|
-
|
|
7
5
|
dotenv.config({ silent: true });
|
|
8
6
|
|
|
9
7
|
(async () => {
|
|
10
8
|
try {
|
|
11
|
-
await ensureSqliteBinaryFile();
|
|
12
9
|
await import('../store/migrate').then((m) => m.default());
|
|
13
10
|
process.exit(0);
|
|
14
11
|
} catch (err) {
|
package/api/src/index.ts
CHANGED
|
@@ -24,11 +24,12 @@ import { startPaymentQueue } from './queues/payment';
|
|
|
24
24
|
import { startRefundQueue } from './queues/refund';
|
|
25
25
|
import { startSubscriptionQueue } from './queues/subscription';
|
|
26
26
|
import routes from './routes';
|
|
27
|
+
import changePaymentHandlers from './routes/connect/change-payment';
|
|
28
|
+
import changePlanHandlers from './routes/connect/change-plan';
|
|
27
29
|
import collectHandlers from './routes/connect/collect';
|
|
28
30
|
import payHandlers from './routes/connect/pay';
|
|
29
31
|
import setupHandlers from './routes/connect/setup';
|
|
30
32
|
import subscribeHandlers from './routes/connect/subscribe';
|
|
31
|
-
import updateHandlers from './routes/connect/update';
|
|
32
33
|
import { initialize } from './store/models';
|
|
33
34
|
import { sequelize } from './store/sequelize';
|
|
34
35
|
|
|
@@ -56,7 +57,8 @@ handlers.attach(Object.assign({ app: router }, collectHandlers));
|
|
|
56
57
|
handlers.attach(Object.assign({ app: router }, payHandlers));
|
|
57
58
|
handlers.attach(Object.assign({ app: router }, setupHandlers));
|
|
58
59
|
handlers.attach(Object.assign({ app: router }, subscribeHandlers));
|
|
59
|
-
handlers.attach(Object.assign({ app: router },
|
|
60
|
+
handlers.attach(Object.assign({ app: router }, changePaymentHandlers));
|
|
61
|
+
handlers.attach(Object.assign({ app: router }, changePlanHandlers));
|
|
60
62
|
|
|
61
63
|
router.use('/api', routes);
|
|
62
64
|
|
|
@@ -3,6 +3,7 @@ import pick from 'lodash/pick';
|
|
|
3
3
|
import pWaitFor from 'p-wait-for';
|
|
4
4
|
import type Stripe from 'stripe';
|
|
5
5
|
|
|
6
|
+
import { getLock } from '../../../libs/lock';
|
|
6
7
|
import logger from '../../../libs/logger';
|
|
7
8
|
import {
|
|
8
9
|
CheckoutSession,
|
|
@@ -72,11 +73,15 @@ export async function syncStripeInvoice(invoice: Invoice) {
|
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
export async function ensureStripeInvoice(stripeInvoice: any, subscription: Subscription, client: Stripe) {
|
|
76
|
+
const lock = getLock(`mirror-stripe-invoice-${stripeInvoice.id}`);
|
|
77
|
+
await lock.acquire();
|
|
78
|
+
|
|
75
79
|
const customer = await Customer.findByPk(subscription.customer_id);
|
|
76
80
|
const checkoutSession = await CheckoutSession.findOne({ where: { subscription_id: subscription.id } });
|
|
77
81
|
|
|
78
82
|
let invoice = await Invoice.findOne({ where: { 'metadata.stripe_id': stripeInvoice.id } });
|
|
79
83
|
if (invoice) {
|
|
84
|
+
lock.release();
|
|
80
85
|
return invoice;
|
|
81
86
|
}
|
|
82
87
|
|
|
@@ -175,6 +180,7 @@ export async function ensureStripeInvoice(stripeInvoice: any, subscription: Subs
|
|
|
175
180
|
})
|
|
176
181
|
);
|
|
177
182
|
|
|
183
|
+
lock.release();
|
|
178
184
|
return invoice;
|
|
179
185
|
}
|
|
180
186
|
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import type Stripe from 'stripe';
|
|
2
2
|
|
|
3
3
|
import logger from '../../../libs/logger';
|
|
4
|
-
import { CheckoutSession, Subscription, TEventExpanded } from '../../../store/models';
|
|
4
|
+
import { CheckoutSession, SetupIntent, Subscription, TEventExpanded } from '../../../store/models';
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
export async function handleSetupIntentEvent(event: TEventExpanded, _: Stripe) {
|
|
8
|
-
const stripeIntentId = event.data.object.id;
|
|
6
|
+
async function handleSubscriptionOnSetupSucceeded(event: TEventExpanded, stripeIntentId: string) {
|
|
9
7
|
const subscription = await Subscription.findOne({
|
|
10
8
|
where: { 'payment_details.stripe.setup_intent_id': stripeIntentId },
|
|
11
9
|
});
|
|
@@ -15,8 +13,6 @@ export async function handleSetupIntentEvent(event: TEventExpanded, _: Stripe) {
|
|
|
15
13
|
return;
|
|
16
14
|
}
|
|
17
15
|
|
|
18
|
-
logger.info('received setup intent event', { id: event.id, type: event.type, subscriptionId: subscription.id });
|
|
19
|
-
|
|
20
16
|
if (event.type === 'setup_intent.succeeded') {
|
|
21
17
|
if (subscription.status === 'incomplete') {
|
|
22
18
|
await subscription.start();
|
|
@@ -40,3 +36,54 @@ export async function handleSetupIntentEvent(event: TEventExpanded, _: Stripe) {
|
|
|
40
36
|
// FIXME:
|
|
41
37
|
}
|
|
42
38
|
}
|
|
39
|
+
|
|
40
|
+
async function handleSetupIntentOnSetupSucceeded(event: TEventExpanded, stripeIntentId: string) {
|
|
41
|
+
const setupIntent = await SetupIntent.findOne({
|
|
42
|
+
where: { 'setup_details.stripe.setup_intent_id': stripeIntentId },
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (!setupIntent) {
|
|
46
|
+
logger.warn('local subscription not found for setup intent', { id: event.id, type: event.type, stripeIntentId });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (event.type === 'setup_intent.succeeded') {
|
|
51
|
+
setupIntent.update({
|
|
52
|
+
status: 'succeeded',
|
|
53
|
+
last_setup_error: null,
|
|
54
|
+
payment_method_types: ['stripe'],
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (
|
|
58
|
+
setupIntent.metadata?.subscription_id &&
|
|
59
|
+
setupIntent.metadata?.from_currency &&
|
|
60
|
+
setupIntent.metadata?.to_currency
|
|
61
|
+
) {
|
|
62
|
+
const subscription = await Subscription.findByPk(setupIntent.metadata.subscription_id);
|
|
63
|
+
if (subscription) {
|
|
64
|
+
await subscription.update({
|
|
65
|
+
currency_id: setupIntent.currency_id,
|
|
66
|
+
default_payment_method_id: setupIntent.payment_method_id,
|
|
67
|
+
payment_settings: {
|
|
68
|
+
payment_method_types: ['stripe'],
|
|
69
|
+
payment_method_options: {},
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
logger.info('subscription payment changed to stripe on stripe setup intent succeeded', {
|
|
73
|
+
subscriptionId: subscription.id,
|
|
74
|
+
setupIntentId: setupIntent.id,
|
|
75
|
+
stripeIntentId,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
83
|
+
export async function handleSetupIntentEvent(event: TEventExpanded, _: Stripe) {
|
|
84
|
+
const stripeIntentId = event.data.object.id;
|
|
85
|
+
logger.info('received setup intent event', { id: event.id, type: event.type, stripeIntentId });
|
|
86
|
+
|
|
87
|
+
await handleSubscriptionOnSetupSucceeded(event, stripeIntentId);
|
|
88
|
+
await handleSetupIntentOnSetupSucceeded(event, stripeIntentId);
|
|
89
|
+
}
|
|
@@ -43,9 +43,11 @@ export async function handleSubscriptionEvent(event: TEventExpanded, _: Stripe)
|
|
|
43
43
|
return;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
const fields = ['cancel_at', 'cancel_at_period_end', 'canceled_at'];
|
|
47
|
+
if (subscription.payment_settings?.payment_method_types?.includes('stripe')) {
|
|
48
|
+
fields.push('pause_collection');
|
|
49
|
+
}
|
|
50
|
+
await subscription.update(pick(event.data.object, fields));
|
|
49
51
|
return;
|
|
50
52
|
}
|
|
51
53
|
|
|
@@ -192,7 +192,8 @@ export async function ensureStripeSubscription(
|
|
|
192
192
|
method: PaymentMethod,
|
|
193
193
|
currency: PaymentCurrency,
|
|
194
194
|
items: TLineItemExpanded[],
|
|
195
|
-
trialInDays: number = 0
|
|
195
|
+
trialInDays: number = 0,
|
|
196
|
+
trialEnds: number = 0
|
|
196
197
|
) {
|
|
197
198
|
const client = method.getStripeClient();
|
|
198
199
|
|
|
@@ -226,12 +227,11 @@ export async function ensureStripeSubscription(
|
|
|
226
227
|
.filter((x) => x.price.type !== 'recurring')
|
|
227
228
|
.map((x) => ({ price: x.stripePrice.id, quantity: x.quantity }));
|
|
228
229
|
|
|
229
|
-
|
|
230
|
+
const props: any = {
|
|
230
231
|
currency: currency.symbol.toLowerCase(),
|
|
231
232
|
customer: customer.id,
|
|
232
233
|
items: recurringItems,
|
|
233
234
|
add_invoice_items: onetimeItems,
|
|
234
|
-
trial_period_days: trialInDays,
|
|
235
235
|
payment_behavior: 'default_incomplete',
|
|
236
236
|
payment_settings: { save_default_payment_method: 'on_subscription' },
|
|
237
237
|
metadata: {
|
|
@@ -239,7 +239,15 @@ export async function ensureStripeSubscription(
|
|
|
239
239
|
id: internal.id,
|
|
240
240
|
},
|
|
241
241
|
expand: ['latest_invoice.payment_intent', 'pending_setup_intent'],
|
|
242
|
-
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
if (trialInDays) {
|
|
245
|
+
props.trial_period_days = trialInDays;
|
|
246
|
+
} else if (trialEnds) {
|
|
247
|
+
props.trial_end = trialEnds;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
stripeSubscription = await client.subscriptions.create(props);
|
|
243
251
|
logger.info('stripe subscription created', { local: internal.id, remote: stripeSubscription.id });
|
|
244
252
|
|
|
245
253
|
await Promise.all(
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from '../store/models';
|
|
17
17
|
import dayjs from './dayjs';
|
|
18
18
|
import logger from './logger';
|
|
19
|
-
import { getPriceUintAmountByCurrency, getRecurringPeriod } from './session';
|
|
19
|
+
import { getPriceCurrencyOptions, getPriceUintAmountByCurrency, getRecurringPeriod } from './session';
|
|
20
20
|
import { getConnectQueryParam } from './util';
|
|
21
21
|
|
|
22
22
|
export function getCustomerSubscriptionPageUrl({
|
|
@@ -315,3 +315,14 @@ export function formatSubscriptionProduct(items: TLineItemExpanded[], maxLength
|
|
|
315
315
|
names.slice(0, maxLength).join(', ') + (names.length > maxLength ? ` and ${names.length - maxLength} more` : '')
|
|
316
316
|
);
|
|
317
317
|
}
|
|
318
|
+
|
|
319
|
+
export async function canChangePaymentMethod(subscriptionId: string) {
|
|
320
|
+
const item = await SubscriptionItem.findOne({ where: { subscription_id: subscriptionId } });
|
|
321
|
+
const expanded = await Price.findOne({ where: { id: item!.price_id } });
|
|
322
|
+
return expanded && getPriceCurrencyOptions(expanded).length > 1;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export async function expandSubscriptionItems(subscriptionId: string) {
|
|
326
|
+
const items = await SubscriptionItem.findAll({ where: { subscription_id: subscriptionId } });
|
|
327
|
+
return Price.expand(items.map((x) => x.toJSON()));
|
|
328
|
+
}
|
|
@@ -615,7 +615,6 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
615
615
|
intent: setupIntent.id,
|
|
616
616
|
});
|
|
617
617
|
} else {
|
|
618
|
-
// ensure payment intent
|
|
619
618
|
setupIntent = await SetupIntent.create({
|
|
620
619
|
livemode: !!checkoutSession.livemode,
|
|
621
620
|
customer_id: customer.id,
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { toTypeInfo } from '@arcblock/did';
|
|
2
|
+
import type { Transaction } from '@ocap/client';
|
|
3
|
+
import { fromPublicKey } from '@ocap/wallet';
|
|
4
|
+
|
|
5
|
+
import type { CallbackArgs } from '../../libs/auth';
|
|
6
|
+
import { getGasPayerExtra } from '../../libs/payment';
|
|
7
|
+
import { getTxMetadata } from '../../libs/util';
|
|
8
|
+
import type { TLineItemExpanded } from '../../store/models';
|
|
9
|
+
import { ensureChangePaymentContext, getAuthPrincipalClaim, getDelegationTxClaim } from './shared';
|
|
10
|
+
|
|
11
|
+
export default {
|
|
12
|
+
action: 'change-payment',
|
|
13
|
+
authPrincipal: false,
|
|
14
|
+
claims: {
|
|
15
|
+
authPrincipal: async ({ extraParams }: CallbackArgs) => {
|
|
16
|
+
const { paymentMethod } = await ensureChangePaymentContext(extraParams.subscriptionId);
|
|
17
|
+
return getAuthPrincipalClaim(paymentMethod, 'continue');
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
onConnect: async ({ userDid, userPk, extraParams }: CallbackArgs) => {
|
|
21
|
+
const { subscriptionId } = extraParams;
|
|
22
|
+
const { subscription, paymentMethod, paymentCurrency } = await ensureChangePaymentContext(subscriptionId);
|
|
23
|
+
|
|
24
|
+
if (paymentMethod.type === 'arcblock') {
|
|
25
|
+
// @ts-ignore
|
|
26
|
+
const items = subscription!.items as TLineItemExpanded[];
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
signature: await getDelegationTxClaim({
|
|
30
|
+
mode: 'setup',
|
|
31
|
+
userDid,
|
|
32
|
+
userPk,
|
|
33
|
+
nonce: subscription!.id,
|
|
34
|
+
data: getTxMetadata({ subscriptionId: subscription!.id }),
|
|
35
|
+
paymentCurrency,
|
|
36
|
+
paymentMethod,
|
|
37
|
+
trial: false,
|
|
38
|
+
items,
|
|
39
|
+
}),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
throw new Error(`Payment method ${paymentMethod.type} not supported`);
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
onAuth: async ({ userDid, userPk, claims, extraParams }: CallbackArgs) => {
|
|
47
|
+
const { subscriptionId } = extraParams;
|
|
48
|
+
const { subscription, setupIntent, paymentCurrency, paymentMethod } = await ensureChangePaymentContext(
|
|
49
|
+
subscriptionId
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
if (paymentMethod.type === 'arcblock') {
|
|
53
|
+
await subscription?.update({
|
|
54
|
+
payment_settings: {
|
|
55
|
+
payment_method_types: ['arcblock'],
|
|
56
|
+
payment_method_options: {
|
|
57
|
+
arcblock: { payer: userDid },
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const client = paymentMethod.getOcapClient();
|
|
63
|
+
const claim = claims.find((x) => x.type === 'signature');
|
|
64
|
+
|
|
65
|
+
// execute the delegate tx
|
|
66
|
+
const tx: Partial<Transaction> = client.decodeTx(claim.origin);
|
|
67
|
+
tx.signature = claim.sig;
|
|
68
|
+
|
|
69
|
+
// @ts-ignore
|
|
70
|
+
const { buffer } = await client.encodeDelegateTx({ tx });
|
|
71
|
+
const txHash = await client.sendDelegateTx(
|
|
72
|
+
// @ts-ignore
|
|
73
|
+
{ tx, wallet: fromPublicKey(userPk, toTypeInfo(userDid)) },
|
|
74
|
+
getGasPayerExtra(buffer)
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
await setupIntent.update({
|
|
78
|
+
status: 'succeeded',
|
|
79
|
+
last_setup_error: null,
|
|
80
|
+
payment_method_types: ['arcblock'],
|
|
81
|
+
payment_method_options: {
|
|
82
|
+
arcblock: { payer: userDid },
|
|
83
|
+
},
|
|
84
|
+
setup_details: {
|
|
85
|
+
arcblock: {
|
|
86
|
+
tx_hash: txHash,
|
|
87
|
+
payer: userDid,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await subscription?.update({
|
|
93
|
+
currency_id: paymentCurrency.id,
|
|
94
|
+
default_payment_method_id: paymentMethod.id,
|
|
95
|
+
payment_settings: {
|
|
96
|
+
payment_method_types: [paymentMethod.type],
|
|
97
|
+
payment_method_options: {},
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return { hash: txHash };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
throw new Error(`Payment method ${paymentMethod.type} not supported`);
|
|
105
|
+
},
|
|
106
|
+
};
|
|
@@ -11,7 +11,7 @@ import type { TLineItemExpanded } from '../../store/models';
|
|
|
11
11
|
import { ensureSubscription, getAuthPrincipalClaim, getDelegationTxClaim } from './shared';
|
|
12
12
|
|
|
13
13
|
export default {
|
|
14
|
-
action: '
|
|
14
|
+
action: 'change-plan',
|
|
15
15
|
authPrincipal: false,
|
|
16
16
|
claims: {
|
|
17
17
|
authPrincipal: async ({ extraParams }: CallbackArgs) => {
|
|
@@ -87,14 +87,16 @@ export default {
|
|
|
87
87
|
|
|
88
88
|
await setupIntent.update({
|
|
89
89
|
status: 'succeeded',
|
|
90
|
-
payment_method_types: ['arcblock'],
|
|
91
90
|
last_setup_error: null,
|
|
91
|
+
payment_method_types: ['arcblock'],
|
|
92
92
|
payment_method_options: {
|
|
93
93
|
arcblock: { payer: userDid },
|
|
94
94
|
},
|
|
95
95
|
setup_details: {
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
arcblock: {
|
|
97
|
+
tx_hash: txHash,
|
|
98
|
+
payer: userDid,
|
|
99
|
+
},
|
|
98
100
|
},
|
|
99
101
|
});
|
|
100
102
|
|
|
@@ -11,12 +11,11 @@ import dayjs from '../../libs/dayjs';
|
|
|
11
11
|
import logger from '../../libs/logger';
|
|
12
12
|
import { getTokenLimitsForDelegation } from '../../libs/payment';
|
|
13
13
|
import {
|
|
14
|
-
expandLineItems,
|
|
15
14
|
getFastCheckoutAmount,
|
|
16
15
|
getPriceUintAmountByCurrency,
|
|
17
16
|
getStatementDescriptor,
|
|
18
17
|
} from '../../libs/session';
|
|
19
|
-
import { getSubscriptionItemPrice } from '../../libs/subscription';
|
|
18
|
+
import { expandSubscriptionItems, getSubscriptionItemPrice } from '../../libs/subscription';
|
|
20
19
|
import type { TLineItemExpanded } from '../../store/models';
|
|
21
20
|
import { CheckoutSession } from '../../store/models/checkout-session';
|
|
22
21
|
import { Customer } from '../../store/models/customer';
|
|
@@ -26,7 +25,6 @@ import { PaymentCurrency } from '../../store/models/payment-currency';
|
|
|
26
25
|
import { PaymentIntent } from '../../store/models/payment-intent';
|
|
27
26
|
import { PaymentMethod } from '../../store/models/payment-method';
|
|
28
27
|
import { Price } from '../../store/models/price';
|
|
29
|
-
import { Product } from '../../store/models/product';
|
|
30
28
|
import { SetupIntent } from '../../store/models/setup-intent';
|
|
31
29
|
import { Subscription } from '../../store/models/subscription';
|
|
32
30
|
import { SubscriptionItem } from '../../store/models/subscription-item';
|
|
@@ -709,15 +707,8 @@ export async function ensureSubscription(subscriptionId: string): Promise<Result
|
|
|
709
707
|
throw new Error(`Payment method ${paymentMethod.type} should not be here`);
|
|
710
708
|
}
|
|
711
709
|
|
|
712
|
-
const items = (await SubscriptionItem.findAll({ where: { subscription_id: subscription.id } })).map((x) =>
|
|
713
|
-
x.toJSON()
|
|
714
|
-
);
|
|
715
|
-
const products = (await Product.findAll()).map((x) => x.toJSON());
|
|
716
|
-
const prices = (await Price.findAll()).map((x) => x.toJSON());
|
|
717
|
-
// @ts-ignore
|
|
718
|
-
expandLineItems(items, products, prices);
|
|
719
710
|
// @ts-ignore
|
|
720
|
-
subscription.items = await
|
|
711
|
+
subscription.items = await expandSubscriptionItems(subscription.id);
|
|
721
712
|
|
|
722
713
|
return {
|
|
723
714
|
// @ts-ignore
|
|
@@ -729,3 +720,51 @@ export async function ensureSubscription(subscriptionId: string): Promise<Result
|
|
|
729
720
|
invoice,
|
|
730
721
|
};
|
|
731
722
|
}
|
|
723
|
+
|
|
724
|
+
export async function ensureChangePaymentContext(subscriptionId: string) {
|
|
725
|
+
const subscription = await Subscription.findByPk(subscriptionId);
|
|
726
|
+
if (!subscription) {
|
|
727
|
+
throw new Error(`Subscription not found: ${subscriptionId}`);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const context = subscription.metadata?.changePayment || {};
|
|
731
|
+
if (!context || !context.setup_intent_id) {
|
|
732
|
+
throw new Error(`Change payment context not found: ${subscriptionId}`);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
const setupIntent = await SetupIntent.findByPk(context.setup_intent_id);
|
|
736
|
+
if (!setupIntent) {
|
|
737
|
+
throw new Error(`SetupIntent not found for subscription payment change ${subscriptionId}`);
|
|
738
|
+
}
|
|
739
|
+
if (setupIntent.status === 'succeeded') {
|
|
740
|
+
throw new Error(`SetupIntent completed for subscription payment change ${subscriptionId}`);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const paymentCurrencyId = setupIntent.currency_id;
|
|
744
|
+
const paymentMethodId = setupIntent.payment_method_id;
|
|
745
|
+
|
|
746
|
+
const [paymentMethod, paymentCurrency] = await Promise.all([
|
|
747
|
+
PaymentMethod.findByPk(paymentMethodId),
|
|
748
|
+
PaymentCurrency.findByPk(paymentCurrencyId),
|
|
749
|
+
]);
|
|
750
|
+
if (!paymentMethod) {
|
|
751
|
+
throw new Error(`Payment method not found for SetupIntent ${setupIntent.id}`);
|
|
752
|
+
}
|
|
753
|
+
if (!paymentCurrency) {
|
|
754
|
+
throw new Error(`Payment currency not found for SetupIntent ${setupIntent.id}`);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
if (['arcblock', 'ethereum'].includes(paymentMethod.type) === false) {
|
|
758
|
+
throw new Error(`Payment method ${paymentMethod.type} should not be here`);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// @ts-ignore
|
|
762
|
+
subscription.items = await expandSubscriptionItems(subscription.id);
|
|
763
|
+
|
|
764
|
+
return {
|
|
765
|
+
subscription,
|
|
766
|
+
setupIntent,
|
|
767
|
+
paymentMethod,
|
|
768
|
+
paymentCurrency,
|
|
769
|
+
};
|
|
770
|
+
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { isValid } from '@arcblock/did';
|
|
2
2
|
import { Router } from 'express';
|
|
3
3
|
import Joi from 'joi';
|
|
4
|
+
import pick from 'lodash/pick';
|
|
4
5
|
import type { WhereOptions } from 'sequelize';
|
|
5
6
|
|
|
6
7
|
import { syncStripeInvoice } from '../integrations/stripe/handlers/invoice';
|
|
7
8
|
import { getWhereFromKvQuery } from '../libs/api';
|
|
8
9
|
import { authenticate } from '../libs/security';
|
|
9
10
|
import { expandLineItems } from '../libs/session';
|
|
11
|
+
import { formatMetadata } from '../libs/util';
|
|
10
12
|
import { Customer } from '../store/models/customer';
|
|
11
13
|
import { Invoice } from '../store/models/invoice';
|
|
12
14
|
import { InvoiceItem } from '../store/models/invoice-item';
|
|
@@ -18,6 +20,7 @@ import { Product } from '../store/models/product';
|
|
|
18
20
|
import { Subscription } from '../store/models/subscription';
|
|
19
21
|
|
|
20
22
|
const router = Router();
|
|
23
|
+
const authAdmin = authenticate<Subscription>({ component: true, roles: ['owner', 'admin'] });
|
|
21
24
|
const authMine = authenticate<Subscription>({ component: true, roles: ['owner', 'admin'], mine: true });
|
|
22
25
|
const authPortal = authenticate<Invoice>({
|
|
23
26
|
component: true,
|
|
@@ -177,4 +180,25 @@ router.get('/:id', authPortal, async (req, res) => {
|
|
|
177
180
|
}
|
|
178
181
|
});
|
|
179
182
|
|
|
183
|
+
// eslint-disable-next-line consistent-return
|
|
184
|
+
router.put('/:id', authAdmin, async (req, res) => {
|
|
185
|
+
try {
|
|
186
|
+
const doc = await Invoice.findByPk(req.params.id as string);
|
|
187
|
+
if (!doc) {
|
|
188
|
+
return res.status(404).json({ error: 'Invoice not found' });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const raw = pick(req.body, ['metadata']);
|
|
192
|
+
if (raw.metadata) {
|
|
193
|
+
raw.metadata = formatMetadata(raw.metadata);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
await doc.update(raw);
|
|
197
|
+
res.json(doc);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
console.error(err);
|
|
200
|
+
res.json(null);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
180
204
|
export default router;
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { isValid } from '@arcblock/did';
|
|
2
2
|
import { Router } from 'express';
|
|
3
3
|
import Joi from 'joi';
|
|
4
|
+
import pick from 'lodash/pick';
|
|
4
5
|
import type { WhereOptions } from 'sequelize';
|
|
5
6
|
|
|
6
7
|
import { syncStripPayment } from '../integrations/stripe/handlers/payment-intent';
|
|
7
8
|
import { getWhereFromKvQuery, getWhereFromQuery } from '../libs/api';
|
|
8
9
|
import { authenticate } from '../libs/security';
|
|
10
|
+
import { formatMetadata } from '../libs/util';
|
|
9
11
|
import { CheckoutSession } from '../store/models/checkout-session';
|
|
10
12
|
import { Customer } from '../store/models/customer';
|
|
11
13
|
import { Invoice } from '../store/models/invoice';
|
|
@@ -15,6 +17,7 @@ import { PaymentMethod } from '../store/models/payment-method';
|
|
|
15
17
|
import { Subscription } from '../store/models/subscription';
|
|
16
18
|
|
|
17
19
|
const router = Router();
|
|
20
|
+
const authAdmin = authenticate<Subscription>({ component: true, roles: ['owner', 'admin'] });
|
|
18
21
|
const authMine = authenticate<Subscription>({ component: true, roles: ['owner', 'admin'], mine: true });
|
|
19
22
|
const authPortal = authenticate<PaymentIntent>({
|
|
20
23
|
component: true,
|
|
@@ -178,4 +181,25 @@ router.get('/:id', authPortal, async (req, res) => {
|
|
|
178
181
|
}
|
|
179
182
|
});
|
|
180
183
|
|
|
184
|
+
// eslint-disable-next-line consistent-return
|
|
185
|
+
router.put('/:id', authAdmin, async (req, res) => {
|
|
186
|
+
try {
|
|
187
|
+
const doc = await PaymentIntent.findByPk(req.params.id as string);
|
|
188
|
+
if (!doc) {
|
|
189
|
+
return res.status(404).json({ error: 'PaymentIntent not found' });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const raw = pick(req.body, ['metadata']);
|
|
193
|
+
if (raw.metadata) {
|
|
194
|
+
raw.metadata = formatMetadata(raw.metadata);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
await doc.update(raw);
|
|
198
|
+
res.json(doc);
|
|
199
|
+
} catch (err) {
|
|
200
|
+
console.error(err);
|
|
201
|
+
res.json(null);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
181
205
|
export default router;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/* eslint-disable consistent-return */
|
|
2
2
|
import { Router } from 'express';
|
|
3
3
|
import Joi from 'joi';
|
|
4
|
+
import pick from 'lodash/pick';
|
|
4
5
|
import type { WhereOptions } from 'sequelize';
|
|
5
6
|
|
|
6
7
|
import { authenticate } from '../libs/security';
|
|
8
|
+
import { formatMetadata } from '../libs/util';
|
|
7
9
|
import {
|
|
8
10
|
Customer,
|
|
9
11
|
Invoice,
|
|
@@ -15,6 +17,7 @@ import {
|
|
|
15
17
|
} from '../store/models';
|
|
16
18
|
|
|
17
19
|
const router = Router();
|
|
20
|
+
const authAdmin = authenticate<Invoice>({ component: true, roles: ['owner', 'admin'] });
|
|
18
21
|
const auth = authenticate<Invoice>({
|
|
19
22
|
component: true,
|
|
20
23
|
roles: ['owner', 'admin'],
|
|
@@ -119,4 +122,25 @@ router.get('/:id', auth, async (req, res) => {
|
|
|
119
122
|
return res.status(404).json(null);
|
|
120
123
|
});
|
|
121
124
|
|
|
125
|
+
// eslint-disable-next-line consistent-return
|
|
126
|
+
router.put('/:id', authAdmin, async (req, res) => {
|
|
127
|
+
try {
|
|
128
|
+
const doc = await Refund.findByPk(req.params.id as string);
|
|
129
|
+
if (!doc) {
|
|
130
|
+
return res.status(404).json({ error: 'Refund not found' });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const raw = pick(req.body, ['metadata']);
|
|
134
|
+
if (raw.metadata) {
|
|
135
|
+
raw.metadata = formatMetadata(raw.metadata);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
await doc.update(raw);
|
|
139
|
+
res.json(doc);
|
|
140
|
+
} catch (err) {
|
|
141
|
+
console.error(err);
|
|
142
|
+
res.json(null);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
122
146
|
export default router;
|