Skip to content

Commit

Permalink
feat: support tax ids (#108)
Browse files Browse the repository at this point in the history
  • Loading branch information
kevcodez committed Mar 21, 2024
1 parent 35edbfd commit 01ab409
Show file tree
Hide file tree
Showing 12 changed files with 202 additions and 9 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ This server synchronizes your Stripe account to a Postgres database. It can be a
- [x] `customer.subscription.resumed` 🟢
- [x] `customer.subscription.trial_will_end` 🟢
- [x] `customer.subscription.updated` 🟢
- [x] `customer.tax_id.created` 🟢
- [x] `customer.tax_id.deleted` 🟢
- [x] `customer.tax_id.updated` 🟢
- [x] `customer.updated` 🟢
- [x] `invoice.created` 🟢
- [x] `invoice.deleted` 🟢
Expand Down
14 changes: 14 additions & 0 deletions db/migrations/0025_tax_ids.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
create table if not exists
"stripe"."tax_ids" (
"id" text primary key,
"object" text,
"country" text,
"customer" text,
"type" text,
"value" text,
"created" integer not null,
"livemode" boolean,
"owner" jsonb
);

create index stripe_tax_ids_customer_idx on "stripe"."tax_ids" using btree (customer);
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"pg": "^8.11.3",
"pg-node-migrations": "0.0.8",
"pino": "^8.17.2",
"stripe": "^14.13.0",
"stripe": "^14.21.0",
"yesql": "^7.0.0"
},
"devDependencies": {
Expand Down
24 changes: 23 additions & 1 deletion src/lib/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { upsertPaymentIntents } from './payment_intents'
import { upsertPlans } from './plans'
import { upsertSubscriptionSchedules } from './subscription_schedules'
import pLimit from 'p-limit'
import { upsertTaxIds } from './tax_ids'

const config = getConfig()

Expand All @@ -36,6 +37,7 @@ interface SyncBackfill {
paymentMethods?: Sync
disputes?: Sync
charges?: Sync
taxIds?: Sync
}

export interface SyncBackfillParams {
Expand All @@ -58,6 +60,7 @@ type SyncObject =
| 'charge'
| 'payment_intent'
| 'plan'
| 'tax_id'

export async function syncSingleEntity(stripeId: string) {
if (stripeId.startsWith('cus_')) {
Expand All @@ -84,6 +87,8 @@ export async function syncSingleEntity(stripeId: string) {
return stripe.charges.retrieve(stripeId).then((it) => upsertCharges([it]))
} else if (stripeId.startsWith('pi_')) {
return stripe.paymentIntents.retrieve(stripeId).then((it) => upsertPaymentIntents([it]))
} else if (stripeId.startsWith('txi_')) {
return stripe.taxIds.retrieve(stripeId).then((it) => upsertTaxIds([it]))
}
}

Expand All @@ -100,7 +105,8 @@ export async function syncBackfill(params?: SyncBackfillParams): Promise<SyncBac
disputes,
charges,
paymentIntents,
plans
plans,
taxIds

switch (object) {
case 'all':
Expand All @@ -115,6 +121,7 @@ export async function syncBackfill(params?: SyncBackfillParams): Promise<SyncBac
setupIntents = await syncSetupIntents(params)
paymentMethods = await syncPaymentMethods(params)
paymentIntents = await syncPaymentIntents(params)
taxIds = await syncTaxIds(params)
break
case 'customer':
customers = await syncCustomers(params)
Expand Down Expand Up @@ -151,6 +158,9 @@ export async function syncBackfill(params?: SyncBackfillParams): Promise<SyncBac
case 'plan':
plans = await syncPlans(params)
break
case 'tax_id':
taxIds = await syncTaxIds(params)
break
default:
break
}
Expand All @@ -168,6 +178,7 @@ export async function syncBackfill(params?: SyncBackfillParams): Promise<SyncBac
charges,
paymentIntents,
plans,
taxIds,
}
}

Expand Down Expand Up @@ -291,6 +302,17 @@ export async function syncPaymentIntents(syncParams?: SyncBackfillParams): Promi
)
}

export async function syncTaxIds(syncParams?: SyncBackfillParams): Promise<Sync> {
console.log('Syncing tax_ids')

const params: Stripe.TaxIdListParams = { limit: 100 }

return fetchAndUpsert(
() => stripe.taxIds.list(params),
(items) => upsertTaxIds(items, syncParams?.backfillRelatedEntities)
)
}

export async function syncPaymentMethods(syncParams?: SyncBackfillParams): Promise<Sync> {
// 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 -.-)
// Thus, we need to loop through all customers
Expand Down
31 changes: 31 additions & 0 deletions src/lib/tax_ids.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Stripe from 'stripe'
import { getConfig } from '../utils/config'
import { constructUpsertSql } from '../utils/helpers'
import { backfillCustomers } from './customers'
import { getUniqueIds, upsertMany } from './database_utils'
import { taxIdSchema } from '../schemas/tax_id'
import { pg as sql } from 'yesql'
import { query } from '../utils/PostgresConnection'

const config = getConfig()

export const upsertTaxIds = async (
taxIds: Stripe.TaxId[],
backfillRelatedEntities: boolean = true
): Promise<Stripe.TaxId[]> => {
if (backfillRelatedEntities) {
await backfillCustomers(getUniqueIds(taxIds, 'customer'))
}

return upsertMany(taxIds, () => constructUpsertSql(config.SCHEMA, 'tax_ids', taxIdSchema))
}

export const deleteTaxId = async (id: string): Promise<boolean> => {
const prepared = sql(`
delete from "${config.SCHEMA}"."tax_ids"
where id = :id
returning id;
`)({ id })
const { rows } = await query(prepared.text, prepared.values)
return rows.length > 0
}
12 changes: 12 additions & 0 deletions src/routes/webhooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { upsertDisputes } from '../lib/disputes'
import { deletePlan, upsertPlans } from '../lib/plans'
import { upsertPaymentIntents } from '../lib/payment_intents'
import { upsertSubscriptionSchedules } from '../lib/subscription_schedules'
import { deleteTaxId, upsertTaxIds } from '../lib/tax_ids'

const config = getConfig()

Expand Down Expand Up @@ -62,6 +63,17 @@ export default async function routes(fastify: FastifyInstance) {
await upsertSubscriptions([subscription])
break
}
case 'customer.tax_id.updated':
case 'customer.tax_id.created': {
const taxId = event.data.object as Stripe.TaxId
await upsertTaxIds([taxId])
break
}
case 'customer.tax_id.deleted': {
const taxId = event.data.object as Stripe.TaxId
await deleteTaxId(taxId.id)
break
}
case 'invoice.created':
case 'invoice.deleted':
case 'invoice.finalized':
Expand Down
18 changes: 18 additions & 0 deletions src/schemas/tax_id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { JsonSchema } from '../types/types'

export const taxIdSchema: JsonSchema = {
$id: 'taxIdSchema',
type: 'object',
properties: {
id: { type: 'string' },
country: { type: 'string' },
customer: { type: 'string' },
type: { type: 'string' },
value: { type: 'string' },
object: { type: 'string' },
created: { type: 'integer' },
livemode: { type: 'boolean' },
owner: { type: 'object' },
},
required: ['id'],
} as const
30 changes: 30 additions & 0 deletions test/stripe/customer_tax_id_created.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"id": "evt_3KtQThJDPojXS6LN0E06aNxq",
"object": "event",
"api_version": "2020-03-02",
"created": 1619701111,
"data": {
"object": {
"id": "txi_1NuMB12eZvKYlo2CMecoWkZd",
"object": "tax_id",
"country": "DE",
"created": 123456789,
"customer": null,
"livemode": false,
"type": "eu_vat",
"value": "DE123456789",
"verification": null,
"owner": {
"type": "self",
"customer": null
}
}
},
"livemode": false,
"pending_webhooks": 4,
"request": {
"id": "req_QyDCzn33ls4m1t",
"idempotency_key": null
},
"type": "customer.tax_id.created"
}
30 changes: 30 additions & 0 deletions test/stripe/customer_tax_id_deleted.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"id": "evt_3KtQThJDPojXS6LN0E06aNxq",
"object": "event",
"api_version": "2020-03-02",
"created": 1619701111,
"data": {
"object": {
"id": "txi_1NuMB12eZvKYlo2CMecoWkZd",
"object": "tax_id",
"country": "DE",
"created": 123456789,
"customer": null,
"livemode": false,
"type": "eu_vat",
"value": "DE123456789",
"verification": null,
"owner": {
"type": "self",
"customer": null
}
}
},
"livemode": false,
"pending_webhooks": 4,
"request": {
"id": "req_QyDCzn33ls4m1t",
"idempotency_key": null
},
"type": "customer.tax_id.deleted"
}
30 changes: 30 additions & 0 deletions test/stripe/customer_tax_id_updated.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"id": "evt_3KtQThJDPojXS6LN0E06aNxq",
"object": "event",
"api_version": "2020-03-02",
"created": 1619701111,
"data": {
"object": {
"id": "txi_1NuMB12eZvKYlo2CMecoWkZd",
"object": "tax_id",
"country": "DE",
"created": 123456789,
"customer": null,
"livemode": false,
"type": "eu_vat",
"value": "DE123456789",
"verification": null,
"owner": {
"type": "self",
"customer": null
}
}
},
"livemode": false,
"pending_webhooks": 4,
"request": {
"id": "req_QyDCzn33ls4m1t",
"idempotency_key": null
},
"type": "customer.tax_id.updated"
}
3 changes: 3 additions & 0 deletions test/webhooks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ describe('/webhooks', () => {
test.each([
'customer_updated.json',
'customer_deleted.json',
'customer_tax_id_created.json',
'customer_tax_id_deleted.json',
'customer_tax_id_updated.json',
'product_created.json',
'product_deleted.json',
'product_updated.json',
Expand Down

0 comments on commit 01ab409

Please sign in to comment.