From a218ec7a6d14316f4d44a6a4dd02a9d9f14fc8c7 Mon Sep 17 00:00:00 2001 From: Shotaro Nakamura <79000684+nakasyou@users.noreply.github.com> Date: Fri, 8 May 2026 12:23:22 +0900 Subject: [PATCH 1/5] feat(betterzeriya): add Google UCP checkout endpoints --- apps/betterzeriya/src/lib/server/ucp.ts | 59 +++++++++++++++++++ .../src/routes/.well-known/ucp/+server.ts | 21 +++++++ .../api/ucp/checkout-sessions/+server.ts | 26 ++++++++ .../api/ucp/checkout-sessions/[id]/+server.ts | 18 ++++++ bun.lock | 1 - 5 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 apps/betterzeriya/src/lib/server/ucp.ts create mode 100644 apps/betterzeriya/src/routes/.well-known/ucp/+server.ts create mode 100644 apps/betterzeriya/src/routes/api/ucp/checkout-sessions/+server.ts create mode 100644 apps/betterzeriya/src/routes/api/ucp/checkout-sessions/[id]/+server.ts diff --git a/apps/betterzeriya/src/lib/server/ucp.ts b/apps/betterzeriya/src/lib/server/ucp.ts new file mode 100644 index 0000000..0487c9a --- /dev/null +++ b/apps/betterzeriya/src/lib/server/ucp.ts @@ -0,0 +1,59 @@ +interface UcpLineItem { + item: { + id: string + title?: string + } + quantity: number +} + +interface UcpBuyer { + full_name?: string + email?: string +} + +interface CreateCheckoutSessionInput { + line_items: UcpLineItem[] + buyer?: UcpBuyer + currency?: string +} + +interface UcpCheckoutSession { + id: string + status: 'requires_action' | 'completed' + currency: string + buyer?: UcpBuyer + line_items: UcpLineItem[] + created_at: string + updated_at: string +} + +const sessions = new Map() + +export const createUcpCheckoutSession = (input: CreateCheckoutSessionInput) => { + const now = new Date().toISOString() + const session: UcpCheckoutSession = { + id: crypto.randomUUID(), + status: 'requires_action', + currency: input.currency ?? 'JPY', + buyer: input.buyer, + line_items: input.line_items, + created_at: now, + updated_at: now, + } + sessions.set(session.id, session) + return session +} + +export const getUcpCheckoutSession = (id: string) => sessions.get(id) + +export const completeUcpCheckoutSession = (id: string) => { + const session = sessions.get(id) + if (!session) return undefined + const updated = { + ...session, + status: 'completed' as const, + updated_at: new Date().toISOString(), + } + sessions.set(id, updated) + return updated +} diff --git a/apps/betterzeriya/src/routes/.well-known/ucp/+server.ts b/apps/betterzeriya/src/routes/.well-known/ucp/+server.ts new file mode 100644 index 0000000..5d7b7d8 --- /dev/null +++ b/apps/betterzeriya/src/routes/.well-known/ucp/+server.ts @@ -0,0 +1,21 @@ +import { json, type RequestHandler } from '@sveltejs/kit' + +export const GET: RequestHandler = async ({ url }) => { + const origin = url.origin + return json({ + name: 'betterzeriya', + version: '2026-01-11', + capabilities: [ + { + name: 'dev.ucp.shopping.checkout', + bindings: [ + { + type: 'rest', + spec: 'https://ucp.dev/specification/checkout-rest/', + base_url: `${origin}/api/ucp`, + }, + ], + }, + ], + }) +} diff --git a/apps/betterzeriya/src/routes/api/ucp/checkout-sessions/+server.ts b/apps/betterzeriya/src/routes/api/ucp/checkout-sessions/+server.ts new file mode 100644 index 0000000..bab5b14 --- /dev/null +++ b/apps/betterzeriya/src/routes/api/ucp/checkout-sessions/+server.ts @@ -0,0 +1,26 @@ +import { json, type RequestHandler } from '@sveltejs/kit' +import { createUcpCheckoutSession } from '$lib/server/ucp' + +export const POST: RequestHandler = async ({ request, url }) => { + const body = await request.json().catch(() => null) + const lineItems = body?.line_items + + if (!Array.isArray(lineItems) || lineItems.length === 0) { + return json({ error: 'line_items is required' }, { status: 400 }) + } + + const session = createUcpCheckoutSession({ + line_items: lineItems, + buyer: body?.buyer, + currency: body?.currency, + }) + + return json( + { + checkout_session: session, + checkout_url: `${url.origin}/sessions/${session.id}`, + status_url: `${url.origin}/api/ucp/checkout-sessions/${session.id}`, + }, + { status: 201 }, + ) +} diff --git a/apps/betterzeriya/src/routes/api/ucp/checkout-sessions/[id]/+server.ts b/apps/betterzeriya/src/routes/api/ucp/checkout-sessions/[id]/+server.ts new file mode 100644 index 0000000..cb71cda --- /dev/null +++ b/apps/betterzeriya/src/routes/api/ucp/checkout-sessions/[id]/+server.ts @@ -0,0 +1,18 @@ +import { json, type RequestHandler } from '@sveltejs/kit' +import { completeUcpCheckoutSession, getUcpCheckoutSession } from '$lib/server/ucp' + +export const GET: RequestHandler = async ({ params }) => { + const session = getUcpCheckoutSession(params.id) + if (!session) { + return json({ error: 'Checkout session not found' }, { status: 404 }) + } + return json({ checkout_session: session }) +} + +export const POST: RequestHandler = async ({ params }) => { + const session = completeUcpCheckoutSession(params.id) + if (!session) { + return json({ error: 'Checkout session not found' }, { status: 404 }) + } + return json({ checkout_session: session }) +} diff --git a/bun.lock b/bun.lock index cc2ca45..4d23477 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 1, "workspaces": { "": { "name": "saizeriya", From 5dea889bec80d0bff39765c8d66826fe55ad304f Mon Sep 17 00:00:00 2001 From: Shotaro Nakamura Date: Mon, 11 May 2026 21:47:00 +0900 Subject: [PATCH 2/5] fmt --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b44c2ff..94273a2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # saizeriya [![技術者倫理 遵守済み](https://img.shields.io/badge/%E6%8A%80%E8%A1%93%E8%80%85%E5%80%AB%E7%90%86-%E9%81%B5%E5%AE%88%E6%B8%88%E3%81%BF-0a0a0a?style=for-the-badge&labelColor=ffffff)](https://技術者倫理.com) + From 3d16ca5173ca9738054c857f66bc8b2c77c17317 Mon Sep 17 00:00:00 2001 From: Shotaro Nakamura <79000684+nakasyou@users.noreply.github.com> Date: Mon, 11 May 2026 22:33:30 +0900 Subject: [PATCH 3/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../api/ucp/checkout-sessions/+server.ts | 77 +++++++++++++++++-- 1 file changed, 71 insertions(+), 6 deletions(-) diff --git a/apps/betterzeriya/src/routes/api/ucp/checkout-sessions/+server.ts b/apps/betterzeriya/src/routes/api/ucp/checkout-sessions/+server.ts index bab5b14..a4df93e 100644 --- a/apps/betterzeriya/src/routes/api/ucp/checkout-sessions/+server.ts +++ b/apps/betterzeriya/src/routes/api/ucp/checkout-sessions/+server.ts @@ -1,18 +1,83 @@ import { json, type RequestHandler } from '@sveltejs/kit' import { createUcpCheckoutSession } from '$lib/server/ucp' +const MAX_LINE_ITEMS = 100 +const MAX_ITEM_ID_LENGTH = 256 +const MAX_QUANTITY = 1000 + +type NormalizedLineItem = { + id: string + quantity: number +} + +const isRecord = (value: unknown): value is Record => + typeof value === 'object' && value !== null + +const normalizeLineItems = ( + value: unknown, +): { lineItems: NormalizedLineItem[] } | { error: string } => { + if (!Array.isArray(value) || value.length === 0) { + return { error: 'line_items is required' } + } + + if (value.length > MAX_LINE_ITEMS) { + return { error: `line_items must contain at most ${MAX_LINE_ITEMS} items` } + } + + const lineItems: NormalizedLineItem[] = [] + + for (const item of value) { + if (!isRecord(item)) { + return { error: 'Each line_items entry must be an object' } + } + + const id = typeof item.id === 'string' ? item.id.trim() : '' + if (id.length === 0 || id.length > MAX_ITEM_ID_LENGTH) { + return { + error: `Each line_items entry must include a non-empty id up to ${MAX_ITEM_ID_LENGTH} characters`, + } + } + + const rawQuantity = item.quantity + const quantity = + typeof rawQuantity === 'number' + ? rawQuantity + : typeof rawQuantity === 'string' && rawQuantity.trim() !== '' + ? Number(rawQuantity) + : NaN + + if ( + !Number.isInteger(quantity) || + quantity < 1 || + quantity > MAX_QUANTITY + ) { + return { + error: `Each line_items entry must include a quantity between 1 and ${MAX_QUANTITY}`, + } + } + + lineItems.push({ id, quantity }) + } + + return { lineItems } +} + export const POST: RequestHandler = async ({ request, url }) => { const body = await request.json().catch(() => null) - const lineItems = body?.line_items - if (!Array.isArray(lineItems) || lineItems.length === 0) { - return json({ error: 'line_items is required' }, { status: 400 }) + if (!isRecord(body)) { + return json({ error: 'Invalid request body' }, { status: 400 }) + } + + const normalizedLineItems = normalizeLineItems(body.line_items) + if ('error' in normalizedLineItems) { + return json({ error: normalizedLineItems.error }, { status: 400 }) } const session = createUcpCheckoutSession({ - line_items: lineItems, - buyer: body?.buyer, - currency: body?.currency, + line_items: normalizedLineItems.lineItems, + buyer: body.buyer, + currency: body.currency, }) return json( From b0bc556b0ced1c01476ea1288fd883ecc63ef602 Mon Sep 17 00:00:00 2001 From: Shotaro Nakamura Date: Tue, 12 May 2026 21:30:06 +0900 Subject: [PATCH 4/5] fix --- apps/betterzeriya/src/lib/server/ucp.ts | 234 ++++++++++++++++-- .../src/lib/zeriya-gpt/system-prompt.ts | 13 +- .../src/routes/.well-known/ucp/+server.ts | 39 ++- .../api/ucp/checkout-sessions/+server.ts | 71 +++--- .../api/ucp/checkout-sessions/[id]/+server.ts | 58 ++++- .../checkout-sessions/[id]/cancel/+server.ts | 21 ++ .../[id]/complete/+server.ts | 27 ++ .../src/routes/sessions/[id]/ai/+page.svelte | 2 +- bun.lock | 1 + 9 files changed, 387 insertions(+), 79 deletions(-) create mode 100644 apps/betterzeriya/src/routes/api/ucp/checkout-sessions/[id]/cancel/+server.ts create mode 100644 apps/betterzeriya/src/routes/api/ucp/checkout-sessions/[id]/complete/+server.ts diff --git a/apps/betterzeriya/src/lib/server/ucp.ts b/apps/betterzeriya/src/lib/server/ucp.ts index 0487c9a..8659314 100644 --- a/apps/betterzeriya/src/lib/server/ucp.ts +++ b/apps/betterzeriya/src/lib/server/ucp.ts @@ -1,57 +1,253 @@ -interface UcpLineItem { +import menuData from '$lib/assets/data/menu.json' + +export const UCP_VERSION = '2026-04-08' +export const UCP_SHOPPING_SERVICE = 'dev.ucp.shopping' +export const UCP_CHECKOUT_CAPABILITY = 'dev.ucp.shopping.checkout' +export const UCP_GOOGLE_PAY_HANDLER = 'com.google.pay' + +type UcpCheckoutStatus = + | 'incomplete' + | 'requires_escalation' + | 'ready_for_complete' + | 'complete_in_progress' + | 'completed' + | 'canceled' + +type UcpBuyer = Record +type UcpPayment = Record + +export interface UcpLineItemInput { item: { id: string title?: string + price?: number } quantity: number } -interface UcpBuyer { - full_name?: string - email?: string +interface CreateCheckoutSessionInput { + origin: string + line_items: UcpLineItemInput[] + buyer?: UcpBuyer + currency?: string + payment?: UcpPayment } -interface CreateCheckoutSessionInput { - line_items: UcpLineItem[] +interface UpdateCheckoutSessionInput { + line_items?: UcpLineItemInput[] buyer?: UcpBuyer currency?: string + payment?: UcpPayment +} + +interface UcpTotal { + type: 'subtotal' | 'tax' | 'total' + amount: number } interface UcpCheckoutSession { + ucp: ReturnType id: string - status: 'requires_action' | 'completed' + status: UcpCheckoutStatus currency: string buyer?: UcpBuyer - line_items: UcpLineItem[] + line_items: Array + totals: UcpTotal[] + links: Array<{ type: 'terms_of_service' | 'privacy_policy'; url: string }> + payment: { + handlers: Array<{ id: string; handler_id: string; type: 'google_pay' }> + instruments: unknown[] + } + messages?: Array> + continue_url?: string + order?: { + id: string + checkout_id: string + permalink_url: string + } created_at: string updated_at: string + expires_at: string } +type MenuEntry = { + code: string + name: string + price: number +} + +const menuByCode = new Map((menuData as MenuEntry[]).map((item) => [item.code, item])) const sessions = new Map() +const sessionTtlMs = 1000 * 60 * 60 * 6 -export const createUcpCheckoutSession = (input: CreateCheckoutSessionInput) => { +export const createUcpMetadata = () => ({ + version: UCP_VERSION, + capabilities: { + [UCP_CHECKOUT_CAPABILITY]: [{ version: UCP_VERSION }], + }, + payment_handlers: { + [UCP_GOOGLE_PAY_HANDLER]: [ + { + id: 'betterzeriya_google_pay', + version: UCP_VERSION, + available_instruments: [{ type: 'card' }], + config: { + allowed_payment_methods: [ + { + type: 'CARD', + parameters: { + allowed_auth_methods: ['PAN_ONLY', 'CRYPTOGRAM_3DS'], + allowed_card_networks: ['VISA', 'MASTERCARD', 'AMEX', 'JCB'], + }, + }, + ], + }, + }, + ], + }, +}) + +const pruneSessions = () => { + const now = Date.now() + for (const [id, session] of sessions) { + if (Date.parse(session.updated_at) + sessionTtlMs < now) { + sessions.delete(id) + } + } +} + +const buildLinks = (origin: string): UcpCheckoutSession['links'] => [ + { type: 'terms_of_service', url: origin }, + { type: 'privacy_policy', url: origin }, +] + +const resolveLineItem = (lineItem: UcpLineItemInput, index: number) => { + const menuItem = menuByCode.get(lineItem.item.id) + const price = lineItem.item.price ?? menuItem?.price ?? 0 + const title = lineItem.item.title ?? menuItem?.name ?? lineItem.item.id + const amount = price * lineItem.quantity + + return { + id: `li_${index + 1}`, + item: { + ...lineItem.item, + title, + price, + }, + quantity: lineItem.quantity, + totals: [ + { type: 'subtotal' as const, amount }, + { type: 'total' as const, amount }, + ], + } +} + +const calculateTotals = (lineItems: UcpCheckoutSession['line_items']) => { + const subtotal = lineItems.reduce((sum, item) => sum + item.totals[0].amount, 0) + return [ + { type: 'subtotal' as const, amount: subtotal }, + { type: 'tax' as const, amount: 0 }, + { type: 'total' as const, amount: subtotal }, + ] +} + +const createPayment = (): UcpCheckoutSession['payment'] => ({ + handlers: [ + { + id: 'betterzeriya_google_pay', + handler_id: 'betterzeriya_google_pay', + type: 'google_pay', + }, + ], + instruments: [], +}) + +const refreshSessionShape = ( + session: UcpCheckoutSession, + input: UpdateCheckoutSessionInput & { origin?: string }, +) => { + const lineItems = + input.line_items?.map(resolveLineItem) ?? + session.line_items.map(({ item, quantity }) => ({ item, quantity })).map(resolveLineItem) const now = new Date().toISOString() + + return { + ...session, + currency: input.currency ?? session.currency, + buyer: input.buyer ?? session.buyer, + line_items: lineItems, + totals: calculateTotals(lineItems), + payment: input.payment ? { ...session.payment, instruments: [input.payment] } : session.payment, + links: input.origin ? buildLinks(input.origin) : session.links, + updated_at: now, + } +} + +export const createUcpCheckoutSession = (input: CreateCheckoutSessionInput) => { + pruneSessions() + const now = new Date() + const id = `chk_${crypto.randomUUID()}` + const lineItems = input.line_items.map(resolveLineItem) const session: UcpCheckoutSession = { - id: crypto.randomUUID(), - status: 'requires_action', + ucp: createUcpMetadata(), + id, + status: 'ready_for_complete', currency: input.currency ?? 'JPY', buyer: input.buyer, - line_items: input.line_items, - created_at: now, - updated_at: now, + line_items: lineItems, + totals: calculateTotals(lineItems), + links: buildLinks(input.origin), + payment: input.payment ? { ...createPayment(), instruments: [input.payment] } : createPayment(), + continue_url: `${input.origin}/`, + created_at: now.toISOString(), + updated_at: now.toISOString(), + expires_at: new Date(now.getTime() + sessionTtlMs).toISOString(), } sessions.set(session.id, session) return session } -export const getUcpCheckoutSession = (id: string) => sessions.get(id) +export const getUcpCheckoutSession = (id: string) => { + pruneSessions() + return sessions.get(id) +} + +export const updateUcpCheckoutSession = (id: string, input: UpdateCheckoutSessionInput) => { + const session = getUcpCheckoutSession(id) + if (!session || session.status === 'completed' || session.status === 'canceled') { + return undefined + } + const updated = refreshSessionShape(session, input) + sessions.set(id, updated) + return updated +} + +export const completeUcpCheckoutSession = (id: string, payment?: UcpPayment) => { + const session = getUcpCheckoutSession(id) + if (!session || session.status === 'completed' || session.status === 'canceled') { + return undefined + } + const updated = { + ...refreshSessionShape(session, { payment }), + status: 'completed' as const, + order: { + id: `ord_${crypto.randomUUID()}`, + checkout_id: id, + permalink_url: session.continue_url ?? '/', + }, + } + sessions.set(id, updated) + return updated +} -export const completeUcpCheckoutSession = (id: string) => { - const session = sessions.get(id) - if (!session) return undefined +export const cancelUcpCheckoutSession = (id: string) => { + const session = getUcpCheckoutSession(id) + if (!session || session.status === 'completed' || session.status === 'canceled') { + return undefined + } const updated = { ...session, - status: 'completed' as const, + status: 'canceled' as const, updated_at: new Date().toISOString(), } sessions.set(id, updated) diff --git a/apps/betterzeriya/src/lib/zeriya-gpt/system-prompt.ts b/apps/betterzeriya/src/lib/zeriya-gpt/system-prompt.ts index 73e0a8c..8811f8e 100644 --- a/apps/betterzeriya/src/lib/zeriya-gpt/system-prompt.ts +++ b/apps/betterzeriya/src/lib/zeriya-gpt/system-prompt.ts @@ -2,18 +2,15 @@ import menuData from '$lib/assets/data/menu.json' import { normalizeMenuName } from './menu-name-normalization' type MenuItem = { - item_data: { - id: string - name: string - price: number - } - alcohol_check?: number + code: string + name: string + price: number } export const buildMenuList = () => (menuData as MenuItem[]) - .filter((item) => item.item_data.price > 0) - .map((item) => `- ${normalizeMenuName(item.item_data.name)} (${item.item_data.price}円)`) + .filter((item) => item.price > 0) + .map((item) => `- ${normalizeMenuName(item.name)} (${item.price}円)`) .join('\n') export const buildSystemPrompt = diff --git a/apps/betterzeriya/src/routes/.well-known/ucp/+server.ts b/apps/betterzeriya/src/routes/.well-known/ucp/+server.ts index 5d7b7d8..3ddbefd 100644 --- a/apps/betterzeriya/src/routes/.well-known/ucp/+server.ts +++ b/apps/betterzeriya/src/routes/.well-known/ucp/+server.ts @@ -1,21 +1,42 @@ import { json, type RequestHandler } from '@sveltejs/kit' +import { + UCP_CHECKOUT_CAPABILITY, + UCP_GOOGLE_PAY_HANDLER, + UCP_SHOPPING_SERVICE, + UCP_VERSION, +} from '$lib/server/ucp' export const GET: RequestHandler = async ({ url }) => { const origin = url.origin return json({ name: 'betterzeriya', - version: '2026-01-11', - capabilities: [ - { - name: 'dev.ucp.shopping.checkout', - bindings: [ + url: origin, + ucp: { + version: UCP_VERSION, + services: { + [UCP_SHOPPING_SERVICE]: [ { - type: 'rest', - spec: 'https://ucp.dev/specification/checkout-rest/', - base_url: `${origin}/api/ucp`, + version: UCP_VERSION, + bindings: [ + { + type: 'rest', + endpoint: `${origin}/api/ucp`, + }, + ], }, ], }, - ], + capabilities: { + [UCP_CHECKOUT_CAPABILITY]: [{ version: UCP_VERSION }], + }, + payment_handlers: { + [UCP_GOOGLE_PAY_HANDLER]: [ + { + id: 'betterzeriya_google_pay', + version: UCP_VERSION, + }, + ], + }, + }, }) } diff --git a/apps/betterzeriya/src/routes/api/ucp/checkout-sessions/+server.ts b/apps/betterzeriya/src/routes/api/ucp/checkout-sessions/+server.ts index a4df93e..d10ca11 100644 --- a/apps/betterzeriya/src/routes/api/ucp/checkout-sessions/+server.ts +++ b/apps/betterzeriya/src/routes/api/ucp/checkout-sessions/+server.ts @@ -1,21 +1,21 @@ import { json, type RequestHandler } from '@sveltejs/kit' -import { createUcpCheckoutSession } from '$lib/server/ucp' +import { createUcpCheckoutSession, type UcpLineItemInput } from '$lib/server/ucp' const MAX_LINE_ITEMS = 100 const MAX_ITEM_ID_LENGTH = 256 const MAX_QUANTITY = 1000 -type NormalizedLineItem = { - id: string - quantity: number -} - const isRecord = (value: unknown): value is Record => - typeof value === 'object' && value !== null + typeof value === 'object' && value !== null && !Array.isArray(value) + +const normalizeBuyer = (value: unknown) => (isRecord(value) ? value : undefined) +const normalizePayment = (value: unknown) => (isRecord(value) ? value : undefined) +const normalizeCurrency = (value: unknown) => + typeof value === 'string' && /^[A-Z]{3}$/.test(value) ? value : undefined -const normalizeLineItems = ( +export const _normalizeLineItems = ( value: unknown, -): { lineItems: NormalizedLineItem[] } | { error: string } => { +): { lineItems: UcpLineItemInput[] } | { error: string } => { if (!Array.isArray(value) || value.length === 0) { return { error: 'line_items is required' } } @@ -24,21 +24,21 @@ const normalizeLineItems = ( return { error: `line_items must contain at most ${MAX_LINE_ITEMS} items` } } - const lineItems: NormalizedLineItem[] = [] + const lineItems: UcpLineItemInput[] = [] - for (const item of value) { - if (!isRecord(item)) { - return { error: 'Each line_items entry must be an object' } + for (const lineItem of value) { + if (!isRecord(lineItem) || !isRecord(lineItem.item)) { + return { error: 'Each line_items entry must include an item object' } } - const id = typeof item.id === 'string' ? item.id.trim() : '' + const id = typeof lineItem.item.id === 'string' ? lineItem.item.id.trim() : '' if (id.length === 0 || id.length > MAX_ITEM_ID_LENGTH) { return { - error: `Each line_items entry must include a non-empty id up to ${MAX_ITEM_ID_LENGTH} characters`, + error: `Each line_items item must include a non-empty id up to ${MAX_ITEM_ID_LENGTH} characters`, } } - const rawQuantity = item.quantity + const rawQuantity = lineItem.quantity const quantity = typeof rawQuantity === 'number' ? rawQuantity @@ -46,17 +46,23 @@ const normalizeLineItems = ( ? Number(rawQuantity) : NaN - if ( - !Number.isInteger(quantity) || - quantity < 1 || - quantity > MAX_QUANTITY - ) { + if (!Number.isInteger(quantity) || quantity < 1 || quantity > MAX_QUANTITY) { return { error: `Each line_items entry must include a quantity between 1 and ${MAX_QUANTITY}`, } } - lineItems.push({ id, quantity }) + const title = typeof lineItem.item.title === 'string' ? lineItem.item.title.trim() : undefined + const price = typeof lineItem.item.price === 'number' ? lineItem.item.price : undefined + + lineItems.push({ + item: { + id, + ...(title ? { title } : {}), + ...(price !== undefined ? { price } : {}), + }, + quantity, + }) } return { lineItems } @@ -66,26 +72,21 @@ export const POST: RequestHandler = async ({ request, url }) => { const body = await request.json().catch(() => null) if (!isRecord(body)) { - return json({ error: 'Invalid request body' }, { status: 400 }) + return json({ code: 'invalid_request', content: 'Invalid request body' }, { status: 400 }) } - const normalizedLineItems = normalizeLineItems(body.line_items) + const normalizedLineItems = _normalizeLineItems(body.line_items) if ('error' in normalizedLineItems) { - return json({ error: normalizedLineItems.error }, { status: 400 }) + return json({ code: 'invalid_request', content: normalizedLineItems.error }, { status: 400 }) } const session = createUcpCheckoutSession({ + origin: url.origin, line_items: normalizedLineItems.lineItems, - buyer: body.buyer, - currency: body.currency, + buyer: normalizeBuyer(body.buyer), + currency: normalizeCurrency(body.currency), + payment: normalizePayment(body.payment), }) - return json( - { - checkout_session: session, - checkout_url: `${url.origin}/sessions/${session.id}`, - status_url: `${url.origin}/api/ucp/checkout-sessions/${session.id}`, - }, - { status: 201 }, - ) + return json(session, { status: 201 }) } diff --git a/apps/betterzeriya/src/routes/api/ucp/checkout-sessions/[id]/+server.ts b/apps/betterzeriya/src/routes/api/ucp/checkout-sessions/[id]/+server.ts index cb71cda..973553d 100644 --- a/apps/betterzeriya/src/routes/api/ucp/checkout-sessions/[id]/+server.ts +++ b/apps/betterzeriya/src/routes/api/ucp/checkout-sessions/[id]/+server.ts @@ -1,18 +1,62 @@ import { json, type RequestHandler } from '@sveltejs/kit' -import { completeUcpCheckoutSession, getUcpCheckoutSession } from '$lib/server/ucp' +import { getUcpCheckoutSession, updateUcpCheckoutSession } from '$lib/server/ucp' +import { _normalizeLineItems } from '../+server' + +const isRecord = (value: unknown): value is Record => + typeof value === 'object' && value !== null && !Array.isArray(value) + +const normalizeBuyer = (value: unknown) => (isRecord(value) ? value : undefined) +const normalizePayment = (value: unknown) => (isRecord(value) ? value : undefined) +const normalizeCurrency = (value: unknown) => + typeof value === 'string' && /^[A-Z]{3}$/.test(value) ? value : undefined export const GET: RequestHandler = async ({ params }) => { + if (!params.id) { + return json( + { code: 'invalid_request', content: 'Checkout session id is required' }, + { status: 400 }, + ) + } + const session = getUcpCheckoutSession(params.id) if (!session) { - return json({ error: 'Checkout session not found' }, { status: 404 }) + return json({ code: 'not_found', content: 'Checkout session not found' }, { status: 404 }) } - return json({ checkout_session: session }) + return json(session) } -export const POST: RequestHandler = async ({ params }) => { - const session = completeUcpCheckoutSession(params.id) +export const PUT: RequestHandler = async ({ params, request }) => { + if (!params.id) { + return json( + { code: 'invalid_request', content: 'Checkout session id is required' }, + { status: 400 }, + ) + } + + const body = await request.json().catch(() => null) + if (!isRecord(body)) { + return json({ code: 'invalid_request', content: 'Invalid request body' }, { status: 400 }) + } + + const normalizedLineItems = + body.line_items === undefined ? undefined : _normalizeLineItems(body.line_items) + if (normalizedLineItems && 'error' in normalizedLineItems) { + return json({ code: 'invalid_request', content: normalizedLineItems.error }, { status: 400 }) + } + + const session = updateUcpCheckoutSession(params.id, { + line_items: normalizedLineItems?.lineItems, + buyer: normalizeBuyer(body.buyer), + currency: normalizeCurrency(body.currency), + payment: normalizePayment(body.payment), + }) + if (!session) { - return json({ error: 'Checkout session not found' }, { status: 404 }) + return json( + { code: 'not_found_or_closed', content: 'Checkout session not found or already closed' }, + { status: 404 }, + ) } - return json({ checkout_session: session }) + + return json(session) } diff --git a/apps/betterzeriya/src/routes/api/ucp/checkout-sessions/[id]/cancel/+server.ts b/apps/betterzeriya/src/routes/api/ucp/checkout-sessions/[id]/cancel/+server.ts new file mode 100644 index 0000000..787f02d --- /dev/null +++ b/apps/betterzeriya/src/routes/api/ucp/checkout-sessions/[id]/cancel/+server.ts @@ -0,0 +1,21 @@ +import { json, type RequestHandler } from '@sveltejs/kit' +import { cancelUcpCheckoutSession } from '$lib/server/ucp' + +export const POST: RequestHandler = async ({ params }) => { + if (!params.id) { + return json( + { code: 'invalid_request', content: 'Checkout session id is required' }, + { status: 400 }, + ) + } + + const session = cancelUcpCheckoutSession(params.id) + if (!session) { + return json( + { code: 'not_found_or_closed', content: 'Checkout session not found or already closed' }, + { status: 404 }, + ) + } + + return json(session) +} diff --git a/apps/betterzeriya/src/routes/api/ucp/checkout-sessions/[id]/complete/+server.ts b/apps/betterzeriya/src/routes/api/ucp/checkout-sessions/[id]/complete/+server.ts new file mode 100644 index 0000000..9723511 --- /dev/null +++ b/apps/betterzeriya/src/routes/api/ucp/checkout-sessions/[id]/complete/+server.ts @@ -0,0 +1,27 @@ +import { json, type RequestHandler } from '@sveltejs/kit' +import { completeUcpCheckoutSession } from '$lib/server/ucp' + +const isRecord = (value: unknown): value is Record => + typeof value === 'object' && value !== null && !Array.isArray(value) + +export const POST: RequestHandler = async ({ params, request }) => { + if (!params.id) { + return json( + { code: 'invalid_request', content: 'Checkout session id is required' }, + { status: 400 }, + ) + } + + const body = await request.json().catch(() => ({})) + const payment = isRecord(body) && isRecord(body.payment) ? body.payment : undefined + const session = completeUcpCheckoutSession(params.id, payment) + + if (!session) { + return json( + { code: 'not_found_or_closed', content: 'Checkout session not found or already closed' }, + { status: 404 }, + ) + } + + return json(session) +} diff --git a/apps/betterzeriya/src/routes/sessions/[id]/ai/+page.svelte b/apps/betterzeriya/src/routes/sessions/[id]/ai/+page.svelte index fa0e6ba..826f393 100644 --- a/apps/betterzeriya/src/routes/sessions/[id]/ai/+page.svelte +++ b/apps/betterzeriya/src/routes/sessions/[id]/ai/+page.svelte @@ -127,7 +127,7 @@ }, appConfig: { ...prebuiltAppConfig, - useIndexedDBCache: true + cacheBackend: 'indexeddb' } }); engine = next; diff --git a/bun.lock b/bun.lock index 4d23477..8ace529 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "saizeriya", From 1b27c4ffa636ff66c3e157f1f6495219a5c9c05d Mon Sep 17 00:00:00 2001 From: Shotaro Nakamura Date: Tue, 12 May 2026 22:02:56 +0900 Subject: [PATCH 5/5] fix --- a.sh | 40 ++++ .../src/lib/server/official-client.ts | 1 + apps/betterzeriya/src/lib/server/ucp.ts | 200 ++++++++++++++---- .../src/routes/.well-known/ucp/+server.ts | 15 +- .../api/ucp/checkout-sessions/+server.ts | 61 +++++- .../api/ucp/checkout-sessions/[id]/+server.ts | 2 - .../[id]/complete/+server.ts | 20 +- packages/client/src/client.ts | 2 + packages/client/src/process-qr.ts | 2 + 9 files changed, 268 insertions(+), 75 deletions(-) create mode 100644 a.sh diff --git a/a.sh b/a.sh new file mode 100644 index 0000000..f83e225 --- /dev/null +++ b/a.sh @@ -0,0 +1,40 @@ +curl -sS -X POST http://localhost:5173/api/sessions \ + -H 'content-type: application/json' \ + -d '{"qrURLSource":"http://localhost:3000/saizeriya3/qr?table=ba44c06e-2ae3-4c30-9d58-999093265866"}' + +curl -sS -X POST http://localhost:5173/api/ucp/checkout-sessions \ + -H 'content-type: application/json' \ + -d '{"line_items":[{"item":{"id":"1110"},"quantity":2}],"currency":"JPY"}' + +CREATE=$(curl -sS -X POST http://localhost:5173/api/ucp/checkout-sessions \ + -H 'content-type: application/json' \ + -d '{ + "qrURLSource": "http://localhost:3000/saizeriya3/qr?table=ba44c06e-2ae3-4c30-9d58-999093265866", + "peopleCount": 2, + "line_items": [ + { "item": { "id": "1110" }, "quantity": 1 } + ], + "currency": "JPY" + }') + + +curl -sS -X POST "http://localhost:5173/api/ucp/checkout-sessions/chk_ba51b170-c8a7-4722-80c3-1f79f30aa7cd/complete" \ + -H 'content-type: application/json' \ + -d '{}' + + CREATE=$(curl -sS -X POST http://localhost:5173/api/ucp/checkout-sessions \ + -H 'content-type: application/json' \ + -d '{ + "qrURLSource": "http://localhost:3000/saizeriya3/qr?table=ba44c06e-2ae3-4c30-9d58-999093265866", + "peopleCount": 2, + "line_items": [ + { "item": { "id": "1301" }, "quantity": 1 } + ], + "currency": "JPY" + }')curl -sS -X POST "http://localhost:5173/api/ucp/checkout-sessions/$CHECKOUT_ID/complete" \ + -H 'content-type: application/json' \ + -d '{}' + +curl -sS -X POST "http://localhost:5173/api/ucp/checkout-sessions/chk_5b3277dc-937b-4b3c-93de-d5f4c8b5fad8/complete" \ + -H 'content-type: application/json' \ + -d '{}' \ No newline at end of file diff --git a/apps/betterzeriya/src/lib/server/official-client.ts b/apps/betterzeriya/src/lib/server/official-client.ts index 585e1f7..a8d10f0 100644 --- a/apps/betterzeriya/src/lib/server/official-client.ts +++ b/apps/betterzeriya/src/lib/server/official-client.ts @@ -256,6 +256,7 @@ export const submitOfficialCart = async ( snapshot?: OfficialSessionSnapshot, ) => { const session = await createClientFromSnapshot(id, snapshot) + await session.client.goToMenu() while (session.client.getState().cart.length > 0) { await session.client.removeCartItem(0) } diff --git a/apps/betterzeriya/src/lib/server/ucp.ts b/apps/betterzeriya/src/lib/server/ucp.ts index 8659314..8d21381 100644 --- a/apps/betterzeriya/src/lib/server/ucp.ts +++ b/apps/betterzeriya/src/lib/server/ucp.ts @@ -1,9 +1,15 @@ import menuData from '$lib/assets/data/menu.json' +import { + createOfficialSession, + lookupOfficialItem, + setOfficialPeopleCount, + submitOfficialCart, + type OfficialSessionSnapshot, +} from '$lib/server/official-client' export const UCP_VERSION = '2026-04-08' export const UCP_SHOPPING_SERVICE = 'dev.ucp.shopping' export const UCP_CHECKOUT_CAPABILITY = 'dev.ucp.shopping.checkout' -export const UCP_GOOGLE_PAY_HANDLER = 'com.google.pay' type UcpCheckoutStatus = | 'incomplete' @@ -14,7 +20,6 @@ type UcpCheckoutStatus = | 'canceled' type UcpBuyer = Record -type UcpPayment = Record export interface UcpLineItemInput { item: { @@ -30,14 +35,15 @@ interface CreateCheckoutSessionInput { line_items: UcpLineItemInput[] buyer?: UcpBuyer currency?: string - payment?: UcpPayment + qrURLSource?: string + peopleCount?: number + officialSession?: OfficialSessionSnapshot } interface UpdateCheckoutSessionInput { line_items?: UcpLineItemInput[] buyer?: UcpBuyer currency?: string - payment?: UcpPayment } interface UcpTotal { @@ -54,9 +60,9 @@ interface UcpCheckoutSession { line_items: Array totals: UcpTotal[] links: Array<{ type: 'terms_of_service' | 'privacy_policy'; url: string }> - payment: { - handlers: Array<{ id: string; handler_id: string; type: 'google_pay' }> - instruments: unknown[] + official_session?: { + id: string + people_count?: number } messages?: Array> continue_url?: string @@ -78,6 +84,7 @@ type MenuEntry = { const menuByCode = new Map((menuData as MenuEntry[]).map((item) => [item.code, item])) const sessions = new Map() +const officialSessions = new Map() const sessionTtlMs = 1000 * 60 * 60 * 6 export const createUcpMetadata = () => ({ @@ -85,26 +92,6 @@ export const createUcpMetadata = () => ({ capabilities: { [UCP_CHECKOUT_CAPABILITY]: [{ version: UCP_VERSION }], }, - payment_handlers: { - [UCP_GOOGLE_PAY_HANDLER]: [ - { - id: 'betterzeriya_google_pay', - version: UCP_VERSION, - available_instruments: [{ type: 'card' }], - config: { - allowed_payment_methods: [ - { - type: 'CARD', - parameters: { - allowed_auth_methods: ['PAN_ONLY', 'CRYPTOGRAM_3DS'], - allowed_card_networks: ['VISA', 'MASTERCARD', 'AMEX', 'JCB'], - }, - }, - ], - }, - }, - ], - }, }) const pruneSessions = () => { @@ -112,6 +99,7 @@ const pruneSessions = () => { for (const [id, session] of sessions) { if (Date.parse(session.updated_at) + sessionTtlMs < now) { sessions.delete(id) + officialSessions.delete(id) } } } @@ -151,17 +139,6 @@ const calculateTotals = (lineItems: UcpCheckoutSession['line_items']) => { ] } -const createPayment = (): UcpCheckoutSession['payment'] => ({ - handlers: [ - { - id: 'betterzeriya_google_pay', - handler_id: 'betterzeriya_google_pay', - type: 'google_pay', - }, - ], - instruments: [], -}) - const refreshSessionShape = ( session: UcpCheckoutSession, input: UpdateCheckoutSessionInput & { origin?: string }, @@ -177,7 +154,6 @@ const refreshSessionShape = ( buyer: input.buyer ?? session.buyer, line_items: lineItems, totals: calculateTotals(lineItems), - payment: input.payment ? { ...session.payment, instruments: [input.payment] } : session.payment, links: input.origin ? buildLinks(input.origin) : session.links, updated_at: now, } @@ -197,7 +173,6 @@ export const createUcpCheckoutSession = (input: CreateCheckoutSessionInput) => { line_items: lineItems, totals: calculateTotals(lineItems), links: buildLinks(input.origin), - payment: input.payment ? { ...createPayment(), instruments: [input.payment] } : createPayment(), continue_url: `${input.origin}/`, created_at: now.toISOString(), updated_at: now.toISOString(), @@ -222,20 +197,160 @@ export const updateUcpCheckoutSession = (id: string, input: UpdateCheckoutSessio return updated } -export const completeUcpCheckoutSession = (id: string, payment?: UcpPayment) => { +const createOfficialBinding = async ( + checkoutId: string, + input: Pick, +) => { + if (input.officialSession) { + officialSessions.set(checkoutId, input.officialSession) + return { + id: input.officialSession.id, + people_count: input.officialSession.state.peopleCount, + } + } + + if (!input.qrURLSource) { + return undefined + } + + const official = await createOfficialSession(input.qrURLSource) + let officialSession = official.officialSession + let peopleCount = official.state.peopleCount + + if (input.peopleCount && official.state.peopleCount !== input.peopleCount) { + const updated = await setOfficialPeopleCount(official.id, input.peopleCount, officialSession) + officialSession = updated.officialSession + peopleCount = updated.state.peopleCount + } + + officialSessions.set(checkoutId, officialSession) + return { + id: official.id, + people_count: peopleCount, + } +} + +const validateLineItemsWithOfficialSession = async ( + checkoutId: string, + lineItems: UcpLineItemInput[], +) => { + let officialSession = officialSessions.get(checkoutId) + if (!officialSession) { + return lineItems + } + + const officialLineItems: UcpLineItemInput[] = [] + + for (const lineItem of lineItems) { + const lookup = await lookupOfficialItem(officialSession.id, lineItem.item.id, officialSession) + officialSession = lookup.officialSession + officialSessions.set(checkoutId, officialSession) + + if (lookup.result.result !== 'OK' || !lookup.result.item_data) { + throw new Error(`Item ${lineItem.item.id} was not found`) + } + + if (lookup.result.item_data.state === 0) { + throw new Error(`Item ${lineItem.item.id} is sold out`) + } + + officialLineItems.push({ + item: { + id: lookup.result.item_data.id, + title: lookup.result.item_data.name, + price: lookup.result.item_data.price, + }, + quantity: lineItem.quantity, + }) + } + + return officialLineItems +} + +export const createUcpCheckoutSessionWithOfficialSession = async ( + input: CreateCheckoutSessionInput, +) => { + const session = createUcpCheckoutSession(input) + let officialSession + + try { + officialSession = await createOfficialBinding(session.id, input) + input.line_items = await validateLineItemsWithOfficialSession(session.id, input.line_items) + } catch (error) { + sessions.delete(session.id) + officialSessions.delete(session.id) + throw error + } + + if (!officialSession) { + return session + } + + const updated = { + ...refreshSessionShape(session, { line_items: input.line_items }), + official_session: officialSession, + updated_at: new Date().toISOString(), + } + sessions.set(session.id, updated) + return updated +} + +export const completeUcpCheckoutSession = async (id: string) => { const session = getUcpCheckoutSession(id) if (!session || session.status === 'completed' || session.status === 'canceled') { return undefined } + const officialSession = officialSessions.get(id) + if (!officialSession) { + throw new Error('Official session is required to submit a UCP checkout') + } + + const inProgress = { + ...refreshSessionShape(session, {}), + status: 'complete_in_progress' as const, + } + sessions.set(id, inProgress) + + let result + try { + result = await submitOfficialCart( + officialSession.id, + inProgress.line_items.map((lineItem) => ({ + id: lineItem.item.id, + count: lineItem.quantity, + })), + officialSession, + ) + } catch (error) { + sessions.set(id, { + ...inProgress, + status: 'requires_escalation', + messages: [ + { + type: 'error', + content: error instanceof Error ? error.message : 'Failed to submit order', + }, + ], + updated_at: new Date().toISOString(), + }) + throw error + } + officialSessions.set(id, result.officialSession) + const updated = { - ...refreshSessionShape(session, { payment }), + ...inProgress, status: 'completed' as const, + official_session: { + id: result.officialSession.id, + people_count: result.state.peopleCount, + }, order: { id: `ord_${crypto.randomUUID()}`, checkout_id: id, permalink_url: session.continue_url ?? '/', }, } + officialSessions.delete(id) sessions.set(id, updated) return updated } @@ -250,6 +365,7 @@ export const cancelUcpCheckoutSession = (id: string) => { status: 'canceled' as const, updated_at: new Date().toISOString(), } + officialSessions.delete(id) sessions.set(id, updated) return updated } diff --git a/apps/betterzeriya/src/routes/.well-known/ucp/+server.ts b/apps/betterzeriya/src/routes/.well-known/ucp/+server.ts index 3ddbefd..54ea754 100644 --- a/apps/betterzeriya/src/routes/.well-known/ucp/+server.ts +++ b/apps/betterzeriya/src/routes/.well-known/ucp/+server.ts @@ -1,10 +1,5 @@ import { json, type RequestHandler } from '@sveltejs/kit' -import { - UCP_CHECKOUT_CAPABILITY, - UCP_GOOGLE_PAY_HANDLER, - UCP_SHOPPING_SERVICE, - UCP_VERSION, -} from '$lib/server/ucp' +import { UCP_CHECKOUT_CAPABILITY, UCP_SHOPPING_SERVICE, UCP_VERSION } from '$lib/server/ucp' export const GET: RequestHandler = async ({ url }) => { const origin = url.origin @@ -29,14 +24,6 @@ export const GET: RequestHandler = async ({ url }) => { capabilities: { [UCP_CHECKOUT_CAPABILITY]: [{ version: UCP_VERSION }], }, - payment_handlers: { - [UCP_GOOGLE_PAY_HANDLER]: [ - { - id: 'betterzeriya_google_pay', - version: UCP_VERSION, - }, - ], - }, }, }) } diff --git a/apps/betterzeriya/src/routes/api/ucp/checkout-sessions/+server.ts b/apps/betterzeriya/src/routes/api/ucp/checkout-sessions/+server.ts index d10ca11..933465b 100644 --- a/apps/betterzeriya/src/routes/api/ucp/checkout-sessions/+server.ts +++ b/apps/betterzeriya/src/routes/api/ucp/checkout-sessions/+server.ts @@ -1,5 +1,6 @@ import { json, type RequestHandler } from '@sveltejs/kit' -import { createUcpCheckoutSession, type UcpLineItemInput } from '$lib/server/ucp' +import { createUcpCheckoutSessionWithOfficialSession, type UcpLineItemInput } from '$lib/server/ucp' +import { parseOfficialSessionSnapshot } from '$lib/server/official-client' const MAX_LINE_ITEMS = 100 const MAX_ITEM_ID_LENGTH = 256 @@ -9,9 +10,14 @@ const isRecord = (value: unknown): value is Record => typeof value === 'object' && value !== null && !Array.isArray(value) const normalizeBuyer = (value: unknown) => (isRecord(value) ? value : undefined) -const normalizePayment = (value: unknown) => (isRecord(value) ? value : undefined) const normalizeCurrency = (value: unknown) => typeof value === 'string' && /^[A-Z]{3}$/.test(value) ? value : undefined +const normalizePeopleCount = (value: unknown) => { + const peopleCount = Number(value) + return Number.isInteger(peopleCount) && peopleCount >= 1 && peopleCount <= 99 + ? peopleCount + : undefined +} export const _normalizeLineItems = ( value: unknown, @@ -32,9 +38,9 @@ export const _normalizeLineItems = ( } const id = typeof lineItem.item.id === 'string' ? lineItem.item.id.trim() : '' - if (id.length === 0 || id.length > MAX_ITEM_ID_LENGTH) { + if (!/^\d{4}$/.test(id) || id.length > MAX_ITEM_ID_LENGTH) { return { - error: `Each line_items item must include a non-empty id up to ${MAX_ITEM_ID_LENGTH} characters`, + error: 'Each line_items item id must be a 4 digit Saizeriya item code', } } @@ -80,13 +86,46 @@ export const POST: RequestHandler = async ({ request, url }) => { return json({ code: 'invalid_request', content: normalizedLineItems.error }, { status: 400 }) } - const session = createUcpCheckoutSession({ - origin: url.origin, - line_items: normalizedLineItems.lineItems, - buyer: normalizeBuyer(body.buyer), - currency: normalizeCurrency(body.currency), - payment: normalizePayment(body.payment), - }) + const qrURLSource = String(body.qrURLSource ?? body.qr_url_source ?? '').trim() + const officialSession = parseOfficialSessionSnapshot( + body.officialSession ?? body.official_session, + ) + + if (!qrURLSource && !officialSession) { + return json( + { + code: 'invalid_request', + content: 'qrURLSource or officialSession is required to create an orderable checkout', + }, + { status: 400 }, + ) + } + + if (qrURLSource && !URL.canParse(qrURLSource)) { + return json({ code: 'invalid_request', content: 'QR URL is invalid' }, { status: 400 }) + } + + let session + + try { + session = await createUcpCheckoutSessionWithOfficialSession({ + origin: url.origin, + line_items: normalizedLineItems.lineItems, + buyer: normalizeBuyer(body.buyer), + currency: normalizeCurrency(body.currency), + qrURLSource, + peopleCount: normalizePeopleCount(body.peopleCount ?? body.people_count), + officialSession, + }) + } catch (error) { + return json( + { + code: 'official_session_failed', + content: error instanceof Error ? error.message : 'Failed to initialize official session', + }, + { status: 502 }, + ) + } return json(session, { status: 201 }) } diff --git a/apps/betterzeriya/src/routes/api/ucp/checkout-sessions/[id]/+server.ts b/apps/betterzeriya/src/routes/api/ucp/checkout-sessions/[id]/+server.ts index 973553d..1f427f8 100644 --- a/apps/betterzeriya/src/routes/api/ucp/checkout-sessions/[id]/+server.ts +++ b/apps/betterzeriya/src/routes/api/ucp/checkout-sessions/[id]/+server.ts @@ -6,7 +6,6 @@ const isRecord = (value: unknown): value is Record => typeof value === 'object' && value !== null && !Array.isArray(value) const normalizeBuyer = (value: unknown) => (isRecord(value) ? value : undefined) -const normalizePayment = (value: unknown) => (isRecord(value) ? value : undefined) const normalizeCurrency = (value: unknown) => typeof value === 'string' && /^[A-Z]{3}$/.test(value) ? value : undefined @@ -48,7 +47,6 @@ export const PUT: RequestHandler = async ({ params, request }) => { line_items: normalizedLineItems?.lineItems, buyer: normalizeBuyer(body.buyer), currency: normalizeCurrency(body.currency), - payment: normalizePayment(body.payment), }) if (!session) { diff --git a/apps/betterzeriya/src/routes/api/ucp/checkout-sessions/[id]/complete/+server.ts b/apps/betterzeriya/src/routes/api/ucp/checkout-sessions/[id]/complete/+server.ts index 9723511..689565a 100644 --- a/apps/betterzeriya/src/routes/api/ucp/checkout-sessions/[id]/complete/+server.ts +++ b/apps/betterzeriya/src/routes/api/ucp/checkout-sessions/[id]/complete/+server.ts @@ -1,9 +1,6 @@ import { json, type RequestHandler } from '@sveltejs/kit' import { completeUcpCheckoutSession } from '$lib/server/ucp' -const isRecord = (value: unknown): value is Record => - typeof value === 'object' && value !== null && !Array.isArray(value) - export const POST: RequestHandler = async ({ params, request }) => { if (!params.id) { return json( @@ -12,9 +9,20 @@ export const POST: RequestHandler = async ({ params, request }) => { ) } - const body = await request.json().catch(() => ({})) - const payment = isRecord(body) && isRecord(body.payment) ? body.payment : undefined - const session = completeUcpCheckoutSession(params.id, payment) + await request.json().catch(() => ({})) + let session + + try { + session = await completeUcpCheckoutSession(params.id) + } catch (error) { + return json( + { + code: 'submit_failed', + content: error instanceof Error ? error.message : 'Failed to submit order', + }, + { status: 502 }, + ) + } if (!session) { return json( diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index 1294fb4..3d99526 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -53,6 +53,8 @@ export const createClient = async ({ shopId: processedQR!.shopId, tableNo: processedQR!.tableNo, peopleCount: peopleCount ?? processedQR!.peopleCount ?? 0, + token: processedQR!.token, + sessionId: processedQR!.sessionId, pageKind: processedQR!.pageKind, cart: [], } diff --git a/packages/client/src/process-qr.ts b/packages/client/src/process-qr.ts index 5e89356..98d813d 100644 --- a/packages/client/src/process-qr.ts +++ b/packages/client/src/process-qr.ts @@ -18,6 +18,8 @@ export const processQR = async (qrURL: string, fetch: typeof globalThis.fetch) = shopId: parser.getShopId(), tableNo: parser.getTableNo(), peopleCount: parser.getPeopleCount(), + token: parser.getToken(), + sessionId: parser.getSessionId(), pageKind: parser.getPageKind(), } }