stripe-experiment-sync 1.0.13 → 1.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -6,6 +6,22 @@ type PostgresConfig = {
6
6
  schema: string;
7
7
  poolConfig: PoolConfig;
8
8
  };
9
+ type RawJsonUpsertOptions = {
10
+ /**
11
+ * Columns to use as the ON CONFLICT target.
12
+ * Example: ['id'] for standard Stripe objects, or a composite key for Sigma tables.
13
+ */
14
+ conflictTarget: string[];
15
+ /**
16
+ * Additional typed columns to insert alongside `_raw_data` (for tables that don't have `id` keys).
17
+ * Values are read from `entry[entryKey]` and cast to `pgType` in SQL.
18
+ */
19
+ extraColumns?: Array<{
20
+ column: string;
21
+ pgType: string;
22
+ entryKey: string;
23
+ }>;
24
+ };
9
25
  declare class PostgresClient {
10
26
  private config;
11
27
  pool: pg.Pool;
@@ -17,7 +33,7 @@ declare class PostgresClient {
17
33
  }>(entries: T[], table: string): Promise<T[]>;
18
34
  upsertManyWithTimestampProtection<T extends {
19
35
  [Key: string]: any;
20
- }>(entries: T[], table: string, accountId: string, syncTimestamp?: string): Promise<T[]>;
36
+ }>(entries: T[], table: string, accountId: string, syncTimestamp?: string, upsertOptions?: RawJsonUpsertOptions): Promise<T[]>;
21
37
  private cleanseArrayField;
22
38
  findMissingEntries(table: string, ids: string[]): Promise<string[]>;
23
39
  upsertAccount(accountData: {
@@ -157,6 +173,10 @@ declare class PostgresClient {
157
173
  * This considers completed, error, AND running runs to ensure recovery syncs
158
174
  * don't re-process data that was already synced before a crash.
159
175
  * A 'running' status with a cursor means the process was killed mid-sync.
176
+ *
177
+ * Handles two cursor formats:
178
+ * - Numeric: compared as bigint for correct ordering
179
+ * - Composite cursors: compared as strings with COLLATE "C"
160
180
  */
161
181
  getLastCompletedCursor(accountId: string, object: string): Promise<string | null>;
162
182
  /**
@@ -198,6 +218,38 @@ declare class PostgresClient {
198
218
  close(): Promise<void>;
199
219
  }
200
220
 
221
+ type SigmaCursorColumnType = 'timestamp' | 'string' | 'number';
222
+ type SigmaCursorColumnSpec = {
223
+ column: string;
224
+ type: SigmaCursorColumnType;
225
+ };
226
+ type SigmaCursorSpec = {
227
+ version: 1;
228
+ columns: SigmaCursorColumnSpec[];
229
+ };
230
+ type SigmaIngestionConfig = {
231
+ /**
232
+ * The Sigma table name to query (no quoting, no schema).
233
+ */
234
+ sigmaTable: string;
235
+ /**
236
+ * Destination Postgres table name (in the `stripe` schema by convention).
237
+ */
238
+ destinationTable: string;
239
+ /** Limit for each Sigma query page. */
240
+ pageSize: number;
241
+ /**
242
+ * Defines the ordering and cursor semantics. The columns must form a total order (i.e. be unique together) or pagination can be incorrect.
243
+ */
244
+ cursor: SigmaCursorSpec;
245
+ /** Optional additional WHERE clause appended with AND (must not include leading WHERE). */
246
+ additionalWhere?: string;
247
+ /** Columns to SELECT (defaults to `*`). */
248
+ select?: '*' | string[];
249
+ /** Postgres upsert behavior for this table (conflict target and typed columns). */
250
+ upsert: RawJsonUpsertOptions;
251
+ };
252
+
201
253
  /**
202
254
  * Simple logger interface compatible with both pino and console
203
255
  */
@@ -212,6 +264,12 @@ type StripeSyncConfig = {
212
264
  databaseUrl?: string;
213
265
  /** Stripe secret key used to authenticate requests to the Stripe API. Defaults to empty string */
214
266
  stripeSecretKey: string;
267
+ /**
268
+ * Enables syncing Stripe Sigma (reporting) tables via the Sigma API.
269
+ *
270
+ * Default: false (opt-in, so workers don't enqueue Sigma jobs unexpectedly).
271
+ */
272
+ enableSigma?: boolean;
215
273
  /** Stripe account ID. If not provided, will be retrieved from Stripe API. Used as fallback option. */
216
274
  stripeAccountId?: string;
217
275
  /** Stripe webhook signing secret for validating webhook signatures. Required if not using managed webhooks. */
@@ -259,8 +317,14 @@ type StripeSyncConfig = {
259
317
  * Default: 500
260
318
  */
261
319
  retryJitterMs?: number;
320
+ /**
321
+ * Maximum number of customers to process concurrently when syncing payment methods.
322
+ * Lower values reduce API load but increase sync time.
323
+ * Default: 10
324
+ */
325
+ maxConcurrentCustomers?: number;
262
326
  };
263
- type SyncObject = 'all' | 'customer' | 'customer_with_entitlements' | 'invoice' | 'price' | 'product' | 'subscription' | 'subscription_schedules' | 'setup_intent' | 'payment_method' | 'dispute' | 'charge' | 'payment_intent' | 'plan' | 'tax_id' | 'credit_note' | 'early_fraud_warning' | 'refund' | 'checkout_sessions';
327
+ type SyncObject = 'all' | 'customer' | 'customer_with_entitlements' | 'invoice' | 'price' | 'product' | 'subscription' | 'subscription_schedules' | 'setup_intent' | 'payment_method' | 'dispute' | 'charge' | 'payment_intent' | 'plan' | 'tax_id' | 'credit_note' | 'early_fraud_warning' | 'refund' | 'checkout_sessions' | 'subscription_item_change_events_v2_beta' | 'exchange_rates_from_usd';
264
328
  interface Sync {
265
329
  synced: number;
266
330
  }
@@ -282,6 +346,8 @@ interface SyncBackfill {
282
346
  earlyFraudWarnings?: Sync;
283
347
  refunds?: Sync;
284
348
  checkoutSessions?: Sync;
349
+ subscriptionItemChangeEventsV2Beta?: Sync;
350
+ exchangeRatesFromUsd?: Sync;
285
351
  }
286
352
  interface SyncParams {
287
353
  created?: {
@@ -334,6 +400,44 @@ interface ProcessNextParams extends SyncParams {
334
400
  /** Who/what triggered this sync (for observability) */
335
401
  triggeredBy?: string;
336
402
  }
403
+ /**
404
+ * Syncable resource configuration
405
+ */
406
+ type BaseResourceConfig = {
407
+ /** Backfill order: lower numbers sync first; parents before children for FK dependencies */
408
+ order: number;
409
+ /** Whether this resource supports incremental sync via 'created' filter or cursor */
410
+ supportsCreatedFilter: boolean;
411
+ };
412
+ type StripeListResourceConfig = BaseResourceConfig & {
413
+ /** Function to list items from Stripe API */
414
+ listFn: (params: Stripe.PaginationParams & {
415
+ created?: Stripe.RangeQueryParam;
416
+ }) => Promise<{
417
+ data: unknown[];
418
+ has_more: boolean;
419
+ }>;
420
+ /** Function to upsert items to database */
421
+ upsertFn: (items: unknown[], accountId: string, backfillRelated?: boolean) => Promise<unknown[] | void>;
422
+ /** discriminator */
423
+ sigma?: undefined;
424
+ };
425
+ /**
426
+ * Configuration for Sigma query-backed resources.
427
+ * Uses Stripe Sigma SQL queries with composite cursor pagination.
428
+ */
429
+ type SigmaResourceConfig = BaseResourceConfig & {
430
+ /** Sigma uses composite cursors, not created filter */
431
+ supportsCreatedFilter: false;
432
+ /** Sigma ingestion configuration (query, cursor spec, upsert options) */
433
+ sigma: SigmaIngestionConfig;
434
+ /** discriminator */
435
+ listFn?: undefined;
436
+ /** discriminator */
437
+ upsertFn?: undefined;
438
+ };
439
+ /** Union of all resource configuration types */
440
+ type ResourceConfig = StripeListResourceConfig | SigmaResourceConfig;
337
441
  /**
338
442
  * Installation status of the stripe-sync package
339
443
  */
@@ -491,6 +595,8 @@ declare class StripeSync {
491
595
  * Uses the observable sync system for tracking progress.
492
596
  */
493
597
  private fetchOnePage;
598
+ private getSigmaFallbackCursorFromDestination;
599
+ private fetchOneSigmaPage;
494
600
  /**
495
601
  * Process all pages for all (or specified) object types until complete.
496
602
  *
@@ -624,6 +730,7 @@ declare class StripeSync {
624
730
  backfillSubscriptionSchedules: (subscriptionIds: string[], accountId: string) => Promise<void>;
625
731
  /**
626
732
  * Stripe only sends the first 10 entries by default, the option will actively fetch all entries.
733
+ * Uses manual pagination - each fetch() gets automatic retry protection.
627
734
  */
628
735
  private expandEntity;
629
736
  private fetchMissingEntities;
@@ -684,4 +791,4 @@ declare function createStripeWebSocketClient(options: StripeWebSocketOptions): P
684
791
 
685
792
  declare const VERSION: string;
686
793
 
687
- export { type InstallationStatus, type Logger, PostgresClient, type ProcessNextParams, type ProcessNextResult, type RevalidateEntity, StripeSync, type StripeSyncAccountState, type StripeSyncConfig, type StripeSyncState, type StripeWebSocketClient, type StripeWebSocketOptions, type StripeWebhookEvent, type Sync, type SyncBackfill, type SyncEntitlementsParams, type SyncFeaturesParams, type SyncObject, type SyncParams, VERSION, type WebhookProcessingResult, createStripeWebSocketClient, hashApiKey, runMigrations };
794
+ export { type BaseResourceConfig, type InstallationStatus, type Logger, PostgresClient, type ProcessNextParams, type ProcessNextResult, type ResourceConfig, type RevalidateEntity, type SigmaResourceConfig, type StripeListResourceConfig, StripeSync, type StripeSyncAccountState, type StripeSyncConfig, type StripeSyncState, type StripeWebSocketClient, type StripeWebSocketOptions, type StripeWebhookEvent, type Sync, type SyncBackfill, type SyncEntitlementsParams, type SyncFeaturesParams, type SyncObject, type SyncParams, VERSION, type WebhookProcessingResult, createStripeWebSocketClient, hashApiKey, runMigrations };
package/dist/index.d.ts CHANGED
@@ -6,6 +6,22 @@ type PostgresConfig = {
6
6
  schema: string;
7
7
  poolConfig: PoolConfig;
8
8
  };
9
+ type RawJsonUpsertOptions = {
10
+ /**
11
+ * Columns to use as the ON CONFLICT target.
12
+ * Example: ['id'] for standard Stripe objects, or a composite key for Sigma tables.
13
+ */
14
+ conflictTarget: string[];
15
+ /**
16
+ * Additional typed columns to insert alongside `_raw_data` (for tables that don't have `id` keys).
17
+ * Values are read from `entry[entryKey]` and cast to `pgType` in SQL.
18
+ */
19
+ extraColumns?: Array<{
20
+ column: string;
21
+ pgType: string;
22
+ entryKey: string;
23
+ }>;
24
+ };
9
25
  declare class PostgresClient {
10
26
  private config;
11
27
  pool: pg.Pool;
@@ -17,7 +33,7 @@ declare class PostgresClient {
17
33
  }>(entries: T[], table: string): Promise<T[]>;
18
34
  upsertManyWithTimestampProtection<T extends {
19
35
  [Key: string]: any;
20
- }>(entries: T[], table: string, accountId: string, syncTimestamp?: string): Promise<T[]>;
36
+ }>(entries: T[], table: string, accountId: string, syncTimestamp?: string, upsertOptions?: RawJsonUpsertOptions): Promise<T[]>;
21
37
  private cleanseArrayField;
22
38
  findMissingEntries(table: string, ids: string[]): Promise<string[]>;
23
39
  upsertAccount(accountData: {
@@ -157,6 +173,10 @@ declare class PostgresClient {
157
173
  * This considers completed, error, AND running runs to ensure recovery syncs
158
174
  * don't re-process data that was already synced before a crash.
159
175
  * A 'running' status with a cursor means the process was killed mid-sync.
176
+ *
177
+ * Handles two cursor formats:
178
+ * - Numeric: compared as bigint for correct ordering
179
+ * - Composite cursors: compared as strings with COLLATE "C"
160
180
  */
161
181
  getLastCompletedCursor(accountId: string, object: string): Promise<string | null>;
162
182
  /**
@@ -198,6 +218,38 @@ declare class PostgresClient {
198
218
  close(): Promise<void>;
199
219
  }
200
220
 
221
+ type SigmaCursorColumnType = 'timestamp' | 'string' | 'number';
222
+ type SigmaCursorColumnSpec = {
223
+ column: string;
224
+ type: SigmaCursorColumnType;
225
+ };
226
+ type SigmaCursorSpec = {
227
+ version: 1;
228
+ columns: SigmaCursorColumnSpec[];
229
+ };
230
+ type SigmaIngestionConfig = {
231
+ /**
232
+ * The Sigma table name to query (no quoting, no schema).
233
+ */
234
+ sigmaTable: string;
235
+ /**
236
+ * Destination Postgres table name (in the `stripe` schema by convention).
237
+ */
238
+ destinationTable: string;
239
+ /** Limit for each Sigma query page. */
240
+ pageSize: number;
241
+ /**
242
+ * Defines the ordering and cursor semantics. The columns must form a total order (i.e. be unique together) or pagination can be incorrect.
243
+ */
244
+ cursor: SigmaCursorSpec;
245
+ /** Optional additional WHERE clause appended with AND (must not include leading WHERE). */
246
+ additionalWhere?: string;
247
+ /** Columns to SELECT (defaults to `*`). */
248
+ select?: '*' | string[];
249
+ /** Postgres upsert behavior for this table (conflict target and typed columns). */
250
+ upsert: RawJsonUpsertOptions;
251
+ };
252
+
201
253
  /**
202
254
  * Simple logger interface compatible with both pino and console
203
255
  */
@@ -212,6 +264,12 @@ type StripeSyncConfig = {
212
264
  databaseUrl?: string;
213
265
  /** Stripe secret key used to authenticate requests to the Stripe API. Defaults to empty string */
214
266
  stripeSecretKey: string;
267
+ /**
268
+ * Enables syncing Stripe Sigma (reporting) tables via the Sigma API.
269
+ *
270
+ * Default: false (opt-in, so workers don't enqueue Sigma jobs unexpectedly).
271
+ */
272
+ enableSigma?: boolean;
215
273
  /** Stripe account ID. If not provided, will be retrieved from Stripe API. Used as fallback option. */
216
274
  stripeAccountId?: string;
217
275
  /** Stripe webhook signing secret for validating webhook signatures. Required if not using managed webhooks. */
@@ -259,8 +317,14 @@ type StripeSyncConfig = {
259
317
  * Default: 500
260
318
  */
261
319
  retryJitterMs?: number;
320
+ /**
321
+ * Maximum number of customers to process concurrently when syncing payment methods.
322
+ * Lower values reduce API load but increase sync time.
323
+ * Default: 10
324
+ */
325
+ maxConcurrentCustomers?: number;
262
326
  };
263
- type SyncObject = 'all' | 'customer' | 'customer_with_entitlements' | 'invoice' | 'price' | 'product' | 'subscription' | 'subscription_schedules' | 'setup_intent' | 'payment_method' | 'dispute' | 'charge' | 'payment_intent' | 'plan' | 'tax_id' | 'credit_note' | 'early_fraud_warning' | 'refund' | 'checkout_sessions';
327
+ type SyncObject = 'all' | 'customer' | 'customer_with_entitlements' | 'invoice' | 'price' | 'product' | 'subscription' | 'subscription_schedules' | 'setup_intent' | 'payment_method' | 'dispute' | 'charge' | 'payment_intent' | 'plan' | 'tax_id' | 'credit_note' | 'early_fraud_warning' | 'refund' | 'checkout_sessions' | 'subscription_item_change_events_v2_beta' | 'exchange_rates_from_usd';
264
328
  interface Sync {
265
329
  synced: number;
266
330
  }
@@ -282,6 +346,8 @@ interface SyncBackfill {
282
346
  earlyFraudWarnings?: Sync;
283
347
  refunds?: Sync;
284
348
  checkoutSessions?: Sync;
349
+ subscriptionItemChangeEventsV2Beta?: Sync;
350
+ exchangeRatesFromUsd?: Sync;
285
351
  }
286
352
  interface SyncParams {
287
353
  created?: {
@@ -334,6 +400,44 @@ interface ProcessNextParams extends SyncParams {
334
400
  /** Who/what triggered this sync (for observability) */
335
401
  triggeredBy?: string;
336
402
  }
403
+ /**
404
+ * Syncable resource configuration
405
+ */
406
+ type BaseResourceConfig = {
407
+ /** Backfill order: lower numbers sync first; parents before children for FK dependencies */
408
+ order: number;
409
+ /** Whether this resource supports incremental sync via 'created' filter or cursor */
410
+ supportsCreatedFilter: boolean;
411
+ };
412
+ type StripeListResourceConfig = BaseResourceConfig & {
413
+ /** Function to list items from Stripe API */
414
+ listFn: (params: Stripe.PaginationParams & {
415
+ created?: Stripe.RangeQueryParam;
416
+ }) => Promise<{
417
+ data: unknown[];
418
+ has_more: boolean;
419
+ }>;
420
+ /** Function to upsert items to database */
421
+ upsertFn: (items: unknown[], accountId: string, backfillRelated?: boolean) => Promise<unknown[] | void>;
422
+ /** discriminator */
423
+ sigma?: undefined;
424
+ };
425
+ /**
426
+ * Configuration for Sigma query-backed resources.
427
+ * Uses Stripe Sigma SQL queries with composite cursor pagination.
428
+ */
429
+ type SigmaResourceConfig = BaseResourceConfig & {
430
+ /** Sigma uses composite cursors, not created filter */
431
+ supportsCreatedFilter: false;
432
+ /** Sigma ingestion configuration (query, cursor spec, upsert options) */
433
+ sigma: SigmaIngestionConfig;
434
+ /** discriminator */
435
+ listFn?: undefined;
436
+ /** discriminator */
437
+ upsertFn?: undefined;
438
+ };
439
+ /** Union of all resource configuration types */
440
+ type ResourceConfig = StripeListResourceConfig | SigmaResourceConfig;
337
441
  /**
338
442
  * Installation status of the stripe-sync package
339
443
  */
@@ -491,6 +595,8 @@ declare class StripeSync {
491
595
  * Uses the observable sync system for tracking progress.
492
596
  */
493
597
  private fetchOnePage;
598
+ private getSigmaFallbackCursorFromDestination;
599
+ private fetchOneSigmaPage;
494
600
  /**
495
601
  * Process all pages for all (or specified) object types until complete.
496
602
  *
@@ -624,6 +730,7 @@ declare class StripeSync {
624
730
  backfillSubscriptionSchedules: (subscriptionIds: string[], accountId: string) => Promise<void>;
625
731
  /**
626
732
  * Stripe only sends the first 10 entries by default, the option will actively fetch all entries.
733
+ * Uses manual pagination - each fetch() gets automatic retry protection.
627
734
  */
628
735
  private expandEntity;
629
736
  private fetchMissingEntities;
@@ -684,4 +791,4 @@ declare function createStripeWebSocketClient(options: StripeWebSocketOptions): P
684
791
 
685
792
  declare const VERSION: string;
686
793
 
687
- export { type InstallationStatus, type Logger, PostgresClient, type ProcessNextParams, type ProcessNextResult, type RevalidateEntity, StripeSync, type StripeSyncAccountState, type StripeSyncConfig, type StripeSyncState, type StripeWebSocketClient, type StripeWebSocketOptions, type StripeWebhookEvent, type Sync, type SyncBackfill, type SyncEntitlementsParams, type SyncFeaturesParams, type SyncObject, type SyncParams, VERSION, type WebhookProcessingResult, createStripeWebSocketClient, hashApiKey, runMigrations };
794
+ export { type BaseResourceConfig, type InstallationStatus, type Logger, PostgresClient, type ProcessNextParams, type ProcessNextResult, type ResourceConfig, type RevalidateEntity, type SigmaResourceConfig, type StripeListResourceConfig, StripeSync, type StripeSyncAccountState, type StripeSyncConfig, type StripeSyncState, type StripeWebSocketClient, type StripeWebSocketOptions, type StripeWebhookEvent, type Sync, type SyncBackfill, type SyncEntitlementsParams, type SyncFeaturesParams, type SyncObject, type SyncParams, VERSION, type WebhookProcessingResult, createStripeWebSocketClient, hashApiKey, runMigrations };
package/dist/index.js CHANGED
@@ -5,8 +5,8 @@ import {
5
5
  createStripeWebSocketClient,
6
6
  hashApiKey,
7
7
  runMigrations
8
- } from "./chunk-2GSABFXH.js";
9
- import "./chunk-RCU5ZXAX.js";
8
+ } from "./chunk-GOBVFXU7.js";
9
+ import "./chunk-3KVILTN4.js";
10
10
  export {
11
11
  PostgresClient,
12
12
  StripeSync,
@@ -0,0 +1,61 @@
1
+ -- event_timestamp and event_type are not generated columns because they are not immutable.
2
+ -- Postgres requires generated expressions to be immutable.
3
+
4
+ CREATE TABLE IF NOT EXISTS "stripe"."subscription_item_change_events_v2_beta" (
5
+ "_raw_data" jsonb NOT NULL,
6
+ "_last_synced_at" timestamptz,
7
+ "_updated_at" timestamptz DEFAULT now(),
8
+ "_account_id" text NOT NULL,
9
+
10
+ "event_timestamp" timestamptz NOT NULL,
11
+ "event_type" text NOT NULL,
12
+ "subscription_item_id" text NOT NULL,
13
+
14
+ PRIMARY KEY ("_account_id", "event_timestamp", "event_type", "subscription_item_id")
15
+ );
16
+
17
+ -- Foreign key to stripe.accounts
18
+ ALTER TABLE "stripe"."subscription_item_change_events_v2_beta"
19
+ DROP CONSTRAINT IF EXISTS fk_subscription_item_change_events_v2_beta_account;
20
+ ALTER TABLE "stripe"."subscription_item_change_events_v2_beta"
21
+ ADD CONSTRAINT fk_subscription_item_change_events_v2_beta_account
22
+ FOREIGN KEY ("_account_id") REFERENCES "stripe"."accounts" (id);
23
+
24
+ -- Maintain _updated_at on UPDATE
25
+ DROP TRIGGER IF EXISTS handle_updated_at ON "stripe"."subscription_item_change_events_v2_beta";
26
+ CREATE TRIGGER handle_updated_at
27
+ BEFORE UPDATE ON "stripe"."subscription_item_change_events_v2_beta"
28
+ FOR EACH ROW EXECUTE FUNCTION set_updated_at();
29
+
30
+ ALTER TABLE "stripe"."subscription_item_change_events_v2_beta"
31
+ ADD COLUMN IF NOT EXISTS "currency" text
32
+ GENERATED ALWAYS AS ((NULLIF(_raw_data->>'currency', ''))::text) STORED;
33
+
34
+ ALTER TABLE "stripe"."subscription_item_change_events_v2_beta"
35
+ ADD COLUMN IF NOT EXISTS "mrr_change" bigint
36
+ GENERATED ALWAYS AS ((NULLIF(_raw_data->>'mrr_change', ''))::bigint) STORED;
37
+
38
+ ALTER TABLE "stripe"."subscription_item_change_events_v2_beta"
39
+ ADD COLUMN IF NOT EXISTS "quantity_change" bigint
40
+ GENERATED ALWAYS AS ((NULLIF(_raw_data->>'quantity_change', ''))::bigint) STORED;
41
+
42
+ ALTER TABLE "stripe"."subscription_item_change_events_v2_beta"
43
+ ADD COLUMN IF NOT EXISTS "subscription_id" text
44
+ GENERATED ALWAYS AS ((NULLIF(_raw_data->>'subscription_id', ''))::text) STORED;
45
+
46
+ ALTER TABLE "stripe"."subscription_item_change_events_v2_beta"
47
+ ADD COLUMN IF NOT EXISTS "customer_id" text
48
+ GENERATED ALWAYS AS ((NULLIF(_raw_data->>'customer_id', ''))::text) STORED;
49
+
50
+ ALTER TABLE "stripe"."subscription_item_change_events_v2_beta"
51
+ ADD COLUMN IF NOT EXISTS "price_id" text
52
+ GENERATED ALWAYS AS ((NULLIF(_raw_data->>'price_id', ''))::text) STORED;
53
+
54
+ ALTER TABLE "stripe"."subscription_item_change_events_v2_beta"
55
+ ADD COLUMN IF NOT EXISTS "product_id" text
56
+ GENERATED ALWAYS AS ((NULLIF(_raw_data->>'product_id', ''))::text) STORED;
57
+
58
+ -- Keep as text to avoid non-immutable timestamp casts in a generated column
59
+ ALTER TABLE "stripe"."subscription_item_change_events_v2_beta"
60
+ ADD COLUMN IF NOT EXISTS "local_event_timestamp" text
61
+ GENERATED ALWAYS AS ((NULLIF(_raw_data->>'local_event_timestamp', ''))::text) STORED;
@@ -0,0 +1,38 @@
1
+
2
+ CREATE TABLE IF NOT EXISTS "stripe"."exchange_rates_from_usd" (
3
+ "_raw_data" jsonb NOT NULL,
4
+ "_last_synced_at" timestamptz,
5
+ "_updated_at" timestamptz DEFAULT now(),
6
+ "_account_id" text NOT NULL,
7
+
8
+ "date" date NOT NULL,
9
+ "sell_currency" text NOT NULL,
10
+
11
+ PRIMARY KEY ("_account_id", "date", "sell_currency")
12
+ );
13
+
14
+ -- Foreign key to stripe.accounts
15
+ ALTER TABLE "stripe"."exchange_rates_from_usd"
16
+ DROP CONSTRAINT IF EXISTS fk_exchange_rates_from_usd_account;
17
+ ALTER TABLE "stripe"."exchange_rates_from_usd"
18
+ ADD CONSTRAINT fk_exchange_rates_from_usd_account
19
+ FOREIGN KEY ("_account_id") REFERENCES "stripe"."accounts" (id);
20
+
21
+ -- Maintain _updated_at on UPDATE
22
+ DROP TRIGGER IF EXISTS handle_updated_at ON "stripe"."exchange_rates_from_usd";
23
+ CREATE TRIGGER handle_updated_at
24
+ BEFORE UPDATE ON "stripe"."exchange_rates_from_usd"
25
+ FOR EACH ROW EXECUTE FUNCTION set_updated_at();
26
+
27
+ ALTER TABLE "stripe"."exchange_rates_from_usd"
28
+ ADD COLUMN IF NOT EXISTS "buy_currency_exchange_rates" text
29
+ GENERATED ALWAYS AS ((NULLIF(_raw_data->>'buy_currency_exchange_rates', ''))::text) STORED;
30
+
31
+ -- Index on date for efficient range queries
32
+ CREATE INDEX IF NOT EXISTS idx_exchange_rates_from_usd_date
33
+ ON "stripe"."exchange_rates_from_usd" ("date");
34
+
35
+ -- Index on sell_currency for filtering by currency
36
+ CREATE INDEX IF NOT EXISTS idx_exchange_rates_from_usd_sell_currency
37
+ ON "stripe"."exchange_rates_from_usd" ("sell_currency");
38
+