Skip to content

POST /api/v1/orders accepts an unresolvable deal_id, and the reference propagates into a material change request #68

Description

@jaanijuk

Summary

POST /api/v1/orders accepts any string as deal_id and persists the order without resolving the reference against stored deals. The sibling creation endpoint POST /api/v1/quotes does resolve its parent reference and returns a structured 404. A change request against such an order then inherits the unresolvable deal_id and reaches pending_approval at material severity, with validation_errors empty.

Observed — orders accept an unresolvable reference

curl -s -X POST localhost:8001/api/v1/orders \
  -H "Content-Type: application/json" \
  -d '{"deal_id":"does-not-exist-1787208750"}'
{
  "order_id": "ORD-6D0336B37B8A",
  "status": "draft",
  "deal_id": "does-not-exist-1787208750",
  "quote_id": null,
  "created_at": "2026-08-20T06:52:30.485080Z"
}

HTTP 200, persisted. The order survived a server restart, so it is durable rather than a session artefact.

order_service.py:40-50 writes both references straight through:

order_data["deal_id"] = deal_id
order_data["quote_id"] = quote_id

There is no storage lookup for either.

Observed — the sibling endpoint does resolve its reference

curl -s -X POST localhost:8001/api/v1/quotes \
  -H "Content-Type: application/json" \
  -d '{"idempotency_key":"probe-key-0001","product_id":"does-not-exist-xyz","deal_type":"PG","impressions":1000000}'
{"detail":{"error":"product_not_found","message":"Product 'does-not-exist-xyz' not found in catalog."}}

HTTP 404, from quote_service.py:121-127.

Two creation endpoints on the same API, one resolving its parent reference and one not.

Observed — the reference propagates into approval

curl -s -X POST localhost:8001/api/v1/change-requests \
  -H "Content-Type: application/json" \
  -d '{"order_id":"ORD-6D0336B37B8A","change_type":"impressions","reason":"probe"}'
{
  "change_request_id": "CR-14C50C574E82",
  "order_id": "ORD-6D0336B37B8A",
  "deal_id": "does-not-exist-1787208750",
  "status": "pending_approval",
  "severity": "material",
  "validation_errors": []
}

A material change request carrying a deal reference this service cannot resolve, awaiting human approval, with the system reporting no validation errors.

Environment

seller-agent v2.4.2, commit e5b367d2b780aa4d0e03b6363d744ba3adf2b221. Also present on main, which is currently the same commit at time of reporting. Python 3.12.3, WSL2 Ubuntu 24.04.

Inferred

Whether orders are intended to precede deals. draft status, three optional fields and no validation all suggest an order may legitimately be opened before commercial terms are settled. If that is the design, requiring a resolvable deal_id at creation would break it. The narrower observation stands either way: an order opened without terms is a different thing from one carrying a deal reference the service cannot resolve. I have not established whether such a reference might be meaningful in an external commercial system; only that this service does not resolve it and does not surface that.

Reach beyond change requests. order["deal_id"] is also read by gam_reporting_service.py:54-62, where it is used to populate external_order_id when linking to Google Ad Manager. I have not exercised that path and make no claim about its behaviour — only that the unresolved reference is consumed by more than one downstream service.

quote_id is written identically and appears to be equally unvalidated. Not tested.

Relationship to #61

#61 concerns a null deal_id producing an unhandled 500 on the change-request path. This is the non-null but unresolvable case, which succeeds silently rather than crashing. Same non-validation in create_order, different failure mode, and the fix proposed on #61 — normalising None at write side — would not address it.

Suggested remedy

Not prescribed, since it turns on whether orders may precede deals.

If they may not, resolving deal_id at creation and returning a structured 404 for an unknown deal would mirror what /quotes already does for product_id.

If they may, then the reference is optional-but-must-be-real, and validation belongs wherever it is first relied upon — the change-request path being one such place, given the severity classification and approval routing that follow.

Acceptance criteria

  • An order cannot come to carry a deal_id this service cannot resolve, or the condition is detected before a change request against that order is classified and routed for approval.
  • Whichever layer is chosen, a change request that inherits an unresolvable deal reference does not reach pending_approval with validation_errors empty.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions