QuickCart
Dossier ยท Document 06
DOCUMENT 06 ยท ORDERS & DELIVERY

One basket, many platforms, one order

How a shopper adds products sourced from Blinkit, Zepto, BigBasket, Flipkart and Amazon into a single basket โ€” and QuickCart, as merchant of record, creates one internal order, procures each line from the best source, and delivers it as one experience.

Documents 04 and 05 explained how QuickCart finds the best price, stock and ETA for a single product across five platforms. This document explains what happens when a shopper wants several products at once โ€” and those products live on different platforms. The answer is the piece that turns QuickCart from a comparison tool into a marketplace: a cross-platform cart, one internal order, and an orchestrator that procures and delivers everything under a single QuickCart receipt.

1 cart
N source platforms
1 payment
One QuickCart charge
1 delivery
Tracked end-to-end
MoR
Merchant of record

The model: QuickCart is the merchant of record

A price-comparison app stops at "Blinkit is cheapest โ€” tap to open Blinkit." QuickCart goes further: the shopper transacts once, with QuickCart. Behind that single order, QuickCart procures each line item from whichever platform is cheapest and in stock, then delivers the whole basket. The shopper never creates five accounts, pays five times, or tracks five parcels.

Procurement of any given line happens in one of two modes โ€” the same two modes introduced in the Architecture document (ยง9, Payments):

Mode A

Managed checkout

Where a platform exposes a partner commerce API (programmatic cart + checkout), QuickCart places the sub-order automatically via the mcp-adaptor layer. Fast, hands-off, fully tracked. Realistically available on marketplace sources (Amazon/Flipkart via seller/affiliate flows) and on quick-commerce sources only where a partner agreement is in place.

Mode B

Assisted fulfillment

Where a source is handoff-only (no programmatic checkout โ€” see Doc 04), a QuickCart picker/agent buys the item in-store or in-app on the shopper's behalf, or the line is re-sourced to a different platform that can be placed programmatically and is still in stock at a comparable price.

Why "merchant of record" matters Because the shopper's contract is with QuickCart, QuickCart owns the money flow (one authorisation, one capture, per-source settlement), the fulfillment promise (one combined ETA), and the support relationship (one place to raise a refund). Each platform becomes a supplier, not a checkout the user is bounced to.

Building a cross-platform basket

A QuickCart cart is a flat list of lines. Every line references a canonical SKU (the platform-independent product from the Catalogue, Doc 03 ยง6) plus a chosen sourceId. By default the source is the ranked winner from the Price Engine (Doc 05); the shopper can override it ("I'd rather get the butter from Zepto โ€” it's faster"). Because lines carry their own source, one cart naturally spans multiple platforms.

POST /v1/cart โ€” a mixed-source basket
{
  "cartId": "crt_9f2a1c",
  "userId": "usr_512",
  "pincode": "560103",
  "lines": [
    {
      "lineId": "ln_1",
      "canonicalSku": "AMUL-BUTTER-500G",
      "sourceId": "bigbasket",
      "qty": 1,
      "substitutable": true,
      "priceSnapshot": { "mrp": 295, "effectivePrice": 268, "etaMin": 14, "deliveryFee": 0 }
    },
    {
      "lineId": "ln_2",
      "canonicalSku": "MAGGI-MASALA-70G-PACK12",
      "sourceId": "blinkit",
      "qty": 1,
      "substitutable": true,
      "priceSnapshot": { "mrp": 168, "effectivePrice": 152, "etaMin": 9, "deliveryFee": 15 }
    },
    {
      "lineId": "ln_3",
      "canonicalSku": "BOAT-AIRDOPES-311PRO",
      "sourceId": "amazon",
      "qty": 1,
      "substitutable": false,
      "priceSnapshot": { "mrp": 4490, "effectivePrice": 1299, "etaMin": 1440, "deliveryFee": 0 }
    }
  ],
  "reoptimiseAtCheckout": true
}
Cart-line fieldMeaning
canonicalSkuPlatform-independent product id from the Catalogue โ€” the thing the shopper actually wants.
sourceIdChosen platform for this line. Defaults to the Price-Engine winner; user-overridable.
qtyUnits requested.
substitutableMay QuickCart re-source or substitute if this source goes out of stock? Shopper controls it per line.
priceSnapshotPrice/ETA captured when added โ€” advisory. Re-verified at checkout (Doc 05).
reoptimiseAtCheckoutCart-level flag: re-run ranking at checkout so a line auto-moves to a better source if one appeared.
The snapshot is not a promise Prices and stock move minute-to-minute. The snapshot is what the shopper saw; the authoritative price and availability are re-checked at checkout, before any money is captured (see ยง9 and Doc 05, "Caching & freshness").

From cart to one internal order

At checkout, QuickCart authorises a single payment hold for the basket total, creates one Order, and hands it to the Order Orchestrator. The orchestrator splits the order into sub-orders โ€” one per source โ€” and routes each sub-order by what that source can actually do.

flowchart LR CART["๐Ÿงบ Cross-platform cart"] --> CO["Checkout
authorise payment (hold)"] CO --> ORD["Create ONE Order
(merchant of record)"] ORD --> ORCH{{"Order Orchestrator"}} ORCH --> SUB["Split into per-source
sub-orders"] SUB --> DEC{"Source supports
programmatic placement?"} DEC -->|"Yes ยท commerce API"| AUTO["Auto-place sub-order
(Managed checkout)"] DEC -->|"No ยท handoff-only"| ALT{"In stock &
substitutable?"} ALT -->|"Re-source"| AUTO ALT -->|"Keep source"| PICK["Assisted: picker buys item"] AUTO --> CONF["Procurement confirmed"] PICK --> CONF CONF --> CONS["Consolidate items"] CONS --> LM["Last-mile delivery"] LM --> DONE["โœ… Delivered"] DONE --> SET[("Settle to each source")]
Figure 6.1 โ€” One cart becomes one order, which fans out into per-source sub-orders, each routed by the source's capability, then re-consolidated for a single delivery.

Order & sub-order data model

One ORDER owns one PAYMENT and fans out to many SUB_ORDERs โ€” exactly one per source. Each sub-order owns its items and, once fulfilled, a per-source SETTLEMENT. This shape lets QuickCart confirm, cancel, refund and settle a single source without touching the rest of the basket.

erDiagram ORDER ||--|{ SUB_ORDER : "splits into" ORDER ||--|| PAYMENT : "paid by" SUB_ORDER ||--|{ SUB_ORDER_ITEM : contains SUB_ORDER ||--o| SETTLEMENT : "settled by" ORDER { uuid id uuid user_id string status int combined_eta_min int total_authorised int total_captured } SUB_ORDER { uuid id uuid order_id string source_id string mode "managed|assisted" string status int eta_min string external_ref } SUB_ORDER_ITEM { uuid id string canonical_sku int qty int effective_price string state "confirmed|refunded|substituted" } PAYMENT { uuid id string psp_ref int authorised int captured } SETTLEMENT { uuid id string source_id int amount string status }
Figure 6.2 โ€” Order โ†’ sub-orders (one per source) โ†’ items, with a single payment and per-source settlements. Illustrative fields.
Combined ETA The order's combined_eta_min is the max of its sub-order ETAs when the basket is delivered together (pooled). If a slow line (e.g. an Amazon item at 1–2 days) would hold up fast grocery lines, QuickCart offers a split delivery instead โ€” see ยง7.

Orchestration as a saga

The hard part: five sources are independent systems with no shared database and no shared transaction. QuickCart cannot "BEGIN โ€ฆ COMMIT" across Blinkit and Amazon. So the orchestrator runs a saga โ€” a sequence of local steps, each with a compensating action that undoes it if a later step fails.

  1. Authorise, don't capture. Place a hold on the full basket total. Compensation: void the hold.
  2. Re-verify per source. Right before placing, re-check stock & price for each sub-order (a fast line can vanish between "added to cart" and "checkout").
  3. Place each sub-order โ€” managed (commerce API) or assisted (picker task). Compensation: cancel the placed sub-order / cancel the picker task where still possible.
  4. On failure, compensate the failed line only โ€” re-source it to an in-stock alternative if it's substitutable, otherwise mark that line for refund. The rest of the basket proceeds.
  5. Capture the confirmed subtotal. Money actually taken = value of confirmed lines + delivery fee; the held remainder is released.
sequenceDiagram autonumber participant App participant Ord as Order Svc participant Pay as Payment Svc participant Orc as Orchestrator participant SrcA as Source A ยท commerce API participant SrcB as Source B ยท handoff picker App->>Ord: POST /orders (cart, Idempotency-Key) Ord->>Pay: authorise HOLD (basket total) Pay-->>Ord: auth OK Ord->>Orc: orchestrate(order) par Sub-order A Orc->>SrcA: check_stock + create_cart + checkout SrcA-->>Orc: CONFIRMED (ref, eta) and Sub-order B Orc->>SrcB: check_stock SrcB-->>Orc: OUT OF STOCK end Note over Orc: Line on Source B failed โ†’ compensate that line only alt substitutable & alternative in stock Orc->>SrcA: add re-sourced line SrcA-->>Orc: CONFIRMED else no alternative Orc->>Pay: reduce capture (drop that line) end Orc->>Pay: CAPTURE confirmed subtotal + delivery fee Pay-->>Orc: captured Orc-->>Ord: order CONFIRMED / PARTIALLY_CONFIRMED Ord-->>App: 201 order + combined ETA
Figure 6.3 โ€” The saga in action: parallel placement, a per-line compensation when one source is out of stock, then capture of only the confirmed amount.
services/orders/orchestrator.js
// Saga: authorise โ†’ re-verify โ†’ place per source โ†’ compensate on failure โ†’ capture
const { mcp } = require('../mcp-adaptor/client');
const payment = require('../payment/psp');
const pickerQueue = require('../fulfillment/picker-queue');

// Sources that expose a partner commerce API (programmatic placement).
// Quick-commerce sources are assisted-only unless a partner agreement is enabled.
const PLACEABLE = new Set(['amazon', 'flipkart']);

async function orchestrate(order) {
  const saga = new Saga();
  try {
    // 1) hold funds for the whole basket (authorise, do NOT capture yet)
    const auth = await payment.authorise(order.paymentId, order.totalAuthorised);
    saga.add(() => payment.void(auth.id));                 // compensation

    // 2) one sub-order per source
    const subOrders = groupBySource(order.lines);

    // 3) place sub-orders in parallel, isolated from each other
    const settled = await Promise.allSettled(
      subOrders.map((so) => placeSubOrder(so, order, saga))
    );

    const confirmed = settled.filter(r => r.status === 'fulfilled').map(r => r.value);
    const failed    = subOrders.filter((_, i) => settled[i].status === 'rejected');

    // 4) re-source or refund each failed line โ€” never fail the whole basket
    for (const so of failed) {
      const alt = await resourceLines(so.lines, order.pincode);   // find in-stock alternative
      if (alt) confirmed.push(await placeSubOrder(alt, order, saga));
      else     order.markLinesRefunded(so.lines);
    }

    // 5) capture ONLY what was actually confirmed
    const captureAmount = subtotal(confirmed) + deliveryFee(order);
    await payment.capture(auth.id, captureAmount);

    order.status = failed.length ? 'PARTIALLY_CONFIRMED' : 'CONFIRMED';
    await dispatchToLastMile(order, confirmed);            // pooled / direct / split โ€” see ยง7
    return order;
  } catch (err) {
    await saga.compensate();          // void hold + cancel any placed sub-orders
    order.status = 'CANCELLED';
    throw err;
  }
}

async function placeSubOrder(so, order, saga) {
  // stock + price re-check immediately before placing (Doc 05)
  const stock = await mcp.call(so.sourceId, 'check_stock', { items: so.lines, location: order.pincode });
  if (!stock.allAvailable) throw new OutOfStock(so.sourceId, stock.missing);

  if (PLACEABLE.has(so.sourceId)) {
    const cart   = await mcp.call(so.sourceId, 'create_cart', { items: so.lines });
    const placed = await mcp.call(so.sourceId, 'checkout',    { cartId: cart.cartId, address: order.address });
    saga.add(() => mcp.call(so.sourceId, 'cancel_order', { ref: placed.ref }));
    return { sourceId: so.sourceId, mode: 'managed', ref: placed.ref, lines: so.lines, etaMin: placed.etaMin };
  }

  // handoff-only source โ†’ create an assisted picker task
  const task = await pickerQueue.enqueue({ sourceId: so.sourceId, lines: so.lines, pincode: order.pincode });
  saga.add(() => pickerQueue.cancel(task.id));
  return { sourceId: so.sourceId, mode: 'assisted', taskId: task.id, lines: so.lines, etaMin: task.etaMin };
}

class Saga {
  constructor() { this.undos = []; }
  add(fn) { this.undos.push(fn); }
  async compensate() { for (const undo of this.undos.reverse()) { try { await undo(); } catch (_) {} } }
}

module.exports = { orchestrate };
Idempotency Order creation carries an Idempotency-Key (Doc 03 ยง3). A retried submit returns the same order instead of double-charging or double-placing โ€” essential when a flaky mobile network makes the app resend a checkout.

Fulfillment & delivery models

Once sub-orders are confirmed, the items still have to physically reach the shopper. QuickCart picks one of three delivery shapes per order, based on geography, source type and the ETA promise.

Model A

Direct

The source delivers straight to the shopper (its own rider), and QuickCart just relays live tracking. Best when a single source covers most of the basket, or for marketplace parcels (Amazon/Flipkart) on their own logistics.

Model B

Pooled / consolidated

A QuickCart rider does a multi-pickup run across the sources' dark stores, optionally via a consolidation hub, then makes one drop. Best for several quick-commerce lines going to the same address within minutes of each other.

Model C

Split

Items arrive in separate deliveries when combining would make everything slower โ€” e.g. groceries now, the Amazon gadget tomorrow. The shopper sees each parcel tracked under the same order.

flowchart LR R(["๐Ÿ›ต QuickCart rider"]) --> P1["Pickup 1
BigBasket dark store"] P1 --> P2["Pickup 2
Blinkit store"] P2 --> HUB["Consolidation hub
(optional)"] HUB --> CUST["๐Ÿ  Customer
single drop"]
Figure 6.4 โ€” Pooled delivery: one rider, multi-pickup across sources, one consolidated drop to the shopper.
How the model is chosen The dispatcher scores batchable sub-orders by pickup proximity and ETA spread. If all confirmed lines are within a tight radius and time window โ†’ pooled. If one line is far slower โ†’ offer split. If one source dominates and delivers itself โ†’ direct.

Order lifecycle state machine

The order moves through a single state machine; each sub-order runs its own parallel mini-lifecycle (PENDING โ†’ PLACED โ†’ PICKED โ†’ HANDED_OVER, or โ€ฆ โ†’ FAILED โ†’ RESOURCED). The order aggregates them: it is CONFIRMED only when every line is sourced, and PARTIALLY_CONFIRMED when some lines were dropped and refunded.

stateDiagram-v2 [*] --> CREATED CREATED --> PAYMENT_AUTHORISED: hold placed CREATED --> PAYMENT_FAILED: auth declined PAYMENT_AUTHORISED --> ORCHESTRATING ORCHESTRATING --> CONFIRMED: all lines sourced ORCHESTRATING --> RESOURCING: a line failed ORCHESTRATING --> PARTIALLY_CONFIRMED: some lines dropped RESOURCING --> CONFIRMED RESOURCING --> PARTIALLY_CONFIRMED CONFIRMED --> PICKING PARTIALLY_CONFIRMED --> PICKING PICKING --> CONSOLIDATING CONSOLIDATING --> OUT_FOR_DELIVERY OUT_FOR_DELIVERY --> DELIVERED DELIVERED --> [*] PARTIALLY_CONFIRMED --> PARTIALLY_REFUNDED: refund dropped lines CREATED --> CANCELLED PAYMENT_AUTHORISED --> CANCELLED CANCELLED --> REFUNDED PAYMENT_FAILED --> [*]
Figure 6.5 โ€” Order lifecycle. Payment is held on entry and captured only after orchestration settles which lines are real.

Money flow

One shopper-facing charge; many supplier payouts. The hold-then-capture pattern is what makes partial baskets safe: QuickCart never captures money for a line it couldn't actually source.

EventWhat happens to money
CheckoutAuthorise (hold) the full basket total on the shopper's UPI/card via the PSP. No funds move yet.
All lines confirmedCapture the full total. Order โ†’ CONFIRMED.
Some lines droppedCapture only the confirmed subtotal + delivery fee; the held remainder is released automatically. Order โ†’ PARTIALLY_CONFIRMED.
Per-source procurementQuickCart pays each platform through its own supplier account; recorded as a SETTLEMENT row per sub-order for reconciliation.
Cancel before pickupVoid the hold (if not captured) or issue a full refund (if captured). Cancel placed sub-orders where the source allows it.
Single-line refundPartial refund of just that line's effectivePrice; other lines and the delivery fee are unaffected.
Delivery feeOne fee per order (not per source). Pooled deliveries absorb multi-pickup cost; QuickCart's margin sits between supplier price and shopper price + fee.
Capture only after re-verification Funds are captured only after stock and price are re-verified at checkout (Doc 05). If a confirmed line's price moved up beyond a tolerance, the shopper is re-prompted before capture โ€” never silently charged more than shown.

Edge cases we handle

Out of stock at fulfillment

Re-source the line to an in-stock alternative (if substitutable), otherwise drop & refund just that line. Basket proceeds.

Partial availability

Shopper chooses the policy up front: all-or-nothing (cancel if anything is missing) or best-effort (ship what's available).

One source is slow

Offer a split delivery so fast grocery lines aren't held hostage by a 1–2 day marketplace parcel.

Price moved up at checkout

Re-confirm with the shopper before capture; re-rank the line in case another source is now cheaper.

Handoff-only, no picker coverage

If the pincode has no assisted-fulfillment coverage, re-source the line; if impossible, disallow that source for the address before checkout.

Duplicate submit

Idempotency-Key returns the same order โ€” no double charge, no double placement.

Full cancellation

Run saga compensation: void/refund payment and cancel every placed sub-order or picker task where the source permits.

Single-line refund post-delivery

Damaged/wrong item โ†’ refund that line only, reconcile the dispute against the specific source's settlement.

Source outage mid-order

Circuit breaker (Doc 05) trips the source; substitutable lines re-source, others refund โ€” order still completes.

Related documents Doc 04 defines which sources support programmatic placement (managed checkout) versus handoff-only (assisted fulfillment). Doc 05 defines how each line's source is chosen and re-verified. Doc 03 holds the Payment service, order data stores and the services referenced here.