Skip to content

Commit 01ab409

Browse files
authored
feat: support tax ids (#108)
1 parent 35edbfd commit 01ab409

File tree

12 files changed

+202
-9
lines changed

12 files changed

+202
-9
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ This server synchronizes your Stripe account to a Postgres database. It can be a
5353
- [x] `customer.subscription.resumed` 🟢
5454
- [x] `customer.subscription.trial_will_end` 🟢
5555
- [x] `customer.subscription.updated` 🟢
56+
- [x] `customer.tax_id.created` 🟢
57+
- [x] `customer.tax_id.deleted` 🟢
58+
- [x] `customer.tax_id.updated` 🟢
5659
- [x] `customer.updated` 🟢
5760
- [x] `invoice.created` 🟢
5861
- [x] `invoice.deleted` 🟢

db/migrations/0025_tax_ids.sql

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
create table if not exists
2+
"stripe"."tax_ids" (
3+
"id" text primary key,
4+
"object" text,
5+
"country" text,
6+
"customer" text,
7+
"type" text,
8+
"value" text,
9+
"created" integer not null,
10+
"livemode" boolean,
11+
"owner" jsonb
12+
);
13+
14+
create index stripe_tax_ids_customer_idx on "stripe"."tax_ids" using btree (customer);

package-lock.json

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"pg": "^8.11.3",
3737
"pg-node-migrations": "0.0.8",
3838
"pino": "^8.17.2",
39-
"stripe": "^14.13.0",
39+
"stripe": "^14.21.0",
4040
"yesql": "^7.0.0"
4141
},
4242
"devDependencies": {

src/lib/sync.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { upsertPaymentIntents } from './payment_intents'
1616
import { upsertPlans } from './plans'
1717
import { upsertSubscriptionSchedules } from './subscription_schedules'
1818
import pLimit from 'p-limit'
19+
import { upsertTaxIds } from './tax_ids'
1920

2021
const config = getConfig()
2122

@@ -36,6 +37,7 @@ interface SyncBackfill {
3637
paymentMethods?: Sync
3738
disputes?: Sync
3839
charges?: Sync
40+
taxIds?: Sync
3941
}
4042

4143
export interface SyncBackfillParams {
@@ -58,6 +60,7 @@ type SyncObject =
5860
| 'charge'
5961
| 'payment_intent'
6062
| 'plan'
63+
| 'tax_id'
6164

6265
export async function syncSingleEntity(stripeId: string) {
6366
if (stripeId.startsWith('cus_')) {
@@ -84,6 +87,8 @@ export async function syncSingleEntity(stripeId: string) {
8487
return stripe.charges.retrieve(stripeId).then((it) => upsertCharges([it]))
8588
} else if (stripeId.startsWith('pi_')) {
8689
return stripe.paymentIntents.retrieve(stripeId).then((it) => upsertPaymentIntents([it]))
90+
} else if (stripeId.startsWith('txi_')) {
91+
return stripe.taxIds.retrieve(stripeId).then((it) => upsertTaxIds([it]))
8792
}
8893
}
8994

@@ -100,7 +105,8 @@ export async function syncBackfill(params?: SyncBackfillParams): Promise<SyncBac
100105
disputes,
101106
charges,
102107
paymentIntents,
103-
plans
108+
plans,
109+
taxIds
104110

105111
switch (object) {
106112
case 'all':
@@ -115,6 +121,7 @@ export async function syncBackfill(params?: SyncBackfillParams): Promise<SyncBac
115121
setupIntents = await syncSetupIntents(params)
116122
paymentMethods = await syncPaymentMethods(params)
117123
paymentIntents = await syncPaymentIntents(params)
124+
taxIds = await syncTaxIds(params)
118125
break
119126
case 'customer':
120127
customers = await syncCustomers(params)
@@ -151,6 +158,9 @@ export async function syncBackfill(params?: SyncBackfillParams): Promise<SyncBac
151158
case 'plan':
152159
plans = await syncPlans(params)
153160
break
161+
case 'tax_id':
162+
taxIds = await syncTaxIds(params)
163+
break
154164
default:
155165
break
156166
}
@@ -168,6 +178,7 @@ export async function syncBackfill(params?: SyncBackfillParams): Promise<SyncBac
168178
charges,
169179
paymentIntents,
170180
plans,
181+
taxIds,
171182
}
172183
}
173184

@@ -291,6 +302,17 @@ export async function syncPaymentIntents(syncParams?: SyncBackfillParams): Promi
291302
)
292303
}
293304

305+
export async function syncTaxIds(syncParams?: SyncBackfillParams): Promise<Sync> {
306+
console.log('Syncing tax_ids')
307+
308+
const params: Stripe.TaxIdListParams = { limit: 100 }
309+
310+
return fetchAndUpsert(
311+
() => stripe.taxIds.list(params),
312+
(items) => upsertTaxIds(items, syncParams?.backfillRelatedEntities)
313+
)
314+
}
315+
294316
export async function syncPaymentMethods(syncParams?: SyncBackfillParams): Promise<Sync> {
295317
// We can't filter by date here, it is also not possible to get payment methods without specifying a customer (you need Stripe Sigma for that -.-)
296318
// Thus, we need to loop through all customers

src/lib/tax_ids.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import Stripe from 'stripe'
2+
import { getConfig } from '../utils/config'
3+
import { constructUpsertSql } from '../utils/helpers'
4+
import { backfillCustomers } from './customers'
5+
import { getUniqueIds, upsertMany } from './database_utils'
6+
import { taxIdSchema } from '../schemas/tax_id'
7+
import { pg as sql } from 'yesql'
8+
import { query } from '../utils/PostgresConnection'
9+
10+
const config = getConfig()
11+
12+
export const upsertTaxIds = async (
13+
taxIds: Stripe.TaxId[],
14+
backfillRelatedEntities: boolean = true
15+
): Promise<Stripe.TaxId[]> => {
16+
if (backfillRelatedEntities) {
17+
await backfillCustomers(getUniqueIds(taxIds, 'customer'))
18+
}
19+
20+
return upsertMany(taxIds, () => constructUpsertSql(config.SCHEMA, 'tax_ids', taxIdSchema))
21+
}
22+
23+
export const deleteTaxId = async (id: string): Promise<boolean> => {
24+
const prepared = sql(`
25+
delete from "${config.SCHEMA}"."tax_ids"
26+
where id = :id
27+
returning id;
28+
`)({ id })
29+
const { rows } = await query(prepared.text, prepared.values)
30+
return rows.length > 0
31+
}

src/routes/webhooks.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { upsertDisputes } from '../lib/disputes'
1414
import { deletePlan, upsertPlans } from '../lib/plans'
1515
import { upsertPaymentIntents } from '../lib/payment_intents'
1616
import { upsertSubscriptionSchedules } from '../lib/subscription_schedules'
17+
import { deleteTaxId, upsertTaxIds } from '../lib/tax_ids'
1718

1819
const config = getConfig()
1920

@@ -62,6 +63,17 @@ export default async function routes(fastify: FastifyInstance) {
6263
await upsertSubscriptions([subscription])
6364
break
6465
}
66+
case 'customer.tax_id.updated':
67+
case 'customer.tax_id.created': {
68+
const taxId = event.data.object as Stripe.TaxId
69+
await upsertTaxIds([taxId])
70+
break
71+
}
72+
case 'customer.tax_id.deleted': {
73+
const taxId = event.data.object as Stripe.TaxId
74+
await deleteTaxId(taxId.id)
75+
break
76+
}
6577
case 'invoice.created':
6678
case 'invoice.deleted':
6779
case 'invoice.finalized':

src/schemas/tax_id.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { JsonSchema } from '../types/types'
2+
3+
export const taxIdSchema: JsonSchema = {
4+
$id: 'taxIdSchema',
5+
type: 'object',
6+
properties: {
7+
id: { type: 'string' },
8+
country: { type: 'string' },
9+
customer: { type: 'string' },
10+
type: { type: 'string' },
11+
value: { type: 'string' },
12+
object: { type: 'string' },
13+
created: { type: 'integer' },
14+
livemode: { type: 'boolean' },
15+
owner: { type: 'object' },
16+
},
17+
required: ['id'],
18+
} as const
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"id": "evt_3KtQThJDPojXS6LN0E06aNxq",
3+
"object": "event",
4+
"api_version": "2020-03-02",
5+
"created": 1619701111,
6+
"data": {
7+
"object": {
8+
"id": "txi_1NuMB12eZvKYlo2CMecoWkZd",
9+
"object": "tax_id",
10+
"country": "DE",
11+
"created": 123456789,
12+
"customer": null,
13+
"livemode": false,
14+
"type": "eu_vat",
15+
"value": "DE123456789",
16+
"verification": null,
17+
"owner": {
18+
"type": "self",
19+
"customer": null
20+
}
21+
}
22+
},
23+
"livemode": false,
24+
"pending_webhooks": 4,
25+
"request": {
26+
"id": "req_QyDCzn33ls4m1t",
27+
"idempotency_key": null
28+
},
29+
"type": "customer.tax_id.created"
30+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"id": "evt_3KtQThJDPojXS6LN0E06aNxq",
3+
"object": "event",
4+
"api_version": "2020-03-02",
5+
"created": 1619701111,
6+
"data": {
7+
"object": {
8+
"id": "txi_1NuMB12eZvKYlo2CMecoWkZd",
9+
"object": "tax_id",
10+
"country": "DE",
11+
"created": 123456789,
12+
"customer": null,
13+
"livemode": false,
14+
"type": "eu_vat",
15+
"value": "DE123456789",
16+
"verification": null,
17+
"owner": {
18+
"type": "self",
19+
"customer": null
20+
}
21+
}
22+
},
23+
"livemode": false,
24+
"pending_webhooks": 4,
25+
"request": {
26+
"id": "req_QyDCzn33ls4m1t",
27+
"idempotency_key": null
28+
},
29+
"type": "customer.tax_id.deleted"
30+
}

0 commit comments

Comments
 (0)