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.
Executive summary
Section titled “Executive summary”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.
| Surface | Vendor-web route | Place-order HTTP API | Nest entry |
|---|---|---|---|
| Classic fullscreen POS | /pos | POST /vendor/point-of-sale/orders (placePosOrder in pos.api.ts) | PointOfSaleStorePosController → PosService.placeOrder |
| POS workspace (sale UI) | /point-of-sale/sale | Same as above | Same |
| Manual create (pickup / delivery, drafts, Khách nợ toggle) | /dashboard/orders/create | POST /vendor/orders/place (postVendorPlaceOrder) | VendorOrdersService → same PosService.placeOrder semantics |
| Wholesale manual create (B2B pricing / sale type wholesale) | /dashboard/orders/create-wholesale | POST /vendor/orders/place | Same — wholesale on-account runs evaluateWholesaleCreditPlacement (Customers § Wholesale) |
Why two HTTP prefixes?
/vendor/point-of-sale/*— Vendor JWT +ordermodule;store_idis mandatory on every handler (query for GET, body/query for POST). This is whatapps/vendor-webuses for catalog, coupons, and checkout so cache and tenancy never rely on an implicit default store./pos/*— UnifiedAuthJwtGuard(admin or vendor tokens);store_idmay be omitted and resolves viaPosService.resolveStoreIdForActor(default active store). Used by the admin panel POS and legacy clients; not the pathvendor-webcheckout calls anymore.
All of the above ultimately calls PosService.placeOrder for POS-shaped payloads (inventory, payments, debt flags, terminal polling metadata).
Roles and modules (quick reference)
Section titled “Roles and modules (quick reference)”| Task | Typical module | Route(s) |
|---|---|---|
| In-lane sale | order + write | /pos or /point-of-sale/sale |
| Manual / delivery order | order + write | /dashboard/orders/create |
| Order list & detail | order | /dashboard/orders, /dashboard/orders/:id |
| Refund approve / mark refunded | finance + write | /dashboard/finance/refunds or order detail |
| Receivable KPI / list | finance | /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)”Preconditions
Section titled “Preconditions”- User is signed in and has
orderwith write (see POS —ProtectedRoute+vendorHasModuleWrite). - Store is selected in the header (
store_idis sent on POS APIs). - Employees without a fixed
pos_register_idon their profile must pick an active lane / register when the shift requires it (UI blocks place until resolved — seepos.laneRequiredBeforeOrderinapps/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).
Checkout steps (happy path)
Section titled “Checkout steps (happy path)”- Open
/posor/point-of-sale/sale→ catalog loads fromGET /vendor/point-of-sale/items/grouped?store_id=…(TanStack keys must includestoreIdso data does not bleed across stores). - Add lines (variants via
PosVariationModalif needed); optional coupon (POST /vendor/point-of-sale/apply-coupon), customer (GET /vendor/point-of-sale/customers—guest, CRM id, or linked retail context per row), shipping/tax/discount overrides as exposed in the UI. - Choose payment method (cash, card, debt / “Khách nợ”, etc. as configured).
- Place order →
POST /vendor/point-of-sale/orders(placePosOrder). Payload includescart,tax,discount,paid_amount,payment_method,store_id(required), optional retailuser_id/ CRMcustomer_id, optionalvendor_pos_register_id, optionalcoupon_code, optionalsubmit_as, optionalorder_idwhen updating a server draft. - If the response indicates terminal / card pending (
payment.required,pending_terminal_payment), the SPA pollsGET /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).
Debt-related behavior (important distinction)
Section titled “Debt-related behavior (important distinction)”- Backend
PlaceOrderDtosupportsforce_customer_debtwhen there is a debt subject: retailuser_idand/or vendor CRMcustomer_id. Whentrue, the sale is recorded on account (no cash collected now); seecomputePosOrderDebtFlagsinapps/backend/src/modules/pos/pos-order-finance.util.tsandapps/backend/src/modules/pos/pos.service.ts. This applies equally whether the client calledPOST /vendor/point-of-sale/ordersorPOST /pos/orders(same DTO / service). - Vendor POS page (
apps/vendor-web/src/app/dashboard/pos/page.tsx) can sendforce_customer_debt: truewithpaid_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_idand/orcustomer_id) andpaid_amount< order total, the backend can setorders.is_debtand insert areceivablesrow (recordPosFinanceAfterOrderInsertinpos.service.ts). Retail users populatereceivables.user_id; CRM-only profiles populatereceivables.customer_id(nullableuser_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_typeispos, the sale is on account (payment_method: debt/force_customer_debt,is_debt),delivery_chargeis effectively zero, and there is nodelivery_partner_id,PosServicesetsorders.order_statustocompletedwhilepayment_statusremainsunpaiduntil collection. Delivery-type orders or POS orders with a delivery charge stay in the usualawaiting_confirmation(or downstream) flow instead. - Wholesale (B2B) on-account: when the cart is wholesale (
orders.type = 1/OrderSaleType.Wholesale) and checkout usespayment_method: debt, placement enforcesevaluateWholesaleCreditPlacement(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 resolvedwholesale_max_open_debt_rounds_rule(customer → customer category → store — Customers (Vendor) § Wholesale (B2B) credit policy). Vendor UI:/dashboard/orders/create-wholesalewith debt payment selected, andGET /vendor/customers/:id/wholesale-credit-status?store_id=&placing_debt_order=1for 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).
Operational checklist
Section titled “Operational checklist”- 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.
submit_as:draft→ order stayspending(hold);submit(default) →awaiting_confirmationfor downstream actors.- Customer debt (“Khách nợ”) — requires a non-guest checkout identity (retail
user_idand/or CRMcustomer_id):- Enable sell on account in the UI → sends
force_customer_debt: trueandpaid_amount: "0.00"so the backend records debt (is_debt) and opensreceivableswhen there is an outstanding balance (same pipeline as POS). - Guest checkout cannot force debt (API validation).
- Enable sell on account in the UI → sends
Persistence of delivery/debt/snapshot fields on the orders row is described in Orders (Vendor) (Indochina-prefixed columns + audit_logs).
3. Returns and refunds
Section titled “3. Returns and refunds”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_deadlinedefaulted viadefaultReceivableInvoiceDates()(UTC invoice date; deadline defaults to +10 days, overridable per store viadashboard_prefs.pos.receivable_payment_deadline_days— seeapps/backend/src/modules/pos/pos-order-finance.util.ts).user_id: retail debtor when linked; nullable for CRM-only debt.customer_id: vendor CRMcustomers.idwhen debt is keyed by CRM without an app login; complementsuser_idwhen 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”| Action | Where | Notes |
|---|---|---|
| See open balances | Finance → Receivables | List + KPIs from finance APIs |
| Mark order paid in the UI | Orders → 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 receivable | Vendor Nest API | Not 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).