Bỏ qua để đến nội dung

Orders (Vendor)

Nội dung này hiện chưa có sẵn bằng ngôn ngữ của bạn.

Vendor orders covers list (tabs, sort, pagination), detail (lines, refunds/returns), and manual create at /dashboard/orders/create. Create matches POS RBAC: order module + write.


GoalsNon-goals
Operate omnichannel orders from dashboardReplace POS for in-lane speed
Document create-order parity with POS permissionsDocument carrier tracking integrations

  • /dashboard/orders — list search supports typing, camera scan (same QR/barcode modal as POS), and USB keyboard-wedge scanners; the query matches order ID, receipt barcode (EAN-13), and customer-oriented fields per the vendor orders list API.
  • /dashboard/orders/createProtectedRoute order + requireModuleWrite.
  • See POS vs dashboard “create order” below.

  1. Open Orders → filter tab → open order.
  2. Actions (refund/return) per UI enablement.
  3. Create (if allowed) → lines → submit.

There are two different flows:

FlowWherePurpose
Web order return requestOrders → order detail (/dashboard/orders/:id)Record a return/refund request for an order placed through the dashboard or other channels. Sets order_status to return requested, marks selected lines, creates/updates a refunds row with status requested / pending.
POS-originated ordersSame order detail path when the order appears in the vendor listThe Nest POS module exposes place order + payments + catalog, not a separate return API. Treat returns/refunds through the web flow above once the order is visible under Orders. See Sales, POS & receivables (procedures) §3.
  1. Eligible — Order delivered or completed (including walk-in / guest orders with no linked customer). Staff with order module access opens detail and uses Request return / refund (pick lines) or Order actions → Return entire order to mark every eligible line in one step.
  2. Pending review — Refund row exists; someone with finance module write approves or rejects (PATCH /vendor/finance/refunds/:id with action: approve | reject). Can be done from Finance → Refunds or from the same order detail page. Approve finalizes settlement immediately (status refunded in one step for most methods).
  3. Settlement — Wallet credit, loyalty rollback, and optional terminal card RETURN run on approve/refund. Inventory: lines marked is_return = 1 are restocked into the store’s default warehouse (stocks.available_qty + aggregated items.stock). This does not create a purchase lot or change avg_purchase_price. Audit metadata may include return_restock (warehouse id, line count). If the store has no warehouse, settlement still completes but stock is skipped (skipped_no_warehouse).
  4. Two-step refund — When a refund row is already approved, Mark as refunded (action: refund) runs the same settlement (including restock once per refund; not repeated if status was already refunded).
  5. Cancel request — While the refund is still requested / pending (not approved or refunded), staff can Cancel return request (POST /vendor/orders/cancel-return), which clears line return flags and sets the order back to delivered (no stock change).

GET /vendor/orders/:id includes audit_logs (newest first): refund actions (return_requested, return_cancelled, approved, rejected, refunded), order patches (order_updated), payment changes, etc. The context field records whether the action came from order_detail, finance_refunds, or vendor_orders so support can see which screen was used. Actor identity is stored as the vendor JWT sub on both actor_sub and actor_id (varchar public ids — not legacy numeric ints).

The same response also exposes debt and delivery context when present on the order row: is_debt, force_customer_debt, delivery_date (date-only string), delivery_partner (id, name, code, scoped to the vendor), and a receivable object when a receivables row exists for that order_id (amounts, status / status_key, deadlines). The vendor order detail screen surfaces this next to delivery details and links to Finance → Receivables for follow-up.

ActionModule
Request / cancel return on order detailorder (write where enforced by route)
Approve / reject / mark refundedfinance with write

  • Refunds are financially sensitive—verify caller role and store context.
  • Treat audit_logs as the source of truth for who changed refund state and from which surface.

IDScenarioExpected
VO1List/detailData matches API
VO2Employee without order writeCannot open create
VO3Refund actionPolicy-compliant result
VO4Order detail activity logShows refund + finance actions with correct context
VO5Create → Save draftOrder created with order_status pending
VO6Create → Khách nợ + CRM / linked retail customeris_debt / receivable path; not available for guest (requires customer_id and/or user_id)
VO7Create wholesale → on-accountwholesale-credit-status banners; deny/warn when policy wholesale_max_open_debt_rounds_rule applies; overdue → warning only

RoutePurpose
/dashboard/ordersPaginated list with tabs (all / paid / pending / cancelled / returns) and sort
/dashboard/orders/wholesaleSame list UX scoped to wholesale channel (orders.type = 1)
/dashboard/orders/:idOrder detail, line items, refund/cancel-return actions where enabled
/dashboard/orders/createManual retail order creation (same RBAC as POS: order module + write)
/dashboard/orders/create-wholesaleManual wholesale order creation (wholesale pricing / sale type wholesale; same module + write)

Access: /dashboard/orders/create and /dashboard/orders/create-wholesale use ProtectedRoute requiredModule="order" requireModuleWrite (see app-router.tsx). The wholesale list tab uses requiredModule="order" without forcing write.

  • /pos and /point-of-sale/sale submit the cart with POST /vendor/point-of-sale/orders (placePosOrder in pos.api.ts). store_id is required on the body (and on related GETs such as items/grouped, customers, payment-status). This delegates to PosService.placeOrder via PointOfSaleStorePosController (apps/backend/src/modules/point-of-sale/). Legacy POST /pos/orders still exists on PosController for unified admin/vendor JWT clients that omit explicit store resolution — vendor-web checkout does not use it.
  • /dashboard/orders/create reuses the same cart/product UX patterns (features/orders/create-order-page.tsx) but submits with postVendorPlaceOrderPOST /vendor/orders/place. The payload shape matches PlacePosOrderPayload (same cart line structure as POS).

Both POS surfaces and manual create can show terminal / pending payment handling via the shared PlacePosOrderResponse helpers imported from pos.api.ts (payment status polling and payment-intent actions are aligned with /vendor/point-of-sale/orders/:id/… helpers).

Manual create: delivery, draft vs submit, and customer debt

Section titled “Manual create: delivery, draft vs submit, and customer debt”
  • Fulfillment modePickup or Delivery. Delivery opens a two-column modal (recipient + full delivery address (street line + optional floor/road/house), delivery date, customer shipping fee and “customer pays”, deposit + method, COD; package weight/dimensions; delivery partner, handoff method, pickup address, waybill, driver notes). Extra structured fields may still be sent on the API payload when set elsewhere. Values are merged into the place-order payload (delivery, delivery_charge, delivery_instruction, etc.) per PlacePosOrderPayload / ManualDeliveryPayload.
  • Customer pays shipping — when enabled on a delivery order, the entered shipping fee is not added to order_amount. The amount is instead kept in delivery metadata/instruction so the delivery partner can collect it from the customer.
  • Delivery modal defaultsDelivery date defaults to today (date-fnsyyyy-MM-dd) when empty, with min set to today (delivery-form-dates.ts). Recipient name / phone / email are applied when the modal opens (and whenever the customer changes while the modal is closed). Pickup address is pre-filled from the header-selected store (store-pickup-address.ts) when the field is empty.
  • submit_asdraft keeps the order as a hold (order_status: pending). submit (default) moves new orders to awaiting_confirmation so employees, the linked customer, and delivery partners can see the order. Implemented in apps/backend/src/modules/pos/pos.service.ts (initialOrderStatus). On /dashboard/orders/create (retail and wholesale), Save draft calls the same place API with submit_as: draft but does not open the checkout payment modal or payment-success panel — only a success toast and cart reset; use CheckoutPay now for the modal flow.
  • Customer debt (“Khách nợ”) — Optional when checkout has a non-guest customer: retail user_id and/or CRM customer_id on the payload (CRM-only profiles without an app login still qualify when customer_id is set). The UI sends force_customer_debt: true and paid_amount: "0.00" so the sale is recorded on account (is_debt on the order). Guest checkout cannot use this flag (API validation). Cash shortcut paths that would auto-close payment now follow fulfillment type: pos / take_awaycompleted, deliverydelivered; all of them skip auto-close when force_customer_debt is true so debt sales are not treated as fully settled cash.
  • Wholesale (B2B) debt policy — Applies when the order is wholesale (OrderSaleType.Wholesale / orders.type = 1) and checkout uses the debt payment method (payment_method: debt / on-account chip). The backend resolves term mode, due-day fields, max open debt rounds, and the “at cap” rule from customer → customer category → store (Customers (Vendor) § Wholesale (B2B) credit policy). Overdue wholesale receivables surface as warnings; hitting max open debt rounds may warn or block place-order depending on the resolved wholesale_max_open_debt_rounds_rule — only for debt checkout, not for cash/card/wallet on the same wholesale cart. Retail debt limits on the customer profile (e.g. limit_amount_debt) are evaluated separately and are not a substitute for this wholesale policy. Dashboard create wholesale loads GET /vendor/customers/:id/wholesale-credit-status?store_id=&placing_debt_order=1 when the cashier selects debt payment, to preview placement before submit.
  • Persistence (Nest) — On place, the backend also writes indochina_delivery_partner_id, indochina_delivery_date, indochina_place_submit_as, indochina_force_customer_debt, and indochina_pos_place_snapshot (JSON: submit/debt/paid summary + structured delivery subset) on orders, in addition to the merged delivery_instruction text. POST /vendor/orders/place appends audit_logs with debt/delivery metadata; admin POST /pos/orders writes a comparable audit_logs row (context: pos_place_order) when the actor is the admin panel.

Statuses are stored on orders.order_status (varchar). Typical progression for dashboard-created delivery orders:

StatusMeaning
pendingHold / quote — e.g. submit_as: draft or not yet submitted to fulfillment.
awaiting_confirmationSubmitted; waiting for an actor (staff, customer, partner) to confirm.
confirmedAt least one party has confirmed.
processingFulfillment in progress.
out_for_deliveryHanded off for delivery (e.g. no partner yet, retry after failure, or manual “ship again”).
deliveredPhysically delivered; payment may still be open (COD, partial, or debt).
completedClosed from delivery through collection / settlement.
returnedReturn path (see refunds section).
failedTerminal failure state for the order.
cancelled / canceledVendor-initiated cancel when allowed (see below).

Legacy values such as pre_order may still appear on older rows. payment_failed is treated like a failure/cancel window for vendor cancel rules.

POST /vendor/orders/cancel (see vendor-orders.service.ts) only allows cancel when the current status is in ORDER_STATUSES_VENDOR_CANCELLABLE: pending, awaiting_confirmation, pre_order, failed, payment_failed. After confirmation into active fulfillment, cancel is blocked (can_not_cancel_after_confirm).

POS server-held drafts (place_submit_as=draft) can be removed from lists without going through cancel: POST /vendor/orders/draft/delete sets indochina_deleted_at (soft delete). Lists, overview, and POS draft reuse by order_id exclude rows with that timestamp.

Implemented in apps/vendor-web/src/services/vendor-orders.api.ts (and used by the orders pages):

ActionEndpoint
KPI stripGET /vendor/orders/overview — optional store_id, date_from / date_to, search, order_type, pos_staff_only (1 / true = only orders with an attributed POS cashier). Counts follow the same filter shape as the list when these are set.
ListGET /vendor/orders (pagination, filters, sort — see VendorOrderListSort). Query search matches numeric order id, invoice_barcode (receipt EAN-13, substring), and linked customer name / email / phone. Additional exact filters: order_type (delivery / parcel / pos / take_away), order_status (raw status key), and pos_staff_only (same semantics as overview). List rows include both created_at and updated_at timestamps plus order_type, place_submit_as (e.g. draft for POS server-held drafts — aligns with tab=draft, which also excludes cancelled / failed order_status rows even if place_submit_as is still draft), and derived delivery_progress_status (waiting / in_delivery / delivered / failed) for table display/filtering; POS rows may include vendor_employee (cashier snapshot).
DetailGET /vendor/orders/:id
Receipt HTMLGET /vendor/orders/:id/receipt-html — default JSON { html, document_title, order_id }; ?format=html returns raw HTML. Optional ?tip_signature=1 (or true / yes / merchant) appends the merchant copy block: suggested tips (15% / 18% / 20% on net excl. tax), blank lines for custom tip/total, signature, and Merchant Copy footer instead of the customer copy.
UpdatePATCH /vendor/orders/:id (delivery details fields delivery_instruction / delivery_date / delivery_partner_id are accepted only for order_type=delivery, and only when progress is waiting or failed)
Refund reasons metaGET /vendor/orders/meta/refund-reasons
Refund requestPOST /vendor/orders/refund-request — optional auto_approve approves in one step and skips the pending “Return requested” vendor inbox notification
POS return (complete refund)POST /vendor/orders/pos-return-submit — same body as refund-request (no auto_approve); approves then marks refunded when the row would otherwise stay approved (e.g. wallet/manual); original paid-card flows may already reach refunded during approve; skips that inbox notification
Cancel returnPOST /vendor/orders/cancel-return
Soft-delete POS draftPOST /vendor/orders/draft/delete — body { order_id }; draft only, not cancelled rows