payment-kit 1.15.33 → 1.15.34
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/integrations/stripe/handlers/setup-intent.ts +3 -1
- package/api/src/integrations/stripe/handlers/subscription.ts +2 -8
- package/api/src/integrations/stripe/resource.ts +0 -11
- package/api/src/libs/invoice.ts +202 -1
- package/api/src/libs/notification/template/subscription-canceled.ts +11 -2
- package/api/src/libs/notification/template/subscription-renew-failed.ts +1 -1
- package/api/src/libs/notification/template/subscription-renewed.ts +1 -1
- package/api/src/libs/notification/template/subscription-trial-will-end.ts +9 -5
- package/api/src/libs/notification/template/subscription-will-canceled.ts +9 -5
- package/api/src/libs/notification/template/subscription-will-renew.ts +10 -12
- package/api/src/libs/payment.ts +3 -2
- package/api/src/libs/subscription.ts +33 -14
- package/api/src/queues/invoice.ts +1 -0
- package/api/src/queues/payment.ts +3 -1
- package/api/src/queues/refund.ts +9 -8
- package/api/src/queues/subscription.ts +109 -38
- package/api/src/routes/checkout-sessions.ts +20 -4
- package/api/src/routes/connect/change-payment.ts +51 -34
- package/api/src/routes/connect/change-plan.ts +25 -3
- package/api/src/routes/connect/setup.ts +27 -6
- package/api/src/routes/connect/shared.ts +135 -1
- package/api/src/routes/connect/subscribe.ts +25 -3
- package/api/src/routes/invoices.ts +23 -105
- package/api/src/routes/subscriptions.ts +66 -17
- package/api/src/store/models/invoice.ts +2 -1
- package/blocklet.yml +1 -1
- package/package.json +4 -4
- package/src/components/invoice/list.tsx +47 -24
- package/src/components/pricing-table/payment-settings.tsx +1 -1
- package/src/components/subscription/actions/cancel.tsx +10 -7
- package/src/components/subscription/metrics.tsx +1 -1
- package/src/pages/admin/billing/invoices/detail.tsx +15 -0
- package/src/pages/admin/billing/invoices/index.tsx +1 -1
- package/src/pages/admin/billing/subscriptions/detail.tsx +12 -4
- package/src/pages/admin/customers/customers/detail.tsx +1 -0
- package/src/pages/customer/index.tsx +1 -1
- package/src/pages/customer/invoice/detail.tsx +28 -14
- package/src/pages/customer/subscription/change-plan.tsx +8 -1
- package/src/pages/customer/subscription/detail.tsx +4 -4
- package/src/pages/customer/subscription/embed.tsx +3 -1
|
@@ -1021,7 +1021,8 @@ export async function executeOcapTransactions(
|
|
|
1021
1021
|
claims: any[],
|
|
1022
1022
|
paymentMethod: PaymentMethod,
|
|
1023
1023
|
request: Request,
|
|
1024
|
-
subscriptionId?: string
|
|
1024
|
+
subscriptionId?: string,
|
|
1025
|
+
paymentCurrencyContract?: string
|
|
1025
1026
|
) {
|
|
1026
1027
|
const client = paymentMethod.getOcapClient();
|
|
1027
1028
|
const delegation = claims.find((x) => x.type === 'signature' && x.meta?.purpose === 'delegation');
|
|
@@ -1031,6 +1032,9 @@ export async function executeOcapTransactions(
|
|
|
1031
1032
|
[staking, 'Stake'],
|
|
1032
1033
|
];
|
|
1033
1034
|
|
|
1035
|
+
const stakingAmount =
|
|
1036
|
+
staking?.requirement?.tokens?.find((x: any) => x.address === paymentCurrencyContract)?.value || '0';
|
|
1037
|
+
|
|
1034
1038
|
const [delegationTxHash, stakingTxHash] = await Promise.all(
|
|
1035
1039
|
transactions.map(async ([claim, type]) => {
|
|
1036
1040
|
if (!claim) {
|
|
@@ -1065,5 +1069,135 @@ export async function executeOcapTransactions(
|
|
|
1065
1069
|
tx_hash: stakingTxHash,
|
|
1066
1070
|
address: await getCustomerStakeAddress(userDid, nonce),
|
|
1067
1071
|
},
|
|
1072
|
+
stakingAmount,
|
|
1068
1073
|
};
|
|
1069
1074
|
}
|
|
1075
|
+
|
|
1076
|
+
export async function ensureStakeInvoice(
|
|
1077
|
+
invoiceProps: { total: string; description?: string; checkout_session_id?: string; currency_id: string; metadata?: any; payment_settings?: any },
|
|
1078
|
+
subscription: Subscription,
|
|
1079
|
+
paymentMethod: PaymentMethod,
|
|
1080
|
+
customer: Customer
|
|
1081
|
+
) {
|
|
1082
|
+
if (paymentMethod.type !== 'arcblock') {
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
try {
|
|
1086
|
+
const stakingInvoice = await Invoice.create({
|
|
1087
|
+
livemode: subscription.livemode,
|
|
1088
|
+
number: await customer.getInvoiceNumber(),
|
|
1089
|
+
description: invoiceProps?.description || 'Stake for subscription',
|
|
1090
|
+
statement_descriptor: '',
|
|
1091
|
+
period_start: dayjs().unix(),
|
|
1092
|
+
period_end: subscription.current_period_end,
|
|
1093
|
+
|
|
1094
|
+
auto_advance: false,
|
|
1095
|
+
paid: true,
|
|
1096
|
+
paid_out_of_band: false,
|
|
1097
|
+
|
|
1098
|
+
status: 'paid',
|
|
1099
|
+
collection_method: 'charge_automatically',
|
|
1100
|
+
billing_reason: 'stake',
|
|
1101
|
+
|
|
1102
|
+
currency_id: invoiceProps.currency_id,
|
|
1103
|
+
customer_id: customer.id,
|
|
1104
|
+
payment_intent_id: '',
|
|
1105
|
+
subscription_id: subscription?.id,
|
|
1106
|
+
checkout_session_id: invoiceProps?.checkout_session_id || '',
|
|
1107
|
+
|
|
1108
|
+
total: invoiceProps.total || '0',
|
|
1109
|
+
subtotal: invoiceProps.total || '0',
|
|
1110
|
+
tax: '0',
|
|
1111
|
+
subtotal_excluding_tax: invoiceProps.total || '0',
|
|
1112
|
+
|
|
1113
|
+
amount_due: '0',
|
|
1114
|
+
amount_paid: invoiceProps.total || '0',
|
|
1115
|
+
amount_remaining: '0',
|
|
1116
|
+
amount_shipping: '0',
|
|
1117
|
+
|
|
1118
|
+
starting_balance: '0',
|
|
1119
|
+
ending_balance: '0',
|
|
1120
|
+
starting_token_balance: {},
|
|
1121
|
+
ending_token_balance: {},
|
|
1122
|
+
|
|
1123
|
+
attempt_count: 0,
|
|
1124
|
+
attempted: false,
|
|
1125
|
+
// next_payment_attempt: undefined,
|
|
1126
|
+
|
|
1127
|
+
custom_fields: [],
|
|
1128
|
+
customer_address: customer.address,
|
|
1129
|
+
customer_email: customer.email,
|
|
1130
|
+
customer_name: customer.name,
|
|
1131
|
+
customer_phone: customer.phone,
|
|
1132
|
+
|
|
1133
|
+
discounts: [],
|
|
1134
|
+
total_discount_amounts: [],
|
|
1135
|
+
|
|
1136
|
+
due_date: undefined,
|
|
1137
|
+
effective_at: dayjs().unix(),
|
|
1138
|
+
status_transitions: {
|
|
1139
|
+
finalized_at: dayjs().unix(),
|
|
1140
|
+
},
|
|
1141
|
+
|
|
1142
|
+
payment_settings: invoiceProps?.payment_settings || subscription?.payment_settings,
|
|
1143
|
+
default_payment_method_id: paymentMethod.id,
|
|
1144
|
+
|
|
1145
|
+
account_country: '',
|
|
1146
|
+
account_name: '',
|
|
1147
|
+
metadata: invoiceProps.metadata || {},
|
|
1148
|
+
});
|
|
1149
|
+
logger.info('create staking invoice success', {
|
|
1150
|
+
stakingInvoice,
|
|
1151
|
+
subscriptionId: subscription?.id,
|
|
1152
|
+
paymentMethod: paymentMethod.id,
|
|
1153
|
+
customerId: customer.id,
|
|
1154
|
+
});
|
|
1155
|
+
} catch (error) {
|
|
1156
|
+
logger.error('ensureStake: create invoice failed', { error, subscriptionId: subscription?.id, paymentMethod: paymentMethod.id, customerId: customer.id });
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
|
|
1161
|
+
export async function updateStripeSubscriptionAfterChangePayment(setupIntent: SetupIntent, subscription: Subscription) {
|
|
1162
|
+
const { from_method: fromMethodId, to_method: toMethodId } = setupIntent.metadata || {};
|
|
1163
|
+
const fromMethod = await PaymentMethod.findByPk(fromMethodId);
|
|
1164
|
+
if (fromMethod?.type === 'stripe') {
|
|
1165
|
+
// pause Stripe
|
|
1166
|
+
const client = fromMethod?.getStripeClient();
|
|
1167
|
+
const stripeSubscriptionId = subscription.payment_details?.stripe?.subscription_id as string;
|
|
1168
|
+
if (client && stripeSubscriptionId) {
|
|
1169
|
+
const stripeSubscription = await client.subscriptions.retrieve(stripeSubscriptionId);
|
|
1170
|
+
if (stripeSubscription) {
|
|
1171
|
+
const result = await client.subscriptions.update(stripeSubscriptionId, {
|
|
1172
|
+
pause_collection: {
|
|
1173
|
+
behavior: 'void',
|
|
1174
|
+
},
|
|
1175
|
+
});
|
|
1176
|
+
logger.info('stripe subscription paused on payment change', {
|
|
1177
|
+
subscription: subscription.id,
|
|
1178
|
+
stripeSubscription: stripeSubscriptionId,
|
|
1179
|
+
result,
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
} else {
|
|
1184
|
+
const toMethod = await PaymentMethod.findByPk(toMethodId);
|
|
1185
|
+
if (toMethod?.type === 'stripe') {
|
|
1186
|
+
// resume stripe
|
|
1187
|
+
const client = toMethod?.getStripeClient();
|
|
1188
|
+
const stripeSubscriptionId = subscription.payment_details?.stripe?.subscription_id as string;
|
|
1189
|
+
if (client && stripeSubscriptionId) {
|
|
1190
|
+
const stripeSubscription = await client.subscriptions.retrieve(stripeSubscriptionId);
|
|
1191
|
+
if (stripeSubscription.status === 'paused') {
|
|
1192
|
+
await client.subscriptions.resume(stripeSubscriptionId, {
|
|
1193
|
+
proration_behavior: 'none',
|
|
1194
|
+
});
|
|
1195
|
+
logger.info('stripe subscription resumed on payment change', {
|
|
1196
|
+
subscription: subscription.id,
|
|
1197
|
+
stripeSubscription: stripeSubscriptionId,
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
@@ -12,6 +12,7 @@ import type { Invoice, TLineItemExpanded } from '../../store/models';
|
|
|
12
12
|
import {
|
|
13
13
|
ensureInvoiceForCheckout,
|
|
14
14
|
ensurePaymentIntent,
|
|
15
|
+
ensureStakeInvoice,
|
|
15
16
|
executeOcapTransactions,
|
|
16
17
|
getAuthPrincipalClaim,
|
|
17
18
|
getDelegationTxClaim,
|
|
@@ -122,7 +123,7 @@ export default {
|
|
|
122
123
|
onAuth: async (args: CallbackArgs) => {
|
|
123
124
|
const { request, userDid, userPk, claims, extraParams } = args;
|
|
124
125
|
const { checkoutSessionId, connectedDid } = extraParams;
|
|
125
|
-
const { checkoutSession, customer, paymentMethod, subscription } = await ensurePaymentIntent(
|
|
126
|
+
const { checkoutSession, customer, paymentMethod, subscription, paymentCurrency } = await ensurePaymentIntent(
|
|
126
127
|
checkoutSessionId,
|
|
127
128
|
connectedDid || userDid
|
|
128
129
|
);
|
|
@@ -159,13 +160,34 @@ export default {
|
|
|
159
160
|
await prepareTxExecution();
|
|
160
161
|
const { invoice } = await ensureInvoiceForCheckout({ checkoutSession, customer, subscription });
|
|
161
162
|
|
|
162
|
-
const paymentDetails = await executeOcapTransactions(
|
|
163
|
+
const { stakingAmount, ...paymentDetails } = await executeOcapTransactions(
|
|
163
164
|
userDid,
|
|
164
165
|
userPk,
|
|
165
166
|
claims,
|
|
166
167
|
paymentMethod,
|
|
167
168
|
request,
|
|
168
|
-
subscription?.id
|
|
169
|
+
subscription?.id,
|
|
170
|
+
paymentCurrency?.contract
|
|
171
|
+
);
|
|
172
|
+
await ensureStakeInvoice(
|
|
173
|
+
{
|
|
174
|
+
total: stakingAmount,
|
|
175
|
+
description: 'Stake for subscription',
|
|
176
|
+
checkout_session_id: checkoutSessionId,
|
|
177
|
+
currency_id: paymentCurrency.id,
|
|
178
|
+
metadata: {
|
|
179
|
+
payment_details: {
|
|
180
|
+
arcblock: {
|
|
181
|
+
tx_hash: paymentDetails?.staking?.tx_hash,
|
|
182
|
+
payer: paymentDetails?.payer,
|
|
183
|
+
address: paymentDetails?.staking?.address,
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
subscription,
|
|
189
|
+
paymentMethod,
|
|
190
|
+
customer
|
|
169
191
|
);
|
|
170
192
|
await afterTxExecution(invoice!, paymentDetails);
|
|
171
193
|
|
|
@@ -10,7 +10,6 @@ import { createListParamSchema, getWhereFromKvQuery, MetadataSchema } from '../l
|
|
|
10
10
|
import { authenticate } from '../libs/security';
|
|
11
11
|
import { expandLineItems } from '../libs/session';
|
|
12
12
|
import { formatMetadata } from '../libs/util';
|
|
13
|
-
import { Refund, SetupIntent } from '../store/models';
|
|
14
13
|
import { Customer } from '../store/models/customer';
|
|
15
14
|
import { Invoice } from '../store/models/invoice';
|
|
16
15
|
import { InvoiceItem } from '../store/models/invoice-item';
|
|
@@ -20,7 +19,7 @@ import { PaymentMethod } from '../store/models/payment-method';
|
|
|
20
19
|
import { Price } from '../store/models/price';
|
|
21
20
|
import { Product } from '../store/models/product';
|
|
22
21
|
import { Subscription } from '../store/models/subscription';
|
|
23
|
-
import {
|
|
22
|
+
import { getReturnStakeInvoices, getStakingInvoices } from '../libs/invoice';
|
|
24
23
|
|
|
25
24
|
const router = Router();
|
|
26
25
|
const authAdmin = authenticate<Subscription>({ component: true, roles: ['owner', 'admin'] });
|
|
@@ -43,6 +42,7 @@ const schema = createListParamSchema<{
|
|
|
43
42
|
currency_id?: string;
|
|
44
43
|
ignore_zero?: boolean;
|
|
45
44
|
include_staking?: boolean;
|
|
45
|
+
include_return_staking?: boolean;
|
|
46
46
|
include_recovered_from?: boolean;
|
|
47
47
|
}>({
|
|
48
48
|
status: Joi.string().empty(''),
|
|
@@ -52,17 +52,16 @@ const schema = createListParamSchema<{
|
|
|
52
52
|
currency_id: Joi.string().empty(''),
|
|
53
53
|
ignore_zero: Joi.boolean().empty(false),
|
|
54
54
|
include_staking: Joi.boolean().empty(false),
|
|
55
|
+
include_return_staking: Joi.boolean().empty(false),
|
|
55
56
|
include_recovered_from: Joi.boolean().empty(false),
|
|
56
57
|
});
|
|
57
58
|
router.get('/', authMine, async (req, res) => {
|
|
58
59
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
59
|
-
const { page, pageSize, livemode, status, ignore_zero, include_staking, ...query } =
|
|
60
|
-
req.query,
|
|
61
|
-
{
|
|
60
|
+
const { page, pageSize, livemode, status, ignore_zero, include_staking, include_return_staking, ...query } =
|
|
61
|
+
await schema.validateAsync(req.query, {
|
|
62
62
|
stripUnknown: false,
|
|
63
63
|
allowUnknown: true,
|
|
64
|
-
}
|
|
65
|
-
);
|
|
64
|
+
});
|
|
66
65
|
const where = getWhereFromKvQuery(query.q);
|
|
67
66
|
|
|
68
67
|
if (status) {
|
|
@@ -108,7 +107,9 @@ router.get('/', authMine, async (req, res) => {
|
|
|
108
107
|
// @ts-ignore
|
|
109
108
|
where[key] = query[key];
|
|
110
109
|
});
|
|
111
|
-
|
|
110
|
+
if (!!(include_staking && query.subscription_id) || !include_staking) {
|
|
111
|
+
where.billing_reason = { [Op.ne]: 'stake' };
|
|
112
|
+
}
|
|
112
113
|
try {
|
|
113
114
|
const { rows: list, count } = await Invoice.findAndCountAll({
|
|
114
115
|
where,
|
|
@@ -117,7 +118,7 @@ router.get('/', authMine, async (req, res) => {
|
|
|
117
118
|
limit: pageSize,
|
|
118
119
|
include: [
|
|
119
120
|
{ model: PaymentCurrency, as: 'paymentCurrency' },
|
|
120
|
-
|
|
121
|
+
{ model: PaymentMethod, as: 'paymentMethod' },
|
|
121
122
|
// { model: PaymentIntent, as: 'paymentIntent' },
|
|
122
123
|
// { model: Subscription, as: 'subscription' },
|
|
123
124
|
{ model: Customer, as: 'customer' },
|
|
@@ -126,113 +127,30 @@ router.get('/', authMine, async (req, res) => {
|
|
|
126
127
|
|
|
127
128
|
// push staking info as first invoice if we are on the last page
|
|
128
129
|
let subscription;
|
|
130
|
+
let invoices = list;
|
|
129
131
|
if (query.subscription_id && include_staking && page === Math.ceil((count || 1) / pageSize)) {
|
|
130
132
|
try {
|
|
131
133
|
subscription = await Subscription.findByPk(query.subscription_id);
|
|
132
134
|
if (subscription?.payment_details?.arcblock?.staking?.tx_hash) {
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
payment_method_id: method?.id,
|
|
138
|
-
metadata: { subscription_id: subscription.id },
|
|
139
|
-
},
|
|
140
|
-
order: [['created_at', 'ASC']],
|
|
141
|
-
});
|
|
142
|
-
const currencyId = setup?.currency_id || subscription.currency_id;
|
|
143
|
-
const currency = await PaymentCurrency.findByPk(currencyId);
|
|
144
|
-
if (method && currency) {
|
|
145
|
-
const { address } = subscription.payment_details.arcblock.staking;
|
|
146
|
-
const firstInvoice = await Invoice.findOne({
|
|
147
|
-
where: { subscription_id: subscription.id, currency_id: currencyId },
|
|
148
|
-
order: [['created_at', 'ASC']],
|
|
149
|
-
include: [{ model: PaymentCurrency, as: 'paymentCurrency' }],
|
|
150
|
-
});
|
|
151
|
-
if (firstInvoice) {
|
|
152
|
-
const customer = await Customer.findByPk(firstInvoice.customer_id);
|
|
153
|
-
const stakeAmountResult = await getSubscriptionStakeAmountSetup(subscription, method);
|
|
154
|
-
// @ts-ignore
|
|
155
|
-
const stakeAmount = stakeAmountResult?.[currency?.contract] || '0';
|
|
156
|
-
|
|
157
|
-
list.push({
|
|
158
|
-
id: address as string,
|
|
159
|
-
status: 'paid',
|
|
160
|
-
description: 'Subscription staking',
|
|
161
|
-
billing_reason: 'subscription_create',
|
|
162
|
-
total: stakeAmount,
|
|
163
|
-
amount_due: '0',
|
|
164
|
-
amount_paid: stakeAmount,
|
|
165
|
-
amount_remaining: '0',
|
|
166
|
-
...pick(firstInvoice, [
|
|
167
|
-
'number',
|
|
168
|
-
'paid',
|
|
169
|
-
'auto_advance',
|
|
170
|
-
'currency_id',
|
|
171
|
-
'customer_id',
|
|
172
|
-
'subscription_id',
|
|
173
|
-
'period_start',
|
|
174
|
-
'period_end',
|
|
175
|
-
'created_at',
|
|
176
|
-
'updated_at',
|
|
177
|
-
]),
|
|
178
|
-
// @ts-ignore
|
|
179
|
-
paymentCurrency: currency,
|
|
180
|
-
paymentMethod: method,
|
|
181
|
-
customer,
|
|
182
|
-
metadata: {
|
|
183
|
-
payment_details: {
|
|
184
|
-
arcblock: {
|
|
185
|
-
tx_hash: subscription.payment_details.arcblock.staking.tx_hash,
|
|
186
|
-
payer: subscription.payment_details.arcblock.payer,
|
|
187
|
-
},
|
|
188
|
-
},
|
|
189
|
-
},
|
|
190
|
-
});
|
|
191
|
-
const stakeRefundRecord = await Refund.findOne({
|
|
192
|
-
where: { subscription_id: subscription.id, status: 'succeeded', type: 'stake_return' },
|
|
193
|
-
});
|
|
194
|
-
if (stakeRefundRecord) {
|
|
195
|
-
list.unshift({
|
|
196
|
-
id: address as string,
|
|
197
|
-
status: 'paid',
|
|
198
|
-
description: 'Return Subscription staking',
|
|
199
|
-
billing_reason: 'subscription_create',
|
|
200
|
-
total: stakeRefundRecord.amount,
|
|
201
|
-
amount_due: '0',
|
|
202
|
-
amount_paid: stakeRefundRecord.amount,
|
|
203
|
-
amount_remaining: '0',
|
|
204
|
-
created_at: stakeRefundRecord.created_at,
|
|
205
|
-
updated_at: stakeRefundRecord.updated_at,
|
|
206
|
-
currency_id: stakeRefundRecord.currency_id,
|
|
207
|
-
customer_id: stakeRefundRecord.customer_id,
|
|
208
|
-
subscription_id: subscription.id,
|
|
209
|
-
period_start: subscription.current_period_start,
|
|
210
|
-
period_end: subscription.current_period_end,
|
|
211
|
-
paid: true,
|
|
212
|
-
...pick(firstInvoice, ['number', 'auto_advance']),
|
|
213
|
-
// @ts-ignore
|
|
214
|
-
paymentCurrency: currency,
|
|
215
|
-
paymentMethod: method,
|
|
216
|
-
customer,
|
|
217
|
-
metadata: {
|
|
218
|
-
payment_details: {
|
|
219
|
-
arcblock: {
|
|
220
|
-
tx_hash: stakeRefundRecord?.payment_details?.arcblock?.tx_hash,
|
|
221
|
-
payer: stakeRefundRecord?.payment_details?.arcblock?.payer,
|
|
222
|
-
},
|
|
223
|
-
},
|
|
224
|
-
},
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
}
|
|
135
|
+
const stakingInvoices = await getStakingInvoices(subscription);
|
|
136
|
+
let returnStakeInvoices: any[] = [];
|
|
137
|
+
if (include_return_staking) {
|
|
138
|
+
returnStakeInvoices = await getReturnStakeInvoices(subscription);
|
|
228
139
|
}
|
|
140
|
+
invoices = [...(stakingInvoices || []), ...(returnStakeInvoices || []), ...list]
|
|
141
|
+
.filter(Boolean)
|
|
142
|
+
.sort((a, b) =>
|
|
143
|
+
query.o === 'asc'
|
|
144
|
+
? new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
|
|
145
|
+
: new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
|
|
146
|
+
);
|
|
229
147
|
}
|
|
230
148
|
} catch (err) {
|
|
231
149
|
console.error('Failed to include staking record in invoice list', err);
|
|
232
150
|
}
|
|
233
151
|
}
|
|
234
152
|
|
|
235
|
-
res.json({ count, list, subscription, paging: { page, pageSize } });
|
|
153
|
+
res.json({ count, list: invoices, subscription, paging: { page, pageSize } });
|
|
236
154
|
} catch (err) {
|
|
237
155
|
console.error(err);
|
|
238
156
|
res.json({ count: 0, list: [], paging: { page, pageSize } });
|
|
@@ -8,7 +8,12 @@ import uniq from 'lodash/uniq';
|
|
|
8
8
|
|
|
9
9
|
import { literal, OrderItem } from 'sequelize';
|
|
10
10
|
import { createEvent } from '../libs/audit';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
ensureStripeCustomer,
|
|
13
|
+
ensureStripePaymentCustomer,
|
|
14
|
+
ensureStripePrice,
|
|
15
|
+
ensureStripeSubscription,
|
|
16
|
+
} from '../integrations/stripe/resource';
|
|
12
17
|
import { createListParamSchema, getWhereFromKvQuery, getWhereFromQuery, MetadataSchema } from '../libs/api';
|
|
13
18
|
import dayjs from '../libs/dayjs';
|
|
14
19
|
import logger from '../libs/logger';
|
|
@@ -258,7 +263,8 @@ router.put('/:id/cancel', authPortal, async (req, res) => {
|
|
|
258
263
|
return res.status(400).json({ error: `comment invalid: ${commentError.message}` });
|
|
259
264
|
}
|
|
260
265
|
|
|
261
|
-
const
|
|
266
|
+
const requestByAdmin = ['owner', 'admin'].includes(req.user?.role as string);
|
|
267
|
+
const slashStake = requestByAdmin && req.body?.staking === 'slash';
|
|
262
268
|
|
|
263
269
|
if (slashStake) {
|
|
264
270
|
const { error: slashReasonError } = SlashStakeSchema.validate(req.body?.slashReason);
|
|
@@ -292,7 +298,6 @@ router.put('/:id/cancel', authPortal, async (req, res) => {
|
|
|
292
298
|
}
|
|
293
299
|
|
|
294
300
|
let canReturnStake = false;
|
|
295
|
-
const requestByAdmin = ['owner', 'admin'].includes(req.user?.role as string);
|
|
296
301
|
if ((requestByAdmin && staking === 'proration') || req.body?.cancel_from === 'customer') {
|
|
297
302
|
canReturnStake = true;
|
|
298
303
|
}
|
|
@@ -680,11 +685,11 @@ const updateSchema = Joi.object<{
|
|
|
680
685
|
.items(
|
|
681
686
|
Joi.object({
|
|
682
687
|
name: Joi.string().optional(),
|
|
683
|
-
color: Joi.string().
|
|
684
|
-
variant: Joi.string().
|
|
688
|
+
color: Joi.string().valid('primary', 'secondary', 'success', 'error', 'warning').optional(),
|
|
689
|
+
variant: Joi.string().valid('text', 'contained', 'outlined').optional(),
|
|
685
690
|
text: Joi.object().required(),
|
|
686
691
|
link: Joi.string().uri().required(),
|
|
687
|
-
type: Joi.string().
|
|
692
|
+
type: Joi.string().valid('notification', 'custom').optional(),
|
|
688
693
|
triggerEvents: Joi.array().items(Joi.string()).optional(),
|
|
689
694
|
})
|
|
690
695
|
)
|
|
@@ -1263,11 +1268,6 @@ router.get('/:id/proration', authPortal, async (req, res) => {
|
|
|
1263
1268
|
return res.status(400).json({ error: 'Subscription is not active' });
|
|
1264
1269
|
}
|
|
1265
1270
|
|
|
1266
|
-
const paymentMethod = await PaymentMethod.findByPk(subscription.default_payment_method_id);
|
|
1267
|
-
if (paymentMethod?.type === 'stripe') {
|
|
1268
|
-
return res.status(400).json({ error: 'Not supported for subscriptions paid with stripe' });
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
1271
|
const invoice = await Invoice.findOne({
|
|
1272
1272
|
where: {
|
|
1273
1273
|
subscription_id: subscription.id,
|
|
@@ -1278,16 +1278,23 @@ router.get('/:id/proration', authPortal, async (req, res) => {
|
|
|
1278
1278
|
});
|
|
1279
1279
|
|
|
1280
1280
|
const anchor = req.query.time ? dayjs(req.query.time as any).unix() : dayjs().unix();
|
|
1281
|
-
const result = await getSubscriptionRefundSetup(subscription, anchor);
|
|
1281
|
+
const result = await getSubscriptionRefundSetup(subscription, anchor, invoice?.currency_id);
|
|
1282
1282
|
if (result.total === '0') {
|
|
1283
1283
|
return res.json(null);
|
|
1284
1284
|
}
|
|
1285
|
+
const paymentCurrency = await PaymentCurrency.findByPk(result.lastInvoice?.currency_id);
|
|
1286
|
+
const paymentMethod = await PaymentMethod.findByPk(paymentCurrency?.payment_method_id);
|
|
1287
|
+
if (paymentMethod?.type === 'stripe') {
|
|
1288
|
+
return res.status(400).json({ error: 'Not supported for subscriptions paid with stripe' });
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1285
1291
|
return res.json({
|
|
1286
1292
|
total: result.total,
|
|
1287
1293
|
latest: invoice?.total,
|
|
1288
1294
|
unused: result.unused,
|
|
1289
1295
|
used: result.used,
|
|
1290
1296
|
prorations: result.prorations,
|
|
1297
|
+
paymentCurrency,
|
|
1291
1298
|
});
|
|
1292
1299
|
} catch (err) {
|
|
1293
1300
|
console.error(err);
|
|
@@ -1418,6 +1425,8 @@ router.post('/:id/change-payment', authPortal, async (req, res) => {
|
|
|
1418
1425
|
to_currency: paymentCurrency.id,
|
|
1419
1426
|
from_method: subscription.default_payment_method_id,
|
|
1420
1427
|
to_method: paymentMethod.id,
|
|
1428
|
+
// @ts-ignore
|
|
1429
|
+
from_payment_details: subscription.payment_details?.[previousPaymentMethod?.type],
|
|
1421
1430
|
},
|
|
1422
1431
|
});
|
|
1423
1432
|
|
|
@@ -1444,9 +1453,32 @@ router.post('/:id/change-payment', authPortal, async (req, res) => {
|
|
|
1444
1453
|
const client = paymentMethod.getStripeClient();
|
|
1445
1454
|
let exist;
|
|
1446
1455
|
if (subscription.payment_details?.stripe?.subscription_id) {
|
|
1447
|
-
exist = await client.subscriptions.retrieve(subscription.payment_details.stripe.subscription_id
|
|
1456
|
+
exist = await client.subscriptions.retrieve(subscription.payment_details.stripe.subscription_id, {
|
|
1457
|
+
expand: ['latest_invoice.payment_intent', 'pending_setup_intent'],
|
|
1458
|
+
});
|
|
1448
1459
|
}
|
|
1449
1460
|
if (exist) {
|
|
1461
|
+
if (!exist.default_payment_method) {
|
|
1462
|
+
const settings = PaymentMethod.decryptSettings(paymentMethod.settings);
|
|
1463
|
+
stripeContext = {
|
|
1464
|
+
type: 'subscription',
|
|
1465
|
+
id: exist.id,
|
|
1466
|
+
client_secret:
|
|
1467
|
+
// @ts-ignore
|
|
1468
|
+
exist.latest_invoice?.payment_intent?.client_secret || exist.pending_setup_intent?.client_secret,
|
|
1469
|
+
// @ts-ignore
|
|
1470
|
+
intent_type: exist.latest_invoice?.payment_intent ? 'payment_intent' : 'setup_intent',
|
|
1471
|
+
publishable_key: settings.stripe?.publishable_key,
|
|
1472
|
+
status: exist.status,
|
|
1473
|
+
};
|
|
1474
|
+
return res.json({
|
|
1475
|
+
setupIntent,
|
|
1476
|
+
stripeContext,
|
|
1477
|
+
subscription,
|
|
1478
|
+
customer,
|
|
1479
|
+
delegation,
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1450
1482
|
if (exist.status === 'paused') {
|
|
1451
1483
|
await client.subscriptions.resume(exist.id, {
|
|
1452
1484
|
proration_behavior: 'none',
|
|
@@ -1457,6 +1489,7 @@ router.post('/:id/change-payment', authPortal, async (req, res) => {
|
|
|
1457
1489
|
proration_behavior: 'none',
|
|
1458
1490
|
pause_collection: '',
|
|
1459
1491
|
});
|
|
1492
|
+
await Lock.acquire(`${subscription.id}-change-plan`, subscription.current_period_end);
|
|
1460
1493
|
logger.info('stripe subscription updated on subscription payment change', {
|
|
1461
1494
|
subscription: subscription.id,
|
|
1462
1495
|
intent: setupIntent.id,
|
|
@@ -1480,6 +1513,7 @@ router.post('/:id/change-payment', authPortal, async (req, res) => {
|
|
|
1480
1513
|
const settings = PaymentMethod.decryptSettings(paymentMethod.settings);
|
|
1481
1514
|
|
|
1482
1515
|
// changing from crypto to stripe: create/resume stripe subscription, pause crypto subscription
|
|
1516
|
+
const stripeCustomer = await ensureStripePaymentCustomer(subscription, paymentMethod);
|
|
1483
1517
|
const stripeSubscription = await ensureStripeSubscription(
|
|
1484
1518
|
subscription,
|
|
1485
1519
|
paymentMethod,
|
|
@@ -1488,7 +1522,18 @@ router.post('/:id/change-payment', authPortal, async (req, res) => {
|
|
|
1488
1522
|
0,
|
|
1489
1523
|
subscription.current_period_end
|
|
1490
1524
|
);
|
|
1491
|
-
|
|
1525
|
+
if (stripeSubscription) {
|
|
1526
|
+
await subscription.update({
|
|
1527
|
+
payment_details: {
|
|
1528
|
+
...subscription.payment_details,
|
|
1529
|
+
stripe: {
|
|
1530
|
+
customer_id: stripeCustomer.id,
|
|
1531
|
+
subscription_id: stripeSubscription.id,
|
|
1532
|
+
setup_intent_id: stripeSubscription.pending_setup_intent?.id,
|
|
1533
|
+
},
|
|
1534
|
+
},
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1492
1537
|
stripeContext = {
|
|
1493
1538
|
type: 'subscription',
|
|
1494
1539
|
id: stripeSubscription.id,
|
|
@@ -1500,7 +1545,6 @@ router.post('/:id/change-payment', authPortal, async (req, res) => {
|
|
|
1500
1545
|
publishable_key: settings.stripe?.publishable_key,
|
|
1501
1546
|
status: stripeSubscription.status,
|
|
1502
1547
|
};
|
|
1503
|
-
|
|
1504
1548
|
await setupIntent.update({
|
|
1505
1549
|
setup_details: {
|
|
1506
1550
|
stripe: {
|
|
@@ -1519,6 +1563,10 @@ router.post('/:id/change-payment', authPortal, async (req, res) => {
|
|
|
1519
1563
|
userDid: customer!.did,
|
|
1520
1564
|
amount: getFastCheckoutAmount(lineItems, 'subscription', paymentCurrency.id, false),
|
|
1521
1565
|
});
|
|
1566
|
+
if (paymentMethod.type === 'arcblock' && delegation.sufficient) {
|
|
1567
|
+
delegation.sufficient = false;
|
|
1568
|
+
delegation.reason = 'NO_ENOUGH_TOKEN';
|
|
1569
|
+
}
|
|
1522
1570
|
if (delegation.sufficient) {
|
|
1523
1571
|
await setupIntent.update({
|
|
1524
1572
|
status: 'succeeded',
|
|
@@ -1541,7 +1589,7 @@ router.post('/:id/change-payment', authPortal, async (req, res) => {
|
|
|
1541
1589
|
|
|
1542
1590
|
// NOTE: this should only happen when local subscription is updated
|
|
1543
1591
|
// changing from stripe to crypto: pause stripe subscription
|
|
1544
|
-
if (previousPaymentMethod!.type === 'stripe') {
|
|
1592
|
+
if (previousPaymentMethod!.type === 'stripe' && setupIntent.status === 'succeeded') {
|
|
1545
1593
|
const client = await previousPaymentMethod?.getStripeClient();
|
|
1546
1594
|
const stripeSubscriptionId = subscription.payment_details?.stripe?.subscription_id as string;
|
|
1547
1595
|
const result = await client?.subscriptions.update(stripeSubscriptionId, {
|
|
@@ -1569,7 +1617,6 @@ router.post('/:id/change-payment', authPortal, async (req, res) => {
|
|
|
1569
1617
|
return res.status(500).json({ code: err.code, error: err.message });
|
|
1570
1618
|
}
|
|
1571
1619
|
});
|
|
1572
|
-
|
|
1573
1620
|
// FIXME: this should be removed in future
|
|
1574
1621
|
// Clean up subscriptions that have invalid invoices and payments
|
|
1575
1622
|
router.delete('/cleanup', auth, async (req, res) => {
|
|
@@ -1767,4 +1814,6 @@ router.get('/:id/payer-token', authMine, async (req, res) => {
|
|
|
1767
1814
|
const token = await getTokenByAddress(paymentAddress, paymentMethod, paymentCurrency);
|
|
1768
1815
|
return res.json({ token, paymentAddress });
|
|
1769
1816
|
});
|
|
1817
|
+
|
|
1818
|
+
router.get('/:id/change-plan');
|
|
1770
1819
|
export default router;
|
package/blocklet.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.15.
|
|
3
|
+
"version": "1.15.34",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"eject": "vite eject",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"@arcblock/validator": "^1.18.139",
|
|
54
54
|
"@blocklet/js-sdk": "1.16.33-beta-20241031-073543-49b1ff9b",
|
|
55
55
|
"@blocklet/logger": "1.16.33-beta-20241031-073543-49b1ff9b",
|
|
56
|
-
"@blocklet/payment-react": "1.15.
|
|
56
|
+
"@blocklet/payment-react": "1.15.34",
|
|
57
57
|
"@blocklet/sdk": "1.16.33-beta-20241031-073543-49b1ff9b",
|
|
58
58
|
"@blocklet/ui-react": "^2.10.65",
|
|
59
59
|
"@blocklet/uploader": "^0.1.51",
|
|
@@ -120,7 +120,7 @@
|
|
|
120
120
|
"devDependencies": {
|
|
121
121
|
"@abtnode/types": "1.16.33-beta-20241031-073543-49b1ff9b",
|
|
122
122
|
"@arcblock/eslint-config-ts": "^0.3.3",
|
|
123
|
-
"@blocklet/payment-types": "1.15.
|
|
123
|
+
"@blocklet/payment-types": "1.15.34",
|
|
124
124
|
"@types/cookie-parser": "^1.4.7",
|
|
125
125
|
"@types/cors": "^2.8.17",
|
|
126
126
|
"@types/debug": "^4.1.12",
|
|
@@ -165,5 +165,5 @@
|
|
|
165
165
|
"parser": "typescript"
|
|
166
166
|
}
|
|
167
167
|
},
|
|
168
|
-
"gitHead": "
|
|
168
|
+
"gitHead": "95c0d3ddd12382f694014c05015ca46c19424e3e"
|
|
169
169
|
}
|