Order Module — Vendor surface
Vendor-facing HTTP surface for sub-order fulfillment (assign shipping provider, mark fulfilled, mark delivered, cancel, correct the delivery address), returns processing (approve / reject / pickup / receive /…
Vendor-facing HTTP surface for sub-order fulfillment (assign shipping provider, mark fulfilled, mark delivered, cancel), returns processing (approve / reject / pickup / receive / QC), and payouts (balance, ledger, and payout history). A parent order row can have one or more order_vendor sub-orders; the vendor surface only ever sees its own sub-orders and the customer's shipping address — never other vendors' slices, billing address, or the parent's payment internals.
Source:
api-modules/order/src/controllers/vendor-orders.controller.ts,api-modules/order/src/controllers/vendor-returns.controller.ts,api-modules/order/src/controllers/vendor-payouts.controller.ts.
Conventions
Authentication
All endpoints require a Better-Auth bearer session with an active vendor.
Authorization: Bearer <session-token>The vendor is resolved via resolveActiveVendorId(session) — prefers session.session.activeOrganizationId (better-auth canonical) and falls back to activeVendorId. Sessions missing an active vendor are rejected with 403 Forbidden (FORBIDDEN).
Tenant scoping
:id on order endpoints is order_vendor.id (a sub-order). :id on return endpoints is return_request.id. :id on payout endpoints is vendor_payout.id. Ids that belong to a different vendor return 404 Not Found, not 403 — no leak of foreign ids.
Response envelope
{
"data": <payload>,
"message": "Success",
"statusCode": 200,
"metadata": { /* optional, e.g. pagination */ }
}Error envelope
statusCode | errorCode examples |
|---|---|
| 400 | BAD_REQUEST, VALIDATION_ERROR |
| 401 | UNAUTHORIZED |
| 403 | FORBIDDEN |
| 404 | NOT_FOUND |
| 409 | CONFLICT, INVALID_TRANSITION, SUB_ORDER_NOT_CANCELLABLE |
| 500 | INTERNAL_SERVER_ERROR, DATABASE_ERROR |
Money + dates
All monetary fields (subtotals, totals, refund amounts, ledger amounts, etc.) are integer subunits (paise / cents / eurocents — single currency per tenant). Date fields are ISO datetimes.
Audit trail
Every status mutation writes one or more rows to order_events in the same transaction as the change. Each row carries actor_type (user / vendor / admin / system / webhook), actor_id, source, structured changes, and optional metadata. The most recent rows ship inline on detail responses (events[]).
Domain types
Sub-order lifecycle — OrderVendorStatus
type OrderVendorStatus = "pending" | "fulfilled" | "delivered" | "cancelled";| Status | Set by | Reversible | Notes |
|---|---|---|---|
pending | created at place-order | yes | awaiting vendor to assign a shipping provider |
fulfilled | POST /vendor/orders/:id/fulfilled (or bulk-fulfill) | partial | provider/method/tracking persisted; cancel still allowed (courier rejected, RTO) |
delivered | POST /vendor/orders/:id/delivered (or courier webhook) | no | stamps delivered_at. COD stays payment_status=pending until admin mark-paid (cash collected) |
cancelled | POST /vendor/orders/:id/cancel, parent cancel cascade | no | releases this vendor's reservation, and restocks it if the bag was still pending and already paid for; if all siblings cancelled, parent auto-cancels |
VendorOrderResponse
Vendor-scoped sub-order shape. Strips other vendors' sub-orders and the customer's billing address — vendors only see their slice + shipping address.
type TaxComponent = { type: string; rate: number; amount: number };
type VendorOrderResponse = {
id: string; // order_vendor.id
orderId: string;
orderNumber: string;
customer: { id: string; name: string; email: string } | null; // The customer account that placed the order; null for legacy/guest orders.
parentStatus: "pending_payment" | "confirmed" | "fulfilled" | "delivered" | "cancelled" | "refunded";
fulfillmentStatus: OrderVendorStatus;
// What this seller may do to the order, decided server-side — the panel
// renders from these rather than re-deriving the rule.
canEditShippingAddress: boolean; // sole vendor on the order AND nothing booked
canAdvanceFulfillment: boolean; // not store-processed, AND (per-vendor grouping OR sole vendor)
subtotal: number; // subunits
discountAllocated: number;
shippingCost: number;
taxAmount: number;
total: number;
shippingProviderId: string | null; // null while pending
shippingMethod: string | null;
trackingCode: string | null;
awbNumber: string | null;
taxBreakdown: TaxComponent[]; // aggregated across lines + shipping
shippingNetAmount: number | null;
shippingTaxBreakdown: TaxComponent[];
shippingAddress: {
firstName: string; lastName: string;
fullAddress: string;
city: string; pincode: string; state: string;
phone: string; country: string;
};
customerNote: string | null; // parent order's shopper instruction; null when none
lines: OrderLineResponse[];
events: OrderEventResponse[];
fulfilledAt: string | null; // ISO
deliveredAt: string | null;
cancelledAt: string | null;
cancellationReason: string | null;
placedAt: string;
};OrderLineResponse
type OrderLineResponse = {
id: string;
vendorId: string;
variantId: string | null;
productId: string | null;
productSlug: string | null; // CURRENT store slug, resolved live for `/product/{slug}` links; null if not store-visible
sku: string;
productNameAtOrder: string;
variantNameAtOrder: string | null;
imageAtOrder: string | null;
hsnCodeAtOrder: string | null; // snapshotted from variant at place-order
type: "product" | "gift" | "shipping"; // see order.md for full type set
quantity: number;
unitPrice: number;
lineSubtotal: number;
discountAllocated: number;
lineTotal: number;
netAmount: number | null; // pre-tax portion (null when vendor has no taxes)
taxBreakdown: TaxComponent[];
};ReturnResponse
type ReturnResponse = {
id: string;
returnNumber: string;
orderId: string;
orderVendorId: string;
customerId: string | null;
customer: { id: string; name: string; email: string } | null;
vendorId: string;
type: string; // e.g. "refund", "exchange"
status: string; // requested → approved → picked_up → received → qc_passed/qc_failed → refunded | rejected | cancelled
reasonCode: string | null;
reasonNotes: string | null;
refundAmount: number; // subunits
refundedAmount: number;
externalRefundReference: string | null;
shippingProvider: string | null; // not currently populated by any flow — always null today
awbNumber: string | null;
trackingCode: string | null;
rejectionReason: string | null;
qcFailureReason: string | null;
requestedAt: string; // ISO
approvedAt: string | null;
rejectedAt: string | null;
pickedUpAt: string | null;
receivedAt: string | null;
qcPassedAt: string | null;
qcFailedAt: string | null;
refundedAt: string | null;
cancelledAt: string | null;
lines: ReturnLineResponse[];
photos: ReturnPhotoResponse[];
};
type ReturnLineResponse = {
id: string;
orderLineId: string;
variantId: string | null;
productId: string | null;
sku: string;
productNameAtOrder: string;
variantNameAtOrder: string | null;
imageAtOrder: string | null;
quantity: number;
unitPrice: number;
taxPortion: number;
lineRefundAmount: number;
reasonCode: string | null;
reasonNotes: string | null;
restocked: boolean;
};
type ReturnPhotoResponse = {
id: string;
storageKey: string;
contentType: string | null;
fileSizeBytes: number | null;
sortOrder: number;
uploadedAt: string;
};Payouts — VendorBalanceResponse, LedgerEntryResponse, PayoutResponse
type VendorBalanceResponse = {
vendorId: string;
pending: number; // 'pending' ledger entries (return window not elapsed)
available: number; // 'available' entries not yet on a draft payout
lifetimeEarned: number;
lifetimeRefunded: number;
lifetimePaidOut: number;
payoutHold: boolean; // vendor.payouts.payout_hold flag
commissionRate: number; // basis points (10000 = 100.00%)
};
type LedgerEntryResponse = {
id: string;
vendorId: string;
kind: "sale" | "refund" | "manual" | "commission_adjustment";
status: "pending" | "available" | "paid_out" | "cancelled";
grossAmount: number;
commissionRate: number; // basis points
commissionAmount: number;
netAmount: number;
orderId: string | null;
orderVendorId: string | null;
orderReturnId: string | null;
payoutId: string | null;
pendingUntil: string | null;
availableAt: string | null;
paidOutAt: string | null;
cancelledAt: string | null;
description: string | null;
createdAt: string;
};
type PayoutResponse = {
id: string;
payoutNumber: string;
vendorId: string;
status: "pending" | "paid" | "cancelled" | "failed";
periodStart: string;
periodEnd: string;
grossTotal: number;
commissionTotal: number;
netTotal: number;
entryCount: number;
bankAccountId: string | null;
bankReference: string | null;
notes: string | null;
createdAt: string;
paidAt: string | null;
cancelledAt: string | null;
entries?: LedgerEntryResponse[]; // only on detail
};Vendor sub-orders
Base path: /vendor/orders. :id is order_vendor.id.
GET /vendor/orders — List my sub-orders
Paginated, newest placed first unless sortBy says otherwise. Always scoped to the caller's active vendor: the vendor predicate is applied first and unconditionally, so no combination of filters can reach another tenant's rows.
status filters the seller's own bag (order_vendor.fulfillment_status); every other filter reaches through to the parent order, which is why orderStatus is named apart from it.
Query
| Name | Type | Default | Notes |
|---|---|---|---|
page | int | 1 | >= 1 |
limit | int | 20 | 1..100 |
status | OrderVendorStatus? | — | The seller's own bag state |
orderStatus | OrderStatus? | — | The parent order's lifecycle status |
search | string? | — | Order-number search. The ORD-<year>- prefix and the serial's leading zeros are both optional |
customerSearch | string? | — | Case-insensitive partial match on the order's shipping first name, last name or phone, or the linked account's name or email |
productSearch | string? | — | Matches a product name or SKU among this bag's own lines. Correlated on order_vendor_id, not order_id, so a seller's search never matches because another vendor's half of the same order holds the product |
dateField | "paidAt" | "createdAt" | "updatedAt"? | — | Which of the parent order's timestamps the range filters on. createdAt means the placed date |
dateFrom | string? (ISO datetime) | — | Inclusive lower bound. Ignored without dateField |
dateTo | string? (ISO datetime) | — | Inclusive upper bound |
sortBy | "paidAt" | "createdAt" | "updatedAt"? | createdAt | Parent-order timestamp the list is ordered by. Rows with no value sort last in either direction |
sortDirection | "asc" | "desc"? | desc | — |
excludeRefunded | boolean? | — | Drops bags whose parent order was fully refunded. partially_refunded is kept — see the admin note for why |
excludeUnpaid | boolean? | — | Drops bags whose parent order is confirmed but still unpaid, keeping Cash-on-Delivery — COD stays pending until the courier remits the cash and still has to be shipped. See the admin note for why |
The New Orders queue in the seller panel is these filters composed: status=pending&orderStatus=confirmed&excludeRefunded=true&excludeUnpaid=true&sortBy=paidAt&sortDirection=desc.
Response 200 — paginated envelope of VendorOrderResponse.
Order exports
Base path: /vendor/orders/exports. The seller-side counterpart of the admin order export, scoped to the active vendor.
Ownership is taken from the session on every call and never from the request body — there is no vendorId field to send. It is stored on order_export.vendor_id as the row's owner, deliberately separate from the admin filter of the same name: an admin export filtered to one vendor carries whole-order money and every other supplier's rows, and must never surface in that vendor's own list. A mismatched id reads as 404, not 403, so an id cannot be probed for existence.
POST /vendor/orders/exports — Queue an export
Body — the same filters as GET /vendor/orders (status, orderStatus, search, customerSearch, productSearch, dateField, dateFrom, dateTo, excludeRefunded, excludeUnpaid), plus format ("csv" | "xlsx", default csv). Omitting every filter exports all of the seller's orders.
Response 201 — OrderExport with status: "pending".
GET /vendor/orders/exports — My export history
GET /vendor/orders/exports/:id — Export status
GET /vendor/orders/exports/:id/download — Download the file
Columns written (one row per item; money in major units, datetimes as YYYY-MM-DD HH:mm:ss):
Order columns — Order No · Placed At · Paid At · Updated At · Order Status · Payment Status · Fulfillment Status · Platform · Customer Name · Customer Email · Shipping Name · Shipping Phone · Shipping Address · Shipping City · Shipping State · Shipping Pincode · Shipping Country · Customer Note · My Subtotal · My Discount · My Shipping · My Tax · My Total · Courier · Courier Status · AWB Number · Tracking Code · Fulfilled At · Delivered At · Cancelled At · Cancellation Reason
Item columns — Item SKU · Item Name · Item Variant · Item Type · Item HSN · Item Quantity · Item Unit Price · Item Discount · Item Total
The column set is a strict subset of what VendorOrderResponse already exposes, so the export never widens a seller's view of an order. Specifically, and unlike the admin file:
- money is the seller's own bag allocation (
My …), never the whole-order subtotal, grand total or refunded amount — those would reveal what the customer paid other suppliers on a shared order; - no billing address, customer IP, cart id or redeemed points, none of which reach the vendor surface at all;
- no payment provider or method.
Payment Statusis included because the vendor orders report already carries it.
Every row belongs to the requesting seller, so there is no vendor column, and Fulfillment Status is this bag's own rather than a comma-joined list.
The My … columns repeat on every row of a multi-item order, so they must not be summed across one — sum Item Total instead.
GET /vendor/orders/:id — Sub-order detail
Returns the active vendor's VendorOrderResponse for one sub-order.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Sub-order does not belong to active vendor |
POST /vendor/orders/:id/cancel — Cancel sub-order
Allowed from pending (declining fulfillment) or fulfilled (courier rejected, package returned, etc.). delivered and cancelled reject. For fulfilled cancels, a non-empty reason is required.
Body
{ "reason": "Out of stock at warehouse" }| Field | Type | Constraints |
|---|---|---|
reason | string? | Trimmed; 1..500 chars. Required when sub-order is fulfilled |
Response 200 — updated VendorOrderResponse with fulfillmentStatus="cancelled" and cancelledAt stamped.
Side effects
- Releases this vendor's inventory reservation (no-op once it has been committed).
- Restocks the bag when it was cancelled out of
pendingand its reservation had already been committed at payment — nothing shipped, so anorder_cancelledmovement puts the units back on hand. Afulfilledcancel is not restocked; those goods return through return → QC pass. - Fires the shipping registry's
cancelShipment()hook (best-effort courier void). - Writes an audit row + emits
order.vendor.cancelled. - If every sibling sub-order is now cancelled, the parent order auto-cancels.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Sub-order not owned by vendor |
| 409 | SUB_ORDER_NOT_CANCELLABLE | Status is delivered or already cancelled |
| 400 | VALIDATION_ERROR | Cancelling a fulfilled sub-order without a reason |
| 409 | SUB_ORDER_CHANGED_CONCURRENTLY | The bag was fulfilled between the eligibility check and the write. Nothing was cancelled — reload and retry |
PATCH /vendor/orders/:id/shipping-address — Correct the delivery address
:id is order_vendor.id. Overwrites the order's shipping snapshot and logs an order.shipping_address_updated event with before → after, in the same transaction.
Available only when canEditShippingAddress is true on the sub-order. Two things close it:
| Condition | Why |
|---|---|
| Another seller has a bag on the order | The address is a snapshot on the parent order, shared by every bag — editing it would redirect the other seller's parcel too. The store operator maintains it instead. |
| A courier already holds the address | It is read off the order row at booking and sent to the carrier, so the waybill and printed label carry the old one. Editing after that changes this panel and nothing else. |
Body
{
"address": {
"firstName": "Priya",
"lastName": "Menon",
"fullAddress": "88 Brigade Road",
"city": "Bengaluru",
"state": "Karnataka",
"pincode": "560001",
"phone": "+919000011111",
"country": "IN" // optional, defaults to IN
}
}Same validation the storefront applies at checkout, so a corrected address can never be weaker than a placed one.
Response 200 — the sub-order after the edit.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown sub-order, or it belongs to another seller |
| 409 | ORDER_MULTI_VENDOR | Another seller has a bag on this order |
| 409 | ORDER_ADDRESS_LOCKED | A courier already holds this address |
POST /vendor/orders/:id/fulfilled — Mark fulfilled
Assigns a shipping provider, dispatches provider.createShipment(), and transitions pending → fulfilled. Only allowed when the sub-order is pending.
Body
{
"providerId": "clickpost",
"method": "express",
"trackingCode": "CP123456", // optional
"awbNumber": "AWB987654" // optional
}| Field | Type | Constraints |
|---|---|---|
providerId | string | Required. Must be enabled for the active vendor |
method | string | Required. Must belong to that provider |
trackingCode | string? | Trimmed; 1..200 chars. Some providers fill this asynchronously via webhook |
awbNumber | string? | Trimmed; 1..200 chars |
Response 200 — updated VendorOrderResponse.
Side effects
- Validates
(providerId, method)against the vendor's enabled providers. - Dispatches
provider.createShipment()— provider failure rolls back the transition (sub-order stayspending). - Writes audit row + emits
order.vendor.fulfilled.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Sub-order not owned by vendor |
| 409 | INVALID_TRANSITION | Sub-order is not pending |
| 400 | VALIDATION_ERROR | Provider/method invalid for this vendor |
POST /vendor/orders/bulk-fulfill — Bulk fulfill
Apply the same provider/method/(tracking)/(awb) across up to 200 sub-orders in one call. Each sub-order runs in its own transaction so one failure does not roll back the rest. Vendors needing per-order tracking codes should call the single-order endpoint.
Body
{
"orderVendorIds": ["01J9...", "01J9..."],
"providerId": "clickpost",
"method": "express",
"trackingCode": "CP123456", // optional, applies to every id
"awbNumber": "AWB987654" // optional, applies to every id
}| Field | Constraints |
|---|---|
orderVendorIds | 1..200 ids |
Response 200
{
"data": {
"successful": ["01J9...", "01J9..."],
"errors": [
{ "orderVendorId": "01J9...", "reason": "Sub-order not in pending status" }
]
},
"message": "Success",
"statusCode": 200
}POST /vendor/orders/:id/delivered — Mark delivered
Allowed only when the sub-order is fulfilled. Stamps deliveredAt and transitions to delivered.
Body — empty.
Response 200 — updated VendorOrderResponse.
Side effects
- Audit row +
order.vendor.deliveredevent. - COD payment: delivery does not mark paid. Admin mark-paid records cash collected; vendor sale earnings land only after payment is paid.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Sub-order not owned by vendor |
| 409 | INVALID_TRANSITION | Sub-order is not fulfilled |
Fulfilment ownership
GET /vendor/fulfillment/ownership — May I process my own orders?
Resolves the per-vendor override on this seller's profile, then the platform default at admin.fulfillment.owner, then the hard default vendor. The seller panel calls it once on load to decide whether to show anything shipping-related at all — per-order fulfillmentOwner arrives too late to hide a whole nav section.
Response 200
{
"data": { "owner": "vendor", "canProcess": true },
"message": "Success",
"statusCode": 200
}| Field | Type | Notes |
|---|---|---|
owner | "vendor" | "admin" | Who processes this vendor's sub-orders |
canProcess | boolean | Mirror of owner === "vendor", for UI gating |
When owner is admin the store operator ships from their central warehouse: fulfilled, delivered, cancel, and the return transitions all return 403 FULFILLMENT_OWNER_ADMIN on this panel, and the shipping configuration surfaces are hidden. Only the operator can change this — the setting is admin-only and never editable by the vendor.
Vendor returns
Base path: /vendor/returns. :id is return_request.id. Return requests are created by the customer (or admin) against a delivered sub-order; the vendor approves/rejects, schedules pickup, receives the parcel, and runs QC. Refunds themselves are admin-actioned.
GET /vendor/returns — List my returns
Query
| Name | Type | Default | Notes |
|---|---|---|---|
page | int | 1 | >= 1 |
limit | int | 20 | 1..100 |
status | string? | — | Trimmed; 1..32 chars. Filter by return status |
Response 200 — paginated envelope of ReturnResponse.
GET /vendor/returns/:id — Return detail
Response 200 — ReturnResponse.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Return not owned by active vendor |
POST /vendor/returns/:id/approve — Approve a return
Optionally override the computed refund_amount (capped between 0 and the system-computed amount).
Body
{ "refundAmountOverride": 49900 } // optional| Field | Type | Constraints |
|---|---|---|
refundAmountOverride | int? | >= 0 and ≤ computed refundAmount |
Response 200 — updated ReturnResponse with approvedAt stamped.
POST /vendor/returns/:id/reject — Reject a return
Body
{ "reason": "Item shows signs of wear inconsistent with the return reason" }| Field | Type | Constraints |
|---|---|---|
reason | string | Trimmed; 1..500 chars. Required |
Response 200 — updated ReturnResponse with rejectedAt and rejectionReason stamped.
POST /vendor/returns/:id/pickup — Mark courier pickup confirmed
Optionally attach courier identifiers from the pickup attempt.
Body
{
"awbNumber": "AWB12345", // optional
"trackingCode": "TRK67890" // optional
}| Field | Type | Constraints |
|---|---|---|
awbNumber | string? | Trimmed; 1..200 chars |
trackingCode | string? | Trimmed; 1..200 chars |
Response 200 — updated ReturnResponse with pickedUpAt stamped.
POST /vendor/returns/:id/receive — Mark the returned parcel as received
Body — empty.
Response 200 — updated ReturnResponse with receivedAt stamped.
POST /vendor/returns/:id/qc-pass — Pass QC and restock
Restocks each return line via the inventory module. Restock is best-effort per line — each call is idempotent on (return_request, return_line). Refund itself still requires admin action.
Body — empty.
Response 200 — updated ReturnResponse with qcPassedAt stamped and lines[].restocked = true where applicable.
POST /vendor/returns/:id/qc-fail — Fail QC (no restock)
Terminal for the vendor flow. Admin can still force-refund via the admin override endpoint.
Body
{ "reason": "Item arrived damaged beyond resale" }| Field | Type | Constraints |
|---|---|---|
reason | string | Trimmed; 1..500 chars. Required |
Response 200 — updated ReturnResponse with qcFailedAt and qcFailureReason stamped.
Vendor payouts
Base path: /vendor. Read-only — payout creation, marking paid, balance adjustments, and cancellation are all admin actions; the vendor sees the ledger and the payouts admin has cut.
GET /vendor/balance — Vendor balance
Snapshot of the active vendor's funds and commission rate.
Response 200 — VendorBalanceResponse.
{
"data": {
"vendorId": "01J9...",
"pending": 125000,
"available": 240000,
"lifetimeEarned": 5400000,
"lifetimeRefunded": 120000,
"lifetimePaidOut": 5040000,
"payoutHold": false,
"commissionRate": 1500 // 15.00%
}
}GET /vendor/ledger — Ledger entries
Paginated ledger for the active vendor. One row per sale, refund, manual adjustment, or commission adjustment.
Query
| Name | Type | Default | Notes |
|---|---|---|---|
page | int | 1 | >= 1 |
limit | int | 20 | 1..100 |
kind | "sale" | "refund" | "manual" | "commission_adjustment"? | — | Filter by entry kind |
status | "pending" | "available" | "paid_out" | "cancelled"? | — | Filter by entry status |
Response 200 — paginated envelope of LedgerEntryResponse.
GET /vendor/payouts — List my payouts
Query
| Name | Type | Default | Notes |
|---|---|---|---|
page | int | 1 | >= 1 |
limit | int | 20 | 1..100 |
status | "pending" | "paid" | "cancelled" | "failed"? | — | Filter by payout status |
Response 200 — paginated envelope of PayoutResponse (without entries[]).
GET /vendor/payouts/:id — Payout detail
Returns the payout plus its constituent entries[] (full LedgerEntryResponse[]).
Response 200 — PayoutResponse with entries[] populated.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Payout not owned by active vendor |
Domain events
Emitted via EventEmitter2. The vendor surface only emits a subset; the full catalogue is in order.md.
| Event | Fired when |
|---|---|
order.vendor.fulfilled | Sub-order pending → fulfilled via POST /vendor/orders/:id/fulfilled |
order.vendor.delivered | Sub-order fulfilled → delivered via POST /vendor/orders/:id/delivered |
order.vendor.cancelled | Sub-order * → cancelled via POST /vendor/orders/:id/cancel |
order.cancelled | Auto-cascade when every sibling sub-order is cancelled |
order.paid | admin mark-paid (COD cash collection or prepaid capture) |
order.return.* | Return lifecycle transitions on the vendor returns endpoints |
vendor.payout.* | Payout lifecycle (admin actions — read-only for vendors) |
Related modules
cart— orders are created by committing a prepared checkout; cart never appears on the vendor surface.inventory— sub-order cancel releases reservations, and restocks an unshipped bag whose reservation was already committed at payment; a shipped bag comes back only through return QC-pass.shipping+shipping-clickpost—providerId/methodvalidation and thecreateShipment()/cancelShipment()hooks live there. Seeshipping.md,shipping-clickpost.md.tax-flat— line and shippingtaxBreakdownis computed from per-vendor tax config. Seetax.md.notifications— listens toorder.vendor.*events for vendor-targeted email/push.
Notifications Module — Vendor surface
Vendor-facing HTTP surface for mobile device registration so the vendor app can receive push notifications (new-order alerts, payout updates, application status, etc.). The wider…
Product Attribute Module — Vendor surface
Vendor-facing read endpoints for platform-defined product attributes and attribute groups that the vendor product form binds to. Attributes themselves (definitions, codes, types,…