Skip to content

Commit 578f5d4

Browse files
feat: Added support for invoice payment object (#245)
1 parent 16ba283 commit 578f5d4

File tree

8 files changed

+167
-2
lines changed

8 files changed

+167
-2
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ To deploy the sync-engine to a Supabase Edge Function, follow this [guide](./doc
106106
- [x] `invoice.overpaid` 🟢
107107
- [x] `invoice.will_be_due` 🟢
108108
- [x] `invoice.voided` 🟢
109+
- [x] `invoice_payment.paid` 🟢
109110
- [ ] `issuing_authorization.request`
110111
- [ ] `issuing_card.created`
111112
- [ ] `issuing_cardholder.created`

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ This project synchronizes your Stripe account to a PostgreSQL database. It can b
7171
- [x] `invoice.overpaid` 🟢
7272
- [x] `invoice.will_be_due` 🟢
7373
- [x] `invoice.voided` 🟢
74+
- [x] `invoice_payment.paid` 🟢
7475
- [ ] `issuing_authorization.request`
7576
- [ ] `issuing_card.created`
7677
- [ ] `issuing_cardholder.created`

packages/fastify-app/src/test/helpers/mockStripe.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,30 @@ export const mockStripe = {
277277
},
278278
})),
279279
},
280+
invoicePayments: {
281+
retrieve: vitest.fn((id) =>
282+
Promise.resolve({
283+
id: id,
284+
object: 'invoice_payment',
285+
amount_paid: 2000,
286+
amount_requested: 2000,
287+
created: 1391288554,
288+
currency: 'usd',
289+
invoice: 'in_103Q0w2eZvKYlo2C5PYwf6Wf',
290+
is_default: true,
291+
livemode: false,
292+
payment: {
293+
type: 'payment_intent',
294+
payment_intent: 'pi_103Q0w2eZvKYlo2C364X582Z',
295+
},
296+
status: 'paid',
297+
status_transitions: {
298+
canceled_at: null,
299+
paid_at: 1391288554,
300+
},
301+
})
302+
),
303+
},
280304
subscriptions: {
281305
retrieve: vitest.fn((id) =>
282306
Promise.resolve({
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"id": "evt_test_invoice_payment_paid",
3+
"object": "event",
4+
"api_version": "2023-10-16",
5+
"created": 1642649111,
6+
"data": {
7+
"object": {
8+
"id": "inpay_1M3USa2eZvKYlo2CBjuwbq0N",
9+
"object": "invoice_payment",
10+
"amount_paid": 2000,
11+
"amount_requested": 2000,
12+
"created": 1391288554,
13+
"currency": "usd",
14+
"invoice": "in_103Q0w2eZvKYlo2C5PYwf6Wf",
15+
"is_default": true,
16+
"livemode": false,
17+
"payment": {
18+
"type": "payment_intent",
19+
"payment_intent": "pi_103Q0w2eZvKYlo2C364X582Z"
20+
},
21+
"status": "paid",
22+
"status_transitions": {
23+
"canceled_at": null,
24+
"paid_at": 1391288554
25+
}
26+
}
27+
},
28+
"livemode": false,
29+
"pending_webhooks": 1,
30+
"request": {
31+
"id": null,
32+
"idempotency_key": null
33+
},
34+
"type": "invoice_payment.paid"
35+
}

packages/fastify-app/src/test/webhooks.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ describe('POST /webhooks', () => {
125125
'refund_failed.json',
126126
'refund_updated.json',
127127
'checkout_session_completed.json',
128+
'invoice_payment_paid.json',
128129
])('event %s is upserted', async (jsonFile) => {
129130
const eventBody = await import(`./stripe/${jsonFile}`).then(({ default: myData }) => myData)
130131
// Update the event body created timestamp to be the current time
@@ -147,7 +148,7 @@ describe('POST /webhooks', () => {
147148
})
148149

149150
if (response.statusCode != 200) {
150-
logger.error('error: ', response.body)
151+
logger.error({ responseBody: response.body }, 'error')
151152
}
152153
expect(response.statusCode).toBe(200)
153154

@@ -192,7 +193,7 @@ describe('POST /webhooks', () => {
192193
})
193194

194195
if (response.statusCode != 200) {
195-
logger.error('error: ', response.body)
196+
logger.error({ responseBody: response.body }, 'error')
196197
}
197198
expect(response.statusCode).toBe(200)
198199
expect(JSON.parse(response.body)).toMatchObject({ received: true })
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
create table
2+
if not exists "stripe"."invoice_payments" (
3+
"id" text primary key,
4+
object text,
5+
amount_paid bigint,
6+
amount_requested bigint,
7+
created integer,
8+
currency text,
9+
invoice text,
10+
is_default boolean,
11+
livemode boolean,
12+
payment jsonb,
13+
status text,
14+
status_transitions jsonb,
15+
last_synced_at timestamptz,
16+
updated_at timestamptz default timezone('utc'::text, now()) not null
17+
);
18+
19+
create index stripe_invoice_payments_invoice_idx on "stripe"."invoice_payments" using btree (invoice);
20+
21+
create trigger handle_updated_at
22+
before update
23+
on stripe.invoice_payments
24+
for each row
25+
execute procedure set_updated_at();
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { EntitySchema } from './types'
2+
3+
export const invoicePaymentSchema: EntitySchema = {
4+
properties: [
5+
'id',
6+
'object',
7+
'amount_paid',
8+
'amount_requested',
9+
'created',
10+
'currency',
11+
'invoice',
12+
'is_default',
13+
'livemode',
14+
'payment',
15+
'status',
16+
'status_transitions',
17+
],
18+
} as const

packages/sync-engine/src/stripeSync.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { reviewSchema } from './schemas/review'
3232
import { refundSchema } from './schemas/refund'
3333
import { activeEntitlementSchema } from './schemas/active_entitlement'
3434
import { featureSchema } from './schemas/feature'
35+
import { invoicePaymentSchema } from './schemas/invoice_payment'
3536
import type { PoolConfig } from 'pg'
3637

3738
function getUniqueIds<T>(entries: T[], key: string): string[] {
@@ -553,6 +554,24 @@ export class StripeSync {
553554
)
554555
break
555556
}
557+
case 'invoice_payment.paid': {
558+
const { entity: invoicePayment, refetched } = await this.fetchOrUseWebhookData(
559+
event.data.object as Stripe.InvoicePayment,
560+
(id) => this.stripe.invoicePayments.retrieve(id)
561+
)
562+
563+
this.config.logger?.info(
564+
`Received webhook ${event.id}: ${event.type} for invoicePayment ${invoicePayment.id}`
565+
)
566+
567+
await this.upsertInvoicePayments(
568+
[invoicePayment],
569+
false,
570+
this.getSyncTimestamp(event, refetched)
571+
)
572+
573+
break
574+
}
556575
default:
557576
throw new Error('Unhandled webhook event')
558577
}
@@ -627,6 +646,10 @@ export class StripeSync {
627646
return this.stripe.reviews.retrieve(stripeId).then((it) => this.upsertReviews([it]))
628647
} else if (stripeId.startsWith('re_')) {
629648
return this.stripe.refunds.retrieve(stripeId).then((it) => this.upsertRefunds([it]))
649+
} else if (stripeId.startsWith('inpay_')) {
650+
return this.stripe.invoicePayments
651+
.retrieve(stripeId)
652+
.then((it) => this.upsertInvoicePayments([it]))
630653
} else if (stripeId.startsWith('feat_')) {
631654
return this.stripe.entitlements.features
632655
.retrieve(stripeId)
@@ -1280,6 +1303,43 @@ export class StripeSync {
12801303
)
12811304
}
12821305

1306+
async upsertInvoicePayments(
1307+
invoicePayments: Stripe.InvoicePayment[],
1308+
backfillRelatedEntities?: boolean,
1309+
syncTimestamp?: string
1310+
): Promise<Stripe.InvoicePayment[]> {
1311+
if (backfillRelatedEntities ?? this.config.backfillRelatedEntities) {
1312+
const invoiceIds = getUniqueIds(invoicePayments, 'invoice')
1313+
const paymentIntentIds: string[] = []
1314+
const chargeIds: string[] = []
1315+
1316+
for (const invoicePayment of invoicePayments) {
1317+
const payment = invoicePayment.payment as
1318+
| { type?: string; payment_intent?: string; charge?: string }
1319+
| undefined
1320+
1321+
if (payment?.type === 'payment_intent' && payment.payment_intent) {
1322+
paymentIntentIds.push(payment.payment_intent.toString())
1323+
} else if (payment?.type === 'charge' && payment.charge) {
1324+
chargeIds.push(payment.charge.toString())
1325+
}
1326+
}
1327+
1328+
await Promise.all([
1329+
this.backfillInvoices(invoiceIds),
1330+
this.backfillPaymentIntents([...new Set(paymentIntentIds)]),
1331+
this.backfillCharges([...new Set(chargeIds)]),
1332+
])
1333+
}
1334+
1335+
return this.postgresClient.upsertManyWithTimestampProtection(
1336+
invoicePayments,
1337+
'invoice_payments',
1338+
invoicePaymentSchema,
1339+
syncTimestamp
1340+
)
1341+
}
1342+
12831343
async upsertPlans(
12841344
plans: Stripe.Plan[],
12851345
backfillRelatedEntities?: boolean,

0 commit comments

Comments
 (0)