Skip to content

feat: create an order from a quote - #421

Merged
korsvanloon merged 4 commits into
mainfrom
feat/order-from-quote
Aug 27, 2026
Merged

feat: create an order from a quote#421
korsvanloon merged 4 commits into
mainfrom
feat/order-from-quote

Conversation

@korsvanloon

Copy link
Copy Markdown
Collaborator

Fixes #412.

Endpoints

Endpoint Draft
POST /{projectKey}/orders/quotes OrderFromQuoteDraft
POST /{projectKey}/me/orders/quotes MyOrderFromQuoteDraft

Both land on a new OrderRepository.createFromQuote; the /me route maps { id, version, quoteStateToAccepted } onto the full draft.

Behaviour

As the issue notes, the guards are the part worth having:

  • The quote must be in Pending state → InvalidOperation (400) otherwise.
  • The quote must not be past its validToInvalidOperation (400) otherwise.
  • The draft version is checked against the stored quote → ConcurrentModification (409) on a stale version.
  • An unknown quote reference → ReferencedResourceNotFound (400), which is what resolving a reference in a draft returns everywhere else in the mock.
  • quoteStateToAccepted: true moves the quote to Accepted and bumps its version; without it the quote is left alone.

The order carries the quote's line items, custom line items, prices and taxed price as-is — nothing is re-priced, which is the point of converting rather than rebuilding a cart — plus quote as a reference and origin: "Quote".

Note on validation

The draft is validated in the repository rather than through a generated zod schema: scripts/generate-schemas.ts needs the commercetools-api-reference checkout, which I do not have here, so OrderFromQuoteDraft is not in src/schemas/generated. Happy to add it to DRAFT_SCHEMAS and regenerate if you'd prefer that.

Coverage

order-from-quote.test.ts: create, quoteStateToAccepted transitioning the quote, the quote left untouched without it, non-pending quote, expired quote, stale version, unknown quote, and the /me variant.

Full suite: 798 passing.

POST /{projectKey}/orders/quotes and POST /{projectKey}/me/orders/quotes had no
route, so the last step of the B2B quote flow could not be exercised at all.
Consumers could unit-test their own orderability guards, but not the conversion
itself, the resulting order, or the quote's transition.

The guards are the interesting part and are modelled: the quote must be Pending
and not past its validTo, both InvalidOperation otherwise, and the draft version
is checked against the stored quote so a stale version raises
ConcurrentModification. quoteStateToAccepted moves the quote to Accepted as part
of creating the order.

The order is built from the quote's own line items and prices rather than
recalculated, since not re-pricing is the point of converting instead of
rebuilding a cart. It references the quote and has origin "Quote".

The draft is validated in the repository rather than through a generated zod
schema; generating one needs the commercetools-api-reference checkout that
scripts/generate-schemas.ts expects.

Fixes #412
@changeset-bot

changeset-bot Bot commented Aug 26, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0c6462b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@labdigital/commercetools-mock Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

The route validated its body by hand, which meant the /orders/quotes draft was
the only create endpoint not checked against a zod schema in strict mode.
OrderFromQuoteDraft and MyOrderFromQuoteDraft are now generated like every other
draft, and both routes validate through them the way AbstractService.post does.

Running the generator also picks up the optional fields the spec gained since it
last ran; that drift is additive and is included here rather than left to
surprise the next person who regenerates.

AGENTS.md now describes the workflow, since hand-rolling the validation was the
avoidable mistake.
@korsvanloon

Copy link
Copy Markdown
Collaborator Author

Addressed the note on validation — you were right that this should go through the generator rather than be hand-rolled.

  • OrderFromQuoteDraft and MyOrderFromQuoteDraft added to DRAFT_SCHEMAS/DRAFT_FILE_MAP and generated with pnpm generate:schemas.
  • Both routes now validate through those schemas in strict mode, the way AbstractService.post does for the standard create route. The hand-written if (!draft?.quote?.id …) check is gone.
  • Two tests for it: a draft without a quote reference, and a /me draft without a version.

Two things worth a look when reviewing:

  1. Spec drift. Running the generator regenerated everything, and the spec has moved since the last run. That drift is in this PR: optional fields on extensions (dependencies, expansionPaths, additionalContext), inventory entries (stockLevels, reservationExpirationInMinutes), shipping methods (stores, carrier) and stores (storefront), plus reservation/variant reference type ids and the ReserveOnCart inventory mode. All additive and optional. Happy to split it into its own PR if you'd rather keep this one narrow.
  2. The generator emits unformatted output, so pnpm generate:schemas on its own produces a diff against every committed file. It needs pnpm biome format --write src/schemas/generated afterwards. I noted that in AGENTS.md along with the rest of the workflow, but folding the format step into the generator script itself would make the diff honest by default.

@korsvanloon
korsvanloon merged commit c131aae into main Aug 27, 2026
10 checks passed
@korsvanloon
korsvanloon deleted the feat/order-from-quote branch August 27, 2026 14:00
@github-actions github-actions Bot mentioned this pull request Aug 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Order from Quote is not implemented (/orders/quotes and /me/orders/quotes)

2 participants