Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/sync-engine/src/resourceRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ const RESOURCE_MAP: Record<string, ResourceDef> = {
supportsCreatedFilter: true,
sync: true,
},
coupon: {
order: 1,
tableName: 'coupons',
list: (s) => (p) => s.coupons.list(p),
retrieve: (s) => (id) => s.coupons.retrieve(id),
supportsCreatedFilter: true,
sync: true,
isFinalState: (c: Stripe.Coupon | Stripe.DeletedCoupon) => 'deleted' in c && c.deleted === true,
},
price: {
order: 2,
tableName: 'prices',
Expand Down
1 change: 1 addition & 0 deletions packages/sync-engine/src/stripeSyncWebhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ export class StripeSyncWebhook {
'price.deleted',
'plan.deleted',
'invoice.deleted',
'coupon.deleted',
'customer.tax_id.deleted',
])

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
upsertTestAccount,
resetMockCounters,
createMockCustomerBatch,
createMockCouponBatch,
createPaginatedResponse,
DatabaseValidator,
type MockStripeObject,
Expand All @@ -19,6 +20,7 @@ describe('StripeSync Integration Tests', () => {
let db: TestDatabase
let validator: DatabaseValidator
let mockCustomers: MockStripeObject[] = []
let mockCoupons: MockStripeObject[] = []

beforeAll(async () => {
db = await setupTestDatabase()
Expand All @@ -36,8 +38,13 @@ describe('StripeSync Integration Tests', () => {

resetMockCounters()
mockCustomers = []
mockCoupons = []

await validator.clearAccountData(TEST_ACCOUNT_ID, ['stripe.customers', 'stripe.plans'])
await validator.clearAccountData(TEST_ACCOUNT_ID, [
'stripe.customers',
'stripe.plans',
'stripe.coupons',
])

sync = await createTestStripeSync({
databaseUrl: db.databaseUrl,
Expand All @@ -59,6 +66,20 @@ describe('StripeSync Integration Tests', () => {
Promise.resolve(mockCustomers.find((c) => c.id === id) ?? null)
),
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(sync.stripe as any).coupons = {
list: vi
.fn()
.mockImplementation((params) =>
Promise.resolve(createPaginatedResponse(mockCoupons, params))
),
retrieve: vi
.fn()
.mockImplementation((id: string) =>
Promise.resolve(mockCoupons.find((c) => c.id === id) ?? null)
),
}
})

it('should have validator connected to database', async () => {
Expand Down Expand Up @@ -142,4 +163,89 @@ describe('StripeSync Integration Tests', () => {
)
})
})

describe('coupon fullSync', () => {
it('should sync all coupons via fullSync', async () => {
mockCoupons = createMockCouponBatch(150)

const result = await sync.fullSync(['coupon'], true, 1, 50, false)

expect(result.totalSynced).toStrictEqual(150)

const countInDb = await validator.getRowCount('stripe.coupons', TEST_ACCOUNT_ID)
expect(countInDb).toStrictEqual(150)

const couponsInDb = await validator.getColumnValues('stripe.coupons', 'id', TEST_ACCOUNT_ID)
expect(couponsInDb).toStrictEqual(mockCoupons.map((c) => c.id))
})

it('should sync new coupons for incremental consistency', async () => {
await sync.fullSync(['coupon'], true, 2, 50, false)

mockCoupons = createMockCouponBatch(50)

const result = await sync.fullSync(['coupon'], true, 2, 50, false, 0)

expect(result.totalSynced).toStrictEqual(50)

const countInDb = await validator.getRowCount('stripe.coupons', TEST_ACCOUNT_ID)
expect(countInDb).toStrictEqual(50)

const couponsInDb = await validator.getColumnValues('stripe.coupons', 'id', TEST_ACCOUNT_ID)
expect(couponsInDb).toStrictEqual(mockCoupons.map((c) => c.id))
})

it('should backfill historical coupons and then pick up new coupons on next sync', async () => {
const historicalStartTimestamp = Math.floor(Date.now() / 1000) - 10000
const historicalCoupons = createMockCouponBatch(100, historicalStartTimestamp)
mockCoupons = historicalCoupons

const result = await sync.fullSync(['coupon'], true, 2, 50, false, 0)
expect(result.totalSynced).toStrictEqual(100)

let countInDb = await validator.getRowCount('stripe.coupons', TEST_ACCOUNT_ID)
expect(countInDb).toStrictEqual(100)

const newStartTimestamp = Math.floor(Date.now() / 1000)
const newCoupons = createMockCouponBatch(3, newStartTimestamp)
mockCoupons = [...newCoupons, ...mockCoupons]

const couponsAfterBackfill = await validator.getColumnValues(
'stripe.coupons',
'id',
TEST_ACCOUNT_ID
)
expect(couponsAfterBackfill).toStrictEqual(historicalCoupons.map((c) => c.id))

await sync.fullSync(['coupon'], true, 2, 50, false, 0)

countInDb = await validator.getRowCount('stripe.coupons', TEST_ACCOUNT_ID)
expect(countInDb).toStrictEqual(103)

const couponsAfterIncremental = await validator.getColumnValues(
'stripe.coupons',
'id',
TEST_ACCOUNT_ID
)
expect(couponsAfterIncremental).toStrictEqual(
[...historicalCoupons, ...newCoupons].map((c) => c.id)
)
})

it('should handle deleted coupons during sync', async () => {
mockCoupons = createMockCouponBatch(10)

await sync.fullSync(['coupon'], true, 1, 50, false)

const countInDb = await validator.getRowCount('stripe.coupons', TEST_ACCOUNT_ID)
expect(countInDb).toStrictEqual(10)

mockCoupons = mockCoupons.map((c, i) => (i < 3 ? { ...c, deleted: true } : c))

await sync.fullSync(['coupon'], true, 1, 50, false, 0)

const finalCount = await validator.getRowCount('stripe.coupons', TEST_ACCOUNT_ID)
expect(finalCount).toStrictEqual(10)
})
})
})
19 changes: 19 additions & 0 deletions packages/sync-engine/src/tests/testSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,10 +226,12 @@ type MockStripeObject = { id: string; created: number; [key: string]: unknown }

let customerIdCounter = 0
let planIdCounter = 0
let couponIdCounter = 0

export function resetMockCounters(): void {
customerIdCounter = 0
planIdCounter = 0
couponIdCounter = 0
}

export function createMockCustomer(
Expand All @@ -254,6 +256,23 @@ export function createMockPlan(
}
}

export function createMockCoupon(
overrides: { id?: string; created?: number; deleted?: boolean } = {}
): MockStripeObject {
couponIdCounter++
return {
id: overrides.id ?? `coupon_test_${couponIdCounter.toString().padStart(6, '0')}`,
object: 'coupon',
created: overrides.created ?? Math.floor(Date.now() / 1000) - couponIdCounter,
...(overrides.deleted != null ? { deleted: overrides.deleted } : {}),
}
}

export function createMockCouponBatch(count: number, startTimestamp?: number): MockStripeObject[] {
const baseTimestamp = startTimestamp ?? Math.floor(Date.now() / 1000)
return Array.from({ length: count }, (_, i) => createMockCoupon({ created: baseTimestamp - i }))
}

export function createMockCustomerBatch(
count: number,
startTimestamp?: number
Expand Down
3 changes: 3 additions & 0 deletions packages/sync-engine/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ export const SUPPORTED_WEBHOOK_EVENTS: Stripe.WebhookEndpointCreateParams.Enable
'customer.deleted',
'customer.created',
'customer.updated',
'coupon.created',
'coupon.deleted',
'coupon.updated',
'checkout.session.async_payment_failed',
'checkout.session.async_payment_succeeded',
'checkout.session.completed',
Expand Down