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.

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 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