Skip to content

fix(rest/python): validate created fulfillment with the create request model - #234

Open
vishkaty wants to merge 1 commit into
Universal-Commerce-Protocol:mainfrom
vishkaty:fix/fulfillment-create-request-model
Open

fix(rest/python): validate created fulfillment with the create request model#234
vishkaty wants to merge 1 commit into
Universal-Commerce-Protocol:mainfrom
vishkaty:fix/fulfillment-create-request-model

Conversation

@vishkaty

Copy link
Copy Markdown
Contributor

Observed

Creating a checkout with a fulfillment block returns HTTP 422 for a body that
omits three members:

POST /checkout-sessions
{"line_items":[{"item":{"id":"bouquet_roses"},"quantity":1}],
 "fulfillment":{"methods":[{"type":"shipping","destinations":[
   {"street_address":"1 Test Way","address_locality":"Springfield",
    "address_region":"CA","address_country":"US","postal_code":"99999"}]}]}}

422
  at fulfillment.methods[0].id
  at fulfillment.methods[0].line_item_ids
  at fulfillment.methods[0].destinations[0].ShippingDestination.id

source/schemas/shopping/types/fulfillment_method.json at 2026-04-08, the
version this server advertises, annotates those as create: "omit",
create: "optional" and optional respectively, so a platform that composes
the advertised schemas does not send them.

Why this is the request model and not a policy choice

Rather than resting on the annotation alone, three artifacts in this project
already treat the create shape as the correct one:

  1. The pinned ucp-sdk ships FulfillmentCreateRequest and
    FulfillmentMethodCreateRequest, and the latter has no id member at all.
    Neither is currently imported here.
  2. The Node server in this repository accepts the create shape and mints the
    identifier itself.
  3. This server already expects it. services/checkout_service.py mints a
    method id when the request omits one, and a comment in the same function
    states that the create path expects the create request model.

The business logic was written for the create model. Only the annotation on
UnifiedCheckoutCreateRequest was left pointing at the response container, and
the line directly above it already uses LineItemCreateRequest.

Change

One import, one annotation.

Testing

Against the reference server, run as the workflow runs it:

case before after
create body omitting the three members 422 201
create body carrying them 201 201
update path 200 200

The supplied postal address now survives into the response rather than being
reduced to a bare identifier. pytest is 245 passed before and after. ruff check and ruff format --check are clean. Reverting only this change returns
the 422, so the fix is load bearing.

Class swept, and one case deliberately left out

I checked every request class in models.py for the same shape:

class and field verdict
UnifiedCheckoutCreateRequest.fulfillment fixed here
UnifiedCheckoutUpdateRequest.fulfillment same shape, not fixed here, see below
UnifiedCheckoutCreateRequest.line_items already correct
*.discounts (four sites) no create or update variant exists in the pinned SDK, and the model requires nothing, so there is no requiredness mismatch to fix

The update path declares the response container too, and the equivalent swap to
FulfillmentUpdateRequest is not a safe two line change. The schema makes
type optional on update, so with the correct model an update that omits it
reaches checkout_service.py where the response FulfillmentMethod is
constructed with type=None and raises, turning a 422 into a 500. That needs a
decision about what an update omitting type should mean, most likely carrying
the existing method type forward, which is a behavioural change rather than a
type swap. I did not want to smuggle that in here. Happy to open it separately,
or to add it to this PR if you would prefer them together.

Note on #232

#232 is the only other open PR touching this file. It edits models.py and
leaves this line unchanged, so there is no overlap in intent, though it is
currently conflicting against main.

…t model

`UnifiedCheckoutCreateRequest` declared `fulfillment` with `Fulfillment`, the
response container, so creating a checkout required three members that the
schema this server advertises marks as not supplied on create:

  fulfillment.methods[0].id                 ucp_request create: omit
  fulfillment.methods[0].line_item_ids      ucp_request create: optional
  fulfillment.methods[0].destinations[0].id ucp_request optional

A platform that composes the advertised schemas and sends a create body without
them gets HTTP 422. Switching the annotation to `FulfillmentCreateRequest`,
which the pinned ucp-sdk already ships, matches the line directly above it that
already uses `LineItemCreateRequest`.

The service layer was already written for this shape: checkout_service mints a
method id when the request omits one, and a comment in the same function says
the create path expects the create request model. The Node server in this repo
also accepts the create shape and mints the id itself.

Testing. Against the reference server, a create body carrying none of the three
members goes from 422 to 201, and the supplied postal address now survives into
the response instead of being reduced to a bare id. A body that does carry them
still returns 201, so no existing client breaks. The update path is unchanged
and still returns 200. pytest is 245 passed before and after. ruff check and
ruff format are clean.
@damaz91 damaz91 added the status:needs-triage Signal that the PR is ready for human triage label Sep 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status:needs-triage Signal that the PR is ready for human triage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants