fix(rest/python): validate created fulfillment with the create request model - #234
Open
vishkaty wants to merge 1 commit into
Open
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Observed
Creating a checkout with a fulfillment block returns HTTP 422 for a body that
omits three members:
source/schemas/shopping/types/fulfillment_method.jsonat 2026-04-08, theversion this server advertises, annotates those as
create: "omit",create: "optional"andoptionalrespectively, so a platform that composesthe 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:
ucp-sdkshipsFulfillmentCreateRequestandFulfillmentMethodCreateRequest, and the latter has noidmember at all.Neither is currently imported here.
identifier itself.
services/checkout_service.pymints amethod 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
UnifiedCheckoutCreateRequestwas left pointing at the response container, andthe line directly above it already uses
LineItemCreateRequest.Change
One import, one annotation.
Testing
Against the reference server, run as the workflow runs it:
The supplied postal address now survives into the response rather than being
reduced to a bare identifier.
pytestis 245 passed before and after.ruff checkandruff format --checkare clean. Reverting only this change returnsthe 422, so the fix is load bearing.
Class swept, and one case deliberately left out
I checked every request class in
models.pyfor the same shape:UnifiedCheckoutCreateRequest.fulfillmentUnifiedCheckoutUpdateRequest.fulfillmentUnifiedCheckoutCreateRequest.line_items*.discounts(four sites)The update path declares the response container too, and the equivalent swap to
FulfillmentUpdateRequestis not a safe two line change. The schema makestypeoptional on update, so with the correct model an update that omits itreaches
checkout_service.pywhere the responseFulfillmentMethodisconstructed with
type=Noneand raises, turning a 422 into a 500. That needs adecision about what an update omitting
typeshould mean, most likely carryingthe 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.pyandleaves this line unchanged, so there is no overlap in intent, though it is
currently conflicting against main.