Skip to main content

Sales Workflow

This page captures the baseline sales journey that the POS system needs to support across mobile and terminal surfaces.

Updating Sales Order Weight​

For a persisted weight-based item, open the item editor and tap แก้ไขน้ำหนัก. The order must still allow item editing and the user must have fillWeight = 1. Enter the total line weight, tap OK, and confirm the global alert. The app sends only the total weight to the dedicated PATCH endpoint, reloads the server-calculated order, and closes Global Loading. Cancel returns to the previous form; failure preserves the entered weight for correction or retry.

See Sales Order: Editing Total Weight for the payload, validation and both cancellation paths, and Storefront POS for customer reconfirmation. Ordinary quantity, price and stock editing remains a separate action.

High-level workflow​

Operator and customer perspectives​

Storefront Promotion Selection​

Tap โปร in the Terminal cashier catalog to request current promotions for the selected warehouse (branchCode=whsGrpCode). Match promotion itemCode to Firestore and show eligible inventory products using their current price and stock. The card displays endDate after price/unit as (ถึงวันที่ DD/MM/YYYY), preserving the API calendar date. Selecting a card uses the normal add-item modal; สินค้า returns to the regular catalog.

See Storefront POS: Promotion Catalog for the response contract and end-date example.

Storefront Plus Sales​

During Terminal order entry, the existing POST /api/orders/upsell response can supply both cart-line stepNudge hints and crossSell product recommendations. Plus Sales is hidden until eligible cross-sell products exist. It resolves product details from Firebase, excludes products already in the order/cart by productCode or itemCode, and hides the panel on both screens when nothing remains. Exclusion affects recommendations only; it never deletes order lines.

The cashier swipes compact text-only cards and taps a card to open the same add-product modal as the left-hand catalog. The customer display only allows horizontal browsing. The normal item permissions, validation, and save queue still apply. This feature belongs to Terminal StorefrontPosStack; it does not add a Mobile upsell panel.

See Storefront POS: Plus Sales for request timing, matching rules, failure handling, and regression checks.

Credit-note return, QC and B1​

In both Mobile and Terminal, invoice returns use the credit-note workflow. Main warehouses do not run QC. Deposit warehouses (isMain === false) show QC for WAITINGCN with no decision, require a reason only for failure, and reload the document after PATCH /api/credit-notes/{id}/qc. The backend supplies QC_FAILED on failure; the client does not assign that status itself.

Send B1 appears only for NEW or INVUPDATE in both warehouse types, with existing permissions and deposit QC locks still applied. Passing QC while the document remains WAITINGCN restores normal saving; the warehouse save must advance it to INVUPDATE before B1 appears. B1 still validates Lots and Q1/Q2/Q3 for physical returns and rejects documents with datetimeToB1 already set.

See QC details and list status colors for the modal, failed-state behavior, and exact palette.

Development and release now use the same QC/Save/B1 gates after removal of the temporary error-testing exception. On API failure, คัดลอกปัญหาส่งทีม IT copies the response, request details, and initiating usercode/deptCode for a support report. See copied diagnostics and usage.

Sales-order payment controls​

Mobile, the Terminal order-detail panel, and Storefront POS use a horizontally scrollable เงินสด → โอน → แบ่งชำระ → QRcode row. Split payment (OTHER) has cash/transfer inputs and the existing transfer-slip attachment; submission requires their sum to equal the payload amount. This target can differ from the displayed net total. The shared helper validates satang and the payload includes numeric split fields only for OTHER.

See Sales Order: Payment Flow for formulas, an API example, slip upload, QR gating, and retry endpoints. The cashier owns payment input; the native Customer Display presents a read-only summary and the selected method. Scanner/printer integrations may still differ by hardware profile.

Cross-surface business parity​

Mobile, the Terminal sales-order workspace, and StorefrontPosStack may use different layouts and interaction patterns, but they must implement the same sales business rules.

  • Preserve Mobile behavior for item selection, duplicate items, step price, weight pricing, booking-price item restrictions, stock validation, permissions, payload fields, and API error messages.
  • Adapt presentation for the device: Mobile can use stacked screens and modals; Terminal can use split panes, a customer display, and denser controls.
  • Do not use an order-level bookingPriceId as an item-edit lock unless Mobile does. Mobile uses it only for booking-source customer-data fallback; per-item currentStepPrice controls the booked-price restriction.
  • Keep stable logic in shared helpers. For Terminal, apps/pos-terminal/src/utils/orderPayload.ts is the shared order payload contract used by both the order-detail workspace and StorefrontPosStack; it omits vehicleRelease and documentMove for main branches, matching Mobile.

Mobile order detail storage print​

orderDetailScreen supports printing storage request documents from the order detail flow.

  • The storage-print modal for choosing พิมพ์ไม่มี Lots or พิมพ์มี Lots includes a required remark textarea.
  • The textarea uses the same stacked textarea treatment as the invoice remark modal.
  • Pressing either print button updates the order first with the current screen state and the storage remark.
  • After the update succeeds, the screen reloads the order detail from the API, updates local state, and then prints the selected document variant.
  • For draft orders, the print flow moves the order to NEW before reloading and printing.

Mobile order credit-note discount​

orderDetailScreen lets users apply a credit-note discount from the order benefit menu.

  • Opening the credit-note picker requires an existing order, benefit-management permission, a customer code, no existing credit-note discount, and no active coupon discount on the order.
  • The picker loads eligible credit notes with GET /api/credit-notes using branchId, isDiscount=1, and the order customerCode.
  • Selecting a credit note validates that the note is not already used and that BFP/NFF credit-note discounts do not exceed the matching company item total in the order.
  • Applying the selected note calls POST /api/orders/{orderId}/credit-notes/{creditNoteId}.
  • If the apply response does not contain requireOtp: true, the screen closes the picker and refreshes the order detail immediately.
  • If the apply response contains requireOtp: true, the screen closes the picker and opens an OTP modal.
  • The OTP modal accepts a 10-digit phone number, then calls POST /api/orders/{orderId}/credit-notes/{creditNoteId}/send-otp with { phone }.
  • After send-otp returns a token, the modal shows the OTP code field and confirms with POST /api/orders/{orderId}/credit-notes/{creditNoteId}/confirm-otp using { token, otpCode }.
  • When OTP confirmation succeeds, the screen closes the OTP modal and refreshes the order detail.

Mobile order shipping fee and route distance​

orderDetailScreen treats orders.amount as the product total and orders.shippingFee as the shipping charge.

  • New draft orders default shippingFee to 0.
  • When the user confirms a shipping-type change between delivery (สายส่ง) and storefront (หน้าร้าน), the screen resets shippingFee to 0.
  • Canceling or dismissing the confirmation keeps the current shipping type and does not clear the existing shipping fee.
  • Delivery orders show delivery-only summary rows for distance, editable shipping fee, and total amount due.
  • The shipping-fee value opens a modal text input when pressed; it is not focused automatically when the modal opens.
  • If the order already has an orderId, editing the shipping fee from the modal persists the order immediately.
  • Branch origin coordinates come from the shared @bsr/utils branch coordinate helper so Mobile and POS Terminal can reuse the same branch source.
  • Destination coordinates come from orderDetail.U_BFP_Latitude and orderDetail.U_BFP_Longitude; if either value is missing, invalid, or 0, the screen skips distance calculation and does not call Google Routes.
  • Road distance is loaded through Google Routes API with GOOGLE_ROUTE_API_KEY and routes.distanceMeters.
  • Distance is rounded to kilometers with the 500-meter threshold rule, equivalent to Math.round(distanceMeters / 1000).
  • Auto shipping fee is max(0, roundedKm - 150) * 15, so a 152 km route produces a 30 baht shipping fee.
  • If auto distance calculation changes shippingFee on an existing order, the screen persists the order immediately.
  • Orders opened from booking price fill missing shipping address, route code, route name, and customer coordinates from the SAP customer default ShipTo address before delivery distance calculation.
  • Selecting a new shipping address updates the address, route, and customer coordinates, then recalculates distance when the order is delivery.
  • orderListScreen card amounts and bulk-payment amounts use invoiceAmount + shippingFee, then fall back to amount + shippingFee when invoiceAmount is missing.

Mobile Lot/Batch warehouse lookup​

storefrontDetailScreen and shippingDetailScreen load Lot/Batch options through:

GET products/lot?itemCode={itemCode}&whsCode={whsCode}&company={company}

Warehouse-code source rules:

  • storefrontDetailScreen uses the item's whsCode for both the auto Lot/Batch pick and the manual picker, then falls back to the selected branch warehouse code.
  • shippingDetailScreen uses the product group's whsCode for both the auto Lot/Batch pick and the manual picker.

Do not use whGrpCode as the primary Lot/Batch lookup warehouse in storefront, because Lot/Batch availability is warehouse-specific.

In shippingDetailScreen, pressing a product to choose Lot/Batch checks transport confirmation before permission checks. If the delivery has already been assigned to transportation but deliveryStatus is null or an empty string, the screen stops the Lot/Batch flow and shows ขนส่งยังไม่ยืนยันใบจัดนี้.

Mobile storefront accept prompt and action override​

storefrontDetailScreen evaluates the accept-work popup only after detail, branch, and user data have loaded successfully. This prevents the popup from appearing briefly from an empty detail object before the API response arrives.

Current rules:

  • While detail data is loading, the accept-work popup is hidden.
  • If detail loading fails, the accept-work popup is closed.
  • If the real detail already has acceptedBy, the accept-work popup is hidden.
  • User code 999997 is an explicit storefront action override: that user can edit product Lot/Batch/location checks and send B1 even when they are not the accepted worker.

POS Terminal booking price split-pane flow​

POS terminal does not mirror the mobile navigation stack literally. The mobile sequence Home > ListScreen > DetailScreen is represented as a split-pane Home workspace.

Booking price layout:

  • panel 1: sales menu entry ใบจองราคา
  • panel 2: booking-price list
  • panel 3: booking-price detail or draft form

Creation flow:

Unsaved-change behavior:

  • leaving panel 3 while the draft/detail is dirty opens a shared alert
  • the alert has three actions:
    • cancel
    • discard without saving
    • save and exit
  • save-and-exit creates the booking price when no id exists and updates it when an id exists

List behavior:

  • panel 2 supports search, filters, and swipe actions
  • loading uses the shared loading overlay
  • booking-price status is represented by card background color
  • panel 2 is refreshed after booking-price create/delete

Detail behavior:

  • panel 3 title is รายละเอียดใบจองราคา
  • remark and product item edits mark the detail as dirty
  • product rows can be pressed to edit or swiped to delete
  • product row display follows mobile conventions: item name, amount, quantity, step-price indicator, weight indicator, and formula text for weight-based pricing
  • panel 3 does not wrap the booking detail in a generic outer ScrollView; booking metadata, remark, and product-list title are the product FlashList header so the product list owns scrolling
  • existing booking prices can be edited only on the same created day, using the server business date from GET /api/healthz when available
  • if the existing booking price is outside the editable created day, panel 3 disables add item, edit item, delete item, remark/header editing, and save actions

Product picker behavior:

  • the picker is a full-screen modal host
  • the product list is on the left side inside the modal
  • the add/edit item form is an overlay inside the same modal, not a second nested modal
  • this single-modal approach is required for iOS compatibility
  • quick quantity options are 1, 5, 10, 50, and 100
  • step-price data is loaded from the real product step-price API
  • add-item overlay follows mobile by using the product name as the title and omitting the price input
  • edit-item overlay follows mobile by using the แก้ไขรายการสินค้า title and keeping the price input
  • quick quantity changes load step price when the selected quantity requires step pricing
  • step-price rows show mobile range labels with unit text, such as ไม่เกิน X {UOM} and เกิน X ไม่เกิน Y {UOM}
  • editing an existing item first refreshes inventory from Firestore by productCode; available stock comes from refreshed onHand - booking
  • editing a step-price item reloads step prices from the MST API; currentStepPrice is saved as JSON fallback data, not treated as the primary current price source

Persistence behavior:

  • creating a booking price calls POST /api/booking-price
  • updating a booking price calls PATCH /api/booking-price/{bookingPriceId}
  • adding a booking-price item calls POST /api/booking-price/{bookingPriceId}/items
  • updating a booking-price item calls PATCH /api/booking-price/{bookingPriceId}/items/{itemId}
  • deleting a booking-price item calls DELETE /api/booking-price/{bookingPriceId}/items/{itemId}
  • deleting a booking price calls DELETE /api/booking-price/{bookingPriceId}
  • branchId must be numeric in the booking-price payload
  • add-item persistence must match mobile order: ensure/create booking header, create item, update booking totals, reload detail, and refresh panel 2 when the booking was newly created
  • edit-item persistence must patch the item immediately, update booking totals, and reload detail
  • item payload flags must be booleans: weightBaseFlag and stepPriceFlag
  • item payload company fields must follow mobile fallbacks: companyCode ?? company and companyName ?? ComName ?? company ?? comName