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

Sales, POS & receivables (procedures)

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

This page ties together vendor-web routes and Nest pos / point-of-sale / vendor-orders / vendor-finance behavior for staff who sell, handle returns, and work with customer debt (BNPL / receivables). Deep dives stay in POS, Orders (Vendor), and Finance (Vendor).


Order placement workflow (vendor checkout)

Section titled “Order placement workflow (vendor checkout)”

Three surfaces share the same cart model (PlacePosOrderPayload) at the HTTP layer for in-lane / counter sales; manual / delivery orders use a separate vendor-orders endpoint.

SurfaceVendor-web routePlace-order HTTP APINest entry
Classic fullscreen POS/posPOST /vendor/point-of-sale/orders (placePosOrder in pos.api.ts)PointOfSaleStorePosControllerPosService.placeOrder
POS workspace (sale UI)/point-of-sale/saleSame as aboveSame
Manual create (pickup / delivery, drafts, Khách nợ toggle)/dashboard/orders/createPOST /vendor/orders/place (postVendorPlaceOrder)VendorOrdersService → same PosService.placeOrder semantics
Wholesale manual create (B2B pricing / sale type wholesale)/dashboard/orders/create-wholesalePOST /vendor/orders/placeSame — wholesale on-account runs evaluateWholesaleCreditPlacement (Customers § Wholesale)

Why two HTTP prefixes?

  • /vendor/point-of-sale/* — Vendor JWT + order module; store_id is mandatory on every handler (query for GET, body/query for POST). This is what apps/vendor-web uses for catalog, coupons, and checkout so cache and tenancy never rely on an implicit default store.
  • /pos/* — Unified AuthJwtGuard (admin or vendor tokens); store_id may be omitted and resolves via PosService.resolveStoreIdForActor (default active store). Used by the admin panel POS and legacy clients; not the path vendor-web checkout calls anymore.

All of the above ultimately calls PosService.placeOrder for POS-shaped payloads (inventory, payments, debt flags, terminal polling metadata).


TaskTypical moduleRoute(s)
In-lane saleorder + write/pos or /point-of-sale/sale
Manual / delivery orderorder + write/dashboard/orders/create
Order list & detailorder/dashboard/orders, /dashboard/orders/:id
Refund approve / mark refundedfinance + write/dashboard/finance/refunds or order detail
Receivable KPI / listfinance/dashboard/finance/receivables

1. Selling from the POS screen (/pos or /point-of-sale/sale)

Section titled “1. Selling from the POS screen (/pos or /point-of-sale/sale)”
  1. User is signed in and has order with write (see POSProtectedRoute + vendorHasModuleWrite).
  2. Store is selected in the header (store_id is sent on POS APIs).
  3. Employees without a fixed pos_register_id on their profile must pick an active lane / register when the shift requires it (UI blocks place until resolved — see pos.laneRequiredBeforeOrder in apps/vendor-web/src/app/dashboard/pos/page.tsx). The full cashier-session model (open, attribute, reassign, close, slip suggestions) is documented in Work shifts (Vendor).
  1. Open /pos or /point-of-sale/sale → catalog loads from GET /vendor/point-of-sale/items/grouped?store_id=… (TanStack keys must include storeId so data does not bleed across stores).
  2. Add lines (variants via PosVariationModal if needed); optional coupon (POST /vendor/point-of-sale/apply-coupon), customer (GET /vendor/point-of-sale/customersguest, CRM id, or linked retail context per row), shipping/tax/discount overrides as exposed in the UI.
  3. Choose payment method (cash, card, debt / “Khách nợ”, etc. as configured).
  4. Place orderPOST /vendor/point-of-sale/orders (placePosOrder). Payload includes cart, tax, discount, paid_amount, payment_method, store_id (required), optional retail user_id / CRM customer_id, optional vendor_pos_register_id, optional coupon_code, optional submit_as, optional order_id when updating a server draft.
  5. If the response indicates terminal / card pending (payment.required, pending_terminal_payment), the SPA polls GET /vendor/point-of-sale/orders/:orderId/payment-status?store_id=… and may call payment-intent / mark-success / cancel helpers under the same prefix (see POS payments & webhooks).
Section titled “Debt-related behavior (important distinction)”
  • Backend PlaceOrderDto supports force_customer_debt when there is a debt subject: retail user_id and/or vendor CRM customer_id. When true, the sale is recorded on account (no cash collected now); see computePosOrderDebtFlags in apps/backend/src/modules/pos/pos-order-finance.util.ts and apps/backend/src/modules/pos/pos.service.ts. This applies equally whether the client called POST /vendor/point-of-sale/orders or POST /pos/orders (same DTO / service).
  • Vendor POS page (apps/vendor-web/src/app/dashboard/pos/page.tsx) can send force_customer_debt: true with paid_amount: "0.00" when payment mode is debt (“Khách nợ”) and the checkout customer is not guest — same debt semantics as manual order create (CRM-only customers included).
  • Partial cash today, balance on account is still a supported API shape: if a debt subject is set (user_id and/or customer_id) and paid_amount < order total, the backend can set orders.is_debt and insert a receivables row (recordPosFinanceAfterOrderInsert in pos.service.ts). Retail users populate receivables.user_id; CRM-only profiles populate receivables.customer_id (nullable user_id). Any client may use that; the default vendor POS UI does not expose a full “partial pay” editor today.
  • Counter POS + debt + no delivery: when order_type is pos, the sale is on account (payment_method: debt / force_customer_debt, is_debt), delivery_charge is effectively zero, and there is no delivery_partner_id, PosService sets orders.order_status to completed while payment_status remains unpaid until collection. Delivery-type orders or POS orders with a delivery charge stay in the usual awaiting_confirmation (or downstream) flow instead.
  • Wholesale (B2B) on-account: when the cart is wholesale (orders.type = 1 / OrderSaleType.Wholesale) and checkout uses payment_method: debt, placement enforces evaluateWholesaleCreditPlacement (max open debt rounds and store debt caps apply only on that path; cash/card/wallet wholesale checkout is not blocked by max rounds). Overdue wholesale receivables → warnings; at or over max open debt rounds, behavior follows the resolved wholesale_max_open_debt_rounds_rule (customer → customer category → store — Customers (Vendor) § Wholesale (B2B) credit policy). Vendor UI: /dashboard/orders/create-wholesale with debt payment selected, and GET /vendor/customers/:id/wholesale-credit-status?store_id=&placing_debt_order=1 for previews.

2. Creating an order outside POS (/dashboard/orders/create)

Section titled “2. Creating an order outside POS (/dashboard/orders/create)”

Same cart/product UX patterns as POS, but submission uses POST /vendor/orders/place (postVendorPlaceOrder).

  1. Confirm store and fulfillment (pickup vs delivery). Delivery opens the structured modal (recipient, address, date, COD flags, delivery partner, etc.) — details in Orders (Vendor) § Manual create.
  2. submit_as: draft → order stays pending (hold); submit (default) → awaiting_confirmation for downstream actors.
  3. Customer debt (“Khách nợ”) — requires a non-guest checkout identity (retail user_id and/or CRM customer_id):
    • Enable sell on account in the UI → sends force_customer_debt: true and paid_amount: "0.00" so the backend records debt (is_debt) and opens receivables when there is an outstanding balance (same pipeline as POS).
    • Guest checkout cannot force debt (API validation).

Persistence of delivery/debt/snapshot fields on the orders row is described in Orders (Vendor) (Indochina-prefixed columns + audit_logs).


A. Web / dashboard return (primary flow today)

Section titled “A. Web / dashboard return (primary flow today)”

Use Orders → order detail for eligible orders (delivered or completed, including walk-in without a linked customer). Steps, refund states, PATCH /vendor/finance/refunds/:id, and cancel-return API are documented in Orders (Vendor) § Returns and refunds.

B. “POS receipt return” in a separate POS catalog tab

Section titled “B. “POS receipt return” in a separate POS catalog tab”

There is no dedicated return/exchange endpoint group under apps/backend/src/modules/pos/pos.controller.ts today (place order, payment status, coupon, catalog only). If a store needs to reverse a POS sale, locate the order in /dashboard/orders and follow the same web refund workflow (subject to eligibility). On approve / refund settlement, the backend restocks lines flagged is_return into the store default warehouse (mirror of POS sale decrement; no purchase lot / no cost price change). Manual stock adjustments are only needed when goods are not put back on hand or the store has no warehouse configured.


4. Customer debt and receivables (data path)

Section titled “4. Customer debt and receivables (data path)”

When an order is placed such that the customer still owes (is_debt / force_customer_debt / partial paid_amount) and there is a debt subject (orders.user_id and/or orders.customer_id), recordPosFinanceAfterOrderInsert inserts a row into receivables with:

  • export_date / payment_deadline defaulted via defaultReceivableInvoiceDates() (UTC invoice date; deadline defaults to +10 days, overridable per store via dashboard_prefs.pos.receivable_payment_deadline_days — see apps/backend/src/modules/pos/pos-order-finance.util.ts).
  • user_id: retail debtor when linked; nullable for CRM-only debt.
  • customer_id: vendor CRM customers.id when debt is keyed by CRM without an app login; complements user_id when both exist.

Staff monitor exposure under /dashboard/finance/receivables (GET /vendor/finance/receivables/overview and GET /vendor/finance/receivables — see vendor-finance.api.ts).


5. Collecting debt (“Thu nợ”) — current capabilities

Section titled “5. Collecting debt (“Thu nợ”) — current capabilities”
ActionWhereNotes
See open balancesFinance → ReceivablesList + KPIs from finance APIs
Mark order paid in the UIOrders → detail → Edit order (PATCH /vendor/orders/:id)Updates orders.payment_status (and related fields). Does not, in the current vendor-orders.service.ts update implementation, automatically write back receivables.paid_amount / status — finance and order state can diverge until a receivable payment pipeline exists. Treat receivables as the BNPL ledger snapshot at sale time; use order edit only when your ops policy treats “paid” on the order as the source of truth.
Record another cash/card collection against the receivableVendor Nest APINot exposed as a first-class mutation in vendor-finance.controller.ts today (read-only receivable list). Prefer new order payment / future receivable-payment endpoint when implemented.

For card/terminal completion on a pending POS card order, continue to use the POS payment status flow (POS, POS payments & webhooks).