Skip to main content

Sales Order

This menu is used to view, create, edit, and process customer sales orders, from customer selection, delivery/storefront order type, item entry, invoice creation, and payment.

This page is written as a developer handoff reference so another developer can understand the runtime flow without manually walking through the app.

App Entry Points

AppEntry pointMain files
MobileHome -> OrderList mode default -> CustomerList mode default -> OrderDetailapps/mobile/src/screens/customerStack/customerListScreen/index.tsx, apps/mobile/src/screens/orderStack/orderDetailScreen/index.tsx
POS TerminalHomeScreen split-pane -> menu salesOrder -> customer picker/detail panelapps/pos-terminal/src/screens/HomeScreen/index.tsx, apps/pos-terminal/src/screens/HomeScreen/OrderDetailForm.tsx

Shared service/API contracts:

TaskMobile serviceTerminal serviceMain endpoint
list ordersservices/orders.ts#getOrdersservices/orders.ts#getOrdersGET /orders
order detailgetOrderDetailgetOrderDetailGET /orders/{orderId}
create/update ordercreateOrder, updateOrdercreateOrder, updateOrderPOST /orders, PATCH /orders/{orderId}
change delivery/storefront typeupdateOrderShippingTypeupdateOrderShippingTypePATCH /orders/{orderId}/shipping-type
add/edit/delete itemcreateOrderItem, updateOrderItem, deleteOrderItemsame set/orders/{orderId}/items
create invoiceservices/invoices.ts#createInvoiceservices/invoices.ts#createInvoicePOST /invoices payload { orderNo }
find invoices by ordergetInvoicesByOrderNogetInvoicesByOrderNoGET /orders/{orderNo}/invoices
paymentpayOrderpayOrderPOST /payment

Main Flow From Customer Selection

Customer Selection And Customer Type

CustomerList is the starting point for creating a sales order from a customer.

  • mode = default means sales order flow.
  • filter shippingType = 1 means delivery customers.
  • filter shippingType = 2 means storefront customers.
  • route filtering uses routeCode from getRoutesByBranch(...).
  • the list is loaded through getCustomerSapList({ page, search, shippingType, routeCode, isRequest }).
  • the create sales order action is the card swipe action.

Before opening the sales order, the app checks:

  1. getCustomerListAccess(user, mode, branch) must allow opening the screen and showing the primary action.
  2. getSalesOrderBlockedReason(customer, user) must not return a blocking reason.
  3. The selected customer must have customerId or id.
  4. Full customer data is loaded with getCustomerSapById(customerId).
  5. Today's existing sales order is checked with getExistingCustomerOrderForBranch(customerDetail, branch).

If the customer already has a sales order for the current branch today, the app shows an alert:

User choiceResult
Open existingnavigate to OrderDetail with orderId, orderNo
Create newnavigate to OrderDetail with orderId: null, customerId

Draft From Customer Data

When OrderDetail receives customerId and does not have orderId, it loads the customer and maps it into a draft through mapCustomerToDraftOrder(...).

Important fields seeded into the draft:

FieldSourceNotes
branchIdbranch storageuses BranchId or branchId
customerId, customerCode, customerNamecustomer detailcustomerName is built from firstname/lastname
phone1, phone2customer detailused for display and payload
shippingTypecustomer detail / branchnon-main branches force 2
routeCode, routeNameShipTo addressused by delivery orders
shippingAddressShipTo addressdelivery orders require this before invoice creation
billAddressBillTo addressfalls back to the first address
U_BFP_Latitude, U_BFP_LongitudeShipTo addressdelivery orders require coordinates before invoice creation
whGrpCode, whGrpNamebranch storagebinds the order to the selected branch warehouse group
orderStatuslocalstarts as DRAFT

Mobile does not immediately call POST /orders when a draft is opened from a customer. The real order is created when the user adds an item, saves the draft, prints a document that requires an order id, or confirms invoice creation.

Terminal also creates a local draft in panel 3 first. If the flow starts from adding a product and then choosing a customer, Terminal creates the order header immediately with createOrder(...), adds the item, updates totals, and opens the detail in the split-pane workspace.

Delivery vs Storefront

shippingType is the main behavior switch:

ValueMeaningEffect in sales order detail
1Deliveryrequires shipping address and coordinates, shows route/distance/shipping fee data, uses Google Routes to calculate shipping fee
2Storefrontfills dateOfPickup with the current time when missing, does not require shipping coordinates, can continue into storefront/POS flows after invoice creation

Delivery shipping fee rules:

  • origin coordinate comes from branch coordinate helpers in @bsr/utils
  • destination coordinate comes from U_BFP_Latitude and U_BFP_Longitude
  • empty, non-numeric, 0, or missing branch coordinates stop calculation and show an alert
  • distance is fetched through getGoogleRouteDistanceMeters(...)
  • first 150 km are free
  • chargeable distance is 15 THB/km
  • remaining distance greater than or equal to 500 meters rounds up by 1 km

Switching Delivery/Storefront In Order Detail

Users can switch order type from the header/meta section in OrderDetail.

Developer notes:

  • Mobile and Terminal use the same pattern: update local state first, then call updateOrderShippingType(...) if orderId exists.
  • If the request fails, rollback local detail to the previous value.
  • If switching to storefront fills dateOfPickup, call updateOrder(...) again to persist pickup time.
  • Delivery orders must pass shipping address and coordinate checks before invoice creation.
  • Non-main branches include additional logistics fields such as vehicleRelease and documentMove; missing values block important actions.

Adding Items And Persisting The Order

When an item is added from the product picker:

  1. validate quantity as an integer greater than 0
  2. ensure quantity does not exceed available stock
  3. if the product has step pricing and quantity qualifies, load step price before price calculation
  4. if the order has no orderId, call createOrder(buildOrderApiPayload(detail, currentUser))
  5. after orderId exists, create the item with createOrderItem(orderId, orderItemPayload)
  6. item payload is built through buildOrderItemPayloadForSubmit(...)
  7. if payload has no whsCode, refresh inventory from Firestore and retry lookup before throwing ไม่พบข้อมูลคลังของสินค้านี้

Order totals:

  • weight-based item amount = qty * price * weight
  • normal item amount = qty * price
  • summary is calculated with calculateOrderSummary(...)
  • orders in DRAFT or NEW are updated when detail load detects header amount/weight differs from item totals

Creating Invoice And Opening Payment

The main detail action opens the invoice flow, or opens payment directly when the order status already expects payment.

Validation before invoice creation:

ConditionSource
actionState.canOpenInvoiceFlow must passgetOrderActionState(...)
branch logistics fields must be complete for non-main branchesgetBranchActionIssue()
item/amount/status validation must passgetOrderInvoiceValidationIssues(detail)
delivery orders must have shipping coordinatesgetShippingCoordinateIssue(...)
latest detail must pass recheckgetOrderRecheckValidationIssues(...)

Important behavior:

  • if the order is still DRAFT or has no orderNo, the app calls PATCH /orders/{id} with orderStatus: NEW
  • if invoices already exist, the app does not create duplicates and opens payment immediately
  • if POST /invoices errors, the backend may still have created the invoice; the app calls GET /orders/{orderNo}/invoices again before failing
  • Mobile uses navigation.navigate('OrderPayment', { orderId, orderNo })
  • Terminal uses setOrderDetailView('payment') and opens the payment section in panel 3

Payment Flow

Mobile payment lives in apps/mobile/src/screens/orderStack/orderPaymentScreen/index.tsx.

Terminal payment lives inside apps/pos-terminal/src/screens/HomeScreen/OrderDetailForm.tsx.

Both sides use the same model:

FieldCalculation
amountorder product amount or summed item amount
baseDiscountAmountusePoint + useCoupon + useCreditNote
shippingAmountunpaid shipping/fee invoices
canceledInvoiceAmountsum invoice amount where invoiceStatus = CANCELED
totalDueamount + shippingAmount - baseDiscountAmount - canceledInvoiceAmount - discount + charge
paymentChannelCASH, TRANSFER, QR
QR payloadsent as paymentChannel: CASH
transfer slipMobile does not require slip attachment; Terminal sends slipPath only for TRANSFER

Payload sent to POST /payment:

{
orderNo,
discount,
charge,
amount: amount + shippingAmount - canceledInvoiceAmount,
remark,
paymentChannel: paymentChannel === 'QR' ? 'CASH' : paymentChannel,
slipPath,
}

After payOrder(payload) succeeds:

  • if there is no slipPath, show success: ชำระเงินสำเร็จ กรุณาส่ง Incoming ที่หน้าใบแจ้งหนี้
  • reload order detail/payment data
  • if the error message includes ใบสั่งขายนี้ถูกใช้ไปเเล้ว, Mobile reloads data instead of immediately alerting

Mobile vs POS Terminal Differences

TopicMobilePOS Terminal
Navigationstack: CustomerList -> OrderDetail -> OrderPaymentsplit-pane in HomeScreen: menu, list/customer picker, detail/payment
Draft from customeropens OrderDetail with customerId; does not create until necessarycreates local draft in panel 3 through openSalesOrderDraftFromCustomer(...)
Existing order todayalert asks to open existing or create newsame alert, but opens through openOrderSplitDetail(...)
Switching delivery/storefrontlocal state + PATCH /shipping-type when orderId existssame pattern, then refreshes detail/list in split-pane
PaymentOrderPayment routeorderDetailView = payment in panel 3
Storefront POShas separate storefront list/detail after invoicehas Terminal-only storefront POS menu

Developer Handoff Map

When changing sales order behavior, start here:

Change areaStart with
customer selection / delivery-storefront filterapps/mobile/src/screens/customerStack/customerListScreen/index.tsx, apps/pos-terminal/src/components/CustomerPickerModal.tsx
draft from customerapps/mobile/src/screens/orderStack/orderDetailScreen/helpers.ts#mapCustomerToDraftOrder, apps/pos-terminal/src/screens/HomeScreen/index.tsx#buildSalesOrderPayloadFromCustomer
existing order todaygetExistingCustomerOrderForBranch(...) and handlers in CustomerList/HomeScreen
delivery/storefront switchhandleChangeShippingType in OrderDetailScreen and OrderDetailForm
shipping fee / coordinatesgetGoogleRouteDistanceMeters, calculateShippingAmountFromDistance, branch coordinate helpers in @bsr/utils
adding itemsuseProductPickerFlowBase, buildOrderItemPayloadForSubmit, createOrderItem
invoice creationrunPrimaryAction in Mobile, payment section opener in Terminal
paymentOrderPaymentScreen, payment section in OrderDetailForm, utils/orderPayment.ts
service contractapps/mobile/src/services/orders.ts, apps/mobile/src/services/invoices.ts, apps/pos-terminal/src/services/orders.ts, apps/pos-terminal/src/services/invoices.ts