payment-kit 1.15.33 → 1.15.35

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.
Files changed (63) hide show
  1. package/api/src/integrations/stripe/handlers/setup-intent.ts +3 -1
  2. package/api/src/integrations/stripe/handlers/subscription.ts +2 -8
  3. package/api/src/integrations/stripe/resource.ts +0 -11
  4. package/api/src/libs/invoice.ts +202 -1
  5. package/api/src/libs/notification/template/subscription-canceled.ts +15 -2
  6. package/api/src/libs/notification/template/subscription-renew-failed.ts +1 -1
  7. package/api/src/libs/notification/template/subscription-renewed.ts +1 -1
  8. package/api/src/libs/notification/template/subscription-trial-will-end.ts +9 -5
  9. package/api/src/libs/notification/template/subscription-will-canceled.ts +9 -5
  10. package/api/src/libs/notification/template/subscription-will-renew.ts +10 -12
  11. package/api/src/libs/payment.ts +3 -2
  12. package/api/src/libs/refund.ts +4 -0
  13. package/api/src/libs/subscription.ts +58 -14
  14. package/api/src/queues/invoice.ts +1 -0
  15. package/api/src/queues/payment.ts +3 -1
  16. package/api/src/queues/refund.ts +9 -8
  17. package/api/src/queues/subscription.ts +111 -40
  18. package/api/src/routes/checkout-sessions.ts +22 -6
  19. package/api/src/routes/connect/change-payment.ts +51 -34
  20. package/api/src/routes/connect/change-plan.ts +25 -3
  21. package/api/src/routes/connect/recharge.ts +28 -3
  22. package/api/src/routes/connect/setup.ts +27 -6
  23. package/api/src/routes/connect/shared.ts +223 -1
  24. package/api/src/routes/connect/subscribe.ts +25 -3
  25. package/api/src/routes/customers.ts +2 -2
  26. package/api/src/routes/invoices.ts +27 -105
  27. package/api/src/routes/payment-links.ts +3 -0
  28. package/api/src/routes/refunds.ts +22 -1
  29. package/api/src/routes/subscriptions.ts +112 -21
  30. package/api/src/routes/webhook-attempts.ts +14 -1
  31. package/api/src/store/models/invoice.ts +3 -1
  32. package/blocklet.yml +1 -1
  33. package/package.json +4 -4
  34. package/src/app.tsx +3 -1
  35. package/src/components/invoice/list.tsx +83 -31
  36. package/src/components/invoice/recharge.tsx +244 -0
  37. package/src/components/payment-intent/actions.tsx +2 -1
  38. package/src/components/payment-link/actions.tsx +6 -6
  39. package/src/components/payment-link/item.tsx +53 -18
  40. package/src/components/pricing-table/actions.tsx +14 -3
  41. package/src/components/pricing-table/payment-settings.tsx +1 -1
  42. package/src/components/refund/actions.tsx +43 -1
  43. package/src/components/refund/list.tsx +1 -1
  44. package/src/components/subscription/actions/cancel.tsx +10 -7
  45. package/src/components/subscription/metrics.tsx +1 -1
  46. package/src/components/subscription/portal/actions.tsx +22 -1
  47. package/src/components/subscription/portal/list.tsx +1 -0
  48. package/src/components/webhook/attempts.tsx +19 -121
  49. package/src/components/webhook/request-info.tsx +139 -0
  50. package/src/locales/en.tsx +4 -0
  51. package/src/locales/zh.tsx +8 -0
  52. package/src/pages/admin/billing/invoices/detail.tsx +15 -0
  53. package/src/pages/admin/billing/invoices/index.tsx +1 -1
  54. package/src/pages/admin/billing/subscriptions/detail.tsx +12 -4
  55. package/src/pages/admin/customers/customers/detail.tsx +1 -0
  56. package/src/pages/admin/payments/refunds/detail.tsx +2 -2
  57. package/src/pages/admin/products/links/create.tsx +4 -1
  58. package/src/pages/customer/index.tsx +1 -1
  59. package/src/pages/customer/invoice/detail.tsx +34 -14
  60. package/src/pages/customer/recharge.tsx +45 -35
  61. package/src/pages/customer/subscription/change-plan.tsx +8 -1
  62. package/src/pages/customer/subscription/detail.tsx +12 -22
  63. package/src/pages/customer/subscription/embed.tsx +3 -1
@@ -11,6 +11,7 @@ import { addSubscriptionJob } from '../../queues/subscription';
11
11
  import type { TLineItemExpanded } from '../../store/models';
12
12
  import {
13
13
  ensureSetupIntent,
14
+ ensureStakeInvoice,
14
15
  executeOcapTransactions,
15
16
  getAuthPrincipalClaim,
16
17
  getDelegationTxClaim,
@@ -122,10 +123,8 @@ 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 { setupIntent, checkoutSession, paymentMethod, subscription, invoice } = await ensureSetupIntent(
126
- checkoutSessionId,
127
- connectedDid || userDid
128
- );
126
+ const { setupIntent, checkoutSession, paymentMethod, subscription, invoice, paymentCurrency, customer } =
127
+ await ensureSetupIntent(checkoutSessionId, connectedDid || userDid);
129
128
 
130
129
  if (!subscription) {
131
130
  throw new Error('Subscription for checkoutSession not found');
@@ -174,13 +173,35 @@ export default {
174
173
  try {
175
174
  await prepareTxExecution();
176
175
 
177
- const paymentDetails = await executeOcapTransactions(
176
+ const { stakingAmount, ...paymentDetails } = await executeOcapTransactions(
178
177
  userDid,
179
178
  userPk,
180
179
  claims,
181
180
  paymentMethod,
182
181
  request,
183
- subscription?.id
182
+ subscription?.id,
183
+ paymentCurrency?.contract
184
+ );
185
+
186
+ await ensureStakeInvoice(
187
+ {
188
+ total: stakingAmount,
189
+ description: 'Stake for subscription',
190
+ checkout_session_id: checkoutSessionId,
191
+ currency_id: paymentCurrency.id,
192
+ metadata: {
193
+ payment_details: {
194
+ arcblock: {
195
+ tx_hash: paymentDetails?.staking?.tx_hash,
196
+ payer: paymentDetails?.payer,
197
+ address: paymentDetails?.staking?.address,
198
+ },
199
+ },
200
+ },
201
+ },
202
+ subscription,
203
+ paymentMethod,
204
+ customer
184
205
  );
185
206
  await afterTxExecution(paymentDetails);
186
207
 
@@ -624,10 +624,17 @@ export async function ensureSubscriptionRecharge(subscriptionId: string) {
624
624
  if (!receiverAddress) {
625
625
  throw new Error(`Receiver address not found for subscription ${subscriptionId}`);
626
626
  }
627
+
628
+ const customer = await Customer.findByPk(subscription.customer_id);
629
+ if (!customer) {
630
+ throw new Error(`Customer ${subscription.customer_id} not found`);
631
+ }
627
632
  return {
628
633
  paymentCurrency: paymentCurrency as PaymentCurrency,
629
634
  paymentMethod: paymentMethod as PaymentMethod,
630
635
  receiverAddress,
636
+ subscription,
637
+ customer
631
638
  };
632
639
  }
633
640
 
@@ -1021,7 +1028,8 @@ export async function executeOcapTransactions(
1021
1028
  claims: any[],
1022
1029
  paymentMethod: PaymentMethod,
1023
1030
  request: Request,
1024
- subscriptionId?: string
1031
+ subscriptionId?: string,
1032
+ paymentCurrencyContract?: string
1025
1033
  ) {
1026
1034
  const client = paymentMethod.getOcapClient();
1027
1035
  const delegation = claims.find((x) => x.type === 'signature' && x.meta?.purpose === 'delegation');
@@ -1031,6 +1039,9 @@ export async function executeOcapTransactions(
1031
1039
  [staking, 'Stake'],
1032
1040
  ];
1033
1041
 
1042
+ const stakingAmount =
1043
+ staking?.requirement?.tokens?.find((x: any) => x.address === paymentCurrencyContract)?.value || '0';
1044
+
1034
1045
  const [delegationTxHash, stakingTxHash] = await Promise.all(
1035
1046
  transactions.map(async ([claim, type]) => {
1036
1047
  if (!claim) {
@@ -1065,5 +1076,216 @@ export async function executeOcapTransactions(
1065
1076
  tx_hash: stakingTxHash,
1066
1077
  address: await getCustomerStakeAddress(userDid, nonce),
1067
1078
  },
1079
+ stakingAmount,
1068
1080
  };
1069
1081
  }
1082
+
1083
+ export async function ensureStakeInvoice(
1084
+ invoiceProps: { total: string; description?: string; checkout_session_id?: string; currency_id: string; metadata?: any; payment_settings?: any },
1085
+ subscription: Subscription,
1086
+ paymentMethod: PaymentMethod,
1087
+ customer: Customer
1088
+ ) {
1089
+ if (paymentMethod.type !== 'arcblock') {
1090
+ return;
1091
+ }
1092
+ try {
1093
+ const stakingInvoice = await Invoice.create({
1094
+ livemode: subscription.livemode,
1095
+ number: await customer.getInvoiceNumber(),
1096
+ description: invoiceProps?.description || 'Stake for subscription',
1097
+ statement_descriptor: '',
1098
+ period_start: dayjs().unix(),
1099
+ period_end: subscription.current_period_end,
1100
+
1101
+ auto_advance: false,
1102
+ paid: true,
1103
+ paid_out_of_band: false,
1104
+
1105
+ status: 'paid',
1106
+ collection_method: 'charge_automatically',
1107
+ billing_reason: 'stake',
1108
+
1109
+ currency_id: invoiceProps.currency_id,
1110
+ customer_id: customer.id,
1111
+ payment_intent_id: '',
1112
+ subscription_id: subscription?.id,
1113
+ checkout_session_id: invoiceProps?.checkout_session_id || '',
1114
+
1115
+ total: invoiceProps.total || '0',
1116
+ subtotal: invoiceProps.total || '0',
1117
+ tax: '0',
1118
+ subtotal_excluding_tax: invoiceProps.total || '0',
1119
+
1120
+ amount_due: '0',
1121
+ amount_paid: invoiceProps.total || '0',
1122
+ amount_remaining: '0',
1123
+ amount_shipping: '0',
1124
+
1125
+ starting_balance: '0',
1126
+ ending_balance: '0',
1127
+ starting_token_balance: {},
1128
+ ending_token_balance: {},
1129
+
1130
+ attempt_count: 0,
1131
+ attempted: false,
1132
+ // next_payment_attempt: undefined,
1133
+
1134
+ custom_fields: [],
1135
+ customer_address: customer.address,
1136
+ customer_email: customer.email,
1137
+ customer_name: customer.name,
1138
+ customer_phone: customer.phone,
1139
+
1140
+ discounts: [],
1141
+ total_discount_amounts: [],
1142
+
1143
+ due_date: undefined,
1144
+ effective_at: dayjs().unix(),
1145
+ status_transitions: {
1146
+ finalized_at: dayjs().unix(),
1147
+ },
1148
+
1149
+ payment_settings: invoiceProps?.payment_settings || subscription?.payment_settings,
1150
+ default_payment_method_id: paymentMethod.id,
1151
+
1152
+ account_country: '',
1153
+ account_name: '',
1154
+ metadata: invoiceProps.metadata || {},
1155
+ });
1156
+ logger.info('create staking invoice success', {
1157
+ stakingInvoice,
1158
+ subscriptionId: subscription?.id,
1159
+ paymentMethod: paymentMethod.id,
1160
+ customerId: customer.id,
1161
+ });
1162
+ } catch (error) {
1163
+ logger.error('ensureStake: create invoice failed', { error, subscriptionId: subscription?.id, paymentMethod: paymentMethod.id, customerId: customer.id });
1164
+ }
1165
+ }
1166
+
1167
+
1168
+ export async function updateStripeSubscriptionAfterChangePayment(setupIntent: SetupIntent, subscription: Subscription) {
1169
+ const { from_method: fromMethodId, to_method: toMethodId } = setupIntent.metadata || {};
1170
+ const fromMethod = await PaymentMethod.findByPk(fromMethodId);
1171
+ if (fromMethod?.type === 'stripe') {
1172
+ // pause Stripe
1173
+ const client = fromMethod?.getStripeClient();
1174
+ const stripeSubscriptionId = subscription.payment_details?.stripe?.subscription_id as string;
1175
+ if (client && stripeSubscriptionId) {
1176
+ const stripeSubscription = await client.subscriptions.retrieve(stripeSubscriptionId);
1177
+ if (stripeSubscription) {
1178
+ const result = await client.subscriptions.update(stripeSubscriptionId, {
1179
+ pause_collection: {
1180
+ behavior: 'void',
1181
+ },
1182
+ });
1183
+ logger.info('stripe subscription paused on payment change', {
1184
+ subscription: subscription.id,
1185
+ stripeSubscription: stripeSubscriptionId,
1186
+ result,
1187
+ });
1188
+ }
1189
+ }
1190
+ } else {
1191
+ const toMethod = await PaymentMethod.findByPk(toMethodId);
1192
+ if (toMethod?.type === 'stripe') {
1193
+ // resume stripe
1194
+ const client = toMethod?.getStripeClient();
1195
+ const stripeSubscriptionId = subscription.payment_details?.stripe?.subscription_id as string;
1196
+ if (client && stripeSubscriptionId) {
1197
+ const stripeSubscription = await client.subscriptions.retrieve(stripeSubscriptionId);
1198
+ if (stripeSubscription.status === 'paused') {
1199
+ await client.subscriptions.resume(stripeSubscriptionId, {
1200
+ proration_behavior: 'none',
1201
+ });
1202
+ logger.info('stripe subscription resumed on payment change', {
1203
+ subscription: subscription.id,
1204
+ stripeSubscription: stripeSubscriptionId,
1205
+ });
1206
+ }
1207
+ }
1208
+ }
1209
+ }
1210
+ }
1211
+
1212
+ export async function ensureRechargeInvoice(
1213
+ invoiceProps: { total: string; description?: string; checkout_session_id?: string; currency_id: string; metadata?: any; payment_settings?: any },
1214
+ subscription: Subscription,
1215
+ paymentMethod: PaymentMethod,
1216
+ customer: Customer
1217
+ ) {
1218
+ try {
1219
+ const rechargeInvoice = await Invoice.create({
1220
+ livemode: subscription.livemode,
1221
+ number: await customer.getInvoiceNumber(),
1222
+ description: invoiceProps?.description || 'Subscription recharge',
1223
+ statement_descriptor: '',
1224
+ period_start: dayjs().unix(),
1225
+ period_end: dayjs().unix(),
1226
+
1227
+ auto_advance: false,
1228
+ paid: true,
1229
+ paid_out_of_band: false,
1230
+
1231
+ status: 'paid',
1232
+ collection_method: 'charge_automatically',
1233
+ billing_reason: 'recharge',
1234
+
1235
+ currency_id: invoiceProps.currency_id,
1236
+ customer_id: customer.id,
1237
+ payment_intent_id: '',
1238
+ subscription_id: subscription?.id,
1239
+ checkout_session_id: invoiceProps?.checkout_session_id || '',
1240
+
1241
+ total: invoiceProps.total || '0',
1242
+ subtotal: invoiceProps.total || '0',
1243
+ tax: '0',
1244
+ subtotal_excluding_tax: invoiceProps.total || '0',
1245
+
1246
+ amount_due: '0',
1247
+ amount_paid: invoiceProps.total || '0',
1248
+ amount_remaining: '0',
1249
+ amount_shipping: '0',
1250
+
1251
+ starting_balance: '0',
1252
+ ending_balance: '0',
1253
+ starting_token_balance: {},
1254
+ ending_token_balance: {},
1255
+
1256
+ attempt_count: 0,
1257
+ attempted: false,
1258
+ // next_payment_attempt: undefined,
1259
+
1260
+ custom_fields: [],
1261
+ customer_address: customer.address,
1262
+ customer_email: customer.email,
1263
+ customer_name: customer.name,
1264
+ customer_phone: customer.phone,
1265
+
1266
+ discounts: [],
1267
+ total_discount_amounts: [],
1268
+
1269
+ due_date: undefined,
1270
+ effective_at: dayjs().unix(),
1271
+ status_transitions: {
1272
+ finalized_at: dayjs().unix(),
1273
+ },
1274
+
1275
+ payment_settings: invoiceProps?.payment_settings || subscription?.payment_settings,
1276
+ default_payment_method_id: paymentMethod.id,
1277
+
1278
+ account_country: '',
1279
+ account_name: '',
1280
+ metadata: invoiceProps.metadata || {},
1281
+ });
1282
+ logger.info('create recharge invoice success', {
1283
+ rechargeInvoice,
1284
+ subscriptionId: subscription?.id,
1285
+ paymentMethod: paymentMethod.id,
1286
+ customerId: customer.id,
1287
+ });
1288
+ } catch (error) {
1289
+ logger.error('ensureRechargeInvoice: create invoice failed', { error, subscriptionId: subscription?.id, paymentMethod: paymentMethod.id, customerId: customer.id });
1290
+ }
1291
+ }
@@ -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
 
@@ -1,4 +1,4 @@
1
- import { user } from '@blocklet/sdk/lib/middlewares';
1
+ import sessionMiddleware from '@blocklet/sdk/lib/middlewares/session';
2
2
  import { Router } from 'express';
3
3
  import Joi from 'joi';
4
4
  import pick from 'lodash/pick';
@@ -79,7 +79,7 @@ router.get('/search', auth, async (req, res) => {
79
79
  });
80
80
 
81
81
  // eslint-disable-next-line consistent-return
82
- router.get('/me', user(), async (req, res) => {
82
+ router.get('/me', sessionMiddleware(), async (req, res) => {
83
83
  if (!req.user) {
84
84
  return res.status(403).json({ error: 'Unauthorized' });
85
85
  }
@@ -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 { getSubscriptionStakeAmountSetup } from '../libs/subscription';
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 } = await schema.validateAsync(
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,13 @@ router.get('/', authMine, async (req, res) => {
108
107
  // @ts-ignore
109
108
  where[key] = query[key];
110
109
  });
111
-
110
+ const excludeBillingReasons = ['recharge'];
111
+ if (!!(include_staking && query.subscription_id) || !include_staking) {
112
+ excludeBillingReasons.push('stake');
113
+ }
114
+ if (excludeBillingReasons.length > 0) {
115
+ where.billing_reason = { [Op.notIn]: excludeBillingReasons };
116
+ }
112
117
  try {
113
118
  const { rows: list, count } = await Invoice.findAndCountAll({
114
119
  where,
@@ -117,7 +122,7 @@ router.get('/', authMine, async (req, res) => {
117
122
  limit: pageSize,
118
123
  include: [
119
124
  { model: PaymentCurrency, as: 'paymentCurrency' },
120
- // { model: PaymentMethod, as: 'paymentMethod' },
125
+ { model: PaymentMethod, as: 'paymentMethod' },
121
126
  // { model: PaymentIntent, as: 'paymentIntent' },
122
127
  // { model: Subscription, as: 'subscription' },
123
128
  { model: Customer, as: 'customer' },
@@ -126,113 +131,30 @@ router.get('/', authMine, async (req, res) => {
126
131
 
127
132
  // push staking info as first invoice if we are on the last page
128
133
  let subscription;
134
+ let invoices = list;
129
135
  if (query.subscription_id && include_staking && page === Math.ceil((count || 1) / pageSize)) {
130
136
  try {
131
137
  subscription = await Subscription.findByPk(query.subscription_id);
132
138
  if (subscription?.payment_details?.arcblock?.staking?.tx_hash) {
133
- const method = await PaymentMethod.findOne({ where: { type: 'arcblock', livemode: subscription.livemode } });
134
- const setup = await SetupIntent.findOne({
135
- where: {
136
- customer_id: subscription.customer_id,
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
- }
139
+ const stakingInvoices = await getStakingInvoices(subscription);
140
+ let returnStakeInvoices: any[] = [];
141
+ if (include_return_staking) {
142
+ returnStakeInvoices = await getReturnStakeInvoices(subscription);
228
143
  }
144
+ invoices = [...(stakingInvoices || []), ...(returnStakeInvoices || []), ...list]
145
+ .filter(Boolean)
146
+ .sort((a, b) =>
147
+ query.o === 'asc'
148
+ ? new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
149
+ : new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
150
+ );
229
151
  }
230
152
  } catch (err) {
231
153
  console.error('Failed to include staking record in invoice list', err);
232
154
  }
233
155
  }
234
156
 
235
- res.json({ count, list, subscription, paging: { page, pageSize } });
157
+ res.json({ count, list: invoices, subscription, paging: { page, pageSize } });
236
158
  } catch (err) {
237
159
  console.error(err);
238
160
  res.json({ count: 0, list: [], paging: { page, pageSize } });
@@ -93,6 +93,9 @@ const formatBeforeSave = (payload: any) => {
93
93
 
94
94
  raw.line_items?.forEach((x) => {
95
95
  if (x.adjustable_quantity?.enabled) {
96
+ if (Number(x.adjustable_quantity?.minimum) >= Number(x.adjustable_quantity?.maximum)) {
97
+ throw new Error('adjustable_quantity.minimum must be less than adjustable_quantity.maximum');
98
+ }
96
99
  x.adjustable_quantity.minimum = Number(x.adjustable_quantity?.minimum);
97
100
  x.adjustable_quantity.maximum = Number(x.adjustable_quantity?.maximum);
98
101
  }
@@ -261,7 +261,28 @@ router.put('/:id', authAdmin, async (req, res) => {
261
261
  requestBody: req.body,
262
262
  requestedBy: req.user?.did,
263
263
  });
264
- res.status(500).json({ error: 'Internal server error' });
264
+ return res.status(400).json({ error: err.message });
265
+ }
266
+ });
267
+
268
+ router.post('/:id/cancel', authAdmin, async (req, res) => {
269
+ const doc = await Refund.findByPk(req.params.id as string);
270
+ if (!doc) {
271
+ return res.status(404).json({ error: 'Refund not found' });
272
+ }
273
+ if (doc.status === 'succeeded') {
274
+ return res.status(400).json({ error: 'Refund is already succeeded' });
275
+ }
276
+ try {
277
+ await doc.update({ status: 'canceled' });
278
+ return res.json(doc);
279
+ } catch (err) {
280
+ logger.error('Cancel refund failed', {
281
+ refundId: req.params.id,
282
+ error: err.message,
283
+ requestedBy: req.user?.did,
284
+ });
285
+ return res.status(400).json({ error: err.message });
265
286
  }
266
287
  });
267
288