From 87c6358b16d6196fa2687a7a181dc92ff89d626f Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Wed, 21 Oct 2026 12:14:23 +0000 Subject: [PATCH] fix(checkout): write the catalog episode row before the first order order_items and entitlements reference episodes(id) and nothing seeds that table, so every checkout failed with order_items_episode_id_episodes_id_fk and left an orphan order behind. createOrder now upserts the catalog's episode row and writes the order and its item in one transaction. Co-Authored-By: Claude Opus 5.5 --- services/api/src/repository.ts | 58 +++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 19 deletions(-) diff --git a/services/api/src/repository.ts b/services/api/src/repository.ts index 8f2f8f5..235c440 100644 --- a/services/api/src/repository.ts +++ b/services/api/src/repository.ts @@ -7,6 +7,7 @@ import { offlineLicenses, entitlementEvents, entitlements, + episodes, feedback, multiplayerBans, multiplayerBlocks, @@ -21,7 +22,7 @@ import { users, } from "@nightcell7/database"; import type { AccountContext } from "@nightcell7/auth"; -import type { OrderStatus } from "@nightcell7/entitlements"; +import { findCatalogEntry, type OrderStatus } from "@nightcell7/entitlements"; /** * Data access for the API service. @@ -263,24 +264,43 @@ export function createRepositories(db: Database): Repositories { }, async createOrder(input) { - await db.insert(orders).values({ - id: input.id, - userId: input.userId, - email: input.email, - provider: "coinpay", - currency: input.currency, - subtotal: input.unitAmount, - tax: 0, - total: input.unitAmount, - status: "created", - idempotencyKey: input.idempotencyKey, - }); - await db.insert(orderItems).values({ - id: `oi_${input.id}`, - orderId: input.id, - episodeId: input.episodeId, - unitAmount: input.unitAmount, - quantity: 1, + const entry = findCatalogEntry(input.episodeId); + await db.transaction(async (tx) => { + // order_items (and entitlements) reference episodes(id), and nothing + // seeds that table: the server catalog is the authority, so the row + // it needs is written from the catalog on first sale. Without it every + // checkout failed on the foreign key. + if (entry) { + await tx + .insert(episodes) + .values({ + id: entry.episodeId, + slug: entry.episodeId, + title: entry.title, + theater: "NIGHTCELL 7", + status: entry.status, + }) + .onConflictDoNothing(); + } + await tx.insert(orders).values({ + id: input.id, + userId: input.userId, + email: input.email, + provider: "coinpay", + currency: input.currency, + subtotal: input.unitAmount, + tax: 0, + total: input.unitAmount, + status: "created", + idempotencyKey: input.idempotencyKey, + }); + await tx.insert(orderItems).values({ + id: `oi_${input.id}`, + orderId: input.id, + episodeId: input.episodeId, + unitAmount: input.unitAmount, + quantity: 1, + }); }); },