Skip to content

Commit 43f1e89

Browse files
(SP:3)[SHOP] add canonical payment/shipping/admin audit tables + dedupe helper with flagged atomic dual-write
1 parent 513bfbe commit 43f1e89

15 files changed

Lines changed: 6410 additions & 203 deletions

frontend/app/api/shop/webhooks/stripe/route.ts

Lines changed: 346 additions & 83 deletions
Large diffs are not rendered by default.

frontend/db/schema/shop.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,116 @@ export const monobankEvents = pgTable(
413413
]
414414
);
415415

416+
export const paymentEvents = pgTable(
417+
'payment_events',
418+
{
419+
id: uuid('id').defaultRandom().primaryKey(),
420+
orderId: uuid('order_id')
421+
.notNull()
422+
.references(() => orders.id, { onDelete: 'cascade' }),
423+
provider: text('provider').notNull(),
424+
eventName: text('event_name').notNull(),
425+
eventSource: text('event_source').notNull(),
426+
eventRef: text('event_ref'),
427+
attemptId: uuid('attempt_id').references(() => paymentAttempts.id, {
428+
onDelete: 'set null',
429+
}),
430+
providerPaymentIntentId: text('provider_payment_intent_id'),
431+
providerChargeId: text('provider_charge_id'),
432+
amountMinor: bigint('amount_minor', { mode: 'number' }).notNull(),
433+
currency: currencyEnum('currency').notNull(),
434+
payload: jsonb('payload')
435+
.$type<Record<string, unknown>>()
436+
.notNull()
437+
.default(sql`'{}'::jsonb`),
438+
dedupeKey: text('dedupe_key').notNull(),
439+
occurredAt: timestamp('occurred_at', { withTimezone: true })
440+
.notNull()
441+
.defaultNow(),
442+
createdAt: timestamp('created_at', { withTimezone: true })
443+
.notNull()
444+
.defaultNow(),
445+
},
446+
t => [
447+
uniqueIndex('payment_events_dedupe_key_uq').on(t.dedupeKey),
448+
index('payment_events_order_id_idx').on(t.orderId),
449+
index('payment_events_attempt_id_idx').on(t.attemptId),
450+
index('payment_events_event_ref_idx').on(t.eventRef),
451+
index('payment_events_occurred_at_idx').on(t.occurredAt),
452+
]
453+
);
454+
455+
export const shippingEvents = pgTable(
456+
'shipping_events',
457+
{
458+
id: uuid('id').defaultRandom().primaryKey(),
459+
orderId: uuid('order_id')
460+
.notNull()
461+
.references(() => orders.id, { onDelete: 'cascade' }),
462+
shipmentId: uuid('shipment_id').references(() => shippingShipments.id, {
463+
onDelete: 'set null',
464+
}),
465+
provider: text('provider').notNull(),
466+
eventName: text('event_name').notNull(),
467+
eventSource: text('event_source').notNull(),
468+
eventRef: text('event_ref'),
469+
statusFrom: text('status_from'),
470+
statusTo: text('status_to'),
471+
trackingNumber: text('tracking_number'),
472+
payload: jsonb('payload')
473+
.$type<Record<string, unknown>>()
474+
.notNull()
475+
.default(sql`'{}'::jsonb`),
476+
dedupeKey: text('dedupe_key').notNull(),
477+
occurredAt: timestamp('occurred_at', { withTimezone: true })
478+
.notNull()
479+
.defaultNow(),
480+
createdAt: timestamp('created_at', { withTimezone: true })
481+
.notNull()
482+
.defaultNow(),
483+
},
484+
t => [
485+
uniqueIndex('shipping_events_dedupe_key_uq').on(t.dedupeKey),
486+
index('shipping_events_order_id_idx').on(t.orderId),
487+
index('shipping_events_shipment_id_idx').on(t.shipmentId),
488+
index('shipping_events_occurred_at_idx').on(t.occurredAt),
489+
]
490+
);
491+
492+
export const adminAuditLog = pgTable(
493+
'admin_audit_log',
494+
{
495+
id: uuid('id').defaultRandom().primaryKey(),
496+
orderId: uuid('order_id').references(() => orders.id, {
497+
onDelete: 'set null',
498+
}),
499+
actorUserId: text('actor_user_id').references(() => users.id, {
500+
onDelete: 'set null',
501+
}),
502+
action: text('action').notNull(),
503+
targetType: text('target_type').notNull(),
504+
targetId: text('target_id').notNull(),
505+
requestId: text('request_id'),
506+
payload: jsonb('payload')
507+
.$type<Record<string, unknown>>()
508+
.notNull()
509+
.default(sql`'{}'::jsonb`),
510+
dedupeKey: text('dedupe_key').notNull(),
511+
occurredAt: timestamp('occurred_at', { withTimezone: true })
512+
.notNull()
513+
.defaultNow(),
514+
createdAt: timestamp('created_at', { withTimezone: true })
515+
.notNull()
516+
.defaultNow(),
517+
},
518+
t => [
519+
uniqueIndex('admin_audit_log_dedupe_key_uq').on(t.dedupeKey),
520+
index('admin_audit_log_order_id_idx').on(t.orderId),
521+
index('admin_audit_log_actor_user_id_idx').on(t.actorUserId),
522+
index('admin_audit_log_occurred_at_idx').on(t.occurredAt),
523+
]
524+
);
525+
416526
export const monobankRefunds = pgTable(
417527
'monobank_refunds',
418528
{
@@ -837,6 +947,9 @@ export type DbInternalJobState = typeof internalJobState.$inferSelect;
837947
export type DbPaymentAttempt = typeof paymentAttempts.$inferSelect;
838948
export type DbApiRateLimit = typeof apiRateLimits.$inferSelect;
839949
export type DbMonobankEvent = typeof monobankEvents.$inferSelect;
950+
export type DbPaymentEvent = typeof paymentEvents.$inferSelect;
951+
export type DbShippingEvent = typeof shippingEvents.$inferSelect;
952+
export type DbAdminAuditLog = typeof adminAuditLog.$inferSelect;
840953
export type DbMonobankRefund = typeof monobankRefunds.$inferSelect;
841954
export type DbMonobankPaymentCancel =
842955
typeof monobankPaymentCancels.$inferSelect;
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
CREATE TABLE "admin_audit_log" (
2+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
3+
"order_id" uuid,
4+
"actor_user_id" text,
5+
"action" text NOT NULL,
6+
"target_type" text NOT NULL,
7+
"target_id" text NOT NULL,
8+
"request_id" text,
9+
"payload" jsonb DEFAULT '{}'::jsonb NOT NULL,
10+
"dedupe_key" text NOT NULL,
11+
"occurred_at" timestamp with time zone DEFAULT now() NOT NULL,
12+
"created_at" timestamp with time zone DEFAULT now() NOT NULL
13+
);
14+
--> statement-breakpoint
15+
CREATE TABLE "payment_events" (
16+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
17+
"order_id" uuid NOT NULL,
18+
"provider" text NOT NULL,
19+
"event_name" text NOT NULL,
20+
"event_source" text NOT NULL,
21+
"event_ref" text,
22+
"attempt_id" uuid,
23+
"provider_payment_intent_id" text,
24+
"provider_charge_id" text,
25+
"amount_minor" bigint NOT NULL,
26+
"currency" "currency" NOT NULL,
27+
"payload" jsonb DEFAULT '{}'::jsonb NOT NULL,
28+
"dedupe_key" text NOT NULL,
29+
"occurred_at" timestamp with time zone DEFAULT now() NOT NULL,
30+
"created_at" timestamp with time zone DEFAULT now() NOT NULL
31+
);
32+
--> statement-breakpoint
33+
CREATE TABLE "shipping_events" (
34+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
35+
"order_id" uuid NOT NULL,
36+
"shipment_id" uuid,
37+
"provider" text NOT NULL,
38+
"event_name" text NOT NULL,
39+
"event_source" text NOT NULL,
40+
"event_ref" text,
41+
"status_from" text,
42+
"status_to" text,
43+
"tracking_number" text,
44+
"payload" jsonb DEFAULT '{}'::jsonb NOT NULL,
45+
"dedupe_key" text NOT NULL,
46+
"occurred_at" timestamp with time zone DEFAULT now() NOT NULL,
47+
"created_at" timestamp with time zone DEFAULT now() NOT NULL
48+
);
49+
--> statement-breakpoint
50+
ALTER TABLE "admin_audit_log" ADD CONSTRAINT "admin_audit_log_order_id_orders_id_fk" FOREIGN KEY ("order_id") REFERENCES "public"."orders"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
51+
ALTER TABLE "admin_audit_log" ADD CONSTRAINT "admin_audit_log_actor_user_id_users_id_fk" FOREIGN KEY ("actor_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
52+
ALTER TABLE "payment_events" ADD CONSTRAINT "payment_events_order_id_orders_id_fk" FOREIGN KEY ("order_id") REFERENCES "public"."orders"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
53+
ALTER TABLE "payment_events" ADD CONSTRAINT "payment_events_attempt_id_payment_attempts_id_fk" FOREIGN KEY ("attempt_id") REFERENCES "public"."payment_attempts"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
54+
ALTER TABLE "shipping_events" ADD CONSTRAINT "shipping_events_order_id_orders_id_fk" FOREIGN KEY ("order_id") REFERENCES "public"."orders"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
55+
ALTER TABLE "shipping_events" ADD CONSTRAINT "shipping_events_shipment_id_shipping_shipments_id_fk" FOREIGN KEY ("shipment_id") REFERENCES "public"."shipping_shipments"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
56+
CREATE UNIQUE INDEX "admin_audit_log_dedupe_key_uq" ON "admin_audit_log" USING btree ("dedupe_key");--> statement-breakpoint
57+
CREATE INDEX "admin_audit_log_order_id_idx" ON "admin_audit_log" USING btree ("order_id");--> statement-breakpoint
58+
CREATE INDEX "admin_audit_log_actor_user_id_idx" ON "admin_audit_log" USING btree ("actor_user_id");--> statement-breakpoint
59+
CREATE INDEX "admin_audit_log_occurred_at_idx" ON "admin_audit_log" USING btree ("occurred_at");--> statement-breakpoint
60+
CREATE UNIQUE INDEX "payment_events_dedupe_key_uq" ON "payment_events" USING btree ("dedupe_key");--> statement-breakpoint
61+
CREATE INDEX "payment_events_order_id_idx" ON "payment_events" USING btree ("order_id");--> statement-breakpoint
62+
CREATE INDEX "payment_events_attempt_id_idx" ON "payment_events" USING btree ("attempt_id");--> statement-breakpoint
63+
CREATE INDEX "payment_events_event_ref_idx" ON "payment_events" USING btree ("event_ref");--> statement-breakpoint
64+
CREATE INDEX "payment_events_occurred_at_idx" ON "payment_events" USING btree ("occurred_at");--> statement-breakpoint
65+
CREATE UNIQUE INDEX "shipping_events_dedupe_key_uq" ON "shipping_events" USING btree ("dedupe_key");--> statement-breakpoint
66+
CREATE INDEX "shipping_events_order_id_idx" ON "shipping_events" USING btree ("order_id");--> statement-breakpoint
67+
CREATE INDEX "shipping_events_shipment_id_idx" ON "shipping_events" USING btree ("shipment_id");--> statement-breakpoint
68+
CREATE INDEX "shipping_events_occurred_at_idx" ON "shipping_events" USING btree ("occurred_at");

0 commit comments

Comments
 (0)