Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 35 additions & 13 deletions rest/nodejs/src/api/checkout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -879,6 +911,7 @@ export class CheckoutService {
try {
this.recalculateTotals(existing);
this.validateInventory(existing);
existing.status = this.openStatus(existing);

saveCheckout(id, existing.status, existing);

Expand Down Expand Up @@ -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(
Expand Down
9 changes: 9 additions & 0 deletions rest/nodejs/test/lifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down
Loading