Sales Workflow
This page captures the baseline sales journey that the POS system needs to support across mobile and terminal surfaces.
High-level workflow
Operator and customer perspectives
Implementation notes
- The exact scanner, printer, and payment integrations can differ by hardware profile.
- The terminal app should eventually treat the customer display as a first-class runtime surface, not only a capability check.
- Once payment integration stabilizes, split this page into separate workflow pages for cash, card, and mixed-payment handling.
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
bookingPriceIdas an item-edit lock unless Mobile does. Mobile uses it only for booking-source customer-data fallback; per-itemcurrentStepPricecontrols the booked-price restriction. - Keep stable logic in shared helpers. For Terminal,
apps/pos-terminal/src/utils/orderPayload.tsis the shared order payload contract used by both the order-detail workspace andStorefrontPosStack; it omitsvehicleReleaseanddocumentMovefor 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
พิมพ์ไม่มี Lotsorพิมพ์มี Lotsincludes 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
NEWbefore 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-notesusingbranchId,isDiscount=1, and the ordercustomerCode. - 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-otpwith{ phone }. - After
send-otpreturns a token, the modal shows the OTP code field and confirms withPOST /api/orders/{orderId}/credit-notes/{creditNoteId}/confirm-otpusing{ 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
shippingFeeto0. - When the user confirms a shipping-type change between delivery (
สายส่ง) and storefront (หน้าร้าน), the screen resetsshippingFeeto0. - 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/utilsbranch coordinate helper so Mobile and POS Terminal can reuse the same branch source. - Destination coordinates come from
orderDetail.U_BFP_LatitudeandorderDetail.U_BFP_Longitude; if either value is missing, invalid, or0, the screen skips distance calculation and does not call Google Routes. - Road distance is loaded through Google Routes API with
GOOGLE_ROUTE_API_KEYandroutes.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
shippingFeeon 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
ShipToaddress before delivery distance calculation. - Selecting a new shipping address updates the address, route, and customer coordinates, then recalculates distance when the order is delivery.
orderListScreencard amounts and bulk-payment amounts useinvoiceAmount + shippingFee, then fall back toamount + shippingFeewheninvoiceAmountis 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:
storefrontDetailScreenuses the item'swhsCodefor both the auto Lot/Batch pick and the manual picker, then falls back to the selected branch warehouse code.shippingDetailScreenuses the product group'swhsCodefor 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
999997is 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/healthzwhen 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, and100 - 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 refreshedonHand - booking - editing a step-price item reloads step prices from the MST API;
currentStepPriceis 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} branchIdmust 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:
weightBaseFlagandstepPriceFlag - item payload company fields must follow mobile fallbacks:
companyCode ?? companyandcompanyName ?? ComName ?? company ?? comName