payment-kit 1.13.284 → 1.13.286
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +1 -0
- package/api/src/integrations/arcblock/stake.ts +16 -11
- package/api/src/libs/subscription.ts +73 -0
- package/api/src/queues/refund.ts +110 -5
- package/api/src/routes/connect/change-payment.ts +10 -4
- package/api/src/routes/connect/change-plan.ts +8 -1
- package/api/src/routes/connect/setup.ts +8 -1
- package/api/src/routes/connect/shared.ts +13 -9
- package/api/src/routes/connect/subscribe.ts +8 -1
- package/api/src/routes/invoices.ts +41 -0
- package/api/src/routes/subscriptions.ts +96 -1
- package/api/src/store/migrations/20240628-stake-return.ts +23 -0
- package/api/src/store/models/refund.ts +6 -0
- package/api/src/store/models/types.ts +2 -1
- package/blocklet.yml +1 -1
- package/package.json +6 -6
- package/src/components/filter-toolbar.tsx +3 -1
- package/src/components/metadata/editor.tsx +16 -2
- package/src/components/metadata/list.tsx +14 -5
- package/src/components/payment-link/chrome.tsx +4 -1
- package/src/components/refund/list.tsx +16 -0
- package/src/components/subscription/actions/cancel.tsx +44 -5
- package/src/components/subscription/actions/index.tsx +1 -0
- package/src/libs/util.ts +9 -0
- package/src/locales/en.tsx +5 -0
- package/src/locales/zh.tsx +5 -0
- package/src/pages/admin/payments/refunds/detail.tsx +1 -0
package/.eslintrc.js
CHANGED
|
@@ -143,10 +143,7 @@ export async function checkStakeRevokeTx() {
|
|
|
143
143
|
return;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
const address =
|
|
147
|
-
if (t.tx.itxJson.address !== address) {
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
146
|
+
const { address } = t.tx.itxJson;
|
|
150
147
|
|
|
151
148
|
// Check related subscriptions in the stake
|
|
152
149
|
const subscriptions = await Subscription.findAll({
|
|
@@ -163,13 +160,21 @@ export async function checkStakeRevokeTx() {
|
|
|
163
160
|
});
|
|
164
161
|
|
|
165
162
|
const { state: stake } = await client.getStakeState({ address });
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
163
|
+
if (stake.nonce) {
|
|
164
|
+
subscriptions
|
|
165
|
+
.filter((s) => s.isActive())
|
|
166
|
+
.forEach((s) => {
|
|
167
|
+
events.emit('customer.stake.revoked', { subscriptionId: s.id, tx: t });
|
|
168
|
+
});
|
|
169
|
+
} else {
|
|
170
|
+
const data = JSON.parse(stake.data?.value || '{}');
|
|
171
|
+
subscriptions
|
|
172
|
+
.filter((s) => s.isActive())
|
|
173
|
+
.filter((s) => data[s.id])
|
|
174
|
+
.forEach((s) => {
|
|
175
|
+
events.emit('customer.stake.revoked', { subscriptionId: s.id, tx: t });
|
|
176
|
+
});
|
|
177
|
+
}
|
|
173
178
|
})
|
|
174
179
|
);
|
|
175
180
|
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
PaymentMethod,
|
|
16
16
|
Price,
|
|
17
17
|
PriceRecurring,
|
|
18
|
+
Refund,
|
|
18
19
|
Subscription,
|
|
19
20
|
SubscriptionItem,
|
|
20
21
|
SubscriptionUpdateItem,
|
|
@@ -632,3 +633,75 @@ export async function onSubscriptionUpdateConnected(subscriptionId: string) {
|
|
|
632
633
|
});
|
|
633
634
|
}
|
|
634
635
|
}
|
|
636
|
+
|
|
637
|
+
export async function getRemainingStakes(subscriptionIds: string[], subscriptionInitStakes: { [key: string]: string }) {
|
|
638
|
+
if (!subscriptionIds || subscriptionIds.length === 0) {
|
|
639
|
+
return '0';
|
|
640
|
+
}
|
|
641
|
+
let total = new BN('0');
|
|
642
|
+
await Promise.all(
|
|
643
|
+
subscriptionIds.map(async (subscriptionId) => {
|
|
644
|
+
const refund = await Refund.findOne({
|
|
645
|
+
where: { subscription_id: subscriptionId, status: 'succeeded', type: 'stake_return' },
|
|
646
|
+
});
|
|
647
|
+
if (!refund) {
|
|
648
|
+
// this subscription not return stake
|
|
649
|
+
total = total.add(new BN(subscriptionInitStakes[subscriptionId] || '0'));
|
|
650
|
+
}
|
|
651
|
+
})
|
|
652
|
+
);
|
|
653
|
+
return total.toString();
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
export async function getSubscriptionStakeReturnSetup(
|
|
657
|
+
subscription: Subscription,
|
|
658
|
+
address: string,
|
|
659
|
+
paymentMethod: PaymentMethod
|
|
660
|
+
) {
|
|
661
|
+
const client = paymentMethod.getOcapClient();
|
|
662
|
+
const { state } = await client.getStakeState({ address });
|
|
663
|
+
const currency = await PaymentCurrency.findByPk(subscription.currency_id);
|
|
664
|
+
if (!state.tokens || !currency) {
|
|
665
|
+
return {
|
|
666
|
+
total: '0',
|
|
667
|
+
return_amount: '0',
|
|
668
|
+
sender: '',
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
const total = new BN(state.tokens.find((x: any) => x.address === currency.contract)?.value || '0');
|
|
672
|
+
const [summary] = await Invoice.getUncollectibleAmount({
|
|
673
|
+
subscriptionId: subscription.id,
|
|
674
|
+
currencyId: subscription.currency_id,
|
|
675
|
+
customerId: subscription.customer_id,
|
|
676
|
+
});
|
|
677
|
+
const subscriptionInitStakes = JSON.parse(state.data?.value || '{}');
|
|
678
|
+
const initStake = subscriptionInitStakes[subscription.id];
|
|
679
|
+
const uncollectibleAmountBN = new BN(summary?.[subscription.currency_id] || '0');
|
|
680
|
+
const lastInvoice = await Invoice.findByPk(subscription.latest_invoice_id);
|
|
681
|
+
if (state.nonce) {
|
|
682
|
+
const returnStake = total.sub(uncollectibleAmountBN);
|
|
683
|
+
return {
|
|
684
|
+
total: total.toString(),
|
|
685
|
+
return_amount: returnStake.lt(new BN(0)) ? '0' : returnStake.toString(),
|
|
686
|
+
sender: state.sender,
|
|
687
|
+
lastInvoice,
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
const getReturnState = async () => {
|
|
691
|
+
const initStakeBN = new BN(initStake);
|
|
692
|
+
const otherSubscriptionIds = Object.keys(subscriptionInitStakes).filter(
|
|
693
|
+
(k) => k !== 'appId' && k !== subscription.id
|
|
694
|
+
);
|
|
695
|
+
const remainingStakes = await getRemainingStakes(otherSubscriptionIds, subscriptionInitStakes);
|
|
696
|
+
const actualStakeBN = new BN(total).sub(new BN(remainingStakes));
|
|
697
|
+
const minStakeBN = initStakeBN.lt(actualStakeBN) ? initStakeBN : actualStakeBN;
|
|
698
|
+
return minStakeBN.sub(uncollectibleAmountBN);
|
|
699
|
+
};
|
|
700
|
+
const returnStake = await getReturnState();
|
|
701
|
+
return {
|
|
702
|
+
total: initStake,
|
|
703
|
+
return_amount: returnStake.lt(new BN(0)) ? '0' : returnStake.toString(),
|
|
704
|
+
sender: state.sender,
|
|
705
|
+
lastInvoice,
|
|
706
|
+
};
|
|
707
|
+
}
|
package/api/src/queues/refund.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { toStakeAddress } from '@arcblock/did-util';
|
|
2
|
+
|
|
1
3
|
import { sendErc20ToUser } from '../integrations/ethereum/token';
|
|
2
4
|
import { wallet } from '../libs/auth';
|
|
3
5
|
import CustomError from '../libs/error';
|
|
@@ -93,11 +95,6 @@ export const handleRefund = async (job: RefundJob) => {
|
|
|
93
95
|
logger.warn(`PaymentMethod not found: ${paymentCurrency.payment_method_id}`);
|
|
94
96
|
return;
|
|
95
97
|
}
|
|
96
|
-
const supportAutoCharge = await PaymentMethod.supportAutoCharge(paymentCurrency.payment_method_id);
|
|
97
|
-
if (supportAutoCharge === false) {
|
|
98
|
-
logger.warn(`PaymentMethod does not support auto charge: ${paymentCurrency.payment_method_id}`);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
98
|
|
|
102
99
|
const customer = await Customer.findByPk(refund.customer_id);
|
|
103
100
|
if (!customer) {
|
|
@@ -105,6 +102,26 @@ export const handleRefund = async (job: RefundJob) => {
|
|
|
105
102
|
return;
|
|
106
103
|
}
|
|
107
104
|
|
|
105
|
+
if (refund?.type === 'stake_return') {
|
|
106
|
+
handleStakeReturnJob(job, refund, paymentCurrency, paymentMethod, customer);
|
|
107
|
+
} else if (paymentMethod) {
|
|
108
|
+
handleRefundJob(job, refund, paymentCurrency, paymentMethod, customer);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const handleRefundJob = async (
|
|
113
|
+
job: RefundJob,
|
|
114
|
+
refund: Refund,
|
|
115
|
+
paymentCurrency: PaymentCurrency,
|
|
116
|
+
paymentMethod: PaymentMethod,
|
|
117
|
+
customer: Customer
|
|
118
|
+
) => {
|
|
119
|
+
const supportAutoCharge = await PaymentMethod.supportAutoCharge(paymentCurrency.payment_method_id);
|
|
120
|
+
if (supportAutoCharge === false) {
|
|
121
|
+
logger.warn(`PaymentMethod does not support auto charge: ${paymentCurrency.payment_method_id}`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
108
125
|
// try refund transfer and reschedule on error
|
|
109
126
|
logger.info('Refund transfer attempt', { id: refund.id, attempt: refund.attempt_count });
|
|
110
127
|
let result;
|
|
@@ -223,6 +240,94 @@ export const handleRefund = async (job: RefundJob) => {
|
|
|
223
240
|
}
|
|
224
241
|
};
|
|
225
242
|
|
|
243
|
+
const handleStakeReturnJob = async (
|
|
244
|
+
job: RefundJob,
|
|
245
|
+
refund: Refund,
|
|
246
|
+
paymentCurrency: PaymentCurrency,
|
|
247
|
+
paymentMethod: PaymentMethod,
|
|
248
|
+
customer: Customer
|
|
249
|
+
) => {
|
|
250
|
+
// try stake return and reschedule on error
|
|
251
|
+
logger.info('Stake return attempt', { id: refund.id, attempt: refund.attempt_count });
|
|
252
|
+
try {
|
|
253
|
+
if (paymentMethod.type === 'arcblock') {
|
|
254
|
+
const arcblockDetail = refund.payment_details?.arcblock;
|
|
255
|
+
if (!arcblockDetail) {
|
|
256
|
+
throw new Error('arcblockDetail info not found');
|
|
257
|
+
}
|
|
258
|
+
const client = paymentMethod.getOcapClient();
|
|
259
|
+
const signed = await client.signSlashStakeTx({
|
|
260
|
+
tx: {
|
|
261
|
+
itx: {
|
|
262
|
+
address: toStakeAddress(customer.did, wallet.address),
|
|
263
|
+
outputs: [
|
|
264
|
+
{
|
|
265
|
+
owner: arcblockDetail?.receiver,
|
|
266
|
+
tokens: [{ address: paymentCurrency.contract, value: refund.amount }],
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
message: 'stake_return_on_subscription_cancel',
|
|
270
|
+
data: {
|
|
271
|
+
typeUrl: 'json',
|
|
272
|
+
// @ts-ignore
|
|
273
|
+
value: {
|
|
274
|
+
appId: wallet.address,
|
|
275
|
+
reason: 'subscription_cancel',
|
|
276
|
+
subscriptionId: refund.subscription_id,
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
wallet,
|
|
282
|
+
});
|
|
283
|
+
// @ts-ignore
|
|
284
|
+
const { buffer } = await client.encodeSlashStakeTx({ tx: signed });
|
|
285
|
+
// @ts-ignore
|
|
286
|
+
const txHash = await client.sendSlashStakeTx({ tx: signed, wallet }, getGasPayerExtra(buffer));
|
|
287
|
+
logger.info('stake return done', { id: refund.id, txHash });
|
|
288
|
+
await refund.update({
|
|
289
|
+
status: 'succeeded',
|
|
290
|
+
last_attempt_error: null,
|
|
291
|
+
payment_details: {
|
|
292
|
+
arcblock: {
|
|
293
|
+
tx_hash: txHash,
|
|
294
|
+
payer: wallet.address,
|
|
295
|
+
type: 'stake_return',
|
|
296
|
+
receiver: arcblockDetail?.receiver,
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
} catch (err: any) {
|
|
302
|
+
logger.error('stake return failed', { error: err, id: refund.id });
|
|
303
|
+
|
|
304
|
+
const error: PaymentError = {
|
|
305
|
+
type: 'card_error',
|
|
306
|
+
code: err.code,
|
|
307
|
+
message: err.message,
|
|
308
|
+
payment_method_id: paymentMethod.id,
|
|
309
|
+
payment_method_type: paymentMethod.type,
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const updates = await handleRefundFailed(refund, error);
|
|
313
|
+
await refund.update(updates.refund);
|
|
314
|
+
|
|
315
|
+
// reschedule next attempt
|
|
316
|
+
const retryAt = updates.refund.next_attempt;
|
|
317
|
+
if (retryAt) {
|
|
318
|
+
refundQueue.push({
|
|
319
|
+
id: refund.id,
|
|
320
|
+
job: { refundId: refund.id, retryOnError: job.retryOnError },
|
|
321
|
+
runAt: retryAt,
|
|
322
|
+
});
|
|
323
|
+
logger.error('stake return retry scheduled', { id: refund.id, retryAt });
|
|
324
|
+
} else {
|
|
325
|
+
logger.info('stake job deleted since no retry', { id: refund.id });
|
|
326
|
+
refundQueue.delete(refund.id);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
|
|
226
331
|
export const refundQueue = createQueue<RefundJob>({
|
|
227
332
|
name: 'refund',
|
|
228
333
|
onJob: handleRefund,
|
|
@@ -105,9 +105,8 @@ export default {
|
|
|
105
105
|
|
|
106
106
|
onAuth: async ({ request, userDid, userPk, claims, extraParams }: CallbackArgs) => {
|
|
107
107
|
const { subscriptionId } = extraParams;
|
|
108
|
-
const { subscription, setupIntent, paymentCurrency, paymentMethod } =
|
|
109
|
-
subscriptionId
|
|
110
|
-
);
|
|
108
|
+
const { subscription, setupIntent, paymentCurrency, paymentMethod } =
|
|
109
|
+
await ensureChangePaymentContext(subscriptionId);
|
|
111
110
|
|
|
112
111
|
const prepareTxExecution = async () => {
|
|
113
112
|
await subscription?.update({
|
|
@@ -141,7 +140,14 @@ export default {
|
|
|
141
140
|
|
|
142
141
|
if (paymentMethod.type === 'arcblock') {
|
|
143
142
|
await prepareTxExecution();
|
|
144
|
-
const paymentDetails = await executeOcapTransactions(
|
|
143
|
+
const paymentDetails = await executeOcapTransactions(
|
|
144
|
+
userDid,
|
|
145
|
+
userPk,
|
|
146
|
+
claims,
|
|
147
|
+
paymentMethod,
|
|
148
|
+
request,
|
|
149
|
+
subscription?.id
|
|
150
|
+
);
|
|
145
151
|
await afterTxExecution(paymentDetails);
|
|
146
152
|
return { hash: paymentDetails.tx_hash };
|
|
147
153
|
}
|
|
@@ -141,7 +141,14 @@ export default {
|
|
|
141
141
|
if (paymentMethod.type === 'arcblock') {
|
|
142
142
|
await prepareTxExecution();
|
|
143
143
|
|
|
144
|
-
const paymentDetails = await executeOcapTransactions(
|
|
144
|
+
const paymentDetails = await executeOcapTransactions(
|
|
145
|
+
userDid,
|
|
146
|
+
userPk,
|
|
147
|
+
claims,
|
|
148
|
+
paymentMethod,
|
|
149
|
+
request,
|
|
150
|
+
subscription?.id
|
|
151
|
+
);
|
|
145
152
|
await afterTxExecution(paymentDetails);
|
|
146
153
|
|
|
147
154
|
return { hash: paymentDetails.tx_hash };
|
|
@@ -168,7 +168,14 @@ export default {
|
|
|
168
168
|
try {
|
|
169
169
|
await prepareTxExecution();
|
|
170
170
|
|
|
171
|
-
const paymentDetails = await executeOcapTransactions(
|
|
171
|
+
const paymentDetails = await executeOcapTransactions(
|
|
172
|
+
userDid,
|
|
173
|
+
userPk,
|
|
174
|
+
claims,
|
|
175
|
+
paymentMethod,
|
|
176
|
+
request,
|
|
177
|
+
subscription?.id
|
|
178
|
+
);
|
|
172
179
|
await afterTxExecution(paymentDetails);
|
|
173
180
|
|
|
174
181
|
return { hash: paymentDetails.tx_hash };
|
|
@@ -387,7 +387,7 @@ export async function ensureInvoiceAndItems({
|
|
|
387
387
|
subscription?: Subscription;
|
|
388
388
|
props: TInvoice;
|
|
389
389
|
lineItems: TLineItemExpanded[];
|
|
390
|
-
|
|
390
|
+
trialing: boolean; // do we have trialing
|
|
391
391
|
metered: boolean; // is the quantity metered
|
|
392
392
|
applyCredit?: boolean; // should we apply customer credit?
|
|
393
393
|
}): Promise<{ invoice: Invoice; items: InvoiceItem[] }> {
|
|
@@ -637,13 +637,13 @@ export async function getDelegationTxClaim({
|
|
|
637
637
|
userDid: string;
|
|
638
638
|
userPk: string;
|
|
639
639
|
nonce: string;
|
|
640
|
-
|
|
640
|
+
mode: string;
|
|
641
641
|
data: any;
|
|
642
642
|
items: TLineItemExpanded[];
|
|
643
643
|
paymentCurrency: PaymentCurrency;
|
|
644
644
|
paymentMethod: PaymentMethod;
|
|
645
|
-
|
|
646
|
-
|
|
645
|
+
trialing: boolean;
|
|
646
|
+
billingThreshold?: number;
|
|
647
647
|
}) {
|
|
648
648
|
const amount = getFastCheckoutAmount(items, mode, paymentCurrency.id);
|
|
649
649
|
const address = toDelegateAddress(userDid, wallet.address);
|
|
@@ -739,16 +739,16 @@ export async function getStakeTxClaim({
|
|
|
739
739
|
if (paymentMethod.type === 'arcblock') {
|
|
740
740
|
// create staking data
|
|
741
741
|
const client = paymentMethod.getOcapClient();
|
|
742
|
-
const address = toStakeAddress(userDid, wallet.address);
|
|
742
|
+
const address = toStakeAddress(userDid, wallet.address, subscription.id);
|
|
743
743
|
const { state } = await client.getStakeState({ address });
|
|
744
744
|
const data = {
|
|
745
745
|
type: 'json',
|
|
746
746
|
value: Object.assign(
|
|
747
747
|
{
|
|
748
748
|
appId: wallet.address,
|
|
749
|
+
subscriptionId: subscription.id,
|
|
749
750
|
},
|
|
750
|
-
JSON.parse(state?.data?.value || '{}')
|
|
751
|
-
{ [subscription.id]: amount }
|
|
751
|
+
JSON.parse(state?.data?.value || '{}')
|
|
752
752
|
),
|
|
753
753
|
};
|
|
754
754
|
|
|
@@ -766,6 +766,7 @@ export async function getStakeTxClaim({
|
|
|
766
766
|
slashers: [wallet.address],
|
|
767
767
|
revokeWaitingPeriod: setup.cycle.duration / 1000, // wait for at least 1 billing cycle
|
|
768
768
|
message: `Stake for subscription ${subscription.id}`,
|
|
769
|
+
nonce: subscription.id,
|
|
769
770
|
inputs: [],
|
|
770
771
|
data,
|
|
771
772
|
},
|
|
@@ -976,7 +977,8 @@ export async function executeOcapTransactions(
|
|
|
976
977
|
userPk: string,
|
|
977
978
|
claims: any[],
|
|
978
979
|
paymentMethod: PaymentMethod,
|
|
979
|
-
request: Request
|
|
980
|
+
request: Request,
|
|
981
|
+
subscriptionId?: string,
|
|
980
982
|
) {
|
|
981
983
|
const client = paymentMethod.getOcapClient();
|
|
982
984
|
const delegation = claims.find((x) => x.type === 'signature' && x.meta?.purpose === 'delegation');
|
|
@@ -1010,13 +1012,15 @@ export async function executeOcapTransactions(
|
|
|
1010
1012
|
})
|
|
1011
1013
|
);
|
|
1012
1014
|
|
|
1015
|
+
const nonce = subscriptionId || '';
|
|
1016
|
+
|
|
1013
1017
|
return {
|
|
1014
1018
|
tx_hash: delegationTxHash,
|
|
1015
1019
|
payer: userDid,
|
|
1016
1020
|
type: 'delegate',
|
|
1017
1021
|
staking: {
|
|
1018
1022
|
tx_hash: stakingTxHash,
|
|
1019
|
-
address: toStakeAddress(userDid, wallet.address),
|
|
1023
|
+
address: toStakeAddress(userDid, wallet.address, nonce),
|
|
1020
1024
|
},
|
|
1021
1025
|
};
|
|
1022
1026
|
}
|
|
@@ -155,7 +155,14 @@ export default {
|
|
|
155
155
|
await prepareTxExecution();
|
|
156
156
|
const { invoice } = await ensureInvoiceForCheckout({ checkoutSession, customer, subscription });
|
|
157
157
|
|
|
158
|
-
const paymentDetails = await executeOcapTransactions(
|
|
158
|
+
const paymentDetails = await executeOcapTransactions(
|
|
159
|
+
userDid,
|
|
160
|
+
userPk,
|
|
161
|
+
claims,
|
|
162
|
+
paymentMethod,
|
|
163
|
+
request,
|
|
164
|
+
subscription?.id
|
|
165
|
+
);
|
|
159
166
|
await afterTxExecution(invoice!, paymentDetails);
|
|
160
167
|
|
|
161
168
|
return { hash: paymentDetails.tx_hash };
|
|
@@ -10,6 +10,7 @@ import { createListParamSchema, getWhereFromKvQuery } from '../libs/api';
|
|
|
10
10
|
import { authenticate } from '../libs/security';
|
|
11
11
|
import { expandLineItems } from '../libs/session';
|
|
12
12
|
import { formatMetadata } from '../libs/util';
|
|
13
|
+
import { Refund } from '../store/models';
|
|
13
14
|
import { Customer } from '../store/models/customer';
|
|
14
15
|
import { Invoice } from '../store/models/invoice';
|
|
15
16
|
import { InvoiceItem } from '../store/models/invoice-item';
|
|
@@ -166,6 +167,46 @@ router.get('/', authMine, async (req, res) => {
|
|
|
166
167
|
},
|
|
167
168
|
},
|
|
168
169
|
});
|
|
170
|
+
const stakeRecord = await Refund.findOne({
|
|
171
|
+
where: { subscription_id: subscription.id, status: 'succeeded', type: 'stake_return' },
|
|
172
|
+
});
|
|
173
|
+
if (stakeRecord) {
|
|
174
|
+
list.unshift({
|
|
175
|
+
id: address as string,
|
|
176
|
+
status: 'paid',
|
|
177
|
+
description: 'Return Subscription staking',
|
|
178
|
+
billing_reason: 'subscription_create',
|
|
179
|
+
total: stakeRecord.amount,
|
|
180
|
+
amount_due: '0',
|
|
181
|
+
amount_paid: stakeRecord.amount,
|
|
182
|
+
amount_remaining: '0',
|
|
183
|
+
...pick(last, [
|
|
184
|
+
'number',
|
|
185
|
+
'paid',
|
|
186
|
+
'auto_advance',
|
|
187
|
+
'currency_id',
|
|
188
|
+
'customer_id',
|
|
189
|
+
'subscription_id',
|
|
190
|
+
'period_start',
|
|
191
|
+
'period_end',
|
|
192
|
+
'created_at',
|
|
193
|
+
'updated_at',
|
|
194
|
+
]),
|
|
195
|
+
// @ts-ignore
|
|
196
|
+
paymentCurrency: await PaymentCurrency.findByPk(last.currency_id),
|
|
197
|
+
paymentMethod: method,
|
|
198
|
+
// @ts-ignore
|
|
199
|
+
customer: await Customer.findByPk(last.customer_id),
|
|
200
|
+
metadata: {
|
|
201
|
+
payment_details: {
|
|
202
|
+
arcblock: {
|
|
203
|
+
tx_hash: stakeRecord?.payment_details?.arcblock?.tx_hash,
|
|
204
|
+
payer: stakeRecord?.payment_details?.arcblock?.payer,
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
}
|
|
169
210
|
}
|
|
170
211
|
}
|
|
171
212
|
}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
finalizeSubscriptionUpdate,
|
|
19
19
|
getSubscriptionCreateSetup,
|
|
20
20
|
getSubscriptionRefundSetup,
|
|
21
|
+
getSubscriptionStakeReturnSetup,
|
|
21
22
|
getUpcomingInvoiceAmount,
|
|
22
23
|
} from '../libs/subscription';
|
|
23
24
|
import { MAX_SUBSCRIPTION_ITEM_COUNT, formatMetadata } from '../libs/util';
|
|
@@ -226,6 +227,7 @@ router.put('/:id/cancel', authPortal, async (req, res) => {
|
|
|
226
227
|
feedback = 'other',
|
|
227
228
|
comment = '',
|
|
228
229
|
reason = 'payment_disputed',
|
|
230
|
+
staking = 'none',
|
|
229
231
|
} = req.body;
|
|
230
232
|
if (at === 'custom' && dayjs(time).unix() < dayjs().unix()) {
|
|
231
233
|
return res.status(400).json({ error: 'cancel at must be a future timestamp' });
|
|
@@ -306,9 +308,10 @@ router.put('/:id/cancel', authPortal, async (req, res) => {
|
|
|
306
308
|
const result = await getSubscriptionRefundSetup(subscription, updates.cancel_at);
|
|
307
309
|
if (result.unused !== '0') {
|
|
308
310
|
const item = await Refund.create({
|
|
311
|
+
type: 'refund',
|
|
309
312
|
livemode: subscription.livemode,
|
|
310
313
|
amount: refund === 'last' ? result.total : result.unused,
|
|
311
|
-
description: '
|
|
314
|
+
description: 'refund_transfer_on_subscription_cancel',
|
|
312
315
|
status: 'pending',
|
|
313
316
|
reason: 'requested_by_admin',
|
|
314
317
|
currency_id: subscription.currency_id,
|
|
@@ -346,6 +349,66 @@ router.put('/:id/cancel', authPortal, async (req, res) => {
|
|
|
346
349
|
}
|
|
347
350
|
}
|
|
348
351
|
|
|
352
|
+
// trigger stake return
|
|
353
|
+
if (staking === 'proration') {
|
|
354
|
+
if (['owner', 'admin'].includes(req.user?.role as string) === false) {
|
|
355
|
+
return res.status(403).json({ error: 'Not authorized to perform this action' });
|
|
356
|
+
}
|
|
357
|
+
const paymentMethod = await PaymentMethod.findByPk(subscription.default_payment_method_id);
|
|
358
|
+
if (paymentMethod?.type !== 'arcblock') {
|
|
359
|
+
return res
|
|
360
|
+
.status(400)
|
|
361
|
+
.json({ error: `Stake return not supported for subscription with payment method ${paymentMethod?.type}` });
|
|
362
|
+
}
|
|
363
|
+
const address = subscription?.payment_details?.arcblock?.staking?.address ?? undefined;
|
|
364
|
+
if (!address) {
|
|
365
|
+
return res.status(400).json({ error: 'Staking not found on subscription payment detail' });
|
|
366
|
+
}
|
|
367
|
+
const result = await getSubscriptionStakeReturnSetup(subscription, address, paymentMethod);
|
|
368
|
+
if (result.return_amount !== '0') {
|
|
369
|
+
// do the stake return
|
|
370
|
+
const item = await Refund.create({
|
|
371
|
+
type: 'stake_return',
|
|
372
|
+
livemode: subscription.livemode,
|
|
373
|
+
amount: result.return_amount,
|
|
374
|
+
description: 'stake_return_on_subscription_cancel',
|
|
375
|
+
status: 'pending',
|
|
376
|
+
reason: 'requested_by_admin',
|
|
377
|
+
currency_id: subscription.currency_id,
|
|
378
|
+
customer_id: subscription.customer_id,
|
|
379
|
+
payment_method_id: subscription.default_payment_method_id,
|
|
380
|
+
payment_intent_id: result?.lastInvoice?.payment_intent_id as string,
|
|
381
|
+
subscription_id: subscription.id,
|
|
382
|
+
attempt_count: 0,
|
|
383
|
+
attempted: false,
|
|
384
|
+
next_attempt: 0,
|
|
385
|
+
last_attempt_error: null,
|
|
386
|
+
starting_balance: '0',
|
|
387
|
+
ending_balance: '0',
|
|
388
|
+
starting_token_balance: {},
|
|
389
|
+
ending_token_balance: {},
|
|
390
|
+
payment_details: {
|
|
391
|
+
// @ts-ignore
|
|
392
|
+
arcblock: {
|
|
393
|
+
receiver: result.sender,
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
});
|
|
397
|
+
logger.info('subscription cancel stake return created', {
|
|
398
|
+
...req.params,
|
|
399
|
+
...req.body,
|
|
400
|
+
...pick(result, ['return_amount']),
|
|
401
|
+
item: item.toJSON(),
|
|
402
|
+
});
|
|
403
|
+
} else {
|
|
404
|
+
logger.info('subscription cancel stake return skipped', {
|
|
405
|
+
...req.params,
|
|
406
|
+
...req.body,
|
|
407
|
+
...pick(result, ['return_amount']),
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
349
412
|
return res.json(subscription);
|
|
350
413
|
});
|
|
351
414
|
|
|
@@ -1181,6 +1244,38 @@ router.get('/:id/proration', authPortal, async (req, res) => {
|
|
|
1181
1244
|
}
|
|
1182
1245
|
});
|
|
1183
1246
|
|
|
1247
|
+
// Simulate stake return when subscription is canceled
|
|
1248
|
+
router.get('/:id/staking', authPortal, async (req, res) => {
|
|
1249
|
+
try {
|
|
1250
|
+
const subscription = await Subscription.findByPk(req.params.id);
|
|
1251
|
+
if (!subscription) {
|
|
1252
|
+
return res.status(404).json({ error: 'Subscription not found' });
|
|
1253
|
+
}
|
|
1254
|
+
if (subscription.isActive() === false) {
|
|
1255
|
+
return res.status(400).json({ error: 'Subscription is not active' });
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
const paymentMethod = await PaymentMethod.findByPk(subscription.default_payment_method_id);
|
|
1259
|
+
if (paymentMethod?.type !== 'arcblock') {
|
|
1260
|
+
return res
|
|
1261
|
+
.status(400)
|
|
1262
|
+
.json({ error: `Stake return not supported for subscription with payment method ${paymentMethod?.type}` });
|
|
1263
|
+
}
|
|
1264
|
+
const address = subscription?.payment_details?.arcblock?.staking?.address ?? undefined;
|
|
1265
|
+
if (!address) {
|
|
1266
|
+
return res.status(400).json({ error: 'Staking not found on subscription payment detail' });
|
|
1267
|
+
}
|
|
1268
|
+
const result = await getSubscriptionStakeReturnSetup(subscription, address, paymentMethod);
|
|
1269
|
+
return res.json({
|
|
1270
|
+
return_amount: result.return_amount,
|
|
1271
|
+
total: result.total,
|
|
1272
|
+
});
|
|
1273
|
+
} catch (err) {
|
|
1274
|
+
console.error(err);
|
|
1275
|
+
return res.status(400).json({ error: err.message });
|
|
1276
|
+
}
|
|
1277
|
+
});
|
|
1278
|
+
|
|
1184
1279
|
// Check payment change status
|
|
1185
1280
|
router.get('/:id/change-payment', authPortal, async (req, res) => {
|
|
1186
1281
|
const subscription = await Subscription.findByPk(req.params.id);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/* eslint-disable no-await-in-loop */
|
|
2
|
+
import { DataTypes } from 'sequelize';
|
|
3
|
+
|
|
4
|
+
import { Migration, safeApplyColumnChanges } from '../migrate';
|
|
5
|
+
|
|
6
|
+
export const up: Migration = async ({ context }) => {
|
|
7
|
+
await safeApplyColumnChanges(context, {
|
|
8
|
+
refunds: [
|
|
9
|
+
{
|
|
10
|
+
name: 'type',
|
|
11
|
+
field: {
|
|
12
|
+
type: DataTypes.ENUM('refund', 'stake_return'),
|
|
13
|
+
allowNull: true,
|
|
14
|
+
defaultValue: 'refund',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const down: Migration = async ({ context }) => {
|
|
22
|
+
await context.removeColumn('refunds', 'type');
|
|
23
|
+
};
|
|
@@ -62,6 +62,7 @@ export class Refund extends Model<InferAttributes<Refund>, InferCreationAttribut
|
|
|
62
62
|
|
|
63
63
|
declare created_at: CreationOptional<Date>;
|
|
64
64
|
declare updated_at: CreationOptional<Date>;
|
|
65
|
+
declare type: LiteralUnion<'refund' | 'stake_return', string>;
|
|
65
66
|
|
|
66
67
|
public static readonly GENESIS_ATTRIBUTES = {
|
|
67
68
|
id: {
|
|
@@ -182,6 +183,11 @@ export class Refund extends Model<InferAttributes<Refund>, InferCreationAttribut
|
|
|
182
183
|
type: DataTypes.STRING(30),
|
|
183
184
|
allowNull: true,
|
|
184
185
|
},
|
|
186
|
+
type: {
|
|
187
|
+
type: DataTypes.ENUM('refund', 'stake_return'),
|
|
188
|
+
allowNull: true,
|
|
189
|
+
defaultValue: 'refund',
|
|
190
|
+
},
|
|
185
191
|
},
|
|
186
192
|
{
|
|
187
193
|
sequelize,
|
|
@@ -266,7 +266,8 @@ export type PaymentDetails = {
|
|
|
266
266
|
arcblock?: {
|
|
267
267
|
tx_hash: string;
|
|
268
268
|
payer: string;
|
|
269
|
-
type?: LiteralUnion<'slash' | 'transfer' | 'delegate', string>;
|
|
269
|
+
type?: LiteralUnion<'slash' | 'transfer' | 'delegate' | 'stake_return', string>;
|
|
270
|
+
receiver?: string;
|
|
270
271
|
staking?: {
|
|
271
272
|
tx_hash: string;
|
|
272
273
|
address: string;
|
package/blocklet.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.13.
|
|
3
|
+
"version": "1.13.286",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"eject": "vite eject",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"@arcblock/validator": "^1.18.123",
|
|
53
53
|
"@blocklet/js-sdk": "1.16.28",
|
|
54
54
|
"@blocklet/logger": "1.16.28",
|
|
55
|
-
"@blocklet/payment-react": "1.13.
|
|
55
|
+
"@blocklet/payment-react": "1.13.286",
|
|
56
56
|
"@blocklet/sdk": "1.16.28",
|
|
57
57
|
"@blocklet/ui-react": "^2.10.1",
|
|
58
58
|
"@blocklet/uploader": "^0.1.11",
|
|
@@ -117,8 +117,8 @@
|
|
|
117
117
|
},
|
|
118
118
|
"devDependencies": {
|
|
119
119
|
"@abtnode/types": "1.16.28",
|
|
120
|
-
"@arcblock/eslint-config-ts": "^0.3.
|
|
121
|
-
"@blocklet/payment-types": "1.13.
|
|
120
|
+
"@arcblock/eslint-config-ts": "^0.3.2",
|
|
121
|
+
"@blocklet/payment-types": "1.13.286",
|
|
122
122
|
"@types/cookie-parser": "^1.4.7",
|
|
123
123
|
"@types/cors": "^2.8.17",
|
|
124
124
|
"@types/debug": "^4.1.12",
|
|
@@ -136,7 +136,7 @@
|
|
|
136
136
|
"lint-staged": "^12.5.0",
|
|
137
137
|
"nodemon": "^2.0.22",
|
|
138
138
|
"npm-run-all": "^4.1.5",
|
|
139
|
-
"prettier": "^
|
|
139
|
+
"prettier": "^3.3.2",
|
|
140
140
|
"prettier-plugin-import-sort": "^0.0.7",
|
|
141
141
|
"ts-jest": "^29.1.4",
|
|
142
142
|
"ts-node": "^10.9.2",
|
|
@@ -158,5 +158,5 @@
|
|
|
158
158
|
"parser": "typescript"
|
|
159
159
|
}
|
|
160
160
|
},
|
|
161
|
-
"gitHead": "
|
|
161
|
+
"gitHead": "fa41b3d6a308d9405f56717beee2f030a91ddec3"
|
|
162
162
|
}
|
|
@@ -398,7 +398,9 @@ const Root = styled(Box)`
|
|
|
398
398
|
|
|
399
399
|
.status-options {
|
|
400
400
|
position: absolute;
|
|
401
|
-
box-shadow:
|
|
401
|
+
box-shadow:
|
|
402
|
+
0px 5px 5px -3px rgba(0, 0, 0, 0.2),
|
|
403
|
+
0px 8px 10px 1px rgba(0, 0, 0, 0.14),
|
|
402
404
|
0px 3px 14px 2px rgba(0, 0, 0, 0.12);
|
|
403
405
|
padding: 0;
|
|
404
406
|
background: #fff;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
2
|
import { Button, CircularProgress, Stack } from '@mui/material';
|
|
3
|
+
import { isObject } from 'lodash';
|
|
3
4
|
import type { EventHandler } from 'react';
|
|
4
5
|
import { FormProvider, useForm } from 'react-hook-form';
|
|
5
6
|
|
|
7
|
+
import { isObjectContent } from '../../libs/util';
|
|
6
8
|
import MetadataForm from './form';
|
|
7
9
|
|
|
8
10
|
export default function MetadataEditor({
|
|
@@ -20,7 +22,10 @@ export default function MetadataEditor({
|
|
|
20
22
|
const metadata = data.metadata || {};
|
|
21
23
|
const methods = useForm<any>({
|
|
22
24
|
defaultValues: {
|
|
23
|
-
metadata: Object.keys(metadata).map((key: string) => ({
|
|
25
|
+
metadata: Object.keys(metadata).map((key: string) => ({
|
|
26
|
+
key,
|
|
27
|
+
value: isObject(metadata[key]) ? JSON.stringify(metadata[key]) : metadata[key],
|
|
28
|
+
})),
|
|
24
29
|
},
|
|
25
30
|
});
|
|
26
31
|
|
|
@@ -31,7 +36,16 @@ export default function MetadataEditor({
|
|
|
31
36
|
};
|
|
32
37
|
const onSubmit = () => {
|
|
33
38
|
handleSubmit(async (formData: any) => {
|
|
34
|
-
|
|
39
|
+
const payload = {
|
|
40
|
+
...(formData || {}),
|
|
41
|
+
metadata: Array.isArray(formData.metadata)
|
|
42
|
+
? formData.metadata.reduce((acc: any, x: any) => {
|
|
43
|
+
acc[x.key] = isObjectContent(x.value) ? JSON.parse(x.value) : x.value;
|
|
44
|
+
return acc;
|
|
45
|
+
}, {})
|
|
46
|
+
: [],
|
|
47
|
+
};
|
|
48
|
+
await onSave(payload);
|
|
35
49
|
reset();
|
|
36
50
|
onCancel(null);
|
|
37
51
|
})();
|
|
@@ -3,6 +3,7 @@ import { Typography } from '@mui/material';
|
|
|
3
3
|
import isEmpty from 'lodash/isEmpty';
|
|
4
4
|
import isObject from 'lodash/isObject';
|
|
5
5
|
|
|
6
|
+
import { isObjectContent } from '../../libs/util';
|
|
6
7
|
import InfoRow from '../info-row';
|
|
7
8
|
|
|
8
9
|
export default function MetadataList({ data }: { data: any }) {
|
|
@@ -12,14 +13,22 @@ export default function MetadataList({ data }: { data: any }) {
|
|
|
12
13
|
return <Typography color="text.secondary">{t('common.metadata.empty')}</Typography>;
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
const formatValue = (value: any) => {
|
|
17
|
+
if (isObjectContent(value)) {
|
|
18
|
+
return <pre>{JSON.stringify(JSON.parse(value), null, 2)}</pre>;
|
|
19
|
+
}
|
|
20
|
+
if (isObject(value)) {
|
|
21
|
+
return <pre>{JSON.stringify(value, null, 2)}</pre>;
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
};
|
|
25
|
+
|
|
15
26
|
// skip non-string values
|
|
16
27
|
return (
|
|
17
28
|
<>
|
|
18
|
-
{Object.keys(data || {})
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
<InfoRow key={key} label={key} value={data[key]} />
|
|
22
|
-
))}
|
|
29
|
+
{Object.keys(data || {}).map((key) => (
|
|
30
|
+
<InfoRow key={key} label={key} value={formatValue(data[key])} />
|
|
31
|
+
))}
|
|
23
32
|
</>
|
|
24
33
|
);
|
|
25
34
|
}
|
|
@@ -11,5 +11,8 @@ const Root = styled(Box)`
|
|
|
11
11
|
margin-top: 40px;
|
|
12
12
|
position: relative;
|
|
13
13
|
overflow: hidden;
|
|
14
|
-
box-shadow:
|
|
14
|
+
box-shadow:
|
|
15
|
+
0 20px 44px #32325d1f,
|
|
16
|
+
0 -1px 32px #32325d0f,
|
|
17
|
+
0 3px 12px #00000014;
|
|
15
18
|
`;
|
|
@@ -141,6 +141,22 @@ export default function RefundList({ customer_id, invoice_id, subscription_id, s
|
|
|
141
141
|
},
|
|
142
142
|
},
|
|
143
143
|
},
|
|
144
|
+
{
|
|
145
|
+
label: t('common.type'),
|
|
146
|
+
name: 'type',
|
|
147
|
+
width: 60,
|
|
148
|
+
options: {
|
|
149
|
+
filter: true,
|
|
150
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
151
|
+
const item = data.list[index] as TRefundExpanded;
|
|
152
|
+
return (
|
|
153
|
+
<Link to={`/admin/payments/${item.id}`}>
|
|
154
|
+
<Status label={t(`refund.type.${item.type}`)} />
|
|
155
|
+
</Link>
|
|
156
|
+
);
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
},
|
|
144
160
|
{
|
|
145
161
|
label: t('common.description'),
|
|
146
162
|
name: 'description',
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
2
|
import { api, formatAmount, formatTime } from '@blocklet/payment-react';
|
|
3
3
|
import type { TSubscriptionExpanded } from '@blocklet/payment-types';
|
|
4
|
-
import { Box, Divider, FormControlLabel, Radio, RadioGroup, Stack, TextField, Typography } from '@mui/material';
|
|
4
|
+
import { Box, Divider, FormControlLabel, Radio, RadioGroup, Stack, TextField, Typography, styled } from '@mui/material';
|
|
5
5
|
import { useRequest } from 'ahooks';
|
|
6
6
|
import { useEffect } from 'react';
|
|
7
7
|
import { Controller, useFormContext, useWatch } from 'react-hook-form';
|
|
@@ -10,18 +10,24 @@ const fetchData = (id: string, time: string): Promise<{ total: string; unused: s
|
|
|
10
10
|
return api.get(`/api/subscriptions/${id}/proration?time=${encodeURIComponent(time)}`).then((res: any) => res.data);
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
+
const fetchStakingData = (id: string, time: string): Promise<{ return_amount: string }> => {
|
|
14
|
+
return api.get(`/api/subscriptions/${id}/staking?time=${encodeURIComponent(time)}`).then((res: any) => res.data);
|
|
15
|
+
};
|
|
16
|
+
|
|
13
17
|
export default function SubscriptionCancelForm({ data }: { data: TSubscriptionExpanded }) {
|
|
14
18
|
const { t } = useLocaleContext();
|
|
15
19
|
const { control, setValue, formState } = useFormContext();
|
|
16
20
|
const cancelAt = useWatch({ control, name: 'cancel.at' });
|
|
17
21
|
const cancelTime = useWatch({ control, name: 'cancel.time' });
|
|
18
22
|
const refundType = useWatch({ control, name: 'cancel.refund' });
|
|
23
|
+
const stakingType = useWatch({ control, name: 'cancel.staking' });
|
|
19
24
|
const {
|
|
20
25
|
loading,
|
|
21
26
|
data: refund,
|
|
22
27
|
refresh,
|
|
23
28
|
} = useRequest(() => fetchData(data.id, cancelAt === 'custom' ? cancelTime : ''));
|
|
24
29
|
|
|
30
|
+
const { data: staking } = useRequest(() => fetchStakingData(data.id, cancelAt === 'custom' ? cancelTime : ''));
|
|
25
31
|
useEffect(() => {
|
|
26
32
|
if (data) {
|
|
27
33
|
refresh();
|
|
@@ -35,9 +41,9 @@ export default function SubscriptionCancelForm({ data }: { data: TSubscriptionEx
|
|
|
35
41
|
const { decimal, symbol } = data.paymentCurrency;
|
|
36
42
|
|
|
37
43
|
return (
|
|
38
|
-
<
|
|
44
|
+
<Root sx={{ width: 400 }}>
|
|
39
45
|
<Stack direction="row" spacing={3} alignItems="flex-start">
|
|
40
|
-
<Typography>{t('admin.subscription.cancel.at.title')}</Typography>
|
|
46
|
+
<Typography className="form-title">{t('admin.subscription.cancel.at.title')}</Typography>
|
|
41
47
|
<Stack>
|
|
42
48
|
<RadioGroup>
|
|
43
49
|
<FormControlLabel
|
|
@@ -84,7 +90,7 @@ export default function SubscriptionCancelForm({ data }: { data: TSubscriptionEx
|
|
|
84
90
|
</Stack>
|
|
85
91
|
<Divider sx={{ my: 1 }} />
|
|
86
92
|
<Stack direction="row" spacing={3} alignItems="flex-start">
|
|
87
|
-
<Typography>{t('admin.subscription.cancel.refund.title')}</Typography>
|
|
93
|
+
<Typography className="form-title">{t('admin.subscription.cancel.refund.title')}</Typography>
|
|
88
94
|
<Stack>
|
|
89
95
|
<RadioGroup>
|
|
90
96
|
<FormControlLabel
|
|
@@ -118,6 +124,39 @@ export default function SubscriptionCancelForm({ data }: { data: TSubscriptionEx
|
|
|
118
124
|
</RadioGroup>
|
|
119
125
|
</Stack>
|
|
120
126
|
</Stack>
|
|
121
|
-
|
|
127
|
+
{data.paymentMethod.type === 'arcblock' && (
|
|
128
|
+
<>
|
|
129
|
+
<Divider sx={{ my: 1 }} />
|
|
130
|
+
<Stack direction="row" spacing={3} alignItems="flex-start">
|
|
131
|
+
<Typography className="form-title">{t('admin.subscription.cancel.staking.title')}</Typography>
|
|
132
|
+
<RadioGroup>
|
|
133
|
+
<FormControlLabel
|
|
134
|
+
value="none"
|
|
135
|
+
disabled={loading || !staking}
|
|
136
|
+
onClick={() => setValue('cancel.staking', 'none')}
|
|
137
|
+
control={<Radio checked={stakingType === 'none'} />}
|
|
138
|
+
label={t('admin.subscription.cancel.staking.none')}
|
|
139
|
+
/>
|
|
140
|
+
<FormControlLabel
|
|
141
|
+
value="proration"
|
|
142
|
+
disabled={loading || !staking}
|
|
143
|
+
onClick={() => setValue('cancel.staking', 'proration')}
|
|
144
|
+
control={<Radio checked={stakingType === 'proration'} />}
|
|
145
|
+
label={t('admin.subscription.cancel.staking.proration', {
|
|
146
|
+
unused: formatAmount(staking?.return_amount || '0', decimal),
|
|
147
|
+
symbol,
|
|
148
|
+
})}
|
|
149
|
+
/>
|
|
150
|
+
</RadioGroup>
|
|
151
|
+
</Stack>
|
|
152
|
+
</>
|
|
153
|
+
)}
|
|
154
|
+
</Root>
|
|
122
155
|
);
|
|
123
156
|
}
|
|
157
|
+
|
|
158
|
+
const Root = styled(Box)`
|
|
159
|
+
.form-title {
|
|
160
|
+
width: 60px;
|
|
161
|
+
}
|
|
162
|
+
`;
|
package/src/libs/util.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
} from '@blocklet/payment-types';
|
|
15
15
|
import { Hasher } from '@ocap/mcrypto';
|
|
16
16
|
import { hexToNumber } from '@ocap/util';
|
|
17
|
+
import { isObject } from 'lodash';
|
|
17
18
|
import cloneDeep from 'lodash/cloneDeep';
|
|
18
19
|
import isEqual from 'lodash/isEqual';
|
|
19
20
|
import { joinURL } from 'ufo';
|
|
@@ -224,3 +225,11 @@ export function goBackOrFallback(fallback: string) {
|
|
|
224
225
|
}
|
|
225
226
|
}, 500);
|
|
226
227
|
}
|
|
228
|
+
|
|
229
|
+
export function isObjectContent(value: string) {
|
|
230
|
+
try {
|
|
231
|
+
return isObject(JSON.parse(value));
|
|
232
|
+
} catch (err) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
}
|
package/src/locales/en.tsx
CHANGED
|
@@ -409,6 +409,11 @@ export default flat({
|
|
|
409
409
|
last: 'Last payment {total}{symbol}',
|
|
410
410
|
proration: 'Proration amount {unused}/{total}{symbol}',
|
|
411
411
|
},
|
|
412
|
+
staking: {
|
|
413
|
+
title: 'Stake',
|
|
414
|
+
none: 'No return',
|
|
415
|
+
proration: 'Return Remaining Stake {unused}{symbol}',
|
|
416
|
+
},
|
|
412
417
|
},
|
|
413
418
|
pause: {
|
|
414
419
|
title: 'Pause payment collection',
|
package/src/locales/zh.tsx
CHANGED
|
@@ -400,6 +400,11 @@ export default flat({
|
|
|
400
400
|
last: '退款最近付款的全部 {total}{symbol}',
|
|
401
401
|
proration: '退款最近付款的未使用部分 {unused}/{total}{symbol}',
|
|
402
402
|
},
|
|
403
|
+
staking: {
|
|
404
|
+
title: '质押',
|
|
405
|
+
none: '不退还质押',
|
|
406
|
+
proration: '退还剩余部分 {unused}{symbol}',
|
|
407
|
+
},
|
|
403
408
|
},
|
|
404
409
|
pause: {
|
|
405
410
|
title: '暂停付款',
|
|
@@ -135,6 +135,7 @@ export default function RefundDetail(props: { id: string }) {
|
|
|
135
135
|
</Stack>
|
|
136
136
|
}
|
|
137
137
|
/>
|
|
138
|
+
<InfoRow label={t('common.type')} value={t(`refund.type.${data.type}`)} />
|
|
138
139
|
<InfoRow label={t('common.description')} value={data.description} />
|
|
139
140
|
<InfoRow label={t('common.createdAt')} value={formatTime(data.created_at)} />
|
|
140
141
|
<InfoRow label={t('common.updatedAt')} value={formatTime(data.updated_at)} />
|