From 4bc402efd6f2f63e05afbd4ecbcf5d79061b3f6e Mon Sep 17 00:00:00 2001 From: Ken Tominaga Date: Sat, 10 Oct 2026 01:24:29 -0500 Subject: [PATCH] fix(nodejs): return ready_for_complete once Complete Checkout would accept the session The checkout handler set status to incomplete at create and never changed it until Complete Checkout set completed, so a platform that waits for ready_for_complete before placing the order never placed one. Create and Update now report ready_for_complete when every fulfillment method has a selected destination and every group a selected option, and incomplete otherwise. That is the condition Complete Checkout already enforced; it moves into one predicate that both paths share. --- rest/nodejs/src/api/checkout.ts | 48 ++++++++++++++++++++++-------- rest/nodejs/test/lifecycle.test.ts | 9 ++++++ 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/rest/nodejs/src/api/checkout.ts b/rest/nodejs/src/api/checkout.ts index 553140a..da774cd 100644 --- a/rest/nodejs/src/api/checkout.ts +++ b/rest/nodejs/src/api/checkout.ts @@ -595,6 +595,37 @@ export class CheckoutService { } } + /** + * Whether every fulfillment method has a selected destination and every + * group a selected option. Complete Checkout refuses a session without it. + */ + private hasFulfillmentSelection(checkout: ExtendedCheckoutResponse): boolean { + // Require at least one method: an empty methods array must not satisfy + // the gate via [].every(...) === true. + const methods = checkout.fulfillment?.methods; + return ( + Array.isArray(methods) && + methods.length > 0 && + methods.every( + (method) => + Boolean(method.selected_destination_id) && + method.groups?.every((group) => group.selected_option_id) === true + ) + ); + } + + /** + * The status of an open session: ready_for_complete once Complete Checkout + * would accept it, incomplete until then. + */ + private openStatus( + checkout: ExtendedCheckoutResponse + ): ExtendedCheckoutResponse["status"] { + return this.hasFulfillmentSelection(checkout) + ? CheckoutResponseStatusSchema.enum.ready_for_complete + : CheckoutResponseStatusSchema.enum.incomplete; + } + createCheckout = async (c: Context) => { const idempotencyKey = c.req.header("Idempotency-Key"); const ucpAgent = c.req.header("UCP-Agent"); @@ -728,6 +759,7 @@ export class CheckoutService { // Calculate totals and validate inventory this.recalculateTotals(checkout); this.validateInventory(checkout); + checkout.status = this.openStatus(checkout); saveCheckout(checkout.id, checkout.status, checkout); @@ -879,6 +911,7 @@ export class CheckoutService { try { this.recalculateTotals(existing); this.validateInventory(existing); + existing.status = this.openStatus(existing); saveCheckout(id, existing.status, existing); @@ -936,19 +969,8 @@ export class CheckoutService { ); } - // Validate Fulfillment is complete. Require at least one method: an empty - // methods array must not satisfy the gate via [].every(...) === true. - const methods = checkout.fulfillment?.methods; - const hasFulfillment = - Array.isArray(methods) && - methods.length > 0 && - methods.every( - (method) => - method.selected_destination_id && - method.groups?.every((group) => group.selected_option_id) - ); - - if (!hasFulfillment) { + // Validate Fulfillment is complete. + if (!this.hasFulfillmentSelection(checkout)) { return ucpErrorResponse( c, new InvalidRequestError( diff --git a/rest/nodejs/test/lifecycle.test.ts b/rest/nodejs/test/lifecycle.test.ts index b4c25cc..7ac3916 100644 --- a/rest/nodejs/test/lifecycle.test.ts +++ b/rest/nodejs/test/lifecycle.test.ts @@ -203,6 +203,15 @@ test("create returns 201 with an id and an incomplete status", async () => { assert.equal(body.status, "incomplete"); }); +test("selecting a destination and an option moves the checkout to ready_for_complete", async () => { + const app = buildApp(); + const id = await createReadyToComplete(app); + const res = await app.request(`/checkout-sessions/${id}`); + assert.equal(res.status, 200); + const body = (await res.json()) as { status: string }; + assert.equal(body.status, "ready_for_complete"); +}); + test("a created checkout is retrievable by id", async () => { const app = buildApp(); const created = (await (await create(app)).json()) as { id: string };