Coverage for atlas_ecommerce/purchases/shopify.py: 100%
30 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-25 16:36 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-25 16:36 +0000
1from datetime import UTC, datetime
3from pydantic import ValidationError
5from atlas_ecommerce.attribution import OrderAttribution, from_note_attributes
6from atlas_ecommerce.models import OrderDb, ShopifyPurchaseOrder
7from atlas_ecommerce.purchases import PurchaseEvent
9PROVIDER = "shopify"
10PAID_TOPIC = "orders/paid"
11PAID_STATUSES = frozenset({"paid", "partially_refunded"})
14def unreportable(order: OrderDb, problem: str) -> PurchaseEvent:
15 return PurchaseEvent(
16 provider=PROVIDER,
17 order_id=str(order.shopify_order_id),
18 happened_at=datetime.now(UTC),
19 attribution=OrderAttribution(),
20 paid=False,
21 problem=problem,
22 source_ref=order,
23 )
26def to_purchase(order: OrderDb) -> PurchaseEvent:
27 if not order.raw:
28 return unreportable(order, "order has no payload")
29 try:
30 shopify_order = ShopifyPurchaseOrder.model_validate(order.raw)
31 except ValidationError as exc:
32 return unreportable(order, f"order payload unparseable: {exc.error_count()} validation error(s)")
33 if shopify_order.happened_at is None:
34 return unreportable(order, "order carries no timestamp")
35 return PurchaseEvent(
36 provider=PROVIDER,
37 order_id=str(shopify_order.id),
38 happened_at=shopify_order.happened_at,
39 attribution=from_note_attributes((a.name, a.value) for a in shopify_order.note_attributes),
40 value=shopify_order.total_price,
41 currency=shopify_order.currency,
42 quantity=shopify_order.quantity,
43 variant_id=shopify_order.first_variant_id,
44 order_number=shopify_order.order_number,
45 paid=shopify_order.financial_status in PAID_STATUSES,
46 source_ref=order,
47 )
50class ShopifyPurchases:
51 provider = PROVIDER
53 async def unplanned(self, limit: int) -> list[PurchaseEvent]:
54 # Deliberately not the provisioning topic setting: a purchase is an orders/paid delivery and
55 # nothing else. Under any other subscription the queue stays empty instead of stamping
56 # pending orders as planned.
57 orders: list[OrderDb] = (
58 await OrderDb.find({"conversions_enqueued_at": None, "topic": PAID_TOPIC}, projection_model=OrderDb)
59 .limit(limit)
60 .to_list()
61 )
62 return [to_purchase(order) for order in orders]
64 async def mark_planned(self, purchase: PurchaseEvent) -> None:
65 # Field-level: the provisioning worker may hold the same document and save its own snapshot.
66 await purchase.source_ref.set({"conversions_enqueued_at": datetime.now(UTC)})
68 async def load(self, order_id: str) -> PurchaseEvent | None:
69 order = await OrderDb.find_one({"shopify_order_id": int(order_id)})
70 return to_purchase(order) if order is not None else None