Wine2You-Order-API-Taxonomy

Wine2You — Order API Taxonomy

Version: 2.0 · Owner: Wine2You Platform · Status: model update (supersedes v1.0) Scope: how an order is captured, priced across a selected offer, fulfilled (estate-first or indirect), documented, settled and reported. Companion documents: PIM Product API Taxonomy, Partner Onboarding.

One sentence. An order is the API representation of the flash sell-buy-sell: at the instant a consumer buys, Wine2You purchases the wine from the offer that was selected (at that originator's seller_price) and sells it to the consumer (at the destination landed price) — atomically. Which offer was selected, and who fulfils it, is now an explicit part of the order.


1. Overview & design principles

Principle Meaning for the API
Two sales in one instant Platform is buyer from the selected offer's originator and seller to the consumer, simultaneously.
Offer-aware buy leg The buy leg is no longer "the estate" by assumption — it is whichever offer won routing (§ estate-first, then other inventories).
Estate-first routing Prefer estate stock when the estate is on W2Y; otherwise route to other enabled inventories (indirect sourcing, gated by feed-curation permissions).
Roles drive who acts Originator (supply) · Fulfiller (pick/pack/ship) · Seller (own orders). One order may involve several partners in different roles.
Split by source & by role An order may split across stock sources (each = a fulfilment leg) and across fulfillers (whoever each source's stock belongs to).
Event-driven Downstream services subscribe to order.placed, order.confirmed, stock.reserved, … rather than poll.
Atomic or not at all Buy and sell both succeed or neither — a saga with compensation.
Idempotency Every write takes Idempotency-Key; a retried create returns the existing order.
One endpoint, not N Every partner (any role) hits one order surface; the platform fans out internally.

2. Resource map

Resource Verb endpoints (REST) Key fields Notes
Quote (pre-order) POST /v1/orders/quote-calculate destination, lines[], → candidate offers, financial_breakdown, settlement_split Stateless pricing preview; returns offer candidates + routing.
Order POST /v1/orders, GET /v1/orders/{id}, GET /v1/orders?partner=&status=, POST /v1/orders/{id}/cancel see §3 Core object.
Order line embedded sku, wine_id, offer_id, qty, p2b (P_B2B from offer), unit_price_net, tax_breakdown offer-bound.
Routing / sourcing GET /v1/orders/{id}/sourcing priority, source[], split_reason Which offers/stock won, and why.
Fulfilment leg GET /v1/orders/{id}/fulfillments location_id, fulfiller_id, items[], carrier, tracking[] One per source/location.
Document GET /v1/orders/{id}/documents, GET /v1/documents/{id} type, language, url Generated set (§7).
Pickup POST /v1/orders/{id}/pickup, GET /v1/orders/{id}/pickup date, time_window, carrier_ref, location_id Per source location.
Settlement GET /v1/orders/{id}/settlement lines[], per-partner payouts, state, payout_date Payouts to originator(s) + fulfiller(s) + seller.
Compliance record GET /v1/orders/{id}/compliance delivery-register fields Per-destination filing.
Inventory snapshot GET /v1/inventory/network-snapshot node + stock + duty context Read side at capture.

3. The order object taxonomy

Field Type Notes
order_id string opaque
status enum see §4
channel enum marketplace | webshop_connect | pos_qr
consumer object {consumer_id, name, email, address, country}
seller object {partner_id, roles[], legal_name}
platform_entity object merchant of record
lines[] array {sku, wine_id, offer_id, qty, box_units, p2b, unit_price_net, tax_breakdown}
sourcing object {priority, source[], split_reason}
fulfillments[] array one per source/location, each with fulfiller_id
destination_country ISO drives VAT + excise + documents
currency ISO
financial_breakdown object §8
settlement_split object §8
documents[] array §7
compliance object §9
created_at / updated_at timestamp

4. Order lifecycle state machine

States (enum):

draft (cart) → placed → paid → confirmed → sourced → reserved
   → fulfilled → in_transit → delivered → settled
Terminal / branch: cancelled, failed, refunded, returned

Transitions:

From To Trigger
draft placed consumer submits
placed paid payment captured
paid confirmed flash buy+sell committed (saga success)
confirmed sourced routing resolved which offers/stock supply each line
sourced reserved stock reserved at each source (ReservationItem)
reserved fulfilled each fulfilment leg picked/packed/shipped
fulfilled in_transit carrier scan
in_transit delivered proof of receipt (all legs)
delivered settled payouts released (shipment + X days)
any (pre-fulfilment) cancelled consumer/partner cancels
paid failed saga compensation
post-delivered returned / refunded within complaint window

sourced and reserved are new — they make multi-provider routing explicit rather than assumed.


5. Sourcing & routing logic (estate-first, then other inventories)

Routing runs at quote-calculate (preview) and confirmed → sourced (commit). Per line:

  1. Estate direct (priority 1). If the wine's estate is an Originator on W2Y and its offer is active with sufficient atp → select estate offer. (Estate sales are preferred.)
  2. Other inventories (priority 2). Otherwise (or if estate ATP is short) select another active offer whose originator permits the transaction: permissions (allowed countries / named sellers / default any) + network_shareable + destination eligibility must pass. This is indirect sourcing, gated by feed-curation.
  3. Split. If no single offer covers the quantity, allocate across offers (cheapest/compliant first, or explicit policy) → each allocation becomes its own sourcing.source[] entry and its own fulfilment leg.

sourcing object:

{ "priority": "estate_first",
  "split_reason": "estate_atp_short",      // or "multi_originator_policy"
  "source": [
    { "offer_id": "off_estate_1", "originator_id": "partner_estate_x",
      "qty": 4, "p2b": 18.00, "incoterm": "FCA", "fulfiller_id": "partner_estate_x" },
    { "offer_id": "off_importer_1", "originator_id": "partner_importer_ch",
      "qty": 2, "p2b": 20.50, "incoterm": "DAP", "fulfiller_id": "partner_3pl_ch" } ] }

Each source's p2b = that offer's seller_price → the line's P_B2B in the waterfall. A line split across offers carries one p2b per source.


6. Fulfilment roles & split legs


7. Pricing & financial taxonomy

The landed-price waterfall (P_final, per destination) — P_B2B now comes from the selected offer:

P_final = P_B2B + R_Recruiter + M_Curator + F_Platform + C_Freight + E_Excise + V_VAT
Component Meaning Source
P_B2B originator's seller_price — willing-to-sell, pre tax/duty/platform/fee/shipping the winning offer (§5)
R_Recruiter recruiter/agent royalty, on every channel partner config
M_Curator Seller's own margin — shelf price over offer (0 on an originator's own shop) listing
F_Platform Wine2You transaction/service fee platform
C_Freight carrier freight by destination + box size (6/12) + incoterm quote
E_Excise destination excise (category × volume × ABV) tax engine
V_VAT destination VAT on the taxable sum tax engine

financial_breakdown (what the consumer pays):

{ "currency": "NOK",
  "lines": [
    { "component": "P_B2B", "amount": 18.00, "offer_id": "off_estate_1" },
    { "component": "M_Curator", "amount": 4.20 },
    { "component": "F_Platform", "amount": 1.50 },
    { "component": "C_Freight", "amount": 6.00 },
    { "component": "E_Excise", "amount": 12.40 },
    { "component": "V_VAT", "amount": 9.48 } ],
  "total": 51.58 }

settlement_split (who gets what — now per partner, since sources/sellers/fulfillers differ):

{ "currency": "EUR",
  "parties": [
    { "partner_id": "partner_estate_x", "role": "originator", "amount": 72.00, "basis": "p2b × 4" },
    { "partner_id": "partner_importer_ch", "role": "originator", "amount": 41.00, "basis": "p2b × 2" },
    { "partner_id": "partner_curator_ch", "role": "seller", "amount": 25.20, "basis": "M_Curator" },
    { "partner_id": "partner_3pl_ch", "role": "fulfiller", "amount": 6.00, "basis": "fulfilment fee" } ],
  "platform_fee": 1.50,
  "escrowed_taxes": { "excise": 12.40, "vat": 9.48 } }

Currency & rounding: consumer charged in destination currency; internal ledger in EUR. Round half-up to the destination's minor unit; tax components rounded per line, summed (never rounded twice).


8. Documents taxonomy

Document Issued by Received by Language In parcel?
Consumer invoice Platform (Seller as merchant) Consumer destination yes
Transport / accompanying document Platform Customs / carrier destination yes
Carrier label (per leg) Platform Carrier — affixed per parcel
Originator proforma Platform Originator(s) of each source — no
Originator final invoice Originator Platform — no; matches per-source leg
Fulfiller instruction Platform Fulfiller(s) — no

All generated from the same order data; a split order emits per-source originator docs and per-leg labels.


9. Compliance & reporting taxonomy

Delivery-register record (per delivery): date · consignee · delivery_address · product_description · excise_category_code · commodity_code (ch.22) · units_delivered · unit_capacity_litres · abv.


10. Settlement & payout taxonomy


11. Webhooks & events taxonomy (proposed)

Event Fires when
order.placed / order.paid consumer submits / payment captured
order.confirmed flash buy+sell committed
order.sourced routing resolved offers for each line
stock.reserved reservation created per source
order.fulfilled / order.picked_up / order.delivered per-leg fulfilment events
order.cancelled / order.failed cancel / saga compensation
order.settled payouts released
document.generated document set ready
settlement.released payout executed

At-least-once, signed, Idempotency-Key = event id; exponential backoff + dead-letter queue.


12. Proposed core endpoints

Method Path Purpose
POST /v1/orders/quote-calculate candidate offers + routing + financial_breakdown + settlement_split
POST /v1/orders create order (flash buy+sell saga, multi-source)
GET /v1/orders/{id} order detail incl. sourcing, legs, split, documents
GET /v1/orders/{id}/sourcing which offers/stock won and why
GET /v1/orders/{id}/fulfillments per-source fulfilment legs
GET /v1/orders list (filter by partner/role, status, date)
POST /v1/orders/{id}/cancel cancel (pre-fulfilment)
POST /v1/orders/{id}/pickup request carrier pickup (per location)
GET /v1/orders/{id}/documents document set
GET /v1/orders/{id}/settlement per-party payouts + state
GET /v1/inventory/network-snapshot node capability + coords + stock + duty context

13. Worked example — estate-first, then split for the short 2 bottles

Quote request:

POST /v1/orders/quote-calculate
{ "destination_country": "NO",
  "seller": "partner_curator_ch",
  "lines": [{ "sku": "CAN-BAR-2019-750", "qty": 6, "box_units": 6 }] }

Quote response (routing preview):

{ "currency": "NOK",
  "sourcing": {
    "priority": "estate_first", "split_reason": "estate_atp_short",
    "source": [
      { "offer_id": "off_estate_1", "originator_id": "partner_estate_x", "qty": 4, "p2b": 18.00 },
      { "offer_id": "off_importer_1", "originator_id": "partner_importer_ch", "qty": 2, "p2b": 20.50 } ] },
  "financial_breakdown": { "lines": [
      { "component": "P_B2B", "amount": 18.00, "offer_id": "off_estate_1" },
      { "component": "M_Curator", "amount": 4.20 },
      { "component": "F_Platform", "amount": 1.50 },
      { "component": "C_Freight", "amount": 6.00 },
      { "component": "E_Excise", "amount": 12.40 },
      { "component": "V_VAT", "amount": 9.48 } ],
    "total": 51.58 },
  "settlement_split": { "parties": [
      { "partner_id": "partner_estate_x", "role": "originator", "amount": 72.00 },
      { "partner_id": "partner_importer_ch", "role": "originator", "amount": 41.00 },
      { "partner_id": "partner_curator_ch", "role": "seller", "amount": 25.20 } ],
    "platform_fee": 1.50 } }

Lifecycle (illustrative):

t State Event
09:00 placed → paid → confirmed flash buy+sell committed atomically
09:00 sourced estate fills 4, importer fills 2 (estate-first, then other inventory)
09:01 reserved 2 × ReservationItem (estate cellar + CH 3PL)
09:01 fulfilled two legs picked/packed/shipped by their fulfillers
+3 d in_transit → delivered both legs delivered; Vipps age-check pre-release
+10 d settled estate paid 72.00, importer 41.00, seller 25.20, 3PL fee released

14. Best-practice notes