payment-kit 1.13.22 → 1.13.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/src/integrations/stripe/handlers/invoice.ts +129 -101
- package/api/src/jobs/event.ts +27 -13
- package/api/src/jobs/invoice.ts +8 -8
- package/api/src/jobs/payment.ts +1 -1
- package/api/src/jobs/subscription.ts +1 -1
- package/api/src/jobs/webhook.ts +26 -19
- package/api/src/libs/audit.ts +3 -3
- package/api/src/libs/event.ts +3 -0
- package/api/src/libs/util.ts +5 -0
- package/api/src/routes/connect/pay.ts +1 -1
- package/api/src/routes/subscriptions.ts +15 -0
- package/api/src/store/models/types.ts +1 -0
- package/blocklet.yml +2 -2
- package/package.json +4 -3
- package/src/components/actions.tsx +4 -10
- package/src/components/blockchain/tx.tsx +38 -9
- package/src/components/click-boundary.tsx +7 -0
- package/src/components/confirm.tsx +2 -18
- package/src/components/customer/actions.tsx +3 -2
- package/src/components/invoice/action.tsx +3 -2
- package/src/components/payment-intent/actions.tsx +4 -3
- package/src/components/payment-intent/list.tsx +2 -2
- package/src/components/payment-link/actions.tsx +22 -10
- package/src/components/payment-link/item.tsx +18 -15
- package/src/components/payment-method/stripe.tsx +7 -4
- package/src/components/price/actions.tsx +9 -6
- package/src/components/price/form.tsx +4 -1
- package/src/components/product/actions.tsx +3 -2
- package/src/components/subscription/actions/index.tsx +3 -2
- package/src/components/subscription/items/actions.tsx +17 -14
- package/src/libs/util.ts +21 -5
- package/src/locales/en.tsx +6 -0
- package/src/pages/admin/billing/subscriptions/detail.tsx +1 -1
- package/src/pages/admin/payments/intents/detail.tsx +1 -6
- package/src/pages/admin/products/products/create.tsx +8 -4
- package/src/pages/admin/settings/payment-methods/create.tsx +3 -0
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
Customer,
|
|
10
10
|
Invoice,
|
|
11
11
|
InvoiceItem,
|
|
12
|
+
PaymentMethod,
|
|
12
13
|
Subscription,
|
|
13
14
|
SubscriptionItem,
|
|
14
15
|
TEventExpanded,
|
|
@@ -29,6 +30,109 @@ export async function handleStripeInvoicePaid(invoice: Invoice, event: TEventExp
|
|
|
29
30
|
});
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
export async function ensureStripeInvoice(stripeInvoice: any, subscription: Subscription, client: Stripe) {
|
|
34
|
+
const customer = await Customer.findByPk(subscription.customer_id);
|
|
35
|
+
const checkoutSession = await CheckoutSession.findOne({ where: { subscription_id: subscription.id } });
|
|
36
|
+
|
|
37
|
+
let invoice = await Invoice.findOne({ where: { 'metadata.stripe_id': stripeInvoice.id } });
|
|
38
|
+
if (invoice) {
|
|
39
|
+
return invoice;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// @ts-ignore
|
|
43
|
+
invoice = await Invoice.create({
|
|
44
|
+
// @ts-ignore
|
|
45
|
+
number: await customer.getInvoiceNumber(),
|
|
46
|
+
...pick(stripeInvoice, [
|
|
47
|
+
'amount_due',
|
|
48
|
+
'amount_paid',
|
|
49
|
+
'amount_remaining',
|
|
50
|
+
'amount_shipping',
|
|
51
|
+
'attempt_count',
|
|
52
|
+
'attempted',
|
|
53
|
+
'auto_advance',
|
|
54
|
+
'billing_reason',
|
|
55
|
+
'collection_method',
|
|
56
|
+
'custom_fields',
|
|
57
|
+
'customer_address',
|
|
58
|
+
'customer_email',
|
|
59
|
+
'customer_name',
|
|
60
|
+
'customer_phone',
|
|
61
|
+
'description',
|
|
62
|
+
'discounts',
|
|
63
|
+
'due_date',
|
|
64
|
+
'effective_at',
|
|
65
|
+
'ending_balance',
|
|
66
|
+
'livemode',
|
|
67
|
+
'paid_out_of_band',
|
|
68
|
+
'paid',
|
|
69
|
+
'period_end',
|
|
70
|
+
'period_start',
|
|
71
|
+
'starting_balance',
|
|
72
|
+
'status_transitions',
|
|
73
|
+
'status',
|
|
74
|
+
'subtotal_excluding_tax',
|
|
75
|
+
'subtotal',
|
|
76
|
+
'tax',
|
|
77
|
+
'total_discount_amounts',
|
|
78
|
+
'total',
|
|
79
|
+
]),
|
|
80
|
+
|
|
81
|
+
currency_id: subscription.currency_id,
|
|
82
|
+
customer_id: subscription.customer_id,
|
|
83
|
+
default_payment_method_id: subscription.default_payment_method_id as string,
|
|
84
|
+
payment_intent_id: '',
|
|
85
|
+
subscription_id: subscription.id,
|
|
86
|
+
checkout_session_id: checkoutSession?.id,
|
|
87
|
+
statement_descriptor: stripeInvoice.statement_descriptor || '',
|
|
88
|
+
|
|
89
|
+
payment_settings: subscription.payment_settings,
|
|
90
|
+
metadata: {
|
|
91
|
+
stripe_id: stripeInvoice.id,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
await client.invoices.update(stripeInvoice.id, { metadata: { appPid: env.appPid, id: invoice.id } });
|
|
95
|
+
logger.info('stripe invoice mirrored', { local: invoice.id, remote: stripeInvoice.id });
|
|
96
|
+
|
|
97
|
+
await Promise.all(
|
|
98
|
+
stripeInvoice.lines.data.map(async (line: any) => {
|
|
99
|
+
const subscriptionItem = line.subscription_item
|
|
100
|
+
? await SubscriptionItem.findOne({ where: { 'metadata.stripe_id': line.subscription_item } })
|
|
101
|
+
: null;
|
|
102
|
+
|
|
103
|
+
// @ts-ignore
|
|
104
|
+
const item = await InvoiceItem.create({
|
|
105
|
+
currency_id: subscription.currency_id,
|
|
106
|
+
customer_id: subscription.customer_id,
|
|
107
|
+
price_id: line.price?.metadata.id as string,
|
|
108
|
+
invoice_id: invoice?.id as string,
|
|
109
|
+
subscription_id: subscription.id,
|
|
110
|
+
subscription_item_id: subscriptionItem?.id,
|
|
111
|
+
...pick(line, [
|
|
112
|
+
'livemode',
|
|
113
|
+
'amount',
|
|
114
|
+
'quantity',
|
|
115
|
+
'description',
|
|
116
|
+
'period',
|
|
117
|
+
'discountable',
|
|
118
|
+
'discount_amounts',
|
|
119
|
+
'discounts',
|
|
120
|
+
'proration',
|
|
121
|
+
'proration_details',
|
|
122
|
+
]),
|
|
123
|
+
metadata: {
|
|
124
|
+
stripe_id: line.id,
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
logger.info('stripe invoice items mirrored', { local: item.id, remote: line.id });
|
|
129
|
+
return item;
|
|
130
|
+
})
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
return invoice;
|
|
134
|
+
}
|
|
135
|
+
|
|
32
136
|
export async function handleStripeInvoiceCreated(event: TEventExpanded, client: Stripe) {
|
|
33
137
|
if (['invoice.created', 'payment_intent.created'].includes(event.type) === false) {
|
|
34
138
|
logger.warn('abort because event type not expected', { id: event.id, type: event.type });
|
|
@@ -74,98 +178,7 @@ export async function handleStripeInvoiceCreated(event: TEventExpanded, client:
|
|
|
74
178
|
const checkoutSession = await CheckoutSession.findOne({ where: { subscription_id: subscription.id } });
|
|
75
179
|
|
|
76
180
|
// create stripe invoice
|
|
77
|
-
|
|
78
|
-
if (!invoice) {
|
|
79
|
-
invoice = await Invoice.create({
|
|
80
|
-
// @ts-ignore
|
|
81
|
-
number: await customer.getInvoiceNumber(),
|
|
82
|
-
...pick(stripeInvoice, [
|
|
83
|
-
'amount_due',
|
|
84
|
-
'amount_paid',
|
|
85
|
-
'amount_remaining',
|
|
86
|
-
'amount_shipping',
|
|
87
|
-
'attempt_count',
|
|
88
|
-
'attempted',
|
|
89
|
-
'auto_advance',
|
|
90
|
-
'billing_reason',
|
|
91
|
-
'collection_method',
|
|
92
|
-
'custom_fields',
|
|
93
|
-
'customer_address',
|
|
94
|
-
'customer_email',
|
|
95
|
-
'customer_name',
|
|
96
|
-
'customer_phone',
|
|
97
|
-
'description',
|
|
98
|
-
'discounts',
|
|
99
|
-
'due_date',
|
|
100
|
-
'effective_at',
|
|
101
|
-
'ending_balance',
|
|
102
|
-
'livemode',
|
|
103
|
-
'paid_out_of_band',
|
|
104
|
-
'paid',
|
|
105
|
-
'period_end',
|
|
106
|
-
'period_start',
|
|
107
|
-
'starting_balance',
|
|
108
|
-
'status_transitions',
|
|
109
|
-
'status',
|
|
110
|
-
'subtotal_excluding_tax',
|
|
111
|
-
'subtotal',
|
|
112
|
-
'tax',
|
|
113
|
-
'total_discount_amounts',
|
|
114
|
-
'total',
|
|
115
|
-
]),
|
|
116
|
-
|
|
117
|
-
currency_id: subscription.currency_id,
|
|
118
|
-
customer_id: subscription.customer_id,
|
|
119
|
-
default_payment_method_id: subscription.default_payment_method_id as string,
|
|
120
|
-
payment_intent_id: '',
|
|
121
|
-
subscription_id: subscription.id,
|
|
122
|
-
checkout_session_id: checkoutSession?.id,
|
|
123
|
-
statement_descriptor: stripeInvoice.statement_descriptor || '',
|
|
124
|
-
|
|
125
|
-
payment_settings: subscription.payment_settings,
|
|
126
|
-
metadata: {
|
|
127
|
-
stripe_id: stripeInvoice.id,
|
|
128
|
-
},
|
|
129
|
-
});
|
|
130
|
-
await client.invoices.update(stripeInvoice.id, { metadata: { appPid: env.appPid, id: invoice.id } });
|
|
131
|
-
logger.info('stripe invoice mirrored', { local: invoice.id, remote: stripeInvoice.id });
|
|
132
|
-
|
|
133
|
-
await Promise.all(
|
|
134
|
-
stripeInvoice.lines.data.map(async (line) => {
|
|
135
|
-
const subscriptionItem = line.subscription_item
|
|
136
|
-
? await SubscriptionItem.findOne({ where: { 'metadata.stripe_id': line.subscription_item } })
|
|
137
|
-
: null;
|
|
138
|
-
|
|
139
|
-
// @ts-ignore
|
|
140
|
-
const item = await InvoiceItem.create({
|
|
141
|
-
currency_id: subscription.currency_id,
|
|
142
|
-
customer_id: subscription.customer_id,
|
|
143
|
-
price_id: line.price?.metadata.id as string,
|
|
144
|
-
invoice_id: invoice?.id as string,
|
|
145
|
-
subscription_id: subscription.id,
|
|
146
|
-
subscription_item_id: subscriptionItem?.id,
|
|
147
|
-
...pick(line, [
|
|
148
|
-
'livemode',
|
|
149
|
-
'amount',
|
|
150
|
-
'quantity',
|
|
151
|
-
'description',
|
|
152
|
-
'period',
|
|
153
|
-
'discountable',
|
|
154
|
-
'discount_amounts',
|
|
155
|
-
'discounts',
|
|
156
|
-
'proration',
|
|
157
|
-
'proration_details',
|
|
158
|
-
]),
|
|
159
|
-
metadata: {
|
|
160
|
-
stripe_id: line.id,
|
|
161
|
-
},
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
logger.info('stripe invoice items mirrored', { local: item.id, remote: line.id });
|
|
165
|
-
return item;
|
|
166
|
-
})
|
|
167
|
-
);
|
|
168
|
-
}
|
|
181
|
+
const invoice = await ensureStripeInvoice(stripeInvoice, subscription, client);
|
|
169
182
|
|
|
170
183
|
return { subscription, invoice, customer, checkoutSession };
|
|
171
184
|
}
|
|
@@ -186,16 +199,32 @@ export async function handleInvoiceEvent(event: TEventExpanded, client: Stripe)
|
|
|
186
199
|
return;
|
|
187
200
|
}
|
|
188
201
|
|
|
189
|
-
|
|
202
|
+
let localInvoiceId = event.data.object.metadata?.id;
|
|
203
|
+
|
|
204
|
+
// in case we missed some of the events
|
|
205
|
+
const subscriptionId = event.data.object.subscription_details?.metadata?.id;
|
|
206
|
+
const appPid = event.data.object.subscription_details?.metadata?.appPid;
|
|
190
207
|
if (!localInvoiceId) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
208
|
+
if (subscriptionId && appPid && appPid === env.appPid) {
|
|
209
|
+
logger.warn('try mirror invoice from stripe', { invoiceId: event.data.object.id });
|
|
210
|
+
const subscription = await Subscription.findByPk(subscriptionId);
|
|
211
|
+
if (subscription) {
|
|
212
|
+
const method = await PaymentMethod.findByPk(subscription.default_payment_method_id);
|
|
213
|
+
if (method && method.type === 'stripe') {
|
|
214
|
+
const tmp = await ensureStripeInvoice(event.data.object, subscription, method.getStripe());
|
|
215
|
+
localInvoiceId = tmp.id;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
try {
|
|
220
|
+
await waitForStripeInvoiceMirrored(event.data.object.id);
|
|
221
|
+
} catch (err) {
|
|
222
|
+
logger.error('wait for stripe invoice mirror error', { localInvoiceId, error: err });
|
|
223
|
+
}
|
|
196
224
|
|
|
197
|
-
|
|
198
|
-
|
|
225
|
+
logger.warn('local invoice id not found in strip event', { localInvoiceId });
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
199
228
|
}
|
|
200
229
|
|
|
201
230
|
const invoice = await Invoice.findByPk(localInvoiceId);
|
|
@@ -232,7 +261,6 @@ export async function handleInvoiceEvent(event: TEventExpanded, client: Stripe)
|
|
|
232
261
|
return;
|
|
233
262
|
}
|
|
234
263
|
|
|
235
|
-
// TODO: handle upcoming invoices?
|
|
236
264
|
if (event.type === 'invoice.upcoming') {
|
|
237
265
|
logger.info('received invoice upcoming event', event);
|
|
238
266
|
}
|
package/api/src/jobs/event.ts
CHANGED
|
@@ -1,44 +1,54 @@
|
|
|
1
1
|
import { Op } from 'sequelize';
|
|
2
2
|
|
|
3
|
+
import { events } from '../libs/event';
|
|
3
4
|
import logger from '../libs/logger';
|
|
4
5
|
import createQueue from '../libs/queue';
|
|
6
|
+
import { getWebhookJobId } from '../libs/util';
|
|
5
7
|
import { Event } from '../store/models/event';
|
|
8
|
+
import { WebhookAttempt } from '../store/models/webhook-attempt';
|
|
6
9
|
import { WebhookEndpoint } from '../store/models/webhook-endpoint';
|
|
7
|
-
import {
|
|
10
|
+
import { webhookQueue } from './webhook';
|
|
8
11
|
|
|
9
12
|
type EventJob = {
|
|
10
13
|
eventId: string;
|
|
11
14
|
};
|
|
12
15
|
|
|
13
16
|
export const handleEvent = async (job: EventJob) => {
|
|
14
|
-
logger.info('
|
|
17
|
+
logger.info('handle event', job);
|
|
15
18
|
|
|
16
19
|
const event = await Event.findByPk(job.eventId);
|
|
17
20
|
if (!event) {
|
|
18
|
-
logger.warn(
|
|
21
|
+
logger.warn('event not found', job);
|
|
19
22
|
return;
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
if (!event.pending_webhooks) {
|
|
23
|
-
logger.warn(
|
|
26
|
+
logger.warn('event already processed', job);
|
|
24
27
|
return;
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
const webhooks = await WebhookEndpoint.findAll({ where: { status: 'enabled', livemode: event.livemode } });
|
|
28
31
|
const eventWebhooks = webhooks.filter((webhook) => webhook.enabled_events.includes(event.type));
|
|
29
32
|
if (eventWebhooks.length === 0) {
|
|
30
|
-
logger.info(
|
|
33
|
+
logger.info('no webhook endpoint for event', job);
|
|
31
34
|
await event.update({ pending_webhooks: 0 });
|
|
32
35
|
return;
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
await event.update({ pending_webhooks: eventWebhooks.length });
|
|
36
|
-
eventWebhooks.forEach((webhook) => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
id: getJobId(event.id, webhook.id),
|
|
40
|
-
job: { eventId: event.id, webhookId: webhook.id },
|
|
39
|
+
eventWebhooks.forEach(async (webhook) => {
|
|
40
|
+
const attempted = await WebhookAttempt.findOne({
|
|
41
|
+
where: { event_id: event.id, webhook_endpoint_id: webhook.id },
|
|
41
42
|
});
|
|
43
|
+
|
|
44
|
+
// we should only push webhook if it's not attempted before
|
|
45
|
+
if (!attempted) {
|
|
46
|
+
logger.info('schedule initial attempt for event', job);
|
|
47
|
+
webhookQueue.push({
|
|
48
|
+
id: getWebhookJobId(event.id, webhook.id),
|
|
49
|
+
job: { eventId: event.id, webhookId: webhook.id },
|
|
50
|
+
});
|
|
51
|
+
}
|
|
42
52
|
});
|
|
43
53
|
};
|
|
44
54
|
|
|
@@ -52,14 +62,14 @@ export const eventQueue = createQueue<EventJob>({
|
|
|
52
62
|
});
|
|
53
63
|
|
|
54
64
|
export const startEventQueue = async () => {
|
|
55
|
-
const
|
|
65
|
+
const docs = await Event.findAll({
|
|
56
66
|
where: {
|
|
57
67
|
pending_webhooks: { [Op.gt]: 0 },
|
|
58
68
|
},
|
|
59
69
|
attributes: ['id'],
|
|
60
70
|
});
|
|
61
71
|
|
|
62
|
-
|
|
72
|
+
docs.forEach(async (x) => {
|
|
63
73
|
const exist = await eventQueue.get(x.id);
|
|
64
74
|
if (!exist) {
|
|
65
75
|
eventQueue.push({ id: x.id, job: { eventId: x.id } });
|
|
@@ -68,5 +78,9 @@ export const startEventQueue = async () => {
|
|
|
68
78
|
};
|
|
69
79
|
|
|
70
80
|
eventQueue.on('failed', ({ id, job, error }) => {
|
|
71
|
-
logger.error('
|
|
81
|
+
logger.error('event job failed', { id, job, error });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
events.on('event.created', (event) => {
|
|
85
|
+
eventQueue.push({ id: event.id, job: { eventId: event.id } });
|
|
72
86
|
});
|
package/api/src/jobs/invoice.ts
CHANGED
|
@@ -18,31 +18,31 @@ type InvoiceJob = {
|
|
|
18
18
|
// handle invoice payment
|
|
19
19
|
// TODO: send invoice to user with email
|
|
20
20
|
export const handleInvoice = async (job: InvoiceJob) => {
|
|
21
|
-
logger.info('
|
|
21
|
+
logger.info('handle invoice', job);
|
|
22
22
|
|
|
23
23
|
const invoice = await Invoice.findByPk(job.invoiceId);
|
|
24
24
|
if (!invoice) {
|
|
25
|
-
logger.warn(`
|
|
25
|
+
logger.warn(`invoice not found: ${job.invoiceId}`);
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
28
|
if (invoice.status !== 'open') {
|
|
29
|
-
logger.warn(`
|
|
29
|
+
logger.warn(`invoice not open: ${job.invoiceId}`);
|
|
30
30
|
return;
|
|
31
31
|
}
|
|
32
32
|
if (invoice.auto_advance === false) {
|
|
33
|
-
logger.warn(`
|
|
33
|
+
logger.warn(`invoice not configured to auto advance: ${job.invoiceId}`);
|
|
34
34
|
return;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
const supportAutoCharge = await PaymentMethod.supportAutoCharge(invoice.default_payment_method_id);
|
|
38
38
|
if (supportAutoCharge === false) {
|
|
39
|
-
logger.warn(`
|
|
39
|
+
logger.warn(`invoice does not support auto charge: ${job.invoiceId}`);
|
|
40
40
|
return;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
// no payment required
|
|
44
44
|
if (invoice.total === '0') {
|
|
45
|
-
logger.warn(`
|
|
45
|
+
logger.warn(`invoice does not require payment: ${job.invoiceId}`);
|
|
46
46
|
|
|
47
47
|
await invoice.update({
|
|
48
48
|
paid: true,
|
|
@@ -59,7 +59,7 @@ export const handleInvoice = async (job: InvoiceJob) => {
|
|
|
59
59
|
const subscription = await Subscription.findByPk(invoice.subscription_id);
|
|
60
60
|
if (subscription && subscription.status === 'incomplete') {
|
|
61
61
|
await subscription.update({ status: subscription.trail_end ? 'trialing' : 'active' });
|
|
62
|
-
logger.info('
|
|
62
|
+
logger.info('invoice subscription updated', subscription.id);
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
@@ -67,7 +67,7 @@ export const handleInvoice = async (job: InvoiceJob) => {
|
|
|
67
67
|
const checkoutSession = await CheckoutSession.findByPk(invoice.checkout_session_id);
|
|
68
68
|
if (checkoutSession && checkoutSession.status === 'open') {
|
|
69
69
|
await checkoutSession.update({ status: 'complete', payment_status: 'no_payment_required' });
|
|
70
|
-
logger.info('
|
|
70
|
+
logger.info('invoice checkout session updated', checkoutSession.id);
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
package/api/src/jobs/payment.ts
CHANGED
|
@@ -19,7 +19,7 @@ type PaymentJob = {
|
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
export const handlePayment = async (job: PaymentJob) => {
|
|
22
|
-
logger.info('
|
|
22
|
+
logger.info('handle payment', job);
|
|
23
23
|
|
|
24
24
|
const paymentIntent = await PaymentIntent.findByPk(job.paymentIntentId);
|
|
25
25
|
if (!paymentIntent) {
|
|
@@ -21,7 +21,7 @@ type SubscriptionJob = {
|
|
|
21
21
|
|
|
22
22
|
// generate invoice for subscription periodically
|
|
23
23
|
export const handleSubscription = async (job: SubscriptionJob) => {
|
|
24
|
-
logger.info('
|
|
24
|
+
logger.info('handle subscription', job);
|
|
25
25
|
|
|
26
26
|
const subscription = await Subscription.findByPk(job.subscriptionId);
|
|
27
27
|
if (!subscription) {
|
package/api/src/jobs/webhook.ts
CHANGED
|
@@ -4,8 +4,10 @@ import axios, { AxiosError } from 'axios';
|
|
|
4
4
|
import { wallet } from '../libs/auth';
|
|
5
5
|
import logger from '../libs/logger';
|
|
6
6
|
import createQueue from '../libs/queue';
|
|
7
|
-
import { MAX_RETRY_COUNT, getNextRetry,
|
|
7
|
+
import { MAX_RETRY_COUNT, getNextRetry, getWebhookJobId } from '../libs/util';
|
|
8
|
+
import { Customer } from '../store/models/customer';
|
|
8
9
|
import { Event } from '../store/models/event';
|
|
10
|
+
import { PaymentCurrency } from '../store/models/payment-currency';
|
|
9
11
|
import { WebhookAttempt } from '../store/models/webhook-attempt';
|
|
10
12
|
import { WebhookEndpoint } from '../store/models/webhook-endpoint';
|
|
11
13
|
|
|
@@ -14,49 +16,54 @@ type WebhookJob = {
|
|
|
14
16
|
webhookId: string;
|
|
15
17
|
};
|
|
16
18
|
|
|
17
|
-
export const getJobId = (eventId: string, webhookId: string) => {
|
|
18
|
-
return md5([eventId, webhookId].join('-'));
|
|
19
|
-
};
|
|
20
|
-
|
|
21
19
|
// https://stripe.com/docs/webhooks
|
|
22
20
|
export const handleWebhook = async (job: WebhookJob) => {
|
|
23
|
-
logger.info('
|
|
21
|
+
logger.info('handle webhook', job);
|
|
24
22
|
|
|
25
23
|
const event = await Event.findByPk(job.eventId);
|
|
26
24
|
if (!event) {
|
|
27
|
-
logger.warn(
|
|
25
|
+
logger.warn('event not found when attempt webhook', job);
|
|
28
26
|
return;
|
|
29
27
|
}
|
|
30
28
|
|
|
31
29
|
const webhook = await WebhookEndpoint.findByPk(job.webhookId);
|
|
32
30
|
if (!webhook) {
|
|
33
|
-
logger.warn(
|
|
31
|
+
logger.warn('webhook not found on attempt', job);
|
|
34
32
|
return;
|
|
35
33
|
}
|
|
36
34
|
if (webhook.status !== 'enabled') {
|
|
37
|
-
logger.warn(
|
|
35
|
+
logger.warn('webhook disabled on attempt', job);
|
|
38
36
|
return;
|
|
39
37
|
}
|
|
40
38
|
|
|
41
|
-
const
|
|
39
|
+
const lastRetryCount = await WebhookAttempt.max('retry_count', {
|
|
42
40
|
where: { event_id: event.id, webhook_endpoint_id: webhook.id },
|
|
43
|
-
order: [['retry_count', 'DESC']],
|
|
44
|
-
attributes: ['retry_count'],
|
|
45
41
|
});
|
|
46
42
|
|
|
47
|
-
const retryCount =
|
|
43
|
+
const retryCount = lastRetryCount ? +lastRetryCount + 1 : 1;
|
|
48
44
|
|
|
49
45
|
try {
|
|
46
|
+
const json = event.toJSON();
|
|
47
|
+
|
|
48
|
+
// expand basic fields
|
|
49
|
+
const { object } = json.data;
|
|
50
|
+
if (object.customer_id && !object.customer) {
|
|
51
|
+
object.customer = await Customer.findByPk(object.customer_id);
|
|
52
|
+
}
|
|
53
|
+
if (object.currency_id && !object.currency) {
|
|
54
|
+
object.currency = await PaymentCurrency.findByPk(object.currency_id);
|
|
55
|
+
}
|
|
56
|
+
|
|
50
57
|
// verify similar to component call, but supports external urls
|
|
51
58
|
const result = await axios({
|
|
52
59
|
url: webhook.url,
|
|
53
60
|
method: 'POST',
|
|
54
61
|
timeout: 60 * 1000,
|
|
55
|
-
data:
|
|
62
|
+
data: json,
|
|
56
63
|
headers: {
|
|
57
64
|
'x-app-id': wallet.address,
|
|
58
65
|
'x-app-pk': wallet.publicKey,
|
|
59
|
-
'x-component-sig': sign(
|
|
66
|
+
'x-component-sig': sign(json),
|
|
60
67
|
'x-component-did': process.env.BLOCKLET_COMPONENT_DID as string,
|
|
61
68
|
},
|
|
62
69
|
});
|
|
@@ -72,9 +79,9 @@ export const handleWebhook = async (job: WebhookJob) => {
|
|
|
72
79
|
});
|
|
73
80
|
|
|
74
81
|
await event.decrement('pending_webhooks');
|
|
75
|
-
logger.info(
|
|
82
|
+
logger.info('webhook attempt success', { ...job, retryCount });
|
|
76
83
|
} catch (err: any) {
|
|
77
|
-
logger.
|
|
84
|
+
logger.warn('webhook attempt error', { ...job, retryCount, message: err.message });
|
|
78
85
|
await WebhookAttempt.create({
|
|
79
86
|
livemode: event.livemode,
|
|
80
87
|
event_id: event.id,
|
|
@@ -89,9 +96,9 @@ export const handleWebhook = async (job: WebhookJob) => {
|
|
|
89
96
|
if (retryCount < MAX_RETRY_COUNT) {
|
|
90
97
|
process.nextTick(() => {
|
|
91
98
|
webhookQueue.push({
|
|
92
|
-
id:
|
|
99
|
+
id: getWebhookJobId(event.id, webhook.id),
|
|
93
100
|
job: { eventId: event.id, webhookId: webhook.id },
|
|
94
|
-
runAt: getNextRetry(retryCount
|
|
101
|
+
runAt: getNextRetry(retryCount),
|
|
95
102
|
});
|
|
96
103
|
});
|
|
97
104
|
}
|
package/api/src/libs/audit.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import pick from 'lodash/pick';
|
|
2
2
|
|
|
3
|
-
import { eventQueue } from '../jobs/event';
|
|
4
3
|
import { Event } from '../store/models/event';
|
|
4
|
+
import { events } from './event';
|
|
5
5
|
|
|
6
6
|
export async function createEvent(scope: string, type: string, model: any, options: any) {
|
|
7
7
|
// console.log('createEvent', scope, type, model, options);
|
|
@@ -28,7 +28,7 @@ export async function createEvent(scope: string, type: string, model: any, optio
|
|
|
28
28
|
pending_webhooks: 99, // force all events goto the event queue
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
events.emit('event.created', { id: event.id });
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
export async function createStatusEvent(
|
|
@@ -69,5 +69,5 @@ export async function createStatusEvent(
|
|
|
69
69
|
pending_webhooks: 99, // force all events goto the event queue
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
events.emit('event.created', { id: event.id });
|
|
73
73
|
}
|
package/api/src/libs/util.ts
CHANGED
|
@@ -66,6 +66,7 @@ export function createCodeGenerator(prefix: string, size: number = 24) {
|
|
|
66
66
|
return prefix ? () => `${prefix}_${generator()}` : generator;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
// FIXME: merge with old metadata
|
|
69
70
|
export function formatMetadata(metadata?: Record<string, any>): Record<string, any> {
|
|
70
71
|
if (!metadata) {
|
|
71
72
|
return {};
|
|
@@ -137,3 +138,7 @@ export const getNextRetry = (retryCount: number) => {
|
|
|
137
138
|
const now = dayjs().unix();
|
|
138
139
|
return now + delay;
|
|
139
140
|
};
|
|
141
|
+
|
|
142
|
+
export const getWebhookJobId = (eventId: string, webhookId: string) => {
|
|
143
|
+
return md5([eventId, webhookId].join('-'));
|
|
144
|
+
};
|
|
@@ -8,7 +8,7 @@ import dayjs from '../../libs/dayjs';
|
|
|
8
8
|
import { ensureInvoiceForCheckout, ensurePaymentIntent, getAuthPrincipalClaim } from './shared';
|
|
9
9
|
|
|
10
10
|
export default {
|
|
11
|
-
action: '
|
|
11
|
+
action: 'payment',
|
|
12
12
|
authPrincipal: false,
|
|
13
13
|
claims: {
|
|
14
14
|
authPrincipal: async ({ extraParams }: CallbackArgs) => {
|
|
@@ -8,6 +8,7 @@ import dayjs from '../libs/dayjs';
|
|
|
8
8
|
import logger from '../libs/logger';
|
|
9
9
|
import { authenticate } from '../libs/security';
|
|
10
10
|
import { expandLineItems } from '../libs/session';
|
|
11
|
+
import { formatMetadata } from '../libs/util';
|
|
11
12
|
import { Customer } from '../store/models/customer';
|
|
12
13
|
import { PaymentCurrency } from '../store/models/payment-currency';
|
|
13
14
|
import { PaymentMethod } from '../store/models/payment-method';
|
|
@@ -302,4 +303,18 @@ router.put('/:id/resume', auth, async (req, res) => {
|
|
|
302
303
|
return res.json(doc);
|
|
303
304
|
});
|
|
304
305
|
|
|
306
|
+
router.put('/:id', auth, async (req, res) => {
|
|
307
|
+
const doc = await Subscription.findByPk(req.params.id);
|
|
308
|
+
|
|
309
|
+
if (!doc) {
|
|
310
|
+
return res.status(404).json({ error: 'Subscription not found' });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (req.body.metadata) {
|
|
314
|
+
await doc.update({ metadata: formatMetadata(req.body.metadata) });
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return res.json(doc);
|
|
318
|
+
});
|
|
319
|
+
|
|
305
320
|
export default router;
|
package/blocklet.yml
CHANGED
|
@@ -14,7 +14,7 @@ repository:
|
|
|
14
14
|
type: git
|
|
15
15
|
url: git+https://github.com/blocklet/payment-kit.git
|
|
16
16
|
specVersion: 1.2.8
|
|
17
|
-
version: 1.13.
|
|
17
|
+
version: 1.13.24
|
|
18
18
|
logo: logo.png
|
|
19
19
|
files:
|
|
20
20
|
- dist
|
|
@@ -40,7 +40,7 @@ payment:
|
|
|
40
40
|
timeout:
|
|
41
41
|
start: 60
|
|
42
42
|
requirements:
|
|
43
|
-
server: '>=1.16.
|
|
43
|
+
server: '>=1.16.10'
|
|
44
44
|
os: '*'
|
|
45
45
|
cpu: '*'
|
|
46
46
|
scripts:
|