Employees and stores (Vendor)
Executive summary
Section titled “Executive summary”Stores require the store module (read/write per level). Employees require the employee module: read to list/view, manager to create/edit/delete or manage role assignments on staff. Roles (/dashboard/roles) remain owner-only. Forced store_id on an employee locks StoreSelector.
Goals and non-goals
Section titled “Goals and non-goals”| Goals | Non-goals |
|---|---|
| Document owner-only governance surfaces | Let employees self-elevate to owner |
| Explain store lock behavior | Replace HR onboarding systems |
Current platform behavior
Section titled “Current platform behavior”- Stores:
module: "store"inROUTE_RULES(not owner-only). - Employees:
module: "employee"; create/edit paths need employee manager level. - Roles:
ownerOnly: trueinROUTE_RULES(Roles).
Sequence: happy path
Section titled “Sequence: happy path”- User with store write opens Stores → create/edit locations.
- User with employee manager opens Employees → assign roles/modules/store.
- Employee logs in → sees allowed modules only (levels + capabilities).
Security and operations
Section titled “Security and operations”- Owners are highly privileged—use MFA/policy per org.
- Revoke ex-employees promptly in Employees.
Test scenarios (UAT / QA)
Section titled “Test scenarios (UAT / QA)”| ID | Scenario | Expected |
|---|---|---|
| ES1 | Employee without store hits /dashboard/stores | Redirect/block |
| ES2 | User with employee manager creates staff | Login works with assigned modules/levels |
| ES4 | Employee with employee read only | Can list; cannot open create/edit |
| ES3 | Forced store_id | Cannot switch stores |
Routes
Section titled “Routes”| Area | Routes |
|---|---|
| Stores | /dashboard/stores (New store opens a create dialog), /dashboard/stores/create (redirects to /dashboard/stores?storeCreate=1 and opens the same dialog), /dashboard/stores/:id, /dashboard/stores/:id/edit |
| Employees | /dashboard/employees, /dashboard/employees/create, /dashboard/employees/:id/edit |
Access: Stores need the store module. Employees need employee (read for list/detail; manager for create/edit and API mutations). Backend: VendorEmployeesController uses VendorModuleGuard + @RequireVendorModuleLevel('manager') on writes.
Roles (preset templates and module levels) stay owner-only at /dashboard/roles. See Roles (Vendor).
Store switcher (all dashboard users)
Section titled “Store switcher (all dashboard users)”The top bar StoreSelector (components/dashboard/layout/TopBar.tsx) reads useVendorStore:
fetchVendorStoresListloads stores fromGET /auth/vendor/context/stores(seevendor-user-management.api.ts).- Selection is persisted (
nipos-vendor-store-storage) asselectedStoreId. - Vendor employees (
vendor_type === "employee"): ifprofile.store_idis set and exists in the list, selection is forced to that store.
Create/update/delete stores use POST/PATCH/DELETE /vendor/stores.
POS receipt, tip, and cash drawer (store form)
Section titled “POS receipt, tip, and cash drawer (store form)”Optional fields on PATCH /vendor/stores/:id (and create) are merged into stores.dashboard_prefs under pos:
| Body field | pos JSON key | Meaning |
|---|---|---|
pos_receipt_print_mode (0–3) | receipt_print_mode | 0 none, 1 merchant copy, 2 customer, 3 both |
pos_receipt_signature_mode (0–3) | receipt_signature_mode | Same pattern for signature capture |
pos_cash_drawer_mode (0–3) | cash_drawer_mode (+ cash_drawer_on_cash_payment when auto is on) | Same bitmask as receipt copies: 0 off, 1 show “open drawer” in POS / create order (manual), 2 pulse drawer when a cash order completes, 3 both. Legacy rows with only cash_drawer_on_cash_payment (0/1) are read as 0 or 2; canonical cash_drawer_mode wins when present. |
pos_tip_enabled | tip_enabled | Show tip suggestions |
pos_tip_basis | tip_basis | percent or fixed |
pos_tip_percent_presets | tip_percent_presets | Array of quick % buttons (e.g. 15, 18, 20) |
pos_tip_default_percent | tip_default_percent | Default selected % (0–100) |
pos_tip_default_fixed_amount | tip_default_fixed_amount | Default fixed amount hint (string) |
On create, the store form still uses separate tabs for basic fields (cover photo, logo, contact, tax/minimum order, address) and Business location (latitude/longitude + inline OpenStreetMap picker: click map or search to set coordinates and optionally refresh address), plus POS receipts, tips & cash drawer. On edit, open Configuration: General groups Store profile (cover, logo, name, contact, module, status, tax/minimum order) and Business location (full address textarea, Pick on map modal, coordinates, regional fields). Inner tab POS holds receipt copies, signature, barcode, cash drawer, and tip settings—the same checkboxes and selects as the create tab; saves map to dashboard_prefs.pos as the 0–3 receipt_print_mode, receipt_signature_mode, and cash-drawer fields in the table above. POS and create-order flows read the same dashboard_prefs JSON.
Wholesale (B2B) credit defaults (store configuration)
Section titled “Wholesale (B2B) credit defaults (store configuration)”The Configuration tab also persists store-wide defaults for wholesale sales recorded on account (debt). These fields are sent on PATCH /vendor/stores/:id/configuration (vendor store configuration API). Customer rows and customer categories override each field when the customer or category stores a non-null value (precedence: customer → category → store).
| Body field | Meaning |
|---|---|
wholesale_credit_term_mode | 0 no automatic due rule, 1 due N calendar days after invoice date, 2 due on day X of the next calendar month |
wholesale_credit_deadline_days | Positive integer when mode is 1 |
wholesale_credit_due_day_of_month | 1–28 when mode is 2 (server clamps to month length when needed) |
wholesale_max_open_debt_rounds | Maximum concurrent open wholesale debt rounds (receivable-based); null = no cap |
wholesale_max_open_debt_rounds_rule | When at-cap on a new wholesale debt order: 0 allow, 1 warn, 2 deny |
Schema: columns live on store_configurations (migration 1782800000000-WholesaleCreditPolicyColumns). Backend helpers: resolveWholesaleCreditPolicy, evaluateWholesaleCreditPlacement in apps/backend/src/modules/pos/wholesale-credit-policy.util.ts.
Negative stock at checkout (store configuration)
Section titled “Negative stock at checkout (store configuration)”When sale_allow_negative_stock is true on store_configurations, POS and vendor order flows skip the server-side aggregated stock guard so lines may exceed on-hand quantity (inventory can go negative). The flag is exposed under Configuration → Sales and sent on PATCH /vendor/stores/:id/configuration as sale_allow_negative_stock.
Schema: column sale_allow_negative_stock (migration 1783200000000-StoreConfigurationSaleAllowNegativeStock). Stock validation relaxation: validateAggregatedCartStock / PosService.placeOrderWithStock in apps/backend/src/modules/pos/pos.service.ts.
Open API key (Business location → store configuration)
Section titled “Open API key (Business location → store configuration)”Under Configuration → Business location, vendors can set or clear an optional Open API key (third-party integrations). GET configuration returns open_api_key_set and open_api_key_masked only — the plaintext secret is never exposed. PATCH /vendor/stores/:id/configuration accepts open_api_key: a non-empty string updates the stored value (trimmed, max length enforced server-side); null clears it. Clearing is also wired from the UI via a checkbox that sends null on save.
Schema: column open_api_key on store_configurations (migration 1783100000000-StoreConfigurationOpenApiKey). Serialization and masking:apps/backend/src/common/open-api-key-mask.util.ts; service tests:vendor-store-configuration.service.spec.ts.
Store Configuration tab — full settings checklist
Section titled “Store Configuration tab — full settings checklist”Per-field audit (all four inner tabs: UI, save, runtime, tests): Store configuration settings checklist.
Store Configuration tab — QA checklist vs automated tests
Section titled “Store Configuration tab — QA checklist vs automated tests”Run npm run test --workspace=@indochina/vendor-web for Vitest. Rows marked [AUTO] are exercised by unit tests on pure helpers (not full React/UI/E2E). Rows marked [MANUAL] still need browser/API verification.
| ID | Scenario | Check |
|---|---|---|
| SC-AUTO-01 | Inner-tab query params (warehouse, legacy aliases, VAT currency tab id) | [AUTO] store-configuration-inner-tab.spec.ts |
| SC-AUTO-02 | Document number preview / formatting helpers | [AUTO] vendor-store-configuration-sections.spec.ts (previewStoreConfigurationDocNumber) |
| SC-AUTO-03 | Section dirty-state + PATCH bundles (store_contact, address_prefs, money parsing, documents suffix → null, empty POS {}, open_api_key set/clear omission when unchanged) | [AUTO] vendor-store-configuration-sections.spec.ts |
| SC-AUTO-05 | Open API key: GET masking only, PATCH set/clear, length guardrails | [AUTO] vendor-store-configuration.service.spec.ts |
| SC-AUTO-04 | POS dashboard_prefs payload bits (receipt/signature/barcode/drawer, tip presets percent vs fixed) | [AUTO] vendor-store-pos-update-payload.spec.ts |
| SC-MAN-01 | Load store by id, save succeeds, toast/query invalidation | [MANUAL] |
| SC-MAN-02 | Field validation messages (Zod + i18n) on submit | [MANUAL] |
| SC-MAN-03 | Business location: create inline map (click/search → form); edit map preview + Pick on map modal; Open in Google Maps opens external URL; optional Open API key masked display, new-value field, clear checkbox | [MANUAL] (Configuration → Business location; create-store form right column) |
| SC-MAN-04 | Cross-section discard/revert UI when navigating away | [MANUAL] |
Employees vs roles
Section titled “Employees vs roles”| Concept | UI | API (representative) |
|---|---|---|
| Employee accounts (login users attached to vendor) | /dashboard/employees/* | GET/POST/PATCH/DELETE /vendor/vendor-employees and .../:id |
| Roles (named module bundles) | /dashboard/roles/* | GET/POST/PUT/DELETE /vendor/roles |
Employees are assigned roles that define modules / permissions; roles are not the same object as employee records.
Related
Section titled “Related”- Roles (Vendor) — module slugs and
/vendor/rolesAPI. - Vendor panel overview — owner-only routes.