payment-kit 1.13.130 → 1.13.132

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 (40) hide show
  1. package/api/src/index.ts +2 -0
  2. package/api/src/libs/payment.ts +30 -1
  3. package/api/src/libs/session.ts +2 -2
  4. package/api/src/libs/subscription.ts +134 -1
  5. package/api/src/queues/invoice.ts +1 -0
  6. package/api/src/queues/payment.ts +2 -0
  7. package/api/src/queues/refund.ts +212 -0
  8. package/api/src/routes/checkout-sessions.ts +2 -2
  9. package/api/src/routes/index.ts +2 -0
  10. package/api/src/routes/refunds.ts +93 -0
  11. package/api/src/routes/subscriptions.ts +116 -130
  12. package/api/src/store/migrations/20240205-refund.ts +10 -0
  13. package/api/src/store/models/index.ts +12 -0
  14. package/api/src/store/models/refund.ts +226 -0
  15. package/blocklet.yml +1 -1
  16. package/package.json +4 -4
  17. package/src/components/customer/link.tsx +16 -0
  18. package/src/components/info-card.tsx +1 -1
  19. package/src/components/invoice/list.tsx +3 -3
  20. package/src/components/payment-intent/list.tsx +3 -3
  21. package/src/components/refund/actions.tsx +44 -0
  22. package/src/components/refund/list.tsx +207 -0
  23. package/src/components/subscription/actions/cancel.tsx +62 -2
  24. package/src/components/subscription/list.tsx +2 -1
  25. package/src/components/subscription/status.tsx +1 -3
  26. package/src/global.css +0 -3
  27. package/src/libs/util.ts +0 -50
  28. package/src/locales/en.tsx +10 -0
  29. package/src/locales/zh.tsx +10 -0
  30. package/src/pages/admin/billing/invoices/detail.tsx +3 -6
  31. package/src/pages/admin/billing/subscriptions/detail.tsx +1 -2
  32. package/src/pages/admin/customers/customers/index.tsx +12 -1
  33. package/src/pages/admin/payments/index.tsx +7 -0
  34. package/src/pages/admin/payments/intents/detail.tsx +11 -7
  35. package/src/pages/admin/payments/refunds/detail.tsx +223 -0
  36. package/src/pages/admin/payments/refunds/index.tsx +5 -0
  37. package/src/pages/customer/invoice.tsx +1 -3
  38. package/src/pages/customer/subscription/detail.tsx +1 -2
  39. package/src/pages/customer/subscription/update.tsx +4 -4
  40. package/src/components/blockchain/tx.tsx +0 -77
package/api/src/index.ts CHANGED
@@ -21,6 +21,7 @@ import { startEventQueue } from './queues/event';
21
21
  import { startInvoiceQueue } from './queues/invoice';
22
22
  import { startNotificationQueue } from './queues/notification';
23
23
  import { startPaymentQueue } from './queues/payment';
24
+ import { startRefundQueue } from './queues/refund';
24
25
  import { startSubscriptionQueue } from './queues/subscription';
25
26
  import routes from './routes';
26
27
  import collectHandlers from './routes/connect/collect';
@@ -108,6 +109,7 @@ export const server = app.listen(port, (err?: any) => {
108
109
  startEventQueue().then(() => logger.info('event queue started'));
109
110
  startCheckoutSessionQueue().then(() => logger.info('checkoutSession queue started'));
110
111
  startNotificationQueue().then(() => logger.info('notification queue started'));
112
+ startRefundQueue().then(() => logger.info('refund queue started'));
111
113
 
112
114
  if (process.env.BLOCKLET_MODE === 'production') {
113
115
  ensureWebhookRegistered().catch(console.error);
@@ -104,7 +104,7 @@ export async function isDelegationSufficientForPayment(args: {
104
104
  throw new Error(`Payment method ${paymentMethod.type} not supported`);
105
105
  }
106
106
 
107
- export function isBalanceSufficientForPayment(args: {
107
+ export function isCreditSufficientForPayment(args: {
108
108
  paymentMethod: PaymentMethod;
109
109
  paymentCurrency: TPaymentCurrency;
110
110
  customer: TCustomer;
@@ -255,3 +255,32 @@ export async function getTokenLimitsForDelegation(
255
255
 
256
256
  return [entry];
257
257
  }
258
+
259
+ export async function isBalanceSufficientForPayment(args: {
260
+ paymentMethod: PaymentMethod;
261
+ paymentCurrency: TPaymentCurrency;
262
+ amount: string;
263
+ }): Promise<SufficientForPaymentResult> {
264
+ const { paymentCurrency, paymentMethod, amount } = args;
265
+ const tokenAddress = paymentCurrency.contract as string;
266
+
267
+ if (paymentMethod.type === 'arcblock') {
268
+ const client = paymentMethod.getOcapClient();
269
+ const { tokens } = await client.getAccountTokens({ address: wallet.address, token: tokenAddress });
270
+ const [token] = tokens;
271
+ if (!token) {
272
+ return { sufficient: false, reason: 'NO_TOKEN' };
273
+ }
274
+ if (new BN(token.balance).lt(new BN(amount))) {
275
+ return { sufficient: false, reason: 'NO_ENOUGH_TOKEN', token };
276
+ }
277
+
278
+ return { sufficient: true, token };
279
+ }
280
+
281
+ if (paymentMethod.type === 'stripe') {
282
+ return { sufficient: false, reason: 'NOT_SUPPORTED' };
283
+ }
284
+
285
+ throw new Error(`Payment method ${paymentMethod.type} not supported`);
286
+ }
@@ -60,13 +60,13 @@ export function getCheckoutAmount(items: TLineItemExpanded[], currencyId: string
60
60
  const total = items
61
61
  .reduce((acc, x) => {
62
62
  const price = x.upsell_price || x.price;
63
- if (price.type === 'recurring') {
63
+ if (price?.type === 'recurring') {
64
64
  renew = renew.add(new BN(getPriceUintAmountByCurrency(price, currencyId)).mul(new BN(x.quantity)));
65
65
 
66
66
  if (includeFreeTrial) {
67
67
  return acc;
68
68
  }
69
- if (price.recurring?.usage_type === 'metered') {
69
+ if (price?.recurring?.usage_type === 'metered') {
70
70
  return acc;
71
71
  }
72
72
  }
@@ -1,8 +1,18 @@
1
1
  import component from '@blocklet/sdk/lib/component';
2
2
  import { BN } from '@ocap/util';
3
3
 
4
- import type { PriceRecurring, TLineItemExpanded } from '../store/models';
4
+ import {
5
+ Customer,
6
+ Invoice,
7
+ InvoiceItem,
8
+ Price,
9
+ PriceRecurring,
10
+ Subscription,
11
+ SubscriptionItem,
12
+ TLineItemExpanded,
13
+ } from '../store/models';
5
14
  import dayjs from './dayjs';
15
+ import logger from './logger';
6
16
  import { getPriceUintAmountByCurrency, getRecurringPeriod } from './session';
7
17
 
8
18
  export function getCustomerSubscriptionPageUrl(subscriptionId: string, locale: string = 'en') {
@@ -129,3 +139,126 @@ export function getSubscriptionCycleAmount(items: TLineItemExpanded[], currencyI
129
139
  total: amount.toString(),
130
140
  };
131
141
  }
142
+
143
+ export async function createProration(
144
+ subscription: Subscription,
145
+ setup: ReturnType<typeof getSubscriptionCreateSetup>,
146
+ anchor: number
147
+ ) {
148
+ // FIXME: should we enforce cycle invoices here?
149
+ const lastInvoice = await Invoice.findByPk(subscription.latest_invoice_id);
150
+ if (!lastInvoice) {
151
+ throw new Error('Subscription should have latest invoice when create proration');
152
+ }
153
+
154
+ const customer = await Customer.findByPk(subscription.customer_id);
155
+ if (!customer) {
156
+ throw new Error('Subscription should have customer when create proration');
157
+ }
158
+
159
+ // 1. get last invoice, and invoice items, filter invoice items that are in licensed recurring mode
160
+ const invoiceItems = await InvoiceItem.findAll({ where: { invoice_id: lastInvoice.id, proration: false } });
161
+ const invoiceItemsExpanded = await Price.expand(invoiceItems.map((x) => x.toJSON()));
162
+ const prorationItems = invoiceItemsExpanded.filter(
163
+ (x) => x.price.type === 'recurring' && x.price.recurring?.usage_type === 'licensed'
164
+ );
165
+
166
+ // 2. calculate proration args based on the filtered invoice items
167
+ const precision = 10000;
168
+ const prorationStart = lastInvoice.period_start;
169
+ const prorationEnd = lastInvoice.period_end;
170
+ if (anchor > prorationEnd) {
171
+ throw new Error('Subscription proration anchor should not be larger than prorationEnd');
172
+ }
173
+
174
+ const prorationRate = Math.ceil(((prorationEnd - anchor) / (prorationEnd - prorationStart)) * precision);
175
+ let unused = new BN(0);
176
+ const prorations = await Promise.all(
177
+ prorationItems.map((x: TLineItemExpanded & { [key: string]: any }) => {
178
+ const unitAmount = getPriceUintAmountByCurrency(x.price, subscription.currency_id);
179
+ const amount = new BN(unitAmount)
180
+ .mul(new BN(x.quantity))
181
+ .mul(new BN(prorationRate))
182
+ .div(new BN(precision))
183
+ .toString();
184
+ logger.info('subscription proration item', {
185
+ subscription: subscription.id,
186
+ invoice: x.invoice_id,
187
+ invoiceItem: x.id,
188
+ amount,
189
+ });
190
+ unused = unused.add(new BN(amount));
191
+
192
+ return {
193
+ price_id: x.price_id,
194
+ amount: `-${amount}`,
195
+ quantity: x.quantity,
196
+ // @ts-ignore
197
+ description: `Unused time on ${x.price.product.name} after ${dayjs().format('lll')}`,
198
+ period: {
199
+ start: lastInvoice.period_start,
200
+ end: lastInvoice.period_end,
201
+ },
202
+ proration: true,
203
+ proration_details: {
204
+ credited_items: {
205
+ invoice_id: lastInvoice.id,
206
+ // @ts-ignore
207
+ invoice_line_items: [x.id],
208
+ },
209
+ },
210
+ };
211
+ })
212
+ );
213
+ logger.info('subscription prorations created', {
214
+ subscription: subscription.id,
215
+ prorations,
216
+ });
217
+
218
+ // 5. adjust invoice total && update customer token balance
219
+ const total = setup.amount.setup;
220
+ let due = setup.amount.setup;
221
+ let newCredit = '0';
222
+ let appliedCredit = '0';
223
+ if (new BN(total).gte(unused)) {
224
+ // Proration amount is less than total, all proration are used,
225
+ due = new BN(total).sub(unused).toString();
226
+ // Besides, we need to try to apply customer credit
227
+ appliedCredit = customer.getBalanceToApply(subscription.currency_id, due);
228
+ due = new BN(due).sub(new BN(appliedCredit)).toString();
229
+ } else {
230
+ // Proration amount is greater than total, we need to increase customer credit
231
+ newCredit = unused.sub(new BN(total)).toString();
232
+ due = '0';
233
+ }
234
+
235
+ logger.info('subscription proration result', {
236
+ subscription: subscription.id,
237
+ prorationStart,
238
+ prorationEnd,
239
+ prorationRate,
240
+ unused: unused.toString(),
241
+ total,
242
+ due,
243
+ newCredit,
244
+ appliedCredit,
245
+ });
246
+
247
+ return {
248
+ lastInvoice,
249
+ total,
250
+ due,
251
+ used: new BN(lastInvoice.amount_due).sub(unused).toString(),
252
+ unused: unused.toString(),
253
+ prorations,
254
+ newCredit,
255
+ appliedCredit,
256
+ };
257
+ }
258
+
259
+ export async function getSubscriptionRefundSetup(subscription: Subscription, anchor: number) {
260
+ const items = await SubscriptionItem.findAll({ where: { subscription_id: subscription.id } });
261
+ const expanded = await Price.expand(items.map((x) => x.toJSON()));
262
+ const setup = getSubscriptionCreateSetup(expanded, subscription.currency_id, 0);
263
+ return createProration(subscription, setup, anchor);
264
+ }
@@ -94,6 +94,7 @@ export const handleInvoice = async (job: InvoiceJob) => {
94
94
  subscription_create: 'Subscription creation',
95
95
  subscription_cycle: 'Subscription cycle',
96
96
  subscription_update: 'Subscription update',
97
+ subscription_threshold: 'Subscription threshold',
97
98
  };
98
99
  // TODO: support partial payment from user balance
99
100
  paymentIntent = await PaymentIntent.create({
@@ -329,6 +329,7 @@ export const handlePayment = async (job: PaymentJob) => {
329
329
  // @ts-ignore
330
330
  value: {
331
331
  appId: wallet.address,
332
+ reason: invoice ? invoice.billing_reason : 'payment',
332
333
  paymentIntentId: paymentIntent.id,
333
334
  },
334
335
  },
@@ -346,6 +347,7 @@ export const handlePayment = async (job: PaymentJob) => {
346
347
 
347
348
  await paymentIntent.update({
348
349
  status: 'succeeded',
350
+ last_payment_error: null,
349
351
  amount_received: paymentIntent.amount,
350
352
  payment_details: {
351
353
  arcblock: {
@@ -0,0 +1,212 @@
1
+ import { wallet } from '../libs/auth';
2
+ import CustomError from '../libs/error';
3
+ import { events } from '../libs/event';
4
+ import logger from '../libs/logger';
5
+ import { getGasPayerExtra, isBalanceSufficientForPayment } from '../libs/payment';
6
+ import createQueue from '../libs/queue';
7
+ import { MAX_RETRY_COUNT, getNextRetry } from '../libs/util';
8
+ import { Customer } from '../store/models/customer';
9
+ import { PaymentCurrency } from '../store/models/payment-currency';
10
+ import { PaymentMethod } from '../store/models/payment-method';
11
+ import { Refund } from '../store/models/refund';
12
+ import type { PaymentError } from '../store/models/types';
13
+
14
+ type RefundJob = {
15
+ refundId: string;
16
+ retryOnError?: boolean;
17
+ };
18
+
19
+ type Updates = {
20
+ retry: {
21
+ refund: Partial<Refund>;
22
+ };
23
+ terminate: {
24
+ refund: Partial<Refund>;
25
+ };
26
+ };
27
+
28
+ export const handleRefundFailed = (refund: Refund, error: PaymentError) => {
29
+ const attemptCount = refund.attempt_count + 1;
30
+ const updates: Updates = {
31
+ retry: {
32
+ refund: {
33
+ status: 'pending',
34
+ last_attempt_error: error,
35
+ attempt_count: attemptCount,
36
+ attempted: true,
37
+ next_attempt: getNextRetry(attemptCount),
38
+ },
39
+ },
40
+ terminate: {
41
+ refund: {
42
+ status: 'requires_action',
43
+ last_attempt_error: error,
44
+ attempt_count: attemptCount,
45
+ attempted: true,
46
+ },
47
+ },
48
+ };
49
+
50
+ // check max retry
51
+ if (refund.attempt_count > MAX_RETRY_COUNT) {
52
+ return updates.terminate;
53
+ }
54
+
55
+ return updates.retry;
56
+ };
57
+
58
+ export const handleRefund = async (job: RefundJob) => {
59
+ logger.info('handle refund', job);
60
+
61
+ const refund = await Refund.findByPk(job.refundId);
62
+ if (!refund) {
63
+ logger.warn(`refund not found: ${job.refundId}`);
64
+ return;
65
+ }
66
+
67
+ if (['pending'].includes(refund.status) === false) {
68
+ logger.warn(`refund status not expected: ${refund.status}`);
69
+ return;
70
+ }
71
+
72
+ // check max retry before doing any hard work
73
+ if (refund.attempt_count > MAX_RETRY_COUNT) {
74
+ logger.info(`refund transfer aborted since max retry exceeded: ${refund.id}`);
75
+ const updates = handleRefundFailed(refund, {
76
+ type: 'card_error',
77
+ code: 'max_retry_exceeded',
78
+ message: 'max_retry_exceeded',
79
+ });
80
+ await refund.update(updates.refund);
81
+ return;
82
+ }
83
+
84
+ const paymentCurrency = await PaymentCurrency.findByPk(refund.currency_id);
85
+ if (!paymentCurrency) {
86
+ logger.warn(`PaymentCurrency not found: ${refund.currency_id}`);
87
+ return;
88
+ }
89
+ const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
90
+ if (!paymentMethod) {
91
+ logger.warn(`PaymentMethod not found: ${paymentCurrency.payment_method_id}`);
92
+ return;
93
+ }
94
+ const supportAutoCharge = await PaymentMethod.supportAutoCharge(paymentCurrency.payment_method_id);
95
+ if (supportAutoCharge === false) {
96
+ logger.warn(`PaymentMethod does not support auto charge: ${paymentCurrency.payment_method_id}`);
97
+ return;
98
+ }
99
+
100
+ const customer = await Customer.findByPk(refund.customer_id);
101
+ if (!customer) {
102
+ logger.warn(`Customer not found: ${refund.customer_id}`);
103
+ return;
104
+ }
105
+
106
+ // try refund transfer and reschedule on error
107
+ logger.info(`refund transfer attempt: ${refund.id}`);
108
+ let result;
109
+ try {
110
+ const client = paymentMethod.getOcapClient();
111
+
112
+ // check balance before transfer with transaction
113
+ result = await isBalanceSufficientForPayment({ paymentMethod, paymentCurrency, amount: refund.amount });
114
+ if (result.sufficient === false) {
115
+ logger.error('refund transfer aborted on preCheck', { id: refund.id, result });
116
+ throw new CustomError(result.reason, 'app balance not sufficient for this refund');
117
+ }
118
+
119
+ // do the transfer
120
+ const signed = await client.signTransferV2Tx({
121
+ tx: {
122
+ itx: {
123
+ to: customer.did,
124
+ value: '0',
125
+ assets: [],
126
+ tokens: [{ address: paymentCurrency.contract, value: refund.amount }],
127
+ data: {
128
+ typeUrl: 'json',
129
+ // @ts-ignore
130
+ value: {
131
+ appId: wallet.address,
132
+ reason: refund.reason,
133
+ refundId: refund.id,
134
+ },
135
+ },
136
+ },
137
+ },
138
+ wallet,
139
+ });
140
+ // @ts-ignore
141
+ const { buffer } = await client.encodeTransferV2Tx({ tx: signed });
142
+ // @ts-ignore
143
+ const txHash = await client.sendTransferV2Tx({ tx: signed, wallet }, getGasPayerExtra(buffer));
144
+
145
+ logger.info('refund transfer done', { id: refund.id, txHash });
146
+ await refund.update({
147
+ status: 'succeeded',
148
+ last_attempt_error: null,
149
+ payment_details: {
150
+ arcblock: {
151
+ tx_hash: txHash,
152
+ payer: wallet.address,
153
+ },
154
+ },
155
+ });
156
+ } catch (err) {
157
+ logger.error('refund transfer failed', { error: err, id: refund.id });
158
+
159
+ const error: PaymentError = {
160
+ type: 'card_error',
161
+ code: err.code,
162
+ message: err.message,
163
+ payment_method_id: paymentMethod.id,
164
+ payment_method_type: paymentMethod.type,
165
+ };
166
+
167
+ const updates = await handleRefundFailed(refund, error);
168
+ await refund.update(updates.refund);
169
+
170
+ // reschedule next attempt
171
+ const retryAt = updates.refund.next_attempt;
172
+ if (retryAt) {
173
+ refundQueue.push({
174
+ id: refund.id,
175
+ job: { refundId: refund.id, retryOnError: job.retryOnError },
176
+ runAt: retryAt,
177
+ });
178
+ logger.error('refund transfer retry scheduled', { id: refund.id, retryAt });
179
+ } else {
180
+ logger.info('refund job deleted since no retry', { id: refund.id });
181
+ refundQueue.delete(refund.id);
182
+ }
183
+ }
184
+ };
185
+
186
+ export const refundQueue = createQueue<RefundJob>({
187
+ name: 'refund',
188
+ onJob: handleRefund,
189
+ options: {
190
+ concurrency: 1,
191
+ maxRetries: 0,
192
+ enableScheduledJob: true,
193
+ },
194
+ });
195
+
196
+ export const startRefundQueue = async () => {
197
+ events.on('refund.created', (refund: Refund) => {
198
+ refundQueue.push({ id: refund.id, job: { refundId: refund.id } });
199
+ });
200
+
201
+ const refunds = await Refund.findAll({ where: { status: ['pending'] } });
202
+ refunds.forEach(async (x) => {
203
+ const exist = await refundQueue.get(x.id);
204
+ if (!exist) {
205
+ refundQueue.push({ id: x.id, job: { refundId: x.id } });
206
+ }
207
+ });
208
+ };
209
+
210
+ refundQueue.on('failed', ({ id, job, error }) => {
211
+ logger.error('refund job failed', { id, job, error });
212
+ });
@@ -18,7 +18,7 @@ import { handleStripeSubscriptionSucceed } from '../integrations/stripe/handlers
18
18
  import { ensureStripePaymentIntent, ensureStripeSubscription } from '../integrations/stripe/resource';
19
19
  import dayjs from '../libs/dayjs';
20
20
  import logger from '../libs/logger';
21
- import { isBalanceSufficientForPayment, isDelegationSufficientForPayment } from '../libs/payment';
21
+ import { isCreditSufficientForPayment, isDelegationSufficientForPayment } from '../libs/payment';
22
22
  import { authenticate } from '../libs/security';
23
23
  import {
24
24
  canUpsell,
@@ -714,7 +714,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
714
714
  };
715
715
 
716
716
  // if we can complete purchase with customer balance
717
- const balance = isBalanceSufficientForPayment({
717
+ const balance = isCreditSufficientForPayment({
718
718
  paymentMethod,
719
719
  paymentCurrency,
720
720
  customer,
@@ -15,6 +15,7 @@ import prices from './prices';
15
15
  import pricingTables from './pricing-table';
16
16
  import products from './products';
17
17
  import redirect from './redirect';
18
+ import refunds from './refunds';
18
19
  import settings from './settings';
19
20
  import subscriptionItems from './subscription-items';
20
21
  import subscriptions from './subscriptions';
@@ -57,6 +58,7 @@ router.use('/prices', prices);
57
58
  router.use('/pricing-tables', pricingTables);
58
59
  router.use('/products', products);
59
60
  router.use('/redirect', redirect);
61
+ router.use('/refunds', refunds);
60
62
  router.use('/settings', settings);
61
63
  router.use('/subscription-items', subscriptionItems);
62
64
  router.use('/subscriptions', subscriptions);
@@ -0,0 +1,93 @@
1
+ /* eslint-disable consistent-return */
2
+ import { Router } from 'express';
3
+ import Joi from 'joi';
4
+ import type { WhereOptions } from 'sequelize';
5
+
6
+ import { authenticate } from '../libs/security';
7
+ import {
8
+ Customer,
9
+ Invoice,
10
+ PaymentCurrency,
11
+ PaymentIntent,
12
+ PaymentMethod,
13
+ Refund,
14
+ Subscription,
15
+ } from '../store/models';
16
+
17
+ const router = Router();
18
+ const auth = authenticate<Refund>({ component: true, roles: ['owner', 'admin'] });
19
+
20
+ const paginationSchema = Joi.object<{
21
+ page: number;
22
+ pageSize: number;
23
+ livemode?: boolean;
24
+ status?: string;
25
+ }>({
26
+ page: Joi.number().integer().min(1).default(1),
27
+ pageSize: Joi.number().integer().min(1).max(100).default(20),
28
+ livemode: Joi.boolean().empty(''),
29
+ status: Joi.string().empty(''),
30
+ });
31
+ router.get('/', auth, async (req, res) => {
32
+ const { page, pageSize, livemode, status, ...query } = await paginationSchema.validateAsync(req.query, {
33
+ stripUnknown: false,
34
+ allowUnknown: true,
35
+ });
36
+ const where: WhereOptions<Refund> = {};
37
+
38
+ if (typeof livemode === 'boolean') {
39
+ where.livemode = livemode;
40
+ }
41
+ if (status) {
42
+ where.status = status
43
+ .split(',')
44
+ .map((x) => x.trim())
45
+ .filter(Boolean);
46
+ }
47
+
48
+ Object.keys(query)
49
+ .filter((x) => x.startsWith('metadata.'))
50
+ .forEach((key: string) => {
51
+ // @ts-ignore
52
+ where[key] = query[key];
53
+ });
54
+
55
+ const { rows: list, count } = await Refund.findAndCountAll({
56
+ where,
57
+ order: [['created_at', 'DESC']],
58
+ offset: (page - 1) * pageSize,
59
+ limit: pageSize,
60
+ include: [
61
+ { model: Customer, as: 'customer' },
62
+ { model: PaymentCurrency, as: 'paymentCurrency' },
63
+ // { model: PaymentIntent, as: 'paymentIntent' },
64
+ // { model: Invoice, as: 'invoice' },
65
+ // { model: Subscription, as: 'subscription' },
66
+ ],
67
+ distinct: true,
68
+ });
69
+
70
+ res.json({ count, list });
71
+ });
72
+
73
+ router.get('/:id', auth, async (req, res) => {
74
+ const doc = await Refund.findByPk(req.params.id as string, {
75
+ include: [
76
+ { model: Customer, as: 'customer' },
77
+ { model: PaymentCurrency, as: 'paymentCurrency' },
78
+ { model: PaymentIntent, as: 'paymentIntent' },
79
+ { model: Invoice, as: 'invoice' },
80
+ { model: Subscription, as: 'subscription' },
81
+ ],
82
+ });
83
+
84
+ if (doc) {
85
+ // @ts-ignore
86
+ const paymentMethod = await PaymentMethod.findByPk(doc.paymentCurrency.payment_method_id);
87
+ return res.json({ ...doc.toJSON(), paymentMethod: paymentMethod?.toJSON() });
88
+ }
89
+
90
+ return res.status(404).json(null);
91
+ });
92
+
93
+ export default router;