diff --git a/frontend/app/api/shop/webhooks/stripe/route.ts b/frontend/app/api/shop/webhooks/stripe/route.ts index f466a360..6bcc1f12 100644 --- a/frontend/app/api/shop/webhooks/stripe/route.ts +++ b/frontend/app/api/shop/webhooks/stripe/route.ts @@ -1,6 +1,7 @@ import Stripe from 'stripe'; +import crypto from 'node:crypto'; import { NextRequest, NextResponse } from 'next/server'; -import { and, eq, ne, or } from 'drizzle-orm'; +import { and, eq, ne, or, isNull, lt } from 'drizzle-orm'; import { db } from '@/db'; import { verifyWebhookSignature, retrieveCharge } from '@/lib/psp/stripe'; import { orders, stripeEvents } from '@/db/schema'; @@ -15,6 +16,71 @@ import { markStripeAttemptFinal } from '@/lib/services/orders/payment-attempts'; const REFUND_FULLNESS_UNDETERMINED = 'REFUND_FULLNESS_UNDETERMINED' as const; +// P0.8: multi-instance claim/lock (no transactions; safe under parallel deliveries) +const STRIPE_WEBHOOK_INSTANCE_ID = + ( + process.env.STRIPE_WEBHOOK_INSTANCE_ID ?? + process.env.WEBHOOK_INSTANCE_ID ?? + '' + ).trim() || crypto.randomUUID().slice(0, 12); + +const STRIPE_EVENT_CLAIM_TTL_MS = 10 * 60 * 1000; // 10 minutes +const STRIPE_EVENT_RETRY_AFTER_SECONDS = 10; + +function busyRetry() { + return NextResponse.json( + { + code: 'WEBHOOK_CLAIMED', + retryAfterSeconds: STRIPE_EVENT_RETRY_AFTER_SECONDS, + }, + { + status: 503, + headers: { 'Retry-After': String(STRIPE_EVENT_RETRY_AFTER_SECONDS) }, + } + ); +} +async function tryClaimStripeEvent( + eventId: string +): Promise<'claimed' | 'already_processed' | 'busy'> { + const now = new Date(); + const expiresAt = new Date(now.getTime() + STRIPE_EVENT_CLAIM_TTL_MS); + + const claimed = await db + .update(stripeEvents) + .set({ + claimedAt: now, + claimExpiresAt: expiresAt, + claimedBy: STRIPE_WEBHOOK_INSTANCE_ID, + }) + .where( + and( + eq(stripeEvents.eventId, eventId), + isNull(stripeEvents.processedAt), + or( + isNull(stripeEvents.claimedAt), + isNull(stripeEvents.claimExpiresAt), + lt(stripeEvents.claimExpiresAt, now) + ) + ) + ) + .returning({ eventId: stripeEvents.eventId }); + + if (claimed.length > 0) return 'claimed'; + + const [row] = await db + .select({ + processedAt: stripeEvents.processedAt, + claimExpiresAt: stripeEvents.claimExpiresAt, + claimedBy: stripeEvents.claimedBy, + }) + .from(stripeEvents) + .where(eq(stripeEvents.eventId, eventId)) + .limit(1); + + if (row?.processedAt) return 'already_processed'; + return 'busy'; +} + function throwRefundFullnessUndetermined(): never { throw new Error(REFUND_FULLNESS_UNDETERMINED); } @@ -349,10 +415,25 @@ export async function POST(request: NextRequest) { try { const ack = async () => { - await db + const updated = await db .update(stripeEvents) .set({ processedAt: new Date() }) - .where(eq(stripeEvents.eventId, event.id)); + .where( + and( + eq(stripeEvents.eventId, event.id), + eq(stripeEvents.claimedBy, STRIPE_WEBHOOK_INSTANCE_ID) + ) + ) + .returning({ eventId: stripeEvents.eventId }); + + if (updated.length === 0) { + logWarn('stripe_webhook_ack_claim_lost', { + eventId: event.id, + eventType, + }); + return busyRetry(); + } + return NextResponse.json({ received: true }, { status: 200 }); }; // 1) Insert event idempotently (no transactions) @@ -386,7 +467,23 @@ export async function POST(request: NextRequest) { } // processedAt is NULL => previous attempt failed; reprocess } - + // P0.8: claim/lock BEFORE any business logic (multi-instance safe). + const claimState = await tryClaimStripeEvent(event.id); + if (claimState === 'already_processed') { + logInfo('stripe_webhook_duplicate_event', { + eventId: event.id, + eventType, + reason: 'already_processed', + }); + return NextResponse.json({ received: true }, { status: 200 }); + } + if (claimState === 'busy') { + logInfo('stripe_webhook_claimed_by_other_instance', { + eventId: event.id, + eventType, + }); + return busyRetry(); + } //2) Resolve orderId: // primary: metadata.orderId // fallback: orders.paymentIntentId == paymentIntentId (ONLY if unique match) @@ -1123,6 +1220,21 @@ export async function POST(request: NextRequest) { } logError('Stripe webhook processing failed', error); + // P0.8: release claim early so Stripe retries can be claimed immediately. + try { + await db + .update(stripeEvents) + .set({ claimExpiresAt: new Date(0) }) + .where( + and( + eq(stripeEvents.eventId, event.id), + eq(stripeEvents.claimedBy, STRIPE_WEBHOOK_INSTANCE_ID), + isNull(stripeEvents.processedAt) + ) + ); + } catch { + // best-effort + } return NextResponse.json({ error: 'internal_error' }, { status: 500 }); } } diff --git a/frontend/db/schema/shop.ts b/frontend/db/schema/shop.ts index dd3a92ad..04f57182 100644 --- a/frontend/db/schema/shop.ts +++ b/frontend/db/schema/shop.ts @@ -269,10 +269,16 @@ export const stripeEvents = pgTable( orderId: uuid('order_id').references(() => orders.id), eventType: text('event_type').notNull(), paymentStatus: text('payment_status'), + claimedAt: timestamp('claimed_at', { withTimezone: true }), + claimExpiresAt: timestamp('claim_expires_at', { withTimezone: true }), + claimedBy: varchar('claimed_by', { length: 64 }), processedAt: timestamp('processed_at', { withTimezone: true }), createdAt: timestamp('created_at', { mode: 'date' }).defaultNow().notNull(), }, - table => [uniqueIndex('stripe_events_event_id_idx').on(table.eventId)] + table => [ + uniqueIndex('stripe_events_event_id_idx').on(table.eventId), + index('stripe_events_claim_expires_idx').on(table.claimExpiresAt), + ] ); export const productPrices = pgTable( @@ -405,6 +411,8 @@ export const paymentAttempts = pgTable( finalizedAt: timestamp('finalized_at', { withTimezone: true }), }, t => [ + check('payment_attempts_provider_check', sql`${t.provider} in ('stripe')`), + // CHECKs (match SQL migration) check( 'payment_attempts_status_check', diff --git a/frontend/drizzle/0003_add_stripe_events_claim_lock.sql b/frontend/drizzle/0003_add_stripe_events_claim_lock.sql new file mode 100644 index 00000000..11262153 --- /dev/null +++ b/frontend/drizzle/0003_add_stripe_events_claim_lock.sql @@ -0,0 +1,17 @@ +ALTER TABLE "stripe_events" ADD COLUMN "claimed_at" timestamp with time zone;--> statement-breakpoint +ALTER TABLE "stripe_events" ADD COLUMN "claim_expires_at" timestamp with time zone;--> statement-breakpoint +ALTER TABLE "stripe_events" ADD COLUMN "claimed_by" varchar(64);--> statement-breakpoint +CREATE INDEX "stripe_events_claim_expires_idx" ON "stripe_events" USING btree ("claim_expires_at");--> statement-breakpoint + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'payment_attempts_provider_check' + ) THEN + ALTER TABLE "payment_attempts" + ADD CONSTRAINT "payment_attempts_provider_check" + CHECK ("provider" in ('stripe')); + END IF; +END $$;--> statement-breakpoint diff --git a/frontend/drizzle/meta/0003_snapshot.json b/frontend/drizzle/meta/0003_snapshot.json new file mode 100644 index 00000000..4150d714 --- /dev/null +++ b/frontend/drizzle/meta/0003_snapshot.json @@ -0,0 +1,2742 @@ +{ + "id": "1b3c7a10-06b5-4278-89cb-e3c38748f980", + "prevId": "fce941f2-f250-4f6f-b6a9-23c418ef1f85", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.categories": { + "name": "categories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "categories_slug_unique": { + "name": "categories_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.category_translations": { + "name": "category_translations", + "schema": "", + "columns": { + "category_id": { + "name": "category_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "locale": { + "name": "locale", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "category_translations_category_id_categories_id_fk": { + "name": "category_translations_category_id_categories_id_fk", + "tableFrom": "category_translations", + "tableTo": "categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "category_translations_category_id_locale_pk": { + "name": "category_translations_category_id_locale_pk", + "columns": [ + "category_id", + "locale" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.question_translations": { + "name": "question_translations", + "schema": "", + "columns": { + "question_id": { + "name": "question_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "locale": { + "name": "locale", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true + }, + "question": { + "name": "question", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "answer_blocks": { + "name": "answer_blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "question_translations_question_id_questions_id_fk": { + "name": "question_translations_question_id_questions_id_fk", + "tableFrom": "question_translations", + "tableTo": "questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "question_translations_question_id_locale_pk": { + "name": "question_translations_question_id_locale_pk", + "columns": [ + "question_id", + "locale" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.questions": { + "name": "questions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "category_id": { + "name": "category_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "difficulty": { + "name": "difficulty", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false, + "default": "'medium'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "questions_category_sort_order_idx": { + "name": "questions_category_sort_order_idx", + "columns": [ + { + "expression": "category_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "questions_category_id_categories_id_fk": { + "name": "questions_category_id_categories_id_fk", + "tableFrom": "questions", + "tableTo": "categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.quiz_answer_translations": { + "name": "quiz_answer_translations", + "schema": "", + "columns": { + "quiz_answer_id": { + "name": "quiz_answer_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "locale": { + "name": "locale", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true + }, + "answer_text": { + "name": "answer_text", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "quiz_answer_translations_quiz_answer_id_quiz_answers_id_fk": { + "name": "quiz_answer_translations_quiz_answer_id_quiz_answers_id_fk", + "tableFrom": "quiz_answer_translations", + "tableTo": "quiz_answers", + "columnsFrom": [ + "quiz_answer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "quiz_answer_translations_quiz_answer_id_locale_pk": { + "name": "quiz_answer_translations_quiz_answer_id_locale_pk", + "columns": [ + "quiz_answer_id", + "locale" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.quiz_answers": { + "name": "quiz_answers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "quiz_question_id": { + "name": "quiz_question_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "is_correct": { + "name": "is_correct", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "quiz_answers_question_display_order_idx": { + "name": "quiz_answers_question_display_order_idx", + "columns": [ + { + "expression": "quiz_question_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "quiz_answers_quiz_question_id_quiz_questions_id_fk": { + "name": "quiz_answers_quiz_question_id_quiz_questions_id_fk", + "tableFrom": "quiz_answers", + "tableTo": "quiz_questions", + "columnsFrom": [ + "quiz_question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.quiz_attempt_answers": { + "name": "quiz_attempt_answers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "attempt_id": { + "name": "attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "quiz_question_id": { + "name": "quiz_question_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "selected_answer_id": { + "name": "selected_answer_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "is_correct": { + "name": "is_correct", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "answered_at": { + "name": "answered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "quiz_attempt_answers_attempt_idx": { + "name": "quiz_attempt_answers_attempt_idx", + "columns": [ + { + "expression": "attempt_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "quiz_attempt_answers_attempt_id_quiz_attempts_id_fk": { + "name": "quiz_attempt_answers_attempt_id_quiz_attempts_id_fk", + "tableFrom": "quiz_attempt_answers", + "tableTo": "quiz_attempts", + "columnsFrom": [ + "attempt_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "quiz_attempt_answers_quiz_question_id_quiz_questions_id_fk": { + "name": "quiz_attempt_answers_quiz_question_id_quiz_questions_id_fk", + "tableFrom": "quiz_attempt_answers", + "tableTo": "quiz_questions", + "columnsFrom": [ + "quiz_question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "quiz_attempt_answers_selected_answer_id_quiz_answers_id_fk": { + "name": "quiz_attempt_answers_selected_answer_id_quiz_answers_id_fk", + "tableFrom": "quiz_attempt_answers", + "tableTo": "quiz_answers", + "columnsFrom": [ + "selected_answer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.quiz_attempts": { + "name": "quiz_attempts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quiz_id": { + "name": "quiz_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "score": { + "name": "score", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "total_questions": { + "name": "total_questions", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "percentage": { + "name": "percentage", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true + }, + "time_spent_seconds": { + "name": "time_spent_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "integrity_score": { + "name": "integrity_score", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 100 + }, + "points_earned": { + "name": "points_earned", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "quiz_attempts_user_id_idx": { + "name": "quiz_attempts_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "quiz_attempts_quiz_id_idx": { + "name": "quiz_attempts_quiz_id_idx", + "columns": [ + { + "expression": "quiz_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "quiz_attempts_user_completed_at_idx": { + "name": "quiz_attempts_user_completed_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "quiz_attempts_quiz_percentage_completed_at_idx": { + "name": "quiz_attempts_quiz_percentage_completed_at_idx", + "columns": [ + { + "expression": "quiz_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "percentage", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "quiz_attempts_quiz_integrity_score_idx": { + "name": "quiz_attempts_quiz_integrity_score_idx", + "columns": [ + { + "expression": "quiz_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "integrity_score", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "quiz_attempts_user_id_users_id_fk": { + "name": "quiz_attempts_user_id_users_id_fk", + "tableFrom": "quiz_attempts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "quiz_attempts_quiz_id_quizzes_id_fk": { + "name": "quiz_attempts_quiz_id_quizzes_id_fk", + "tableFrom": "quiz_attempts", + "tableTo": "quizzes", + "columnsFrom": [ + "quiz_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.quiz_question_content": { + "name": "quiz_question_content", + "schema": "", + "columns": { + "quiz_question_id": { + "name": "quiz_question_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "locale": { + "name": "locale", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true + }, + "question_text": { + "name": "question_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "explanation": { + "name": "explanation", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "quiz_question_content_quiz_question_id_quiz_questions_id_fk": { + "name": "quiz_question_content_quiz_question_id_quiz_questions_id_fk", + "tableFrom": "quiz_question_content", + "tableTo": "quiz_questions", + "columnsFrom": [ + "quiz_question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "quiz_question_content_quiz_question_id_locale_pk": { + "name": "quiz_question_content_quiz_question_id_locale_pk", + "columns": [ + "quiz_question_id", + "locale" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.quiz_questions": { + "name": "quiz_questions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "quiz_id": { + "name": "quiz_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "source_question_id": { + "name": "source_question_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "difficulty": { + "name": "difficulty", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false, + "default": "'medium'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "quiz_questions_quiz_display_order_idx": { + "name": "quiz_questions_quiz_display_order_idx", + "columns": [ + { + "expression": "quiz_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "quiz_questions_quiz_id_quizzes_id_fk": { + "name": "quiz_questions_quiz_id_quizzes_id_fk", + "tableFrom": "quiz_questions", + "tableTo": "quizzes", + "columnsFrom": [ + "quiz_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.quiz_translations": { + "name": "quiz_translations", + "schema": "", + "columns": { + "quiz_id": { + "name": "quiz_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "locale": { + "name": "locale", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "quiz_translations_quiz_id_quizzes_id_fk": { + "name": "quiz_translations_quiz_id_quizzes_id_fk", + "tableFrom": "quiz_translations", + "tableTo": "quizzes", + "columnsFrom": [ + "quiz_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "quiz_translations_quiz_id_locale_pk": { + "name": "quiz_translations_quiz_id_locale_pk", + "columns": [ + "quiz_id", + "locale" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.quizzes": { + "name": "quizzes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "category_id": { + "name": "category_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "questions_count": { + "name": "questions_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "time_limit_seconds": { + "name": "time_limit_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "quizzes_slug_idx": { + "name": "quizzes_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "quizzes_category_id_categories_id_fk": { + "name": "quizzes_category_id_categories_id_fk", + "tableFrom": "quizzes", + "tableTo": "categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "quizzes_category_id_slug_unique": { + "name": "quizzes_category_id_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "category_id", + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'credentials'" + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "users_provider_provider_id_unique": { + "name": "users_provider_provider_id_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.point_transactions": { + "name": "point_transactions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "points": { + "name": "points", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source": { + "name": "source", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'quiz'" + }, + "source_id": { + "name": "source_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "point_transactions_user_id_idx": { + "name": "point_transactions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "point_transactions_user_id_users_id_fk": { + "name": "point_transactions_user_id_users_id_fk", + "tableFrom": "point_transactions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.internal_job_state": { + "name": "internal_job_state", + "schema": "", + "columns": { + "job_name": { + "name": "job_name", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "next_allowed_at": { + "name": "next_allowed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_run_id": { + "name": "last_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inventory_moves": { + "name": "inventory_moves", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "move_key": { + "name": "move_key", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "order_id": { + "name": "order_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "product_id": { + "name": "product_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "inventory_move_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inventory_moves_move_key_uq": { + "name": "inventory_moves_move_key_uq", + "columns": [ + { + "expression": "move_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inventory_moves_order_id_idx": { + "name": "inventory_moves_order_id_idx", + "columns": [ + { + "expression": "order_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inventory_moves_product_id_idx": { + "name": "inventory_moves_product_id_idx", + "columns": [ + { + "expression": "product_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "inventory_moves_order_id_orders_id_fk": { + "name": "inventory_moves_order_id_orders_id_fk", + "tableFrom": "inventory_moves", + "tableTo": "orders", + "columnsFrom": [ + "order_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "inventory_moves_product_id_products_id_fk": { + "name": "inventory_moves_product_id_products_id_fk", + "tableFrom": "inventory_moves", + "tableTo": "products", + "columnsFrom": [ + "product_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "inventory_moves_quantity_gt_0": { + "name": "inventory_moves_quantity_gt_0", + "value": "\"inventory_moves\".\"quantity\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.order_items": { + "name": "order_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "order_id": { + "name": "order_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "product_id": { + "name": "product_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "selected_size": { + "name": "selected_size", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "selected_color": { + "name": "selected_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "unit_price_minor": { + "name": "unit_price_minor", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "line_total_minor": { + "name": "line_total_minor", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "unit_price": { + "name": "unit_price", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "line_total": { + "name": "line_total", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "product_title": { + "name": "product_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "product_slug": { + "name": "product_slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "product_sku": { + "name": "product_sku", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "order_items_order_id_idx": { + "name": "order_items_order_id_idx", + "columns": [ + { + "expression": "order_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "order_items_order_variant_uq": { + "name": "order_items_order_variant_uq", + "columns": [ + { + "expression": "order_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "product_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "selected_size", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "selected_color", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "order_items_order_id_orders_id_fk": { + "name": "order_items_order_id_orders_id_fk", + "tableFrom": "order_items", + "tableTo": "orders", + "columnsFrom": [ + "order_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "order_items_product_id_products_id_fk": { + "name": "order_items_product_id_products_id_fk", + "tableFrom": "order_items", + "tableTo": "products", + "columnsFrom": [ + "product_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "order_items_quantity_positive": { + "name": "order_items_quantity_positive", + "value": "\"order_items\".\"quantity\" > 0" + }, + "order_items_unit_price_minor_non_negative": { + "name": "order_items_unit_price_minor_non_negative", + "value": "\"order_items\".\"unit_price_minor\" >= 0" + }, + "order_items_line_total_minor_non_negative": { + "name": "order_items_line_total_minor_non_negative", + "value": "\"order_items\".\"line_total_minor\" >= 0" + }, + "order_items_line_total_consistent": { + "name": "order_items_line_total_consistent", + "value": "\"order_items\".\"line_total_minor\" = \"order_items\".\"unit_price_minor\" * \"order_items\".\"quantity\"" + }, + "order_items_unit_price_mirror_consistent": { + "name": "order_items_unit_price_mirror_consistent", + "value": "\"order_items\".\"unit_price\" = (\"order_items\".\"unit_price_minor\"::numeric / 100)" + }, + "order_items_line_total_mirror_consistent": { + "name": "order_items_line_total_mirror_consistent", + "value": "\"order_items\".\"line_total\" = (\"order_items\".\"line_total_minor\"::numeric / 100)" + } + }, + "isRLSEnabled": false + }, + "public.orders": { + "name": "orders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "total_amount_minor": { + "name": "total_amount_minor", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "total_amount": { + "name": "total_amount", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "currency", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "payment_status": { + "name": "payment_status", + "type": "payment_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "payment_provider": { + "name": "payment_provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'stripe'" + }, + "payment_intent_id": { + "name": "payment_intent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "psp_charge_id": { + "name": "psp_charge_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "psp_payment_method": { + "name": "psp_payment_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "psp_status_reason": { + "name": "psp_status_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "psp_metadata": { + "name": "psp_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "status": { + "name": "status", + "type": "order_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'CREATED'" + }, + "inventory_status": { + "name": "inventory_status", + "type": "inventory_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_message": { + "name": "failure_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_request_hash": { + "name": "idempotency_request_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stock_restored": { + "name": "stock_restored", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "restocked_at": { + "name": "restocked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "sweep_claimed_at": { + "name": "sweep_claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "sweep_claim_expires_at": { + "name": "sweep_claim_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "sweep_run_id": { + "name": "sweep_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "sweep_claimed_by": { + "name": "sweep_claimed_by", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "orders_sweep_claim_expires_idx": { + "name": "orders_sweep_claim_expires_idx", + "columns": [ + { + "expression": "sweep_claim_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "orders_user_id_users_id_fk": { + "name": "orders_user_id_users_id_fk", + "tableFrom": "orders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "orders_idempotency_key_unique": { + "name": "orders_idempotency_key_unique", + "nullsNotDistinct": false, + "columns": [ + "idempotency_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "orders_payment_provider_valid": { + "name": "orders_payment_provider_valid", + "value": "\"orders\".\"payment_provider\" in ('stripe', 'none')" + }, + "orders_total_amount_minor_non_negative": { + "name": "orders_total_amount_minor_non_negative", + "value": "\"orders\".\"total_amount_minor\" >= 0" + }, + "orders_payment_intent_id_null_when_none": { + "name": "orders_payment_intent_id_null_when_none", + "value": "\"orders\".\"payment_provider\" <> 'none' OR \"orders\".\"payment_intent_id\" IS NULL" + }, + "orders_psp_fields_null_when_none": { + "name": "orders_psp_fields_null_when_none", + "value": "\"orders\".\"payment_provider\" <> 'none' OR (\n \"orders\".\"psp_charge_id\" IS NULL AND\n \"orders\".\"psp_payment_method\" IS NULL AND\n \"orders\".\"psp_status_reason\" IS NULL\n )" + }, + "orders_total_amount_mirror_consistent": { + "name": "orders_total_amount_mirror_consistent", + "value": "\"orders\".\"total_amount\" = (\"orders\".\"total_amount_minor\"::numeric / 100)" + }, + "orders_payment_status_valid_when_none": { + "name": "orders_payment_status_valid_when_none", + "value": "\"orders\".\"payment_provider\" <> 'none' OR \"orders\".\"payment_status\" in ('paid','failed')" + } + }, + "isRLSEnabled": false + }, + "public.payment_attempts": { + "name": "payment_attempts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "order_id": { + "name": "order_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "attempt_number": { + "name": "attempt_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_payment_intent_id": { + "name": "provider_payment_intent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_message": { + "name": "last_error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finalized_at": { + "name": "finalized_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "payment_attempts_order_provider_attempt_unique": { + "name": "payment_attempts_order_provider_attempt_unique", + "columns": [ + { + "expression": "order_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "attempt_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payment_attempts_idempotency_key_unique": { + "name": "payment_attempts_idempotency_key_unique", + "columns": [ + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payment_attempts_provider_pi_unique": { + "name": "payment_attempts_provider_pi_unique", + "columns": [ + { + "expression": "provider_payment_intent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payment_attempts_order_provider_status_idx": { + "name": "payment_attempts_order_provider_status_idx", + "columns": [ + { + "expression": "order_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payment_attempts_order_provider_active_unique": { + "name": "payment_attempts_order_provider_active_unique", + "columns": [ + { + "expression": "order_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"payment_attempts\".\"status\" = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payment_attempts_order_id_orders_id_fk": { + "name": "payment_attempts_order_id_orders_id_fk", + "tableFrom": "payment_attempts", + "tableTo": "orders", + "columnsFrom": [ + "order_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "payment_attempts_status_check": { + "name": "payment_attempts_status_check", + "value": "\"payment_attempts\".\"status\" in ('active','succeeded','failed','canceled')" + }, + "payment_attempts_attempt_number_check": { + "name": "payment_attempts_attempt_number_check", + "value": "\"payment_attempts\".\"attempt_number\" >= 1" + } + }, + "isRLSEnabled": false + }, + "public.product_prices": { + "name": "product_prices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "product_id": { + "name": "product_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "currency", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "price_minor": { + "name": "price_minor", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "original_price_minor": { + "name": "original_price_minor", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "price": { + "name": "price", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "original_price": { + "name": "original_price", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "product_prices_product_id_idx": { + "name": "product_prices_product_id_idx", + "columns": [ + { + "expression": "product_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "product_prices_product_currency_uq": { + "name": "product_prices_product_currency_uq", + "columns": [ + { + "expression": "product_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "currency", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "product_prices_product_id_products_id_fk": { + "name": "product_prices_product_id_products_id_fk", + "tableFrom": "product_prices", + "tableTo": "products", + "columnsFrom": [ + "product_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "product_prices_price_positive": { + "name": "product_prices_price_positive", + "value": "\"product_prices\".\"price_minor\" > 0" + }, + "product_prices_original_price_valid": { + "name": "product_prices_original_price_valid", + "value": "\"product_prices\".\"original_price_minor\" is null or \"product_prices\".\"original_price_minor\" > \"product_prices\".\"price_minor\"" + }, + "product_prices_price_mirror_consistent": { + "name": "product_prices_price_mirror_consistent", + "value": "\"product_prices\".\"price\" = (\"product_prices\".\"price_minor\"::numeric / 100)" + }, + "product_prices_original_price_null_coupled": { + "name": "product_prices_original_price_null_coupled", + "value": "(\"product_prices\".\"original_price_minor\" is null) = (\"product_prices\".\"original_price\" is null)" + }, + "product_prices_original_price_mirror_consistent": { + "name": "product_prices_original_price_mirror_consistent", + "value": "\"product_prices\".\"original_price_minor\" is null or \"product_prices\".\"original_price\" = (\"product_prices\".\"original_price_minor\"::numeric / 100)" + } + }, + "isRLSEnabled": false + }, + "public.products": { + "name": "products", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image_url": { + "name": "image_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image_public_id": { + "name": "image_public_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "price": { + "name": "price", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "original_price": { + "name": "original_price", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "currency", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "colors": { + "name": "colors", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "sizes": { + "name": "sizes", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "badge": { + "name": "badge", + "type": "product_badge", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'NONE'" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_featured": { + "name": "is_featured", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "stock": { + "name": "stock", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sku": { + "name": "sku", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "products_slug_unique": { + "name": "products_slug_unique", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "products_stock_non_negative": { + "name": "products_stock_non_negative", + "value": "\"products\".\"stock\" >= 0" + }, + "products_currency_usd_only": { + "name": "products_currency_usd_only", + "value": "\"products\".\"currency\" = 'USD'" + }, + "products_price_positive": { + "name": "products_price_positive", + "value": "\"products\".\"price\" > 0" + }, + "products_original_price_valid": { + "name": "products_original_price_valid", + "value": "\"products\".\"original_price\" is null or \"products\".\"original_price\" > \"products\".\"price\"" + } + }, + "isRLSEnabled": false + }, + "public.stripe_events": { + "name": "stripe_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'stripe'" + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payment_intent_id": { + "name": "payment_intent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "order_id": { + "name": "order_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payment_status": { + "name": "payment_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claim_expires_at": { + "name": "claim_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_by": { + "name": "claimed_by", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "stripe_events_event_id_idx": { + "name": "stripe_events_event_id_idx", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "stripe_events_claim_expires_idx": { + "name": "stripe_events_claim_expires_idx", + "columns": [ + { + "expression": "claim_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "stripe_events_order_id_orders_id_fk": { + "name": "stripe_events_order_id_orders_id_fk", + "tableFrom": "stripe_events", + "tableTo": "orders", + "columnsFrom": [ + "order_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_verification_tokens": { + "name": "email_verification_tokens", + "schema": "", + "columns": { + "token": { + "name": "token", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "email_verification_tokens_user_id_idx": { + "name": "email_verification_tokens_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.password_reset_tokens": { + "name": "password_reset_tokens", + "schema": "", + "columns": { + "token": { + "name": "token", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "password_reset_tokens_user_id_idx": { + "name": "password_reset_tokens_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.active_sessions": { + "name": "active_sessions", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "last_activity": { + "name": "last_activity", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "active_sessions_last_activity_idx": { + "name": "active_sessions_last_activity_idx", + "columns": [ + { + "expression": "last_activity", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.currency": { + "name": "currency", + "schema": "public", + "values": [ + "USD", + "UAH" + ] + }, + "public.inventory_move_type": { + "name": "inventory_move_type", + "schema": "public", + "values": [ + "reserve", + "release" + ] + }, + "public.inventory_status": { + "name": "inventory_status", + "schema": "public", + "values": [ + "none", + "reserving", + "reserved", + "release_pending", + "released", + "failed" + ] + }, + "public.order_status": { + "name": "order_status", + "schema": "public", + "values": [ + "CREATED", + "INVENTORY_RESERVED", + "INVENTORY_FAILED", + "PAID", + "CANCELED" + ] + }, + "public.payment_status": { + "name": "payment_status", + "schema": "public", + "values": [ + "pending", + "requires_payment", + "paid", + "failed", + "refunded" + ] + }, + "public.product_badge": { + "name": "product_badge", + "schema": "public", + "values": [ + "NEW", + "SALE", + "NONE" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/frontend/drizzle/meta/_journal.json b/frontend/drizzle/meta/_journal.json index 503103ce..48d82cf4 100644 --- a/frontend/drizzle/meta/_journal.json +++ b/frontend/drizzle/meta/_journal.json @@ -22,6 +22,13 @@ "when": 1768664588311, "tag": "0002_clean_martin_li", "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1768690525242, + "tag": "0003_add_stripe_events_claim_lock", + "breakpoints": true } ] } \ No newline at end of file