Borrow Fish
Borrow Fish manages fish/product borrowing before the normal sales-order flow. It is close to Sales Orders because it starts with a customer, branch/warehouse context, product lines, quantities, and later can become a sales order. The important differences are borrow type, pickup/license-plate rules for non-main SPEC borrows, expected purchase quantity, acceptance/stock-cut workflow, retry handling, and conversion to sales order.
App Entry Points
| App | Target |
|---|---|
| Mobile | BorrowList -> BorrowDetail |
| POS Terminal | borrowFish -> BorrowDetailForm in HomeScreen; StorefrontPosStack borrow list/workspace |
Borrow Types
| Type | Label |
|---|---|
SPEC | Customer sample/inspection |
MARKET | Market visit |
VIDEO | Video shooting |
SEMINAR | Seminar |
DESTROY | Destroy |
LAB | Research/lab |
The type must be selected before products can be added. For a non-main branch, changing a borrow to SPEC is blocked when the document already has product items; users must remove items first because SPEC adds required pickup and customer-license-plate rules.
Lifecycle
| API status | UI label | Meaning |
|---|---|---|
NEW | สร้างใหม่ | Draft; editing follows the owner guard. |
WAITING | รอดำเนินการ | Waiting for processing. |
ACCEPTED | รับงานแล้ว | Work accepted; action guards also check acceptedAt. |
DONE | เสร็จสิ้น | Completed; a failed sub-borrow may still expose retry. |
PARTIAL_CONVERTED | แปลงและรอคืน | Some items became a sales order; items with remaining lots can be returned. |
CONVERTED | แปลงเป็นใบสั่งขายแล้ว | Converted to a sales order. |
CANCELED | ยกเลิก | Canceled. |
The app refreshes status from the backend after each successful action. Conversion requires acceptance and the permission rules below; it does not require DONE. After /return-items, the app displays the refreshed backend status rather than assigning a final status locally.
End-To-End Workflow
Header Rules
| Topic | Behavior |
|---|---|
| Customer | Borrow starts from a selected customer and stores customer code/name context. |
| Branch and warehouse group | Payload carries branchId, whGrpCode, and whGrpName from selected branch/document context. |
| Borrow type | Required before adding products or confirming. |
| Notes | Stored in notes and can be updated while the draft is editable. |
dateOfPickup | Sent only for SPEC; non-SPEC payload clears it to null. |
| Customer license plate | Required before adding items for non-main SPEC; kept in customerLicensePlate. |
| Pickup date edit lock | For non-main SPEC, pickup date cannot be changed after items exist. |
| Past pickup date | Mobile blocks pickup dates before the current day. |
Product And Quantity Rules
Important item rules:
| Rule | Main Branch | Non-main Branch |
|---|---|---|
| Expected purchase quantity | Not required; payload sends 0/undefined. | Required and must be a number greater than 0. |
| Quantity per line | SPEC (customer sample/inspection) only: maximum 3. Other borrow types have no per-line quantity cap. | Maximum 5 for every borrow type. |
| Number of product rows | SPEC only: maximum 3 rows. Other borrow types have no row-count cap. | No 3-row cap in the UI guard. |
| SPEC pickup/license plate | Not required by the branch-specific guard. | Required before adding product. |
| Quantity format | Positive integer only. | Positive integer only. |
| Stock guard | Add checks onHand - booking; edit allows available stock plus the original item quantity. | Same. |
The item payload is built from product inventory data, including product code/name, quantity, expected quantity, price, amount, weight, warehouse, company, tax/VAT, UOM, weight-base flag, step-price flag, and editPriceBy = null.
Editing Existing Items
Existing item edits are allowed only while the document is an editable draft. On edit, the app reloads the current product inventory by product code and calculates maxEditableStock = availableStock + originalQty for the same product. This lets the user keep or reduce an existing reservation while still preventing the final quantity from exceeding stock.
For non-main branch edits, expectedQty remains required and must be greater than 0. The per-line quantity cap remains 5 for every non-main borrow type. On the main branch, the cap is 3 only for SPEC; other borrow types have no per-line cap. All types still require a positive integer quantity and available stock.
Product Picker And First-Item Persistence
- When an existing borrow has a
borrowId, adding products does not show a blocking full-screen loading state. The product picker remains open after a successful add so the operator can continue adding products. - A new draft follows a two-stage persistence flow: create the borrow header first, use the returned
borrowIdas the item-create identity, then create the first item. borrowNois a display/document number and is not used as a substitute forborrowIdwhen creating the item.- After the first item is created, the app refreshes the borrow detail/list and synchronizes the route with the returned borrow identity. The header and first item therefore appear immediately instead of only after a second item is added.
- Mobile and POS Terminal follow the same persistence behavior.
Confirm, Accept, Complete, Retry
Confirm saves the latest header first, then calls confirmBorrow; draft editing is locked after confirmation. Acceptance records acceptedAt. The return/stock-cut action still requires processing permission, an accepted document, and eligible items.
completeBorrowStock(borrowId, detail) dispatches requests in this order on Mobile, POS HomeScreen, and Storefront POS:
| Priority | Current detail | POST endpoint | Body |
|---|---|---|---|
| 1 | borrowStatus === "PARTIAL_CONVERTED" | /api/borrow/{id}/return-items | IDs of all items with nonempty lots |
| 2 | Other eligible status and hasFailedSubBorrow | /api/borrow/{id}/retry | No body |
| 3 | Other eligible status without failed sub-borrow | /api/borrow/{id}/approve | No body |
PARTIAL_CONVERTED takes priority even when hasFailedSubBorrow is true. It can expose return despite convertedOrderId/convertedAt, provided at least one item still has lots. For other statuses, the existing guard permits accepted WAITING/ACCEPTED documents and DONE with a failed sub-borrow, and blocks already-converted documents.
Example: items 11 and 12 have empty lots after conversion, while item 13 still has lots. Confirming return sends:
POST /api/borrow/{id}/return-items
{
"borrowItemIds": [13]
}
Lots already belong to each item; there is no lot-selection or return-item-selection step. The payload includes all items whose lots is a nonempty array, excludes converted rows, and removes duplicate IDs. No request is sent if there are no remaining items or any remaining item has an invalid ID. API errors are shown without falling back to /approve or /retry; successful actions refresh the document and related list/workspace data.
Convert To Sales Order
The convert button requires all of these conditions:
| Condition | Required value |
|---|---|
| Branch | Main branch (isMainBranch = true) |
| Borrow type | SPEC |
| Saved and accepted | Nonempty borrowId and acceptedAt; DONE is not required |
| Not already converted | No convertedOrderId or convertedAt, and neither CONVERTED nor PARTIAL_CONVERTED |
| User | Document owner, or department -2, 1, 2, 4, 7, 8, 9, 13, 16, 20, 21, 23, 29 |
Pressing convert opens the shared BorrowConversionModal on Mobile, POS HomeScreen, and Storefront POS:
- Each opening starts with no items selected. Select individual rows or เลือกทั้งหมด (Select all); selecting all again clears the selection.
- Each row shows only product name, product code, and quantity. A visible checkbox/checkmark and selected border indicate selection; the modal also shows the selected count.
- Select at least one item and press ยืนยัน. The app validates the selection against the current items and submits only those borrow item IDs:
POST /api/borrow/{id}/convert
{
"borrowItemIds": [11, 12]
}
The compact layout uses a product-name font of 14.4 (10% smaller), card padding 7 (50% smaller), list/card gap 6, and line gap 2. While submitting, the label becomes กำลังแปลง… and selection, dismissal, and duplicate submission are blocked. If the API fails, the modal preserves the selection and shows the error inline.
A successful conversion refreshes the borrow detail, including backend status and convertedOrderId. PARTIAL_CONVERTED displays แปลงและรอคืน in the lists. In the detail, a row with empty, missing, or null lots under this status is treated as converted and receives a light red background (rgba(239,68,68,0.16)). Rows with lots retain their normal background and are included in the remaining-return flow. Empty lots under other statuses do not trigger the converted-row styling. A partially converted document cannot be converted again.
In StorefrontPosStack, a converted borrow exposes Go to sales order. The handoff loads getOrderDetail(convertedOrderId) before opening the product workspace in order mode. Persisted order fields are the source of truth for items, pickup/shipping, addresses, route, coordinates, and document flags. The terminal additionally loads the customer profile by customerCode for tier, points, benefits, and address fallback; failure to load that profile does not prevent the order from opening.
Borrow item identity
Both payload builders use the first non-null ID field in itemId → borrowItemId → borrowDetailId → id. Numeric strings are normalized to numbers; IDs must be positive safe integers. Product codes and the borrow document ID are never substituted for item IDs. Conversion also rejects an empty or stale selection and removes duplicate IDs.
Print Rules
Two print paths are exposed:
| Behavior | |
|---|---|
| Borrow pack | Prints customer/borrow/product packing document. |
| Borrow storage | Prints warehouse/storage-facing borrow document. |
Both print actions require at least one product item. PARTIAL_CONVERTED remains printable. Printing does not change business status by itself.
Permissions And Guards
| Permission | Rule |
|---|---|
| Terminal menu key | borrowFish |
| Menu deptCode set | -2, 1, 2, 3, 4, 7, 29, 21, 8, 9, 12, 13, 14, 30, 16, 20, 22, 23, 27, 28 |
| Create borrow | Admin, or dept -2, 1, 2, 4, 7, 8, 9, 13, 16, 21, 29 |
| Cancel/delete borrow | Admin; owner when status NEW; or allowed cancel depts/users while status is NEW or WAITING |
| Cancel departments | -2, 1, 2, 4, 9; dept 8 and 22 also override for NEW/WAITING |
| Cancel override user codes | 008045, 012521, 012508, 008051, 012388, 012428, 008060 |
| B1 processing | Admin, or dept 2, 8, 12, 22, 28 |
| Owner processing | The document owner can process if borrow type is not DESTROY |
| Processing guard | Main branch with B1 permission, or owner processing as above |
| Draft editing | Status must be NEW and current user must own the draft, or the draft is not persisted yet |
| Borrow type editing | Allowed before persistence or by the document owner |
Mobile vs POS Terminal
| Topic | Mobile | POS Terminal |
|---|---|---|
| Navigation | Full borrow stack list/detail | HomeScreen split-pane menu borrowFish |
| Product flow | Borrow detail screen and product-flow hook | Borrow detail form, panels, and product picker |
| Header save | Saves through borrow service when existing detail changes | Saves raw detail through HomeScreen handlers |
| Validation | Same business rules in mobile components | Same business rules in HomeScreen handlers |
| Printing | printBorrowPack, printBorrowStorage | Same print utilities |
| Conversion | Shared item-selection modal and selected borrowItemIds | Same modal and contract in HomeScreen and Storefront POS; Storefront can open convertedOrderId in order mode |
| Partial return | completeBorrowStock sends remaining item IDs to /return-items | Same routing and lots filter in both Terminal flows |
API And Data Map
| Operation | Service |
|---|---|
| List | getBorrows |
| Detail | getBorrowDetail(borrowId) |
| Create/update/delete header | createBorrow, updateBorrow, deleteBorrow |
| Item CRUD | createBorrowItem, updateBorrowItem, deleteBorrowItem |
| Confirm | confirmBorrow |
| Accept | acceptBorrow |
| Complete/cut stock dispatcher | completeBorrowStock(borrowId, detail) |
| Normal completion | approveBorrow |
| Return remaining items | returnBorrowItems(borrowId, payload) |
| Retry failed sub-borrow | retryBorrow |
| Convert to sales order | convertBorrow(borrowId, payload) |
Developer Handoff Map
| Area | Code |
|---|---|
| Mobile permissions/helpers | apps/mobile/src/utils/borrow.ts |
| Mobile list/detail | apps/mobile/src/screens/borrowStack/borrowListScreen, borrowDetailScreen |
| Mobile product flow | apps/mobile/src/screens/borrowStack/borrowDetailScreen/useBorrowProductFlow.ts |
| Terminal permissions/panels | apps/pos-terminal/src/screens/HomeScreen/BorrowPanels.tsx |
| Terminal detail | apps/pos-terminal/src/screens/HomeScreen/BorrowDetailForm.tsx |
| Terminal handlers | apps/pos-terminal/src/screens/HomeScreen/index.tsx |
| Shared conversion UI | packages/rn-ui/src/BorrowConversionModal.tsx |
| Shared guards, status, item IDs and payloads | packages/utils/src/borrow.ts |
| Payload types | packages/types/src/borrow.ts |
| Storefront flow | apps/pos-terminal/src/screens/StorefrontPosStack/borrowFlow.ts |
| Mobile/shared regression tests | apps/mobile/__tests__/borrowConversionModal.test.tsx, borrowConversionService.test.ts, borrowPartialConversion.test.ts, borrowReturnItemsService.test.ts |
| Storefront regression tests | apps/pos-terminal/src/screens/StorefrontPosStack/__tests__/borrowFlow.test.ts |
| Services | apps/*/src/services/borrow.ts |
| Converted target | Sales Order docs and order services |