Skip to content

Employees and stores (Vendor)

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.


GoalsNon-goals
Document owner-only governance surfacesLet employees self-elevate to owner
Explain store lock behaviorReplace HR onboarding systems

  • Stores: module: "store" in ROUTE_RULES (not owner-only).
  • Employees: module: "employee"; create/edit paths need employee manager level.
  • Roles: ownerOnly: true in ROUTE_RULES (Roles).

  1. User with store write opens Stores → create/edit locations.
  2. User with employee manager opens Employees → assign roles/modules/store.
  3. Employee logs in → sees allowed modules only (levels + capabilities).

  • Owners are highly privileged—use MFA/policy per org.
  • Revoke ex-employees promptly in Employees.

IDScenarioExpected
ES1Employee without store hits /dashboard/storesRedirect/block
ES2User with employee manager creates staffLogin works with assigned modules/levels
ES4Employee with employee read onlyCan list; cannot open create/edit
ES3Forced store_idCannot switch stores

AreaRoutes
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).

The top bar StoreSelector (components/dashboard/layout/TopBar.tsx) reads useVendorStore:

  • fetchVendorStoresList loads stores from GET /auth/vendor/context/stores (see vendor-user-management.api.ts).
  • Selection is persisted (nipos-vendor-store-storage) as selectedStoreId.
  • Vendor employees (vendor_type === "employee"): if profile.store_id is 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 fieldpos JSON keyMeaning
pos_receipt_print_mode (0–3)receipt_print_mode0 none, 1 merchant copy, 2 customer, 3 both
pos_receipt_signature_mode (0–3)receipt_signature_modeSame 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_enabledtip_enabledShow tip suggestions
pos_tip_basistip_basispercent or fixed
pos_tip_percent_presetstip_percent_presetsArray of quick % buttons (e.g. 15, 18, 20)
pos_tip_default_percenttip_default_percentDefault selected % (0–100)
pos_tip_default_fixed_amounttip_default_fixed_amountDefault 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 fieldMeaning
wholesale_credit_term_mode0 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_daysPositive integer when mode is 1
wholesale_credit_due_day_of_month1–28 when mode is 2 (server clamps to month length when needed)
wholesale_max_open_debt_roundsMaximum concurrent open wholesale debt rounds (receivable-based); null = no cap
wholesale_max_open_debt_rounds_ruleWhen 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.

IDScenarioCheck
SC-AUTO-01Inner-tab query params (warehouse, legacy aliases, VAT currency tab id)[AUTO] store-configuration-inner-tab.spec.ts
SC-AUTO-02Document number preview / formatting helpers[AUTO] vendor-store-configuration-sections.spec.ts (previewStoreConfigurationDocNumber)
SC-AUTO-03Section 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-05Open API key: GET masking only, PATCH set/clear, length guardrails[AUTO] vendor-store-configuration.service.spec.ts
SC-AUTO-04POS dashboard_prefs payload bits (receipt/signature/barcode/drawer, tip presets percent vs fixed)[AUTO] vendor-store-pos-update-payload.spec.ts
SC-MAN-01Load store by id, save succeeds, toast/query invalidation[MANUAL]
SC-MAN-02Field validation messages (Zod + i18n) on submit[MANUAL]
SC-MAN-03Business 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-04Cross-section discard/revert UI when navigating away[MANUAL]
ConceptUIAPI (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.