Skip to content

Commit 795fcac

Browse files
authored
Sync coupons (#126)
* coupon support * coupon test
1 parent 2d89658 commit 795fcac

5 files changed

Lines changed: 139 additions & 1 deletion

File tree

packages/sync-engine/src/resourceRegistry.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,15 @@ const RESOURCE_MAP: Record<string, ResourceDef> = {
3333
supportsCreatedFilter: true,
3434
sync: true,
3535
},
36+
coupon: {
37+
order: 1,
38+
tableName: 'coupons',
39+
list: (s) => (p) => s.coupons.list(p),
40+
retrieve: (s) => (id) => s.coupons.retrieve(id),
41+
supportsCreatedFilter: true,
42+
sync: true,
43+
isFinalState: (c: Stripe.Coupon | Stripe.DeletedCoupon) => 'deleted' in c && c.deleted === true,
44+
},
3645
price: {
3746
order: 2,
3847
tableName: 'prices',

packages/sync-engine/src/stripeSyncWebhook.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ export class StripeSyncWebhook {
183183
'price.deleted',
184184
'plan.deleted',
185185
'invoice.deleted',
186+
'coupon.deleted',
186187
'customer.tax_id.deleted',
187188
])
188189

packages/sync-engine/src/tests/integration/stripeSync-integration.test.ts

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
upsertTestAccount,
66
resetMockCounters,
77
createMockCustomerBatch,
8+
createMockCouponBatch,
89
createPaginatedResponse,
910
DatabaseValidator,
1011
type MockStripeObject,
@@ -19,6 +20,7 @@ describe('StripeSync Integration Tests', () => {
1920
let db: TestDatabase
2021
let validator: DatabaseValidator
2122
let mockCustomers: MockStripeObject[] = []
23+
let mockCoupons: MockStripeObject[] = []
2224

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

3739
resetMockCounters()
3840
mockCustomers = []
41+
mockCoupons = []
3942

40-
await validator.clearAccountData(TEST_ACCOUNT_ID, ['stripe.customers', 'stripe.plans'])
43+
await validator.clearAccountData(TEST_ACCOUNT_ID, [
44+
'stripe.customers',
45+
'stripe.plans',
46+
'stripe.coupons',
47+
])
4148

4249
sync = await createTestStripeSync({
4350
databaseUrl: db.databaseUrl,
@@ -59,6 +66,20 @@ describe('StripeSync Integration Tests', () => {
5966
Promise.resolve(mockCustomers.find((c) => c.id === id) ?? null)
6067
),
6168
}
69+
70+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
71+
;(sync.stripe as any).coupons = {
72+
list: vi
73+
.fn()
74+
.mockImplementation((params) =>
75+
Promise.resolve(createPaginatedResponse(mockCoupons, params))
76+
),
77+
retrieve: vi
78+
.fn()
79+
.mockImplementation((id: string) =>
80+
Promise.resolve(mockCoupons.find((c) => c.id === id) ?? null)
81+
),
82+
}
6283
})
6384

6485
it('should have validator connected to database', async () => {
@@ -142,4 +163,89 @@ describe('StripeSync Integration Tests', () => {
142163
)
143164
})
144165
})
166+
167+
describe('coupon fullSync', () => {
168+
it('should sync all coupons via fullSync', async () => {
169+
mockCoupons = createMockCouponBatch(150)
170+
171+
const result = await sync.fullSync(['coupon'], true, 1, 50, false)
172+
173+
expect(result.totalSynced).toStrictEqual(150)
174+
175+
const countInDb = await validator.getRowCount('stripe.coupons', TEST_ACCOUNT_ID)
176+
expect(countInDb).toStrictEqual(150)
177+
178+
const couponsInDb = await validator.getColumnValues('stripe.coupons', 'id', TEST_ACCOUNT_ID)
179+
expect(couponsInDb).toStrictEqual(mockCoupons.map((c) => c.id))
180+
})
181+
182+
it('should sync new coupons for incremental consistency', async () => {
183+
await sync.fullSync(['coupon'], true, 2, 50, false)
184+
185+
mockCoupons = createMockCouponBatch(50)
186+
187+
const result = await sync.fullSync(['coupon'], true, 2, 50, false, 0)
188+
189+
expect(result.totalSynced).toStrictEqual(50)
190+
191+
const countInDb = await validator.getRowCount('stripe.coupons', TEST_ACCOUNT_ID)
192+
expect(countInDb).toStrictEqual(50)
193+
194+
const couponsInDb = await validator.getColumnValues('stripe.coupons', 'id', TEST_ACCOUNT_ID)
195+
expect(couponsInDb).toStrictEqual(mockCoupons.map((c) => c.id))
196+
})
197+
198+
it('should backfill historical coupons and then pick up new coupons on next sync', async () => {
199+
const historicalStartTimestamp = Math.floor(Date.now() / 1000) - 10000
200+
const historicalCoupons = createMockCouponBatch(100, historicalStartTimestamp)
201+
mockCoupons = historicalCoupons
202+
203+
const result = await sync.fullSync(['coupon'], true, 2, 50, false, 0)
204+
expect(result.totalSynced).toStrictEqual(100)
205+
206+
let countInDb = await validator.getRowCount('stripe.coupons', TEST_ACCOUNT_ID)
207+
expect(countInDb).toStrictEqual(100)
208+
209+
const newStartTimestamp = Math.floor(Date.now() / 1000)
210+
const newCoupons = createMockCouponBatch(3, newStartTimestamp)
211+
mockCoupons = [...newCoupons, ...mockCoupons]
212+
213+
const couponsAfterBackfill = await validator.getColumnValues(
214+
'stripe.coupons',
215+
'id',
216+
TEST_ACCOUNT_ID
217+
)
218+
expect(couponsAfterBackfill).toStrictEqual(historicalCoupons.map((c) => c.id))
219+
220+
await sync.fullSync(['coupon'], true, 2, 50, false, 0)
221+
222+
countInDb = await validator.getRowCount('stripe.coupons', TEST_ACCOUNT_ID)
223+
expect(countInDb).toStrictEqual(103)
224+
225+
const couponsAfterIncremental = await validator.getColumnValues(
226+
'stripe.coupons',
227+
'id',
228+
TEST_ACCOUNT_ID
229+
)
230+
expect(couponsAfterIncremental).toStrictEqual(
231+
[...historicalCoupons, ...newCoupons].map((c) => c.id)
232+
)
233+
})
234+
235+
it('should handle deleted coupons during sync', async () => {
236+
mockCoupons = createMockCouponBatch(10)
237+
238+
await sync.fullSync(['coupon'], true, 1, 50, false)
239+
240+
const countInDb = await validator.getRowCount('stripe.coupons', TEST_ACCOUNT_ID)
241+
expect(countInDb).toStrictEqual(10)
242+
243+
mockCoupons = mockCoupons.map((c, i) => (i < 3 ? { ...c, deleted: true } : c))
244+
245+
await sync.fullSync(['coupon'], true, 1, 50, false, 0)
246+
247+
const finalCount = await validator.getRowCount('stripe.coupons', TEST_ACCOUNT_ID)
248+
expect(finalCount).toStrictEqual(10)
249+
})
250+
})
145251
})

packages/sync-engine/src/tests/testSetup.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,10 +226,12 @@ type MockStripeObject = { id: string; created: number; [key: string]: unknown }
226226

227227
let customerIdCounter = 0
228228
let planIdCounter = 0
229+
let couponIdCounter = 0
229230

230231
export function resetMockCounters(): void {
231232
customerIdCounter = 0
232233
planIdCounter = 0
234+
couponIdCounter = 0
233235
}
234236

235237
export function createMockCustomer(
@@ -254,6 +256,23 @@ export function createMockPlan(
254256
}
255257
}
256258

259+
export function createMockCoupon(
260+
overrides: { id?: string; created?: number; deleted?: boolean } = {}
261+
): MockStripeObject {
262+
couponIdCounter++
263+
return {
264+
id: overrides.id ?? `coupon_test_${couponIdCounter.toString().padStart(6, '0')}`,
265+
object: 'coupon',
266+
created: overrides.created ?? Math.floor(Date.now() / 1000) - couponIdCounter,
267+
...(overrides.deleted != null ? { deleted: overrides.deleted } : {}),
268+
}
269+
}
270+
271+
export function createMockCouponBatch(count: number, startTimestamp?: number): MockStripeObject[] {
272+
const baseTimestamp = startTimestamp ?? Math.floor(Date.now() / 1000)
273+
return Array.from({ length: count }, (_, i) => createMockCoupon({ created: baseTimestamp - i }))
274+
}
275+
257276
export function createMockCustomerBatch(
258277
count: number,
259278
startTimestamp?: number

packages/sync-engine/src/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ export const SUPPORTED_WEBHOOK_EVENTS: Stripe.WebhookEndpointCreateParams.Enable
111111
'customer.deleted',
112112
'customer.created',
113113
'customer.updated',
114+
'coupon.created',
115+
'coupon.deleted',
116+
'coupon.updated',
114117
'checkout.session.async_payment_failed',
115118
'checkout.session.async_payment_succeeded',
116119
'checkout.session.completed',

0 commit comments

Comments
 (0)