Case file
Run w-20260927-071446-0549
Run replay · the recorded S1–S9 artifact of this run- Upstream PR
- https://github.com/kshiti26-11/demo/compare/15f95af008e5...main
- Companion PR
- https://github.com/Kshiti26-11/Demo/pull/7
- Target branch
- syncsnitch/w-20260927-071446-0549
- Opened
- 2026-09-27T07:41:05+00:00
- Bob sessions export
- .syncsnitch/runs/w-20260927-071446-0549/bob/
How this case was solved
S1–S9 execution pipeline of this runS1
Dust for prints
Detect contract drift
Deterministic
S2
Follow the trail
Trace consumer usages
Deterministic
S3
The Tracer
Schema Diff & AST Tracer
AI subagent
S4
The Transformer
Downstream Code Transformer
AI subagent
S5
The lab
Verify in mock containers
Docker lab
S6
The Verifier
Contract Verifier
AI subagent
S7
The warrant
Human approval
Human
S8
File the case
Open draft companion PR
Deterministic
S9
Close the file
Write run artifact
Deterministic
The suspects
Contract changes found by S1 detect (26 changes)| Surface | Location | Kind | Old Token | Verdict | Note |
|---|---|---|---|---|---|
| rest | Money |
schema_added | - |
Cleared · safe | |
| rest | Customer |
schema_added | - |
Cleared · safe | |
| rest | OrderStatus.PENDING |
enum_value_removed | PENDING |
Guilty · breaking | |
| rest | OrderStatus.AWAITING_PAYMENT |
enum_value_added | - |
Cleared · safe | consumers with exhaustive status handling may break |
| rest | Order.customer_name |
property_removed | customer_name |
Guilty · breaking | |
| rest | Order.total_price |
property_removed | total_price |
Guilty · breaking | |
| rest | Order.customer |
property_added | - |
Cleared · safe | required property added |
| rest | Order.total |
property_added | - |
Cleared · safe | required property added |
| rest | Order.shipping_eta |
property_added | - |
Cleared · safe | optional property added |
| grpc | orders.Customer |
message_added | - |
Cleared · safe | |
| grpc | orders.Money |
message_added | - |
Cleared · safe | |
| grpc | orders.OrderStatus.1 |
enum_value_renamed | ORDER_STATUS_PENDING |
Guilty · breaking | wire-compatible, source-breaking |
| grpc | orders.OrderSummary.2 |
field_removed | customer_name |
Guilty · breaking | |
| grpc | orders.OrderSummary.3 |
field_removed | total_price |
Guilty · breaking | |
| grpc | orders.OrderSummary.5 |
field_added | - |
Cleared · safe | |
| grpc | orders.OrderSummary.6 |
field_added | - |
Cleared · safe | |
| grpc | orders.OrderSummary.7 |
field_added | - |
Cleared · safe | |
| db | orders.total_minor |
column_added | - |
Cleared · safe | |
| db | orders.currency |
column_added | - |
Cleared · safe | |
| db | orders.customer_id |
column_added | - |
Cleared · safe | |
| db | orders.shipping_eta |
column_added | - |
Cleared · safe | |
| db | orders.customer_name |
column_renamed | customer_name |
Guilty · breaking | |
| db | 0002_money_customer_status.py:39 |
data_update | - |
Cleared · safe | |
| db | 0002_money_customer_status.py:40 |
data_update | - |
Cleared · safe | |
| db | orders.status.PENDING |
value_renamed | PENDING |
Guilty · breaking | |
| db | orders.total_price |
column_dropped | total_price |
Guilty · breaking |
The evidence board
Downstream ripple & endpoint impact map · change → consumer file:line → endpoint that breaks
graph LR
N0["billing/models/order.py"]
N1["customer_name · DB/REST/gRPC"]
N2["POST /invoices/{order_id} (LOUD)"]
N3["total_price · DB/REST/gRPC"]
N4["billing/reports/revenue.sql"]
N5["GET /reports/revenue (LOUD)"]
N6["billing/services/invoice.py"]
N7["PENDING · DB/REST"]
N8["billing/services/payments.py"]
N9["GET /payments/{order_id}/status (SILENT)"]
N10["contracts/upstream/orders.proto"]
N11["ORDER_STATUS_PENDING · gRPC"]
N0 --> N2
N1 --> N0
N1 --> N10
N1 --> N6
N11 --> N10
N3 --> N0
N3 --> N10
N3 --> N4
N3 --> N6
N3 --> N8
N4 --> N5
N6 --> N2
N7 --> N6
N8 --> N9
Forensics lab
V1–V6 deterministic verification · the model never grades itself
V1
consumer unit tests9 passed, 1 skipped, 1 warning in 0.45s
Pass
V2
v2 fixtures match the new contract3 v2 fixtures match schema
Pass
V3
consumer vs upstream v1 (backward compatible)5/5 passed
Pass
V4
consumer vs upstream v2 (new contract)5/5 passed
Pass
V5
Prism contract examplestest_rest_contract_examples_parse passed against v1 and v2
Pass
V6
diff scope16 files changed within scope
Pass
The fix on file
Consumer patch diff · the tolerant-reader adapter written by the Transformer (S4)diff --git a/.syncsnitch.json b/.syncsnitch.jsonnew file mode 100644index 0000000..74ecdbc--- /dev/null+++ b/.syncsnitch.json@@ -0,0 +1,7 @@+{+ "upstream_repo": "kshiti26-11/demo",+ "upstream_pr": null,+ "upstream_base": "15f95af008e5060450fa81ff0a58ed50627b1b2e",+ "upstream_head": "61f6af0fb96b0241d60fd011ee4a2ece10439a1f",+ "run_id": "w-20260927-071446-0549"+}diff --git a/billing/adapters/__init__.py b/billing/adapters/__init__.pynew file mode 100644index 0000000..e69de29diff --git a/billing/adapters/orders_contract.py b/billing/adapters/orders_contract.pynew file mode 100644index 0000000..b0b9530--- /dev/null+++ b/billing/adapters/orders_contract.py@@ -0,0 +1,82 @@+from dataclasses import dataclass+from decimal import Decimal, ROUND_HALF_UP+from typing import Any+++def _round_half_up(d: Decimal) -> int:+ return int(d.quantize(Decimal("1"), rounding=ROUND_HALF_UP))+++def normalize_status(value: str) -> str:+ if value.startswith("ORDER_STATUS_"):+ value = value[len("ORDER_STATUS_"):]+ if value == "PENDING":+ return "AWAITING_PAYMENT"+ return value+++@dataclass(frozen=True)+class OrderView:+ order_id: str+ customer_name: str+ amount_minor: int+ currency: str+ status: str+ created_at: Any = None+++def from_rest(payload: dict) -> OrderView:+ order_id = payload["order_id"]+ status = normalize_status(payload.get("status", ""))+ created_at = payload.get("created_at")++ customer = payload.get("customer")+ if isinstance(customer, dict) and "display_name" in customer:+ customer_name = customer["display_name"]+ else:+ customer_name = payload.get("customer_name", "")++ total = payload.get("total")+ if isinstance(total, dict) and "amount_minor" in total:+ amount_minor = int(total["amount_minor"])+ currency = total.get("currency", "USD")+ else:+ total_price = payload.get("total_price", 0.0)+ amount_minor = _round_half_up(Decimal(str(total_price)) * 100)+ currency = "USD"++ return OrderView(+ order_id=order_id,+ customer_name=customer_name,+ amount_minor=amount_minor,+ currency=currency,+ status=status,+ created_at=created_at,+ )+++def from_summary(summary) -> OrderView:+ order_id = summary.order_id+ field = type(summary).DESCRIPTOR.fields_by_name["status"]+ status_raw = field.enum_type.values_by_number[summary.status].name+ status = normalize_status(status_raw)++ if summary.HasField("customer"):+ customer_name = summary.customer.display_name+ else:+ customer_name = summary.customer_name++ if summary.HasField("total"):+ amount_minor = summary.total.amount_minor+ currency = summary.total.currency or "USD"+ else:+ amount_minor = _round_half_up(Decimal(str(summary.total_price)) * 100)+ currency = "USD"++ return OrderView(+ order_id=order_id,+ customer_name=customer_name,+ amount_minor=amount_minor,+ currency=currency,+ status=status,+ )diff --git a/billing/clients/gen/orders_pb2.py b/billing/clients/gen/orders_pb2.pyindex 23b66a7..a8faa08 100644--- a/billing/clients/gen/orders_pb2.py+++ b/billing/clients/gen/orders_pb2.py@@ -24,19 +24,27 @@ _sym_db = _symbol_database.Default() -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0corders.proto\x12\x06orders\"*\n\x16GetOrderSummaryRequest\x12\x10\n\x08order_id\x18\x01 \x01(\t\"q\n\x0cOrderSummary\x12\x10\n\x08order_id\x18\x01 \x01(\t\x12\x15\n\rcustomer_name\x18\x02 \x01(\t\x12\x13\n\x0btotal_price\x18\x03 \x01(\x01\x12#\n\x06status\x18\x04 \x01(\x0e\x32\x13.orders.OrderStatus*\x92\x01\n\x0bOrderStatus\x12\x1c\n\x18ORDER_STATUS_UNSPECIFIED\x10\x00\x12\x18\n\x14ORDER_STATUS_PENDING\x10\x01\x12\x15\n\x11ORDER_STATUS_PAID\x10\x02\x12\x18\n\x14ORDER_STATUS_SHIPPED\x10\x03\x12\x1a\n\x16ORDER_STATUS_CANCELLED\x10\x04\x32V\n\x0bOrderLookup\x12G\n\x0fGetOrderSummary\x12\x1e.orders.GetOrderSummaryRequest\x1a\x14.orders.OrderSummaryb\x06proto3')+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0corders.proto\x12\x06orders\"*\n\x16GetOrderSummaryRequest\x12\x10\n\x08order_id\x18\x01 \x01(\t\"/\n\x05Money\x12\x14\n\x0c\x61mount_minor\x18\x01 \x01(\x03\x12\x10\n\x08\x63urrency\x18\x02 \x01(\t\"5\n\x08\x43ustomer\x12\x13\n\x0b\x63ustomer_id\x18\x01 \x01(\t\x12\x14\n\x0c\x64isplay_name\x18\x02 \x01(\t\"\xd1\x01\n\x0cOrderSummary\x12\x10\n\x08order_id\x18\x01 \x01(\t\x12\x19\n\rcustomer_name\x18\x02 \x01(\tB\x02\x18\x01\x12\x17\n\x0btotal_price\x18\x03 \x01(\x01\x42\x02\x18\x01\x12#\n\x06status\x18\x04 \x01(\x0e\x32\x13.orders.OrderStatus\x12\"\n\x08\x63ustomer\x18\x05 \x01(\x0b\x32\x10.orders.Customer\x12\x1c\n\x05total\x18\x06 \x01(\x0b\x32\r.orders.Money\x12\x14\n\x0cshipping_eta\x18\x07 \x01(\t*\x9b\x01\n\x0bOrderStatus\x12\x1c\n\x18ORDER_STATUS_UNSPECIFIED\x10\x00\x12!\n\x1dORDER_STATUS_AWAITING_PAYMENT\x10\x01\x12\x15\n\x11ORDER_STATUS_PAID\x10\x02\x12\x18\n\x14ORDER_STATUS_SHIPPED\x10\x03\x12\x1a\n\x16ORDER_STATUS_CANCELLED\x10\x04\x32V\n\x0bOrderLookup\x12G\n\x0fGetOrderSummary\x12\x1e.orders.GetOrderSummaryRequest\x1a\x14.orders.OrderSummaryb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'orders_pb2', _globals) if not _descriptor._USE_C_DESCRIPTORS: DESCRIPTOR._loaded_options = None- _globals['_ORDERSTATUS']._serialized_start=184- _globals['_ORDERSTATUS']._serialized_end=330+ _globals['_ORDERSUMMARY'].fields_by_name['customer_name']._loaded_options = None+ _globals['_ORDERSUMMARY'].fields_by_name['customer_name']._serialized_options = b'\030\001'+ _globals['_ORDERSUMMARY'].fields_by_name['total_price']._loaded_options = None+ _globals['_ORDERSUMMARY'].fields_by_name['total_price']._serialized_options = b'\030\001'+ _globals['_ORDERSTATUS']._serialized_start=385+ _globals['_ORDERSTATUS']._serialized_end=540 _globals['_GETORDERSUMMARYREQUEST']._serialized_start=24 _globals['_GETORDERSUMMARYREQUEST']._serialized_end=66- _globals['_ORDERSUMMARY']._serialized_start=68- _globals['_ORDERSUMMARY']._serialized_end=181- _globals['_ORDERLOOKUP']._serialized_start=332- _globals['_ORDERLOOKUP']._serialized_end=418+ _globals['_MONEY']._serialized_start=68+ _globals['_MONEY']._serialized_end=115+ _globals['_CUSTOMER']._serialized_start=117+ _globals['_CUSTOMER']._serialized_end=170+ _globals['_ORDERSUMMARY']._serialized_start=173+ _globals['_ORDERSUMMARY']._serialized_end=382+ _globals['_ORDERLOOKUP']._serialized_start=542+ _globals['_ORDERLOOKUP']._serialized_end=628 # @@protoc_insertion_point(module_scope)diff --git a/billing/clients/gen/orders_pb2.pyi b/billing/clients/gen/orders_pb2.pyiindex ff3c6a0..d7b8dbe 100644--- a/billing/clients/gen/orders_pb2.pyi+++ b/billing/clients/gen/orders_pb2.pyi@@ -1,6 +1,7 @@ from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper from google.protobuf import descriptor as _descriptor from google.protobuf import message as _message+from collections.abc import Mapping as _Mapping from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union DESCRIPTOR: _descriptor.FileDescriptor@@ -8,12 +9,12 @@ DESCRIPTOR: _descriptor.FileDescriptor class OrderStatus(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): __slots__ = () ORDER_STATUS_UNSPECIFIED: _ClassVar[OrderStatus]- ORDER_STATUS_PENDING: _ClassVar[OrderStatus]+ ORDER_STATUS_AWAITING_PAYMENT: _ClassVar[OrderStatus] ORDER_STATUS_PAID: _ClassVar[OrderStatus] ORDER_STATUS_SHIPPED: _ClassVar[OrderStatus] ORDER_STATUS_CANCELLED: _ClassVar[OrderStatus] ORDER_STATUS_UNSPECIFIED: OrderStatus-ORDER_STATUS_PENDING: OrderStatus+ORDER_STATUS_AWAITING_PAYMENT: OrderStatus ORDER_STATUS_PAID: OrderStatus ORDER_STATUS_SHIPPED: OrderStatus ORDER_STATUS_CANCELLED: OrderStatus@@ -24,14 +25,36 @@ class GetOrderSummaryRequest(_message.Message): order_id: str def __init__(self, order_id: _Optional[str] = ...) -> None: ... +class Money(_message.Message):+ __slots__ = ("amount_minor", "currency")+ AMOUNT_MINOR_FIELD_NUMBER: _ClassVar[int]+ CURRENCY_FIELD_NUMBER: _ClassVar[int]+ amount_minor: int+ currency: str+ def __init__(self, amount_minor: _Optional[int] = ..., currency: _Optional[str] = ...) -> None: ...++class Customer(_message.Message):+ __slots__ = ("customer_id", "display_name")+ CUSTOMER_ID_FIELD_NUMBER: _ClassVar[int]+ DISPLAY_NAME_FIELD_NUMBER: _ClassVar[int]+ customer_id: str+ display_name: str+ def __init__(self, customer_id: _Optional[str] = ..., display_name: _Optional[str] = ...) -> None: ...+ class OrderSummary(_message.Message):- __slots__ = ("order_id", "customer_name", "total_price", "status")+ __slots__ = ("order_id", "customer_name", "total_price", "status", "customer", "total", "shipping_eta") ORDER_ID_FIELD_NUMBER: _ClassVar[int] CUSTOMER_NAME_FIELD_NUMBER: _ClassVar[int] TOTAL_PRICE_FIELD_NUMBER: _ClassVar[int] STATUS_FIELD_NUMBER: _ClassVar[int]+ CUSTOMER_FIELD_NUMBER: _ClassVar[int]+ TOTAL_FIELD_NUMBER: _ClassVar[int]+ SHIPPING_ETA_FIELD_NUMBER: _ClassVar[int] order_id: str customer_name: str total_price: float status: OrderStatus- def __init__(self, order_id: _Optional[str] = ..., customer_name: _Optional[str] = ..., total_price: _Optional[float] = ..., status: _Optional[_Union[OrderStatus, str]] = ...) -> None: ...+ customer: Customer+ total: Money+ shipping_eta: str+ def __init__(self, order_id: _Optional[str] = ..., customer_name: _Optional[str] = ..., total_price: _Optional[float] = ..., status: _Optional[_Union[OrderStatus, str]] = ..., customer: _Optional[_Union[Customer, _Mapping]] = ..., total: _Optional[_Union[Money, _Mapping]] = ..., shipping_eta: _Optional[str] = ...) -> None: ...diff --git a/billing/contract_entrypoints.py b/billing/contract_entrypoints.pyindex 2822e7c..4e071de 100644--- a/billing/contract_entrypoints.py+++ b/billing/contract_entrypoints.py@@ -1,6 +1,6 @@-from .models.order import OrderDTO+from .adapters.orders_contract import from_rest from .services.invoice import build_invoice def invoice_from_order_payload(payload: dict) -> dict | None:- return build_invoice(OrderDTO.model_validate(payload))+ return build_invoice(from_rest(payload))diff --git a/billing/reports/revenue.py b/billing/reports/revenue.pyindex dc50557..7af71ab 100644--- a/billing/reports/revenue.py+++ b/billing/reports/revenue.py@@ -4,9 +4,6 @@ from pathlib import Path from sqlalchemy import create_engine, text -_SQL = (Path(__file__).parent / "revenue.sql").read_text()-- def _round_half_up(d: Decimal) -> int: return int(d.quantize(Decimal("1"), rounding=ROUND_HALF_UP)) @@ -15,14 +12,25 @@ def run_revenue_report(db_url: str) -> list[dict]: engine = create_engine(db_url) try: with engine.connect() as conn:- rows = conn.execute(text(_SQL)).fetchall()+ version = "0001"+ try:+ row = conn.execute(text("SELECT version_num FROM alembic_version")).fetchone()+ if row:+ version = row[0]+ except Exception:+ pass++ sql_file = "revenue_v2.sql" if version >= "0002" else "revenue_v1.sql"+ sql = (Path(__file__).parent / sql_file).read_text()+ rows = conn.execute(text(sql)).fetchall() finally: engine.dispose() + is_v2 = version >= "0002" return [ { "day": str(row.day)[:10],- "revenue_minor": _round_half_up(Decimal(str(row.revenue)) * 100),+ "revenue_minor": int(row.revenue) if is_v2 else _round_half_up(Decimal(str(row.revenue)) * 100), } for row in rows ]diff --git a/billing/reports/revenue_v1.sql b/billing/reports/revenue_v1.sqlnew file mode 100644index 0000000..f6d0b41--- /dev/null+++ b/billing/reports/revenue_v1.sql@@ -0,0 +1,5 @@+SELECT date(created_at) AS day, SUM(total_price) AS revenue+FROM orders+WHERE status IN ('PAID', 'SHIPPED')+GROUP BY day+ORDER BY daydiff --git a/billing/reports/revenue_v2.sql b/billing/reports/revenue_v2.sqlnew file mode 100644index 0000000..e682d44--- /dev/null+++ b/billing/reports/revenue_v2.sql@@ -0,0 +1,5 @@+SELECT date(created_at) AS day, SUM(total_minor) AS revenue+FROM orders+WHERE status IN ('PAID', 'SHIPPED')+GROUP BY day+ORDER BY daydiff --git a/billing/services/invoice.py b/billing/services/invoice.pyindex 61a0c87..38cf420 100644--- a/billing/services/invoice.py+++ b/billing/services/invoice.py@@ -1,19 +1,20 @@ from decimal import Decimal, ROUND_HALF_UP+from ..adapters.orders_contract import OrderView TAX_RATE = Decimal("0.0825")-NOT_PAYABLE = {"PENDING", "CANCELLED"}+NOT_PAYABLE = {"AWAITING_PAYMENT", "CANCELLED"} def _round_half_up(d: Decimal) -> int: return int(d.quantize(Decimal("1"), rounding=ROUND_HALF_UP)) -def build_invoice(order) -> dict | None:+def build_invoice(order: OrderView) -> dict | None: if order.status in NOT_PAYABLE: return None - subtotal = _round_half_up(Decimal(str(order.total_price)) * 100)- tax = _round_half_up(Decimal(str(order.total_price)) * 100 * TAX_RATE)+ subtotal = order.amount_minor+ tax = _round_half_up(Decimal(subtotal) * TAX_RATE) total = subtotal + tax return {@@ -22,5 +23,5 @@ def build_invoice(order) -> dict | None: "subtotal_minor": subtotal, "tax_minor": tax, "total_minor": total,- "currency": "USD",+ "currency": order.currency, }diff --git a/billing/services/payments.py b/billing/services/payments.pyindex f04306f..a2bce21 100644--- a/billing/services/payments.py+++ b/billing/services/payments.py@@ -1,21 +1,14 @@-from decimal import Decimal, ROUND_HALF_UP+from ..adapters.orders_contract import from_summary, OrderView -PAID_STATES = {"ORDER_STATUS_PAID", "ORDER_STATUS_SHIPPED"}---def _round_half_up(d: Decimal) -> int:- return int(d.quantize(Decimal("1"), rounding=ROUND_HALF_UP))+PAID_STATES = {"PAID", "SHIPPED"} def status_from_summary(summary) -> dict:- field = type(summary).DESCRIPTOR.fields_by_name["status"]- status_name = field.enum_type.values_by_number[summary.status].name-- amount_minor = _round_half_up(Decimal(str(summary.total_price)) * 100)+ order = summary if isinstance(summary, OrderView) else from_summary(summary) return {- "order_id": summary.order_id,- "paid": status_name in PAID_STATES,- "amount_minor": amount_minor,- "currency": "USD",+ "order_id": order.order_id,+ "paid": order.status in PAID_STATES,+ "amount_minor": order.amount_minor,+ "currency": order.currency, }diff --git a/contracts/upstream/orders.proto b/contracts/upstream/orders.protoindex cf6836e..eab1ec7 100644--- a/contracts/upstream/orders.proto+++ b/contracts/upstream/orders.proto@@ -1,4 +1,6 @@-// Orders gRPC contract v1 - owned by orders-service (upstream).+// Orders gRPC contract v2 - owned by orders-service (upstream).+// v2 = money in minor units, structured customer, PENDING renamed to+// AWAITING_PAYMENT (same wire number), optional shipping_eta. // Reference copy for SyncSnitch - do not edit. syntax = "proto3"; @@ -15,15 +17,28 @@ message GetOrderSummaryRequest { enum OrderStatus { ORDER_STATUS_UNSPECIFIED = 0;- ORDER_STATUS_PENDING = 1;+ ORDER_STATUS_AWAITING_PAYMENT = 1; ORDER_STATUS_PAID = 2; ORDER_STATUS_SHIPPED = 3; ORDER_STATUS_CANCELLED = 4; } +message Money {+ int64 amount_minor = 1; // minor units, e.g. 1999 = 19.99 USD+ string currency = 2; // ISO 4217, e.g. "USD"+}++message Customer {+ string customer_id = 1;+ string display_name = 2;+}+ message OrderSummary { string order_id = 1;- string customer_name = 2;- double total_price = 3; // major units (USD), e.g. 19.99+ string customer_name = 2 [deprecated = true];+ double total_price = 3 [deprecated = true]; OrderStatus status = 4;+ Customer customer = 5;+ Money total = 6;+ string shipping_eta = 7; // RFC 3339 timestamp, empty when unknown }diff --git a/tests/fixtures/order_v2_paid.json b/tests/fixtures/order_v2_paid.jsonnew file mode 100644index 0000000..7f2562a--- /dev/null+++ b/tests/fixtures/order_v2_paid.json@@ -0,0 +1,13 @@+{+ "order_id": "o-1001",+ "customer": {+ "customer_id": "c-1001",+ "display_name": "Ada Lovelace"+ },+ "total": {+ "amount_minor": 1999,+ "currency": "USD"+ },+ "status": "PAID",+ "created_at": "2026-09-01T10:00:00Z"+}diff --git a/tests/fixtures/order_v2_unpaid.json b/tests/fixtures/order_v2_unpaid.jsonnew file mode 100644index 0000000..7f6a9ed--- /dev/null+++ b/tests/fixtures/order_v2_unpaid.json@@ -0,0 +1,13 @@+{+ "order_id": "o-1002",+ "customer": {+ "customer_id": "c-1002",+ "display_name": "Alan Turing"+ },+ "total": {+ "amount_minor": 500,+ "currency": "USD"+ },+ "status": "AWAITING_PAYMENT",+ "created_at": "2026-09-02T11:00:00Z"+}diff --git a/tests/unit/test_api.py b/tests/unit/test_api.pyindex 6850fae..8117e39 100644--- a/tests/unit/test_api.py+++ b/tests/unit/test_api.py@@ -13,9 +13,6 @@ def _load(name: str) -> dict: return json.loads((FIXTURES / name).read_text()) -PAID = _load("order_paid.json")-UNPAID = _load("order_unpaid.json")- EXPECTED_INVOICE = { "order_id": "o-1001", "customer": "Ada Lovelace",@@ -26,47 +23,37 @@ EXPECTED_INVOICE = { } -class MockTransport(httpx.MockTransport if hasattr(httpx, "MockTransport") else object):- pass---def _make_mock_transport():- """Build an httpx transport that returns fixtures or 404."""+@pytest.mark.parametrize("paid_fixture,unpaid_fixture", [+ ("order_paid.json", "order_unpaid.json"),+ ("order_v2_paid.json", "order_v2_unpaid.json"),+])+def test_invoice_endpoints(paid_fixture, unpaid_fixture):+ paid = _load(paid_fixture)+ unpaid = _load(unpaid_fixture) def handler(request: httpx.Request) -> httpx.Response: path = request.url.path if path == "/orders/o-1001":- return httpx.Response(200, json=PAID)+ return httpx.Response(200, json=paid) elif path == "/orders/o-1002":- return httpx.Response(200, json=UNPAID)+ return httpx.Response(200, json=unpaid) else: return httpx.Response(404, json={"detail": "order not found"}) - return httpx.MockTransport(handler)---@pytest.fixture-def client():- transport = _make_mock_transport()+ transport = httpx.MockTransport(handler) app = create_app( orders_rest_url="http://test", orders_rest_transport=transport, ) from fastapi.testclient import TestClient- return TestClient(app)-+ client = TestClient(app) -def test_invoice_paid_order(client): r = client.post("/invoices/o-1001") assert r.status_code == 201 assert r.json() == EXPECTED_INVOICE --def test_invoice_unpaid_order(client): r = client.post("/invoices/o-1002") assert r.status_code == 409 --def test_invoice_unknown_order(client): r = client.post("/invoices/o-9999") assert r.status_code == 404diff --git a/tests/unit/test_invoice.py b/tests/unit/test_invoice.pyindex 89e9649..f206169 100644--- a/tests/unit/test_invoice.py+++ b/tests/unit/test_invoice.py@@ -12,9 +12,6 @@ def _load(name: str) -> dict: return json.loads((FIXTURES / name).read_text()) -PAID = _load("order_paid.json")-UNPAID = _load("order_unpaid.json")- EXPECTED = { "order_id": "o-1001", "customer": "Ada Lovelace",@@ -25,14 +22,16 @@ EXPECTED = { } -def test_paid_fixture_invoice():- assert invoice_from_order_payload(PAID) == EXPECTED+@pytest.mark.parametrize("fixture_name", ["order_paid.json", "order_v2_paid.json"])+def test_paid_fixture_invoice(fixture_name):+ assert invoice_from_order_payload(_load(fixture_name)) == EXPECTED -def test_unpaid_fixture_returns_none():- assert invoice_from_order_payload(UNPAID) is None+@pytest.mark.parametrize("fixture_name", ["order_unpaid.json", "order_v2_unpaid.json"])+def test_unpaid_fixture_returns_none(fixture_name):+ assert invoice_from_order_payload(_load(fixture_name)) is None def test_cancelled_returns_none():- payload = dict(PAID, status="CANCELLED")+ payload = dict(_load("order_paid.json"), status="CANCELLED") assert invoice_from_order_payload(payload) is None