payment-kit 1.18.48 → 1.18.50
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/libs/invoice.ts +231 -0
- package/api/src/libs/security.ts +2 -1
- package/api/src/queues/payment.ts +0 -2
- package/api/src/queues/subscription.ts +24 -1
- package/api/src/routes/checkout-sessions.ts +1 -1
- package/api/src/routes/connect/change-payment.ts +62 -42
- package/api/src/routes/customers.ts +4 -4
- package/api/src/routes/invoices.ts +75 -2
- package/api/src/routes/payouts.ts +1 -1
- package/api/src/routes/subscriptions.ts +37 -6
- package/blocklet.yml +1 -1
- package/package.json +4 -4
- package/src/components/invoice/action.tsx +47 -9
- package/src/libs/util.ts +1 -1
- package/src/locales/en.tsx +5 -0
- package/src/locales/zh.tsx +5 -0
- package/src/pages/customer/subscription/change-payment.tsx +7 -4
- package/src/pages/customer/subscription/detail.tsx +6 -3
package/api/src/libs/invoice.ts
CHANGED
|
@@ -1119,3 +1119,234 @@ export async function retryUncollectibleInvoices(options: {
|
|
|
1119
1119
|
logger.info('Released retry uncollectible lock', { lockKey });
|
|
1120
1120
|
}
|
|
1121
1121
|
}
|
|
1122
|
+
|
|
1123
|
+
/**
|
|
1124
|
+
* Migrate billing when a subscription's payment method changes.
|
|
1125
|
+
* Steps:
|
|
1126
|
+
* 1. Check if all subscription items are prepaid (licensed recurring).
|
|
1127
|
+
* 2. If so, check if there is an unpaid invoice for the current period with the old payment method.
|
|
1128
|
+
* 3. If such an invoice exists, void the old invoice and cancel its payment intent if present.
|
|
1129
|
+
* 4. Create a new invoice for the current period with the new payment method.
|
|
1130
|
+
* 5. If any step fails, throw an error and log details.
|
|
1131
|
+
*
|
|
1132
|
+
* @param subscription Subscription instance
|
|
1133
|
+
* @param oldCurrencyId Old payment currency ID
|
|
1134
|
+
* @param newCurrencyId New payment currency ID
|
|
1135
|
+
* @returns Migration result: { migrated, oldInvoice, newInvoice }
|
|
1136
|
+
*/
|
|
1137
|
+
export const migrateSubscriptionPaymentMethodInvoice = async (
|
|
1138
|
+
subscription: Subscription,
|
|
1139
|
+
oldCurrencyId: string,
|
|
1140
|
+
newCurrencyId: string
|
|
1141
|
+
) => {
|
|
1142
|
+
// 1. Check if all subscription items are prepaid (licensed recurring)
|
|
1143
|
+
const subscriptionItems = await SubscriptionItem.findAll({
|
|
1144
|
+
where: { subscription_id: subscription.id },
|
|
1145
|
+
include: [{ model: Price, as: 'price' }],
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
const subscriptionItemsExpanded = await Price.expand(subscriptionItems.map((x) => x.toJSON()));
|
|
1149
|
+
const isPrepaid = subscriptionItemsExpanded.every((item: TLineItemExpanded) => {
|
|
1150
|
+
const price = getSubscriptionItemPrice(item);
|
|
1151
|
+
return price.type === 'recurring' && price.recurring?.usage_type === 'licensed';
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
if (!isPrepaid) {
|
|
1155
|
+
logger.info('Skip billing migration for non-prepaid items', {
|
|
1156
|
+
subscriptionId: subscription.id,
|
|
1157
|
+
});
|
|
1158
|
+
return { migrated: false, reason: 'has_non_prepaid_items' };
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// 2. Find unpaid invoice for the current period with the old payment method
|
|
1162
|
+
const currentPeriodInvoice = await Invoice.findOne({
|
|
1163
|
+
where: {
|
|
1164
|
+
subscription_id: subscription.id,
|
|
1165
|
+
billing_reason: 'subscription_cycle',
|
|
1166
|
+
status: ['open', 'uncollectible'],
|
|
1167
|
+
currency_id: oldCurrencyId,
|
|
1168
|
+
period_start: {
|
|
1169
|
+
[Op.gte]: subscription.current_period_start,
|
|
1170
|
+
[Op.lt]: subscription.current_period_end,
|
|
1171
|
+
},
|
|
1172
|
+
},
|
|
1173
|
+
order: [['created_at', 'DESC']],
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
let voidedInvoice: Invoice | null = null;
|
|
1177
|
+
|
|
1178
|
+
if (!currentPeriodInvoice) {
|
|
1179
|
+
voidedInvoice = await Invoice.findOne({
|
|
1180
|
+
where: {
|
|
1181
|
+
subscription_id: subscription.id,
|
|
1182
|
+
billing_reason: 'subscription_cycle',
|
|
1183
|
+
status: 'void',
|
|
1184
|
+
currency_id: oldCurrencyId,
|
|
1185
|
+
period_start: {
|
|
1186
|
+
[Op.gte]: subscription.current_period_start,
|
|
1187
|
+
[Op.lt]: subscription.current_period_end,
|
|
1188
|
+
},
|
|
1189
|
+
},
|
|
1190
|
+
order: [['created_at', 'DESC']],
|
|
1191
|
+
});
|
|
1192
|
+
if (!voidedInvoice) {
|
|
1193
|
+
logger.info('Skip billing migration for no unpaid invoice', {
|
|
1194
|
+
subscriptionId: subscription.id,
|
|
1195
|
+
periodStart: subscription.current_period_start,
|
|
1196
|
+
periodEnd: subscription.current_period_end,
|
|
1197
|
+
oldCurrencyId,
|
|
1198
|
+
});
|
|
1199
|
+
return { migrated: false, reason: 'no_unpaid_invoice' };
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// 3. Get old and new payment method/currency
|
|
1204
|
+
const oldPaymentCurrency = await PaymentCurrency.findByPk(oldCurrencyId);
|
|
1205
|
+
if (!oldPaymentCurrency) {
|
|
1206
|
+
throw new Error(`Payment currency ${oldCurrencyId} not found`);
|
|
1207
|
+
}
|
|
1208
|
+
const oldPaymentMethod = await PaymentMethod.findByPk(oldPaymentCurrency.payment_method_id);
|
|
1209
|
+
if (!oldPaymentMethod) {
|
|
1210
|
+
throw new Error(`Payment method for currency ${oldCurrencyId} not found`);
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
const newPaymentCurrency = await PaymentCurrency.findByPk(newCurrencyId);
|
|
1214
|
+
if (!newPaymentCurrency) {
|
|
1215
|
+
throw new Error(`Payment currency ${newCurrencyId} not found`);
|
|
1216
|
+
}
|
|
1217
|
+
const newPaymentMethod = await PaymentMethod.findByPk(newPaymentCurrency.payment_method_id);
|
|
1218
|
+
if (!newPaymentMethod) {
|
|
1219
|
+
throw new Error(`Payment method for currency ${newCurrencyId} not found`);
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// 4. Stripe payment method is not supported for migration
|
|
1223
|
+
if (newPaymentMethod.type === 'stripe') {
|
|
1224
|
+
logger.info('Skip billing migration for stripe payment method', {
|
|
1225
|
+
subscriptionId: subscription.id,
|
|
1226
|
+
});
|
|
1227
|
+
return { migrated: false, reason: 'stripe_payment_method' };
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
try {
|
|
1231
|
+
const customer = await Customer.findByPk(subscription.customer_id);
|
|
1232
|
+
if (!customer) {
|
|
1233
|
+
throw new Error(`Customer ${subscription.customer_id} not found`);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
const cancelOldInvoice = async (invoice: Invoice) => {
|
|
1237
|
+
try {
|
|
1238
|
+
if (invoice.payment_intent_id) {
|
|
1239
|
+
const paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
|
|
1240
|
+
if (paymentIntent && paymentIntent.status !== 'canceled') {
|
|
1241
|
+
await paymentIntent.update({
|
|
1242
|
+
status: 'canceled',
|
|
1243
|
+
canceled_at: dayjs().unix(),
|
|
1244
|
+
cancellation_reason: 'void_invoice',
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
if (oldPaymentMethod.type === 'stripe' && invoice.metadata?.stripe_id) {
|
|
1250
|
+
const client = oldPaymentMethod.getStripeClient();
|
|
1251
|
+
await client.invoices.voidInvoice(invoice.metadata.stripe_id);
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
await invoice.update({
|
|
1255
|
+
status: 'void',
|
|
1256
|
+
status_transitions: {
|
|
1257
|
+
...(invoice.status_transitions || {}),
|
|
1258
|
+
voided_at: dayjs().unix(),
|
|
1259
|
+
},
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
logger.info('Successfully voided old invoice for payment method change', {
|
|
1263
|
+
subscriptionId: subscription.id,
|
|
1264
|
+
oldInvoice: invoice.id,
|
|
1265
|
+
oldCurrency: oldCurrencyId,
|
|
1266
|
+
newCurrency: newCurrencyId,
|
|
1267
|
+
});
|
|
1268
|
+
} catch (error) {
|
|
1269
|
+
logger.error('Failed to void old invoice', {
|
|
1270
|
+
subscription: subscription.id,
|
|
1271
|
+
invoiceId: invoice.id,
|
|
1272
|
+
error: error.message,
|
|
1273
|
+
});
|
|
1274
|
+
throw error;
|
|
1275
|
+
}
|
|
1276
|
+
};
|
|
1277
|
+
|
|
1278
|
+
const createNewInvoice = async () => {
|
|
1279
|
+
const preInvoice = currentPeriodInvoice || voidedInvoice;
|
|
1280
|
+
if (!preInvoice) {
|
|
1281
|
+
throw new Error('No unpaid invoice found');
|
|
1282
|
+
}
|
|
1283
|
+
const metadata: Record<string, any> = {
|
|
1284
|
+
prev_invoice_id: preInvoice.id,
|
|
1285
|
+
};
|
|
1286
|
+
const amount = getSubscriptionCycleAmount(subscriptionItemsExpanded, newCurrencyId);
|
|
1287
|
+
|
|
1288
|
+
const { invoice } = await ensureInvoiceAndItems({
|
|
1289
|
+
customer,
|
|
1290
|
+
currency: newPaymentCurrency,
|
|
1291
|
+
subscription,
|
|
1292
|
+
trialing: subscription.status === 'trialing',
|
|
1293
|
+
metered: false,
|
|
1294
|
+
lineItems: subscriptionItemsExpanded,
|
|
1295
|
+
applyCredit: false,
|
|
1296
|
+
props: {
|
|
1297
|
+
status: 'open',
|
|
1298
|
+
total: amount.total,
|
|
1299
|
+
livemode: subscription.livemode,
|
|
1300
|
+
description: 'Subscription cycle',
|
|
1301
|
+
statement_descriptor: preInvoice.statement_descriptor,
|
|
1302
|
+
period_start: preInvoice.period_start,
|
|
1303
|
+
period_end: preInvoice.period_end,
|
|
1304
|
+
auto_advance: true,
|
|
1305
|
+
billing_reason: 'subscription_cycle',
|
|
1306
|
+
currency_id: newCurrencyId,
|
|
1307
|
+
default_payment_method_id: newPaymentMethod.id,
|
|
1308
|
+
custom_fields: preInvoice.custom_fields || [],
|
|
1309
|
+
footer: preInvoice.footer || '',
|
|
1310
|
+
payment_settings: subscription.payment_settings,
|
|
1311
|
+
metadata,
|
|
1312
|
+
} as Invoice,
|
|
1313
|
+
});
|
|
1314
|
+
return invoice;
|
|
1315
|
+
};
|
|
1316
|
+
|
|
1317
|
+
// 5. Cancel old invoice, then create new invoice
|
|
1318
|
+
if (currentPeriodInvoice) {
|
|
1319
|
+
await cancelOldInvoice(currentPeriodInvoice);
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
const invoice = await createNewInvoice();
|
|
1323
|
+
if (invoice) {
|
|
1324
|
+
await emitAsync('invoice.queued', invoice.id, { invoiceId: invoice.id, retryOnError: true }, { sync: false });
|
|
1325
|
+
logger.info('Successfully queued new invoice for payment method change', {
|
|
1326
|
+
subscriptionId: subscription.id,
|
|
1327
|
+
invoiceId: invoice.id,
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
logger.info('Successfully migrated invoice for payment method change', {
|
|
1332
|
+
subscriptionId: subscription.id,
|
|
1333
|
+
oldInvoiceId: currentPeriodInvoice?.id || voidedInvoice?.id,
|
|
1334
|
+
oldCurrency: oldCurrencyId,
|
|
1335
|
+
newCurrency: newCurrencyId,
|
|
1336
|
+
newInvoiceId: invoice.id,
|
|
1337
|
+
});
|
|
1338
|
+
return {
|
|
1339
|
+
migrated: true,
|
|
1340
|
+
oldInvoice: currentPeriodInvoice,
|
|
1341
|
+
newInvoice: invoice,
|
|
1342
|
+
};
|
|
1343
|
+
} catch (error) {
|
|
1344
|
+
logger.error('Failed to migrate invoice for payment method change', {
|
|
1345
|
+
subscriptionId: subscription.id,
|
|
1346
|
+
oldCurrencyId,
|
|
1347
|
+
newCurrencyId,
|
|
1348
|
+
error: error.message,
|
|
1349
|
+
});
|
|
1350
|
+
throw error;
|
|
1351
|
+
}
|
|
1352
|
+
};
|
package/api/src/libs/security.ts
CHANGED
|
@@ -77,9 +77,10 @@ export function authenticate<T extends Model>({ component, roles, record, mine,
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
if (req.headers['x-user-did']) {
|
|
80
|
+
const role = (<string>req.headers['x-user-role'] || '').replace('blocklet-', '');
|
|
80
81
|
req.user = {
|
|
81
82
|
did: <string>req.headers['x-user-did'],
|
|
82
|
-
role
|
|
83
|
+
role,
|
|
83
84
|
provider: <string>req.headers['x-user-provider'],
|
|
84
85
|
fullName: decodeURIComponent(<string>req.headers['x-user-fullname']),
|
|
85
86
|
walletOS: <string>req.headers['x-user-wallet-os'],
|
|
@@ -846,10 +846,8 @@ export const handlePayment = async (job: PaymentJob) => {
|
|
|
846
846
|
wallet,
|
|
847
847
|
delegator: result.delegator,
|
|
848
848
|
});
|
|
849
|
-
logger.info('PaymentIntent signed', { signed });
|
|
850
849
|
// @ts-ignore
|
|
851
850
|
const { buffer } = await client.encodeTransferV2Tx({ tx: signed });
|
|
852
|
-
logger.info('PaymentIntent buffer', { buffer, gas: getGasPayerExtra(buffer) });
|
|
853
851
|
const txHash = await client.sendTransferV2Tx(
|
|
854
852
|
// @ts-ignore
|
|
855
853
|
{ tx: signed, wallet, delegator: result.delegator },
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
shouldCancelSubscription,
|
|
27
27
|
slashOverdraftProtectionStake,
|
|
28
28
|
} from '../libs/subscription';
|
|
29
|
-
import { ensureInvoiceAndItems } from '../libs/invoice';
|
|
29
|
+
import { ensureInvoiceAndItems, migrateSubscriptionPaymentMethodInvoice } from '../libs/invoice';
|
|
30
30
|
import { PaymentCurrency, PaymentIntent, PaymentMethod, Refund, SetupIntent, UsageRecord } from '../store/models';
|
|
31
31
|
import { Customer } from '../store/models/customer';
|
|
32
32
|
import { Invoice } from '../store/models/invoice';
|
|
@@ -1280,5 +1280,28 @@ events.on('setup_intent.succeeded', async (setupIntent: SetupIntent) => {
|
|
|
1280
1280
|
logger.error('create return overdraft protection stake job failed', { error, subscription: subscription.id });
|
|
1281
1281
|
}
|
|
1282
1282
|
}
|
|
1283
|
+
|
|
1284
|
+
try {
|
|
1285
|
+
const migrationResult = await migrateSubscriptionPaymentMethodInvoice(
|
|
1286
|
+
subscription,
|
|
1287
|
+
setupIntent.metadata?.from_currency,
|
|
1288
|
+
setupIntent.metadata?.to_currency
|
|
1289
|
+
);
|
|
1290
|
+
if (migrationResult.migrated) {
|
|
1291
|
+
logger.info('Subscription payment method billing migration completed', {
|
|
1292
|
+
subscription: subscription.id,
|
|
1293
|
+
migrated: migrationResult.migrated,
|
|
1294
|
+
oldInvoice: migrationResult.oldInvoice?.id,
|
|
1295
|
+
newInvoice: migrationResult.newInvoice?.id,
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
} catch (error) {
|
|
1299
|
+
logger.error('Failed to migrate billing for payment method change', {
|
|
1300
|
+
subscription: subscription.id,
|
|
1301
|
+
fromCurrency: setupIntent.metadata?.from_currency,
|
|
1302
|
+
toCurrency: setupIntent.metadata?.to_currency,
|
|
1303
|
+
error,
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1283
1306
|
}
|
|
1284
1307
|
});
|
|
@@ -89,7 +89,7 @@ import { addSubscriptionJob } from '../queues/subscription';
|
|
|
89
89
|
|
|
90
90
|
const router = Router();
|
|
91
91
|
|
|
92
|
-
const user = sessionMiddleware();
|
|
92
|
+
const user = sessionMiddleware({ accessKey: true });
|
|
93
93
|
const auth = authenticate<CheckoutSession>({ component: true, roles: ['owner', 'admin'] });
|
|
94
94
|
|
|
95
95
|
const getPaymentMethods = async (doc: CheckoutSession) => {
|
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
import { ensureStakeInvoice } from '../../libs/invoice';
|
|
14
14
|
import { EVM_CHAIN_TYPES } from '../../libs/constants';
|
|
15
15
|
import logger from '../../libs/logger';
|
|
16
|
+
import { getFastCheckoutAmount } from '../../libs/session';
|
|
17
|
+
import { isDelegationSufficientForPayment } from '../../libs/payment';
|
|
16
18
|
|
|
17
19
|
export default {
|
|
18
20
|
action: 'change-payment',
|
|
@@ -26,41 +28,53 @@ export default {
|
|
|
26
28
|
},
|
|
27
29
|
onConnect: async ({ userDid, userPk, extraParams }: CallbackArgs) => {
|
|
28
30
|
const { subscriptionId } = extraParams;
|
|
29
|
-
const { subscription, paymentMethod, paymentCurrency } = await ensureChangePaymentContext(subscriptionId);
|
|
31
|
+
const { subscription, paymentMethod, paymentCurrency, customer } = await ensureChangePaymentContext(subscriptionId);
|
|
30
32
|
|
|
31
33
|
const claimsList: any[] = [];
|
|
32
34
|
// @ts-ignore
|
|
33
35
|
const items = subscription!.items as TLineItemExpanded[];
|
|
34
36
|
const trialing = true;
|
|
35
37
|
const billingThreshold = Number(subscription.billing_thresholds?.amount_gte || 0);
|
|
38
|
+
const fastCheckoutAmount = getFastCheckoutAmount(items, 'subscription', paymentCurrency.id, false);
|
|
36
39
|
|
|
37
40
|
if (paymentMethod.type === 'arcblock') {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
nonce: `change-method-${subscription.id}`,
|
|
44
|
-
data: getTxMetadata({ subscriptionId: subscription.id }),
|
|
45
|
-
paymentCurrency,
|
|
46
|
-
paymentMethod,
|
|
47
|
-
trialing,
|
|
48
|
-
billingThreshold,
|
|
49
|
-
items,
|
|
50
|
-
}),
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
claimsList.push({
|
|
54
|
-
prepareTx: await getStakeTxClaim({
|
|
55
|
-
userDid,
|
|
56
|
-
userPk,
|
|
57
|
-
paymentCurrency,
|
|
58
|
-
paymentMethod,
|
|
59
|
-
items,
|
|
60
|
-
subscription,
|
|
61
|
-
}),
|
|
41
|
+
const delegation = await isDelegationSufficientForPayment({
|
|
42
|
+
paymentMethod,
|
|
43
|
+
paymentCurrency,
|
|
44
|
+
userDid: customer!.did,
|
|
45
|
+
amount: fastCheckoutAmount,
|
|
62
46
|
});
|
|
47
|
+
const needDelegation = delegation.sufficient === false;
|
|
48
|
+
const noStake = subscription.billing_thresholds?.no_stake;
|
|
49
|
+
if (needDelegation || noStake) {
|
|
50
|
+
claimsList.push({
|
|
51
|
+
signature: await getDelegationTxClaim({
|
|
52
|
+
mode: 'setup',
|
|
53
|
+
userDid,
|
|
54
|
+
userPk,
|
|
55
|
+
nonce: `change-method-${subscription.id}`,
|
|
56
|
+
data: getTxMetadata({ subscriptionId: subscription.id }),
|
|
57
|
+
paymentCurrency,
|
|
58
|
+
paymentMethod,
|
|
59
|
+
trialing,
|
|
60
|
+
billingThreshold,
|
|
61
|
+
items,
|
|
62
|
+
}),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
63
65
|
|
|
66
|
+
if (!noStake) {
|
|
67
|
+
claimsList.push({
|
|
68
|
+
prepareTx: await getStakeTxClaim({
|
|
69
|
+
userDid,
|
|
70
|
+
userPk,
|
|
71
|
+
paymentCurrency,
|
|
72
|
+
paymentMethod,
|
|
73
|
+
items,
|
|
74
|
+
subscription,
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
64
78
|
return claimsList;
|
|
65
79
|
}
|
|
66
80
|
|
|
@@ -95,6 +109,8 @@ export default {
|
|
|
95
109
|
const { setupIntent, subscription, paymentMethod, paymentCurrency, customer } =
|
|
96
110
|
await ensureChangePaymentContext(subscriptionId);
|
|
97
111
|
|
|
112
|
+
const noStake = subscription.billing_thresholds?.no_stake;
|
|
113
|
+
|
|
98
114
|
const result = request?.context?.store?.result || [];
|
|
99
115
|
result.push({
|
|
100
116
|
step,
|
|
@@ -106,7 +122,8 @@ export default {
|
|
|
106
122
|
|
|
107
123
|
// 判断是否为最后一步
|
|
108
124
|
const staking = result.find((x: any) => x.claim?.type === 'prepareTx' && x.claim?.meta?.purpose === 'staking');
|
|
109
|
-
const isFinalStep =
|
|
125
|
+
const isFinalStep =
|
|
126
|
+
(paymentMethod.type === 'arcblock' && (staking || noStake)) || paymentMethod.type !== 'arcblock';
|
|
110
127
|
|
|
111
128
|
if (!isFinalStep) {
|
|
112
129
|
await updateSession({
|
|
@@ -181,25 +198,28 @@ export default {
|
|
|
181
198
|
subscription?.id,
|
|
182
199
|
paymentCurrency.contract
|
|
183
200
|
);
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
201
|
+
if (stakingAmount && stakingAmount !== '0') {
|
|
202
|
+
await ensureStakeInvoice(
|
|
203
|
+
{
|
|
204
|
+
total: stakingAmount,
|
|
205
|
+
description: 'Stake for subscription payment change',
|
|
206
|
+
currency_id: paymentCurrency.id,
|
|
207
|
+
metadata: {
|
|
208
|
+
payment_details: {
|
|
209
|
+
arcblock: {
|
|
210
|
+
tx_hash: paymentDetails?.staking?.tx_hash,
|
|
211
|
+
payer: paymentDetails?.payer,
|
|
212
|
+
address: paymentDetails?.staking?.address,
|
|
213
|
+
},
|
|
195
214
|
},
|
|
196
215
|
},
|
|
197
216
|
},
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
217
|
+
subscription!,
|
|
218
|
+
paymentMethod,
|
|
219
|
+
customer!
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
203
223
|
await afterTxExecution(paymentDetails);
|
|
204
224
|
return { hash: paymentDetails.tx_hash };
|
|
205
225
|
}
|
|
@@ -96,7 +96,7 @@ router.get('/search', auth, async (req, res) => {
|
|
|
96
96
|
});
|
|
97
97
|
|
|
98
98
|
// eslint-disable-next-line consistent-return
|
|
99
|
-
router.get('/me', sessionMiddleware(), async (req, res) => {
|
|
99
|
+
router.get('/me', sessionMiddleware({ accessKey: true }), async (req, res) => {
|
|
100
100
|
if (!req.user) {
|
|
101
101
|
return res.status(403).json({ error: 'Unauthorized' });
|
|
102
102
|
}
|
|
@@ -282,7 +282,7 @@ router.get('/:id/overdue/invoices', sessionMiddleware(), async (req, res) => {
|
|
|
282
282
|
}
|
|
283
283
|
});
|
|
284
284
|
|
|
285
|
-
router.get('/recharge', sessionMiddleware(), async (req, res) => {
|
|
285
|
+
router.get('/recharge', sessionMiddleware({ accessKey: true }), async (req, res) => {
|
|
286
286
|
if (!req.user) {
|
|
287
287
|
return res.status(403).json({ error: 'Unauthorized' });
|
|
288
288
|
}
|
|
@@ -345,7 +345,7 @@ router.get('/recharge', sessionMiddleware(), async (req, res) => {
|
|
|
345
345
|
});
|
|
346
346
|
|
|
347
347
|
// get address token
|
|
348
|
-
router.get('/payer-token', sessionMiddleware(), async (req, res) => {
|
|
348
|
+
router.get('/payer-token', sessionMiddleware({ accessKey: true }), async (req, res) => {
|
|
349
349
|
if (!req.user) {
|
|
350
350
|
return res.status(403).json({ error: 'Unauthorized' });
|
|
351
351
|
}
|
|
@@ -439,7 +439,7 @@ const updatePreferenceSchema = Joi.object({
|
|
|
439
439
|
}).optional(),
|
|
440
440
|
}).unknown(false);
|
|
441
441
|
|
|
442
|
-
router.put('/preference', sessionMiddleware(), async (req, res) => {
|
|
442
|
+
router.put('/preference', sessionMiddleware({ accessKey: true }), async (req, res) => {
|
|
443
443
|
try {
|
|
444
444
|
if (!req.user) {
|
|
445
445
|
return res.status(403).json({ error: 'Unauthorized' });
|
|
@@ -25,6 +25,8 @@ import { getReturnStakeInvoices, getStakingInvoices, retryUncollectibleInvoices
|
|
|
25
25
|
import { CheckoutSession, PaymentLink, TInvoiceExpanded } from '../store/models';
|
|
26
26
|
import { mergePaginate, defaultTimeOrderBy, getCachedOrFetch, DataSource } from '../libs/pagination';
|
|
27
27
|
import logger from '../libs/logger';
|
|
28
|
+
import { returnOverdraftProtectionQueue, returnStakeQueue } from '../queues/subscription';
|
|
29
|
+
import { checkRemainingStake } from '../libs/subscription';
|
|
28
30
|
|
|
29
31
|
const router = Router();
|
|
30
32
|
const authAdmin = authenticate<Subscription>({ component: true, roles: ['owner', 'admin'] });
|
|
@@ -495,6 +497,77 @@ router.get('/retry-uncollectible', authAdmin, async (req, res) => {
|
|
|
495
497
|
}
|
|
496
498
|
});
|
|
497
499
|
|
|
500
|
+
router.get('/:id/return-stake', authAdmin, async (req, res) => {
|
|
501
|
+
const doc = await Invoice.findByPk(req.params.id as string);
|
|
502
|
+
if (!doc) {
|
|
503
|
+
return res.status(404).json({ error: 'Invoice not found' });
|
|
504
|
+
}
|
|
505
|
+
if (!['stake', 'stake_overdraft_protection'].includes(doc.billing_reason)) {
|
|
506
|
+
return res.status(400).json({ error: 'Invoice is not a stake invoice' });
|
|
507
|
+
}
|
|
508
|
+
const paymentCurrency = await PaymentCurrency.findByPk(doc.currency_id);
|
|
509
|
+
if (!paymentCurrency) {
|
|
510
|
+
return res.status(400).json({ error: 'Payment currency not found' });
|
|
511
|
+
}
|
|
512
|
+
const paymentMethod = await PaymentMethod.findByPk(doc.default_payment_method_id);
|
|
513
|
+
if (!paymentMethod) {
|
|
514
|
+
return res.status(400).json({ error: 'Payment method not found' });
|
|
515
|
+
}
|
|
516
|
+
const stakingAddress = doc.metadata?.payment_details?.arcblock?.address;
|
|
517
|
+
if (!stakingAddress) {
|
|
518
|
+
return res.status(400).json({ error: 'Staking address not found' });
|
|
519
|
+
}
|
|
520
|
+
const { staked } = await checkRemainingStake(paymentMethod, paymentCurrency, stakingAddress, '0');
|
|
521
|
+
return res.json(staked);
|
|
522
|
+
});
|
|
523
|
+
router.post('/:id/return-stake', authAdmin, async (req, res) => {
|
|
524
|
+
const doc = await Invoice.findByPk(req.params.id as string);
|
|
525
|
+
if (!doc) {
|
|
526
|
+
return res.status(404).json({ error: 'Invoice not found' });
|
|
527
|
+
}
|
|
528
|
+
if (!['stake', 'stake_overdraft_protection'].includes(doc.billing_reason)) {
|
|
529
|
+
return res.status(400).json({ error: 'Invoice is not a stake invoice' });
|
|
530
|
+
}
|
|
531
|
+
if (doc.status !== 'paid') {
|
|
532
|
+
return res.status(400).json({ error: 'Invoice is not paid' });
|
|
533
|
+
}
|
|
534
|
+
const paymentMethod = await PaymentMethod.findByPk(doc.default_payment_method_id);
|
|
535
|
+
if (!paymentMethod) {
|
|
536
|
+
return res.status(400).json({ error: 'Payment method not found' });
|
|
537
|
+
}
|
|
538
|
+
if (paymentMethod.type !== 'arcblock') {
|
|
539
|
+
return res.status(400).json({ error: 'Can only return stake for arcblock payment method' });
|
|
540
|
+
}
|
|
541
|
+
const subscription = await Subscription.findByPk(doc.subscription_id);
|
|
542
|
+
if (!subscription) {
|
|
543
|
+
return res.status(400).json({ error: 'Subscription not found' });
|
|
544
|
+
}
|
|
545
|
+
try {
|
|
546
|
+
if (doc.billing_reason === 'stake') {
|
|
547
|
+
await returnStakeQueue.pushAndWait({
|
|
548
|
+
id: `return-stake-${subscription.id}-${doc.id}`,
|
|
549
|
+
job: {
|
|
550
|
+
subscriptionId: subscription.id,
|
|
551
|
+
stakingAddress: doc.metadata?.payment_details?.arcblock?.address,
|
|
552
|
+
paymentCurrencyId: doc.currency_id,
|
|
553
|
+
},
|
|
554
|
+
});
|
|
555
|
+
return res.json({ success: true, subscriptionId: subscription.id });
|
|
556
|
+
}
|
|
557
|
+
if (doc.billing_reason === 'stake_overdraft_protection' && subscription.status === 'canceled') {
|
|
558
|
+
await returnOverdraftProtectionQueue.pushAndWait({
|
|
559
|
+
id: `return-overdraft-protection-${subscription.id}`,
|
|
560
|
+
job: { subscriptionId: subscription.id },
|
|
561
|
+
});
|
|
562
|
+
return res.json({ success: true, subscriptionId: subscription.id });
|
|
563
|
+
}
|
|
564
|
+
return res.json({ success: false, error: 'Subscription is not canceled' });
|
|
565
|
+
} catch (error) {
|
|
566
|
+
logger.error('Failed to return stake', { error, subscriptionId: subscription.id, invoiceId: doc.id });
|
|
567
|
+
return res.status(400).json({ error: 'Failed to return stake' });
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
|
|
498
571
|
router.get('/:id', authPortal, async (req, res) => {
|
|
499
572
|
try {
|
|
500
573
|
const doc = (await Invoice.findOne({
|
|
@@ -554,8 +627,8 @@ router.get('/:id', authPortal, async (req, res) => {
|
|
|
554
627
|
const prices = (await Price.findAll()).map((x) => x.toJSON());
|
|
555
628
|
// @ts-ignore
|
|
556
629
|
expandLineItems(json.lines, products, prices);
|
|
557
|
-
if (doc.metadata?.invoice_id) {
|
|
558
|
-
const relatedInvoice = await Invoice.findByPk(doc.metadata.invoice_id, {
|
|
630
|
+
if (doc.metadata?.invoice_id || doc.metadata?.prev_invoice_id) {
|
|
631
|
+
const relatedInvoice = await Invoice.findByPk(doc.metadata.invoice_id || doc.metadata.prev_invoice_id, {
|
|
559
632
|
attributes: ['id', 'number', 'status', 'billing_reason'],
|
|
560
633
|
});
|
|
561
634
|
return res.json({ ...json, relatedInvoice, paymentLink, checkoutSession });
|
|
@@ -117,7 +117,7 @@ const mineRecordPaginationSchema = createListParamSchema<{
|
|
|
117
117
|
currency_id: Joi.string().empty(''),
|
|
118
118
|
status: Joi.string().empty(''),
|
|
119
119
|
});
|
|
120
|
-
router.get('/mine', sessionMiddleware(), async (req, res) => {
|
|
120
|
+
router.get('/mine', sessionMiddleware({ accessKey: true }), async (req, res) => {
|
|
121
121
|
try {
|
|
122
122
|
const {
|
|
123
123
|
page,
|
|
@@ -55,7 +55,11 @@ import { Subscription, TSubscription } from '../store/models/subscription';
|
|
|
55
55
|
import { SubscriptionItem } from '../store/models/subscription-item';
|
|
56
56
|
import type { LineItem, ServiceAction, SubscriptionUpdateItem } from '../store/models/types';
|
|
57
57
|
import { UsageRecord } from '../store/models/usage-record';
|
|
58
|
-
import {
|
|
58
|
+
import {
|
|
59
|
+
cleanupInvoiceAndItems,
|
|
60
|
+
ensureInvoiceAndItems,
|
|
61
|
+
migrateSubscriptionPaymentMethodInvoice,
|
|
62
|
+
} from '../libs/invoice';
|
|
59
63
|
import { createUsageRecordQueryFn } from './usage-records';
|
|
60
64
|
import { SubscriptionWillCanceledSchedule } from '../crons/subscription-will-canceled';
|
|
61
65
|
import { getTokenByAddress } from '../integrations/arcblock/stake';
|
|
@@ -447,7 +451,7 @@ router.get('/:id/recover-info', authPortal, async (req, res) => {
|
|
|
447
451
|
try {
|
|
448
452
|
const { revoked } = await checkRemainingStake(paymentMethod, paymentCurrency, address, '0');
|
|
449
453
|
const cancelReason = doc.cancelation_details?.reason;
|
|
450
|
-
if (revoked && revoked !== '0' && cancelReason === 'stake_revoked') {
|
|
454
|
+
if (revoked && revoked.value !== '0' && cancelReason === 'stake_revoked') {
|
|
451
455
|
needStake = true;
|
|
452
456
|
revokedStake = revoked;
|
|
453
457
|
}
|
|
@@ -496,7 +500,7 @@ router.put('/:id/recover', authPortal, async (req, res) => {
|
|
|
496
500
|
try {
|
|
497
501
|
const { revoked } = await checkRemainingStake(paymentMethod, paymentCurrency, address, '0');
|
|
498
502
|
const cancelReason = doc.cancelation_details?.reason;
|
|
499
|
-
if (revoked && revoked !== '0' && cancelReason === 'stake_revoked') {
|
|
503
|
+
if (revoked && revoked.value !== '0' && cancelReason === 'stake_revoked') {
|
|
500
504
|
return res.json({
|
|
501
505
|
needStake: true,
|
|
502
506
|
subscription: doc,
|
|
@@ -1449,7 +1453,7 @@ router.post('/:id/change-payment', authPortal, async (req, res) => {
|
|
|
1449
1453
|
if (!subscription) {
|
|
1450
1454
|
return res.status(404).json({ error: `Subscription ${req.params.id} not found when change payment` });
|
|
1451
1455
|
}
|
|
1452
|
-
if (subscription.
|
|
1456
|
+
if (['active', 'trialing', 'past_due'].includes(subscription.status) === false) {
|
|
1453
1457
|
return res.status(400).json({ error: `Subscription ${req.params.id} not active when change payment` });
|
|
1454
1458
|
}
|
|
1455
1459
|
const paymentCurrency = await PaymentCurrency.findByPk(req.body.payment_currency);
|
|
@@ -1655,9 +1659,10 @@ router.post('/:id/change-payment', authPortal, async (req, res) => {
|
|
|
1655
1659
|
userDid: customer!.did,
|
|
1656
1660
|
amount: getFastCheckoutAmount(lineItems, 'subscription', paymentCurrency.id, false),
|
|
1657
1661
|
});
|
|
1658
|
-
|
|
1662
|
+
const noStake = subscription.billing_thresholds?.no_stake;
|
|
1663
|
+
if (paymentMethod.type === 'arcblock' && delegation.sufficient && !noStake) {
|
|
1659
1664
|
delegation.sufficient = false;
|
|
1660
|
-
delegation.reason = '
|
|
1665
|
+
delegation.reason = 'WAIT_STAKE';
|
|
1661
1666
|
}
|
|
1662
1667
|
if (delegation.sufficient) {
|
|
1663
1668
|
await setupIntent.update({
|
|
@@ -2216,4 +2221,30 @@ router.get('/:id/unpaid-invoices', authPortal, async (req, res) => {
|
|
|
2216
2221
|
const count = await getSubscriptionUnpaidInvoicesCount(subscription);
|
|
2217
2222
|
return res.json({ count });
|
|
2218
2223
|
});
|
|
2224
|
+
|
|
2225
|
+
router.get('/:id/change-payment/migrate-invoice', auth, async (req, res) => {
|
|
2226
|
+
const subscription = await Subscription.findByPk(req.params.id);
|
|
2227
|
+
if (!subscription) {
|
|
2228
|
+
return res.status(404).json({ error: 'Subscription not found' });
|
|
2229
|
+
}
|
|
2230
|
+
const context = subscription.metadata.changePayment || {};
|
|
2231
|
+
if (!context.setup_intent_id) {
|
|
2232
|
+
return res.status(404).json({ error: 'Subscription change payment context not found' });
|
|
2233
|
+
}
|
|
2234
|
+
const setupIntent = await SetupIntent.findByPk(context.setup_intent_id);
|
|
2235
|
+
if (!setupIntent) {
|
|
2236
|
+
return res.status(404).json({ error: 'Setup intent not found' });
|
|
2237
|
+
}
|
|
2238
|
+
try {
|
|
2239
|
+
const migrationResult = await migrateSubscriptionPaymentMethodInvoice(
|
|
2240
|
+
subscription,
|
|
2241
|
+
setupIntent.metadata?.from_currency,
|
|
2242
|
+
setupIntent.metadata?.to_currency
|
|
2243
|
+
);
|
|
2244
|
+
return res.json(migrationResult);
|
|
2245
|
+
} catch (error) {
|
|
2246
|
+
logger.error(error);
|
|
2247
|
+
return res.status(400).json({ error: error.message });
|
|
2248
|
+
}
|
|
2249
|
+
});
|
|
2219
2250
|
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.18.
|
|
3
|
+
"version": "1.18.50",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"eject": "vite eject",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"@blocklet/did-space-js": "^1.0.56",
|
|
56
56
|
"@blocklet/js-sdk": "^1.16.43",
|
|
57
57
|
"@blocklet/logger": "^1.16.43",
|
|
58
|
-
"@blocklet/payment-react": "1.18.
|
|
58
|
+
"@blocklet/payment-react": "1.18.50",
|
|
59
59
|
"@blocklet/sdk": "^1.16.43",
|
|
60
60
|
"@blocklet/ui-react": "^2.13.54",
|
|
61
61
|
"@blocklet/uploader": "^0.1.93",
|
|
@@ -123,7 +123,7 @@
|
|
|
123
123
|
"devDependencies": {
|
|
124
124
|
"@abtnode/types": "^1.16.43",
|
|
125
125
|
"@arcblock/eslint-config-ts": "^0.3.3",
|
|
126
|
-
"@blocklet/payment-types": "1.18.
|
|
126
|
+
"@blocklet/payment-types": "1.18.50",
|
|
127
127
|
"@types/cookie-parser": "^1.4.7",
|
|
128
128
|
"@types/cors": "^2.8.17",
|
|
129
129
|
"@types/debug": "^4.1.12",
|
|
@@ -169,5 +169,5 @@
|
|
|
169
169
|
"parser": "typescript"
|
|
170
170
|
}
|
|
171
171
|
},
|
|
172
|
-
"gitHead": "
|
|
172
|
+
"gitHead": "1b7529c51c5ef7d65a288e852e971fbfb281a9bb"
|
|
173
173
|
}
|
|
@@ -2,7 +2,7 @@ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
|
2
2
|
import Toast from '@arcblock/ux/lib/Toast';
|
|
3
3
|
import { ConfirmDialog, api, formatError } from '@blocklet/payment-react';
|
|
4
4
|
import type { TInvoiceExpanded } from '@blocklet/payment-types';
|
|
5
|
-
import { useSetState } from 'ahooks';
|
|
5
|
+
import { useRequest, useSetState } from 'ahooks';
|
|
6
6
|
import { useNavigate } from 'react-router-dom';
|
|
7
7
|
import type { LiteralUnion } from 'type-fest';
|
|
8
8
|
|
|
@@ -23,6 +23,10 @@ InvoiceActions.defaultProps = {
|
|
|
23
23
|
mode: 'admin',
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
+
const fetchStakingData = (id: string): Promise<{ value: string }> => {
|
|
27
|
+
return api.get(`/api/invoices/${id}/return-stake`).then((res: any) => res.data);
|
|
28
|
+
};
|
|
29
|
+
|
|
26
30
|
export default function InvoiceActions({ data, variant, onChange, mode }: Props) {
|
|
27
31
|
const { t } = useLocaleContext();
|
|
28
32
|
const navigate = useNavigate();
|
|
@@ -31,11 +35,39 @@ export default function InvoiceActions({ data, variant, onChange, mode }: Props)
|
|
|
31
35
|
loading: false,
|
|
32
36
|
});
|
|
33
37
|
|
|
38
|
+
const isStakeInvoice = ['stake', 'stake_overdraft_protection'].includes(data.billing_reason);
|
|
39
|
+
const hasPaid = data.status === 'paid';
|
|
40
|
+
const isAdmin = mode === 'admin';
|
|
41
|
+
const showReturnStake = isAdmin && isStakeInvoice && hasPaid && data.paymentMethod?.type === 'arcblock';
|
|
42
|
+
|
|
43
|
+
const {
|
|
44
|
+
data: stakeResult = {
|
|
45
|
+
value: '0',
|
|
46
|
+
},
|
|
47
|
+
runAsync: fetchStakeResultAsync,
|
|
48
|
+
} = useRequest(
|
|
49
|
+
() => {
|
|
50
|
+
if (showReturnStake) {
|
|
51
|
+
return fetchStakingData(data.id);
|
|
52
|
+
}
|
|
53
|
+
return Promise.resolve({ value: '0' });
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
manual: true,
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
|
|
34
60
|
const handleAction = async () => {
|
|
35
61
|
try {
|
|
36
62
|
setState({ loading: true });
|
|
37
|
-
|
|
38
|
-
|
|
63
|
+
if (state.action === 'return-stake') {
|
|
64
|
+
const result = await api.post(`/api/invoices/${data.id}/return-stake`).then((res) => res.data);
|
|
65
|
+
if (result.success) {
|
|
66
|
+
Toast.success(t('admin.invoice.returnStake.success'));
|
|
67
|
+
} else {
|
|
68
|
+
Toast.error(result.error);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
39
71
|
onChange(state.action);
|
|
40
72
|
} catch (err) {
|
|
41
73
|
console.error(err);
|
|
@@ -45,8 +77,6 @@ export default function InvoiceActions({ data, variant, onChange, mode }: Props)
|
|
|
45
77
|
}
|
|
46
78
|
};
|
|
47
79
|
|
|
48
|
-
const isAdmin = mode === 'admin';
|
|
49
|
-
|
|
50
80
|
const actions = [
|
|
51
81
|
isAdmin && {
|
|
52
82
|
label: t('admin.invoice.edit'),
|
|
@@ -61,6 +91,14 @@ export default function InvoiceActions({ data, variant, onChange, mode }: Props)
|
|
|
61
91
|
disabled: data.status !== 'draft',
|
|
62
92
|
divider: true,
|
|
63
93
|
},
|
|
94
|
+
showReturnStake &&
|
|
95
|
+
stakeResult &&
|
|
96
|
+
stakeResult.value !== '0' && {
|
|
97
|
+
label: t('admin.invoice.returnStake.title'),
|
|
98
|
+
handler: () => setState({ action: 'return-stake' }),
|
|
99
|
+
color: 'primary',
|
|
100
|
+
divider: true,
|
|
101
|
+
},
|
|
64
102
|
{
|
|
65
103
|
label: t('admin.customer.view'),
|
|
66
104
|
handler: () => {
|
|
@@ -90,13 +128,13 @@ export default function InvoiceActions({ data, variant, onChange, mode }: Props)
|
|
|
90
128
|
|
|
91
129
|
return (
|
|
92
130
|
<ClickBoundary>
|
|
93
|
-
<Actions variant={variant} actions={actions as any} />
|
|
94
|
-
{state.action === '
|
|
131
|
+
<Actions variant={variant} actions={actions as any} onOpenCallback={fetchStakeResultAsync} />
|
|
132
|
+
{state.action === 'return-stake' && (
|
|
95
133
|
<ConfirmDialog
|
|
96
134
|
onConfirm={handleAction}
|
|
97
135
|
onCancel={() => setState({ action: '' })}
|
|
98
|
-
title={t('admin.invoice.
|
|
99
|
-
message={t('admin.invoice.
|
|
136
|
+
title={t('admin.invoice.returnStake.title')}
|
|
137
|
+
message={t('admin.invoice.returnStake.tip')}
|
|
100
138
|
loading={state.loading}
|
|
101
139
|
/>
|
|
102
140
|
)}
|
package/src/libs/util.ts
CHANGED
|
@@ -227,7 +227,7 @@ export const debounce = (fun: Function, wait: number) => {
|
|
|
227
227
|
|
|
228
228
|
export function canChangePaymentMethod(subscription: TSubscriptionExpanded) {
|
|
229
229
|
return (
|
|
230
|
-
['active', 'trialing'].includes(subscription.status) &&
|
|
230
|
+
['active', 'trialing', 'past_due'].includes(subscription.status) &&
|
|
231
231
|
subscription.items.every((x) => getPriceCurrencyOptions(x.price).length > 1)
|
|
232
232
|
);
|
|
233
233
|
}
|
package/src/locales/en.tsx
CHANGED
|
@@ -518,6 +518,11 @@ export default flat({
|
|
|
518
518
|
download: 'Download PDF',
|
|
519
519
|
edit: 'Edit Invoice',
|
|
520
520
|
duplicate: 'Duplicate Invoice',
|
|
521
|
+
returnStake: {
|
|
522
|
+
title: 'Return Stake',
|
|
523
|
+
tip: 'Are you sure you want to return the stake? This action will return the stake to the customer.',
|
|
524
|
+
success: 'Stake return application has been successfully created',
|
|
525
|
+
},
|
|
521
526
|
},
|
|
522
527
|
subscription: {
|
|
523
528
|
view: 'View subscription',
|
package/src/locales/zh.tsx
CHANGED
|
@@ -203,9 +203,12 @@ function CustomerSubscriptionChangePayment({ subscription, customer, onComplete
|
|
|
203
203
|
};
|
|
204
204
|
|
|
205
205
|
const onConfirm = async () => {
|
|
206
|
-
|
|
207
|
-
if (
|
|
208
|
-
|
|
206
|
+
// only check unpaid invoices when overdraft protection is enabled
|
|
207
|
+
if (subscription.overdraft_protection?.enabled) {
|
|
208
|
+
const result = await checkUnpaidInvoices();
|
|
209
|
+
if (result) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
209
212
|
}
|
|
210
213
|
handleSubmit(onSubmit)();
|
|
211
214
|
};
|
|
@@ -239,7 +242,7 @@ function CustomerSubscriptionChangePayment({ subscription, customer, onComplete
|
|
|
239
242
|
currency={findCurrency(settings.paymentMethods, selectedCurrencyId) as TPaymentCurrency}
|
|
240
243
|
trialInDays={0}
|
|
241
244
|
billingThreshold={0}
|
|
242
|
-
showStaking={method.type === 'arcblock'}
|
|
245
|
+
showStaking={method.type === 'arcblock' && !subscription.billing_thresholds?.no_stake}
|
|
243
246
|
/>
|
|
244
247
|
</Stack>
|
|
245
248
|
<Stack direction="column" spacing={2}>
|
|
@@ -516,9 +516,12 @@ export default function CustomerSubscriptionDetail() {
|
|
|
516
516
|
sx={{ color: 'text.link' }}
|
|
517
517
|
size="small"
|
|
518
518
|
onClick={async () => {
|
|
519
|
-
|
|
520
|
-
if (
|
|
521
|
-
|
|
519
|
+
// only check unpaid invoices when overdraft protection is enabled
|
|
520
|
+
if (data?.overdraft_protection?.enabled) {
|
|
521
|
+
const result = await checkUnpaidInvoices();
|
|
522
|
+
if (result) {
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
522
525
|
}
|
|
523
526
|
navigate(`/customer/subscription/${data.id}/change-payment`);
|
|
524
527
|
}}>
|