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
| App | Entry point | Main files |
|---|---|---|
| Mobile | Home -> OrderList mode default -> CustomerList mode default -> OrderDetail | apps/mobile/src/screens/customerStack/customerListScreen/index.tsx, apps/mobile/src/screens/orderStack/orderDetailScreen/index.tsx |
| POS Terminal | HomeScreen split-pane -> menu salesOrder -> customer picker/detail panel | apps/pos-terminal/src/screens/HomeScreen/index.tsx, apps/pos-terminal/src/screens/HomeScreen/OrderDetailForm.tsx |
Shared service/API contracts:
| Task | Mobile service | Terminal service | Main endpoint |
|---|---|---|---|
| list orders | services/orders.ts#getOrders | services/orders.ts#getOrders | GET /orders |
| order detail | getOrderDetail | getOrderDetail | GET /orders/{orderId} |
| create/update order | createOrder, updateOrder | createOrder, updateOrder | POST /orders, PATCH /orders/{orderId} |
| change delivery/storefront type | updateOrderShippingType | updateOrderShippingType | PATCH /orders/{orderId}/shipping-type |
| add/edit/delete item | createOrderItem, updateOrderItem, deleteOrderItem | same set | /orders/{orderId}/items |
| create invoice | services/invoices.ts#createInvoice | services/invoices.ts#createInvoice | POST /invoices payload { orderNo } |
| find invoices by order | getInvoicesByOrderNo | getInvoicesByOrderNo | GET /orders/{orderNo}/invoices |
| payment | payOrder | payOrder | POST /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 = defaultmeans sales order flow.- filter
shippingType = 1means delivery customers. - filter
shippingType = 2means storefront customers. - route filtering uses
routeCodefromgetRoutesByBranch(...). - 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:
getCustomerListAccess(user, mode, branch)must allow opening the screen and showing the primary action.getSalesOrderBlockedReason(customer, user)must not return a blocking reason.- The selected customer must have
customerIdorid. - Full customer data is loaded with
getCustomerSapById(customerId). - 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 choice | Result |
|---|---|
Open existing | navigate to OrderDetail with orderId, orderNo |
Create new | navigate 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:
| Field | Source | Notes |
|---|---|---|
branchId | branch storage | uses BranchId or branchId |
customerId, customerCode, customerName | customer detail | customerName is built from firstname/lastname |
phone1, phone2 | customer detail | used for display and payload |
shippingType | customer detail / branch | non-main branches force 2 |
routeCode, routeName | ShipTo address | used by delivery orders |
shippingAddress | ShipTo address | delivery orders require this before invoice creation |
billAddress | BillTo address | falls back to the first address |
U_BFP_Latitude, U_BFP_Longitude | ShipTo address | delivery orders require coordinates before invoice creation |
whGrpCode, whGrpName | branch storage | binds the order to the selected branch warehouse group |
orderStatus | local | starts 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:
| Value | Meaning | Effect in sales order detail |
|---|---|---|
1 | Delivery | requires shipping address and coordinates, shows route/distance/shipping fee data, uses Google Routes to calculate shipping fee |
2 | Storefront | fills 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_LatitudeandU_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(...)iforderIdexists. - If the request fails, rollback local detail to the previous value.
- If switching to storefront fills
dateOfPickup, callupdateOrder(...)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
vehicleReleaseanddocumentMove; missing values block important actions.
Adding Items And Persisting The Order
When an item is added from the product picker:
- validate quantity as an integer greater than
0 - ensure quantity does not exceed available stock
- if the product has step pricing and quantity qualifies, load step price before price calculation
- if the order has no
orderId, callcreateOrder(buildOrderApiPayload(detail, currentUser)) - after
orderIdexists, create the item withcreateOrderItem(orderId, orderItemPayload) - item payload is built through
buildOrderItemPayloadForSubmit(...) - 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
DRAFTorNEWare 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:
| Condition | Source |
|---|---|
actionState.canOpenInvoiceFlow must pass | getOrderActionState(...) |
| branch logistics fields must be complete for non-main branches | getBranchActionIssue() |
| item/amount/status validation must pass | getOrderInvoiceValidationIssues(detail) |
| delivery orders must have shipping coordinates | getShippingCoordinateIssue(...) |
| latest detail must pass recheck | getOrderRecheckValidationIssues(...) |
Important behavior:
- if the order is still
DRAFTor has noorderNo, the app callsPATCH /orders/{id}withorderStatus: NEW - if invoices already exist, the app does not create duplicates and opens payment immediately
- if
POST /invoiceserrors, the backend may still have created the invoice; the app callsGET /orders/{orderNo}/invoicesagain 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:
| Field | Calculation |
|---|---|
amount | order product amount or summed item amount |
baseDiscountAmount | usePoint + useCoupon + useCreditNote |
shippingAmount | unpaid shipping/fee invoices |
canceledInvoiceAmount | sum invoice amount where invoiceStatus = CANCELED |
totalDue | amount + shippingAmount - baseDiscountAmount - canceledInvoiceAmount - discount + charge |
paymentChannel | CASH, TRANSFER, QR |
| QR payload | sent as paymentChannel: CASH |
| transfer slip | Mobile 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
| Topic | Mobile | POS Terminal |
|---|---|---|
| Navigation | stack: CustomerList -> OrderDetail -> OrderPayment | split-pane in HomeScreen: menu, list/customer picker, detail/payment |
| Draft from customer | opens OrderDetail with customerId; does not create until necessary | creates local draft in panel 3 through openSalesOrderDraftFromCustomer(...) |
| Existing order today | alert asks to open existing or create new | same alert, but opens through openOrderSplitDetail(...) |
| Switching delivery/storefront | local state + PATCH /shipping-type when orderId exists | same pattern, then refreshes detail/list in split-pane |
| Payment | OrderPayment route | orderDetailView = payment in panel 3 |
| Storefront POS | has separate storefront list/detail after invoice | has Terminal-only storefront POS menu |
Developer Handoff Map
When changing sales order behavior, start here:
| Change area | Start with |
|---|---|
| customer selection / delivery-storefront filter | apps/mobile/src/screens/customerStack/customerListScreen/index.tsx, apps/pos-terminal/src/components/CustomerPickerModal.tsx |
| draft from customer | apps/mobile/src/screens/orderStack/orderDetailScreen/helpers.ts#mapCustomerToDraftOrder, apps/pos-terminal/src/screens/HomeScreen/index.tsx#buildSalesOrderPayloadFromCustomer |
| existing order today | getExistingCustomerOrderForBranch(...) and handlers in CustomerList/HomeScreen |
| delivery/storefront switch | handleChangeShippingType in OrderDetailScreen and OrderDetailForm |
| shipping fee / coordinates | getGoogleRouteDistanceMeters, calculateShippingAmountFromDistance, branch coordinate helpers in @bsr/utils |
| adding items | useProductPickerFlowBase, buildOrderItemPayloadForSubmit, createOrderItem |
| invoice creation | runPrimaryAction in Mobile, payment section opener in Terminal |
| payment | OrderPaymentScreen, payment section in OrderDetailForm, utils/orderPayment.ts |
| service contract | apps/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 |