Feature Reference

Feature Catalog

Everything the PMJ Sales platform does — the web dashboard, the admin console, the Android in-store estimation app, and the services behind them — organized by area. Use this page as the single reference for what exists and where it lives.

Authentication & Roles

Staff sign in at /login with their Employee ID and password. Web sessions use short-lived access tokens with a rotating refresh cookie (both httpOnly); the Android app receives long-lived (5-day) bearer tokens from the same credentials via the compatibility facade. Every request is authorized server-side, deny-by-default — there are no client-side permission lists.

The platform has seven roles:

ADMIN

Full access: user administration, bulk uploads, metal prices, master-data settings, IP allowlist, Synergics — and the entire unscoped product catalog.

MANAGER

Metal-price editing only on the admin side (the daily INR/USD rate board), plus catalogue browsing within assigned scope.

SALES_REP

The sales-floor role: customer check-in, catalogue browsing scoped to assigned locations/departments (minus category exclusions), estimates, wishlist, compare and scan.

CUSTOMER_CARE

Identical sales-floor capabilities to SALES_REP — a separate label for the customer-care desk.

MERCHANDIZER

Read-only product browsers (inventory, catalogue, Best Seller, Social Media) within assigned locations/departments. No users, settings or imports.

HEAD_OFFICE

Everything MERCHANDIZER has, plus the WIP and Design Bank channel browsers.

CUSTOMER

Never logs in. A customer is an identity a rep checks in against — visits, wishlists and estimates are attributed to it, but no dashboard or password exists.

Scoping model: each staff user carries location and department assignments (separate scopes for the estimation floor and the digital catalogue). Product lists, details, similar-product queries, counts and aggregates are all filtered through one policy: assigned locations ∪ assigned departments, minus category-exclusion rules and inactive taxonomy for sales roles. Zero assignments means an empty catalogue — nothing is visible by default. A "select all" option at user creation assigns every location/department explicitly.

Customers never authenticate

The legacy system issued customers passwordless JWTs (password = phone number). In the new model a "customer login" is simply a server-side active visit owned by the rep's session — no customer token exists anywhere.

Customer Management

Customer check-in

At /customer-entry the rep types a phone number with autocomplete, reviews the customer's previous store visits, and starts a visit. The checked-in customer becomes the rep's active-visit context.

Quick create

Unknown phone number? Create a customer instantly with just phone + name (also available from the Android app), then check them in.

Full registration

Complete form: name, phone, date of birth, gender, cascading country/state/city selects, pincode, address, and 'how did you hear about us' (Social Media / Website / Research / Marketing).

Visit history & checkout

Every check-in and check-out is recorded as a Visit with timestamps. Reps browse a customer's full visit history; checkout stamps the visit closed.

Customer directory

Admins browse and search all customers at /admin/customers — card grid with photo, contact details and edit popups.

Customer profiles

Demographics (birthday, anniversary, marital status, profession, gender), address, profile-image upload, and inline profile editing.

Active-visit context

Wishlist adds and estimates automatically attach to the checked-in customer and the current visit — walk-in estimates (no customer) are also supported.

Customers under a rep

Each rep sees the customers who visited them (their own check-ins), with real visit counts — no more hardcoded placeholder stats.

Inventory Catalogue

The live in-store inventory is browsed at /catalogue — a paginated product grid backed by /api/products, rebuilt continuously from the ERP feed.

Full filter bar

Multi-select filters for Classification, Category, Product Type, Sub Category, Location, Size, Collection and Purity — every dropdown backed by live master data.

Range filters

Min/max Price, min/max Net Weight, and min/max Diamond Weight ranges, validated server-side.

Search & sort

Free-text search (title, product code / barcode) plus a sort-by dropdown (price, weight, newest). Removable filter chips show every active filter.

Pagination

12 cards per page with total counts ('X of Y products'), page controls and a 'go to page N' box; a 'Last updated' stamp shows the most recent import.

Product detail

/product/[id]: image zoom/magnifier and gallery, variant name, product code, price with ₹/$ symbol ('Inclusive of all taxes' for INR), stock quantity, colour swatch, and certificate number.

BOM / stone breakdown

The full bill of materials: every stone line with name, diamond flag, pieces, carat weight, rate per carat and amount — the same lines the estimate engine prices.

Related products

A 'similar products' rail on every detail page, scoped by the same visibility rules as the main list.

Ageing & trend buckets

Every product carries barcode-date ageing (days in stock) and a computed trend bucket — Trending, Best Seller, Current and Timeless — powering the Digital Catalogue views.

Aggregate totals

Admin browsers include 'Calculate Total' (/api/products/total-calculation): total gross weight, net weight, diamond weight, stone weight and piece count for the current filter set — now permission-scoped.

Multi-select catalogue PDF

Select any number of products in the admin browser and generate a shareable multi-product catalogue PDF.

Stable product identity

Products are keyed by product_code (the barcode) per channel. Re-imports upsert instead of wiping the table, so estimates, wishlists and links never dangle after a sync.

The Four Channels

Beyond the live inventory, four additional product channels are kept with full functionality — each with its own browse page, settings, bulk uploads, estimates and PDF generation, all implemented on the unified product model with a channel discriminator.

Region policy

Channel visibility is enforced on the server per region (historically the four channels were India-only surfaces) — deep-linking cannot bypass it.

Estimation Engine

Estimates are the heartbeat of the system (thousands per day). Every estimate is computed server-side via POST /api/estimates/preview and persisted with its complete numeric breakdown — the PDFs are derived artifacts, regenerable at any time.

metal    = netWeight × metalRate(purity, currency)   // live rate board
making   = netWeight × makingRatePerGram              // editable by rep
wastage  = wastage% × metal                           // "Vad", editable
stones   = Σ (stoneWeightCt × ratePerCt)              // per-line editable
subtotal = metal + making + wastage + stones

INR: total = subtotal × 1.03            // 3% GST; TCS shown as a 0% line
USD: total = subtotal × (1 + duty%)     // customs duty first…
                      × (1 + salesTax%) // …sales tax on the duty-inclusive amount

Editable rates

In the estimate popup the rep can override the making rate (per gram), the wastage/'Vad' percentage, and every stone or diamond's per-carat rate; totals recompute live.

Currency-aware tax

INR estimates add 3% GST with a 0% TCS display line; USD estimates apply the location's customs-duty % first, then sales-tax % on the duty-inclusive amount.

Discount warning & bounds

Lowering rates below the list price raises a discount warning with the computed discount %. Overrides are validated server-side, with a configurable manager-approval threshold.

Two PDF variants

Each estimate renders a 'With Rate' and a 'Without Rate' PDF (rates hidden) — printed via the browser or shared directly.

WhatsApp share

One tap shares the generated PDF URL through WhatsApp; the share is stamped (sharedAt + which variant) on the estimate record.

Persisted history

Every estimate stores product code, title, purity, weights, currency, all rates and amounts, stone lines, tax lines, total, and printedAt / sharedAt timestamps.

My Estimates

Server-side estimate history per rep (also exposed to the Android app as my-estimates) — history no longer dies with a device.

Customer linkage

Estimates created during an active visit attach the customer and the visit — enabling real conversion reporting for the first time.

Print vs share are distinct events

printedAt and sharedAt are separate timestamps — replacing the ambiguous legacy boolean that only ever meant "shared once via WhatsApp from the dashboard".

Wishlist

Per customer, per channel

One wishlist per checked-in customer, organized into Inventory / Best Seller / Social Media tabs at /wishlist — every heart tap anywhere in the app writes the same unified store.

Date-grouped view

A dates column groups wishlist entries by the day they were added; selecting a date filters the product grid — and the date filter genuinely filters (a legacy bug fixed).

Toggle semantics

Tapping the heart adds; tapping again removes. The toggle is atomic and duplicate-proof — one entry per (customer, channel, product code).

Barcode add

The 'Scan Product' action on the wishlist page adds a product by its scanned/typed stock code (/api/wishlist/toggle).

PDF share

Generate a wishlist PDF (per date, across channels) and share it with the customer via WhatsApp — the share endpoint is properly authenticated.

Rep attribution

The acting sales rep is recorded on every wishlist add — attribution the legacy system never captured.

Compare & Barcode Scan

Compare up to 4

Select up to four products with card checkboxes anywhere in the catalogue and view them side-by-side — images, weights, stones and prices in aligned columns; remove one or clear all.

Barcode / QR scan

Scan a product's barcode or QR (or type the stock code) to jump straight to its detail page — the scan resolves stock code → product server-side.

Scan-to-wishlist

The same scanner is available inside the wishlist page to add scanned items directly to the checked-in customer's list.

Search-as-scan

The catalogue search box accepts full barcodes, so a handheld scanner wedge works everywhere a search field exists (this is also how the Android app scans).

Pricing & Metal-Rate Board

Daily metal rates drive all live pricing. Admins (and Managers — it is their one admin surface) maintain the board at /admin/metal-prices.

Dual-currency tabs

Separate INR and USD rate boards, each editable per purity row.

Every purity

24kt, 22kt, 18kt, 14kt gold, silver, 999 fine, and platinum (95) — classifications flagged 'uses platinum rate' price against the platinum row.

Effective-dated snapshots

Every rate change is a new snapshot row with who-set-it and when — a full audit history instead of a single overwritten record.

Automatic repricing

A worker job reprices every product's list price when rates change and after every import, with immediate cache invalidation (no stale-price window).

The daily gate

Until today's rates are entered, the sales floor is gated — reps cannot produce estimates against yesterday's metal prices.

Purity fallbacks

Products whose purity has no live rate fall back to their imported seed rate, so an incomplete board never zeroes a price.

ERP Ingestion & Bulk Uploads

The product catalog is a projection of the ERP's stock file. Two sources feed it, both visible at /admin/imports:

Automatic email ingestion

An hourly IMAP poll picks up the ERP inventory CSV email, downloads the file and starts an import run — hands-free daily sync.

Manual uploads per channel

Upload buttons for Inventory, Best Seller, Social Media, WIP and Design Bank files — each channel has its own column dialect, parsed accordingly.

Idempotent upsert

Rows upsert by (channel, product code); products missing from the file are soft-deactivated, never deleted. Readers never see an empty catalog mid-sync and references never break.

Import-run status

Every run is tracked PENDING → RUNNING → COMPLETED (or CANCELLED) with live progress, rows read/created/updated/deactivated, and the source-file download.

Row-level error report

Bad rows are quarantined with per-row error details instead of silently cancelling the whole file; the processed-file report is downloadable.

Taxonomy auto-build

The 4-level jewellery taxonomy (product type → category → sub → sub-sub) is derived automatically from VARIANT NAME code slices during import.

Purity & colour parsing

KT fields like '18,ROSE' are parsed into purity + colour — all pairs, not just the first. Sizes are read and maintained.

Safe new locations

Locations first seen in a file enter a 'pending configuration' state instead of silently pricing as INR with zero taxes.

Post-import steps

Each successful run triggers repricing, ageing recomputation and an Excel export; failures send an alert email.

Master Data & Settings

All reference data is administered at /admin/settings — datatables with inline edit dialogs, backed by /api/admin/settings/*.

Jewellery taxonomy

Product Types, Categories, Sub Categories and Sub-Sub Categories across all channels — titles, images, active flags, and 'Specials' flags for WIP / Design Bank taxonomy.

Classifications

Product classifications with an explicit 'uses platinum rate' flag (replacing the legacy magic PLATINUM code).

Locations

Store locations carry the pricing context: currency (INR/USD), customs-duty %, sales-tax %, geofence latitude/longitude/range, active and configured flags.

Collections, vendors, sizes, purities

Full CRUD for collections, vendors, size masters, and metal purities (purity + colour lookup with fallback rates).

Departments

Product departments — the second axis of staff visibility scoping alongside locations.

Category exclusions

The category/product-type exclusion rules that hide internal stock from sales roles are seeded, editable data (/api/admin/settings/category-exclusions) — no longer 10 scattered hardcoded copies.

Excluded location names

The internal/manufacturing location names hidden from curated views (17 legacy constants) are an editable table (/api/admin/settings/excluded-names).

Store IP Allowlist & Location Visibility

IP allowlist admin

/admin/ip-allowlist: register store IPs with labels, toggle entries active/inactive, edit in place. Sales-floor catalogue access from unregistered networks is blocked.

Server-derived IP

The check uses the real connection IP from the platform's own proxy — not a client-supplied header (the legacy spoofable x-ip-address is accepted from Android as advisory telemetry only).

Location/department scoping

Product visibility = assigned locations ∪ assigned departments, applied identically to lists, details, similar products, counts and aggregates.

Separate catalogue scope

The digital catalogue has its own location-assignment scope per user, distinct from the estimation-floor scope.

Geofenced locations

Locations can carry latitude/longitude and a geofence range for proximity-based visibility.

Synergics ERP Integration

The Synergics finance integration is India-only and runs behind a feature flag.

Credentials vault

Synergics user credentials are stored encrypted (plaintext storage in the legacy system is gone) and managed from the admin console.

Finance files

Upload Synergics finance files and browse the finance-file table with dates and download links.

Image fallback

Wishlist/product documents can fall back to Synergics ERP image URLs for codes not in the local catalog.

User Administration & Assignments

Staff directory

/admin/users: searchable card grid filtered by role, showing photo, name, email, role, assigned locations and per-user estimate counts.

Create & edit staff

Add users with name, gender, email, phone, password, photo and a role from the 7-role enum; edit any field later. Roles are a single enum — no more 18 overlapping boolean flags.

Activate / deactivate

One-tap toggle disables a user's login without deleting history (deactivated users fail auth immediately).

Location & department assignment

Assign each user's visible locations and departments per scope (estimation floor vs digital catalogue), with a select-all shortcut (/api/admin/users/[id]/locations).

Customer directory

/admin/customers: the customer database with search and profile editing.

Profile & password

/profile: every staff member manages their own profile and changes their password — with correct validation (the legacy 'new password must equal old password' bug is gone).

Reporting & Attendance

Estimates per rep per day

Because every estimate is persisted with its rep, customer and timestamps, estimates/day, share rate (shared vs printed) and per-product interest are real queries — not the hardcoded stub numbers of the legacy stats endpoints.

Attendance

Check-in/check-out Visit records double as store attendance — daily footfall, per-rep visit counts and durations.

Customer analytics

Distinct customers served per rep, visit history per customer, and estimate-to-customer linkage via visits.

Import & pricing audit

Import-run history with row stats, and the effective-dated metal-price history (who changed which rate, when).

Excel exports

Catalogue Excel exports are generated after each import and downloadable from the admin console.

Regions: India & USA

The platform runs in two regions from one codebase: region is selected by the deployment environment (each region has its own database), and everything currency-related is data-driven on the Location — currency, customs-duty % and sales-tax % live on each store location, not in code.

India (INR, ₹)

Estimates add 3% GST with a TCS display line; PAN-card terms appear on estimate PDFs above ₹2 lakh; Synergics and the four channels are India surfaces.

USA (USD, $)

Estimates apply customs duty then sales tax; PDFs drop the GST/TCS/PAN lines and use State-ID terms; USD amounts render with the $ symbol.

Server-enforced region gating

Region-specific surfaces are hidden and blocked on the server — no separate regional builds or deep-link bypasses.

Dual hostnames

The Android compatibility facade answers on both regional hosts, so shipped APKs keep working in either region.

Android In-Store App

The Android app is the in-store estimate-and-print tool carried by sales reps. It talks to the byte-compatible /sales-backend/ facade (the 32 legacy endpoints plus new my-estimates and health routes), so shipped APKs work unchanged.

Login with region picker

Employee ID + password with an India/US segmented picker and remember-me; sessions persist on-device.

Dashboard

Today's metal rates (22K/18K/14K gold + silver in the region's currency), today's estimate count, and quick actions to start an estimate or find a customer.

Catalogue browsing

Infinite-scroll product grid with search (type or scan a barcode into the search box) and filters: classification, category, product type, sub-category, location, collection, price / net-weight / diamond-weight ranges.

Best Seller & Social Media

Both channels are browsable with their own filters; Best Seller has its own estimate dialog, Social Media is browse/detail with stone details.

Estimate dialog

Full breakdown against live metal rates — gold, making, wastage/'Vad', diamonds, stones, GST or duty+sales-tax, final amount — with an Edit mode to override making rate, wastage % and per-stone rates.

Customers

Recent (own) and All (global search) customer tabs, customer detail, and quick-create by phone + name.

Compare

Long-press multi-select in any grid opens a side-by-side comparison of the selected products.

Print & share

'With rate' or 'Without rate' thermal slips; share posts to estimate-pdf and sends the returned PDF URL via WhatsApp (with a generic share-sheet fallback).

Estimate history

Today/Week/All history with exact reprint of any past slip and WhatsApp re-share — now backed by server-side history via my-estimates.

Offline resilience

Customer lists and estimate history/reprint work offline; browsing and new estimates require connectivity.

Four printer backends:

Printer management includes an add-printer wizard (Wi-Fi / Bluetooth / USB), a default-printer selection, auto-reconnect before each job, 58/80mm paper widths, a print-queue mutex so jobs never interleave, and a test-print screen.

PDF & Document Generation

Estimate PDFs (two variants)

'With Rate' and 'Without Rate' renditions of every estimate: 'Estimate Only' header, item table (Gold / Making / Vad / Dia / Cls), stone-detail table with diamond rows first, and Indian digit grouping for INR amounts.

Currency-correct terms

INR PDFs carry the PAN-card (above ₹2 lakh) and 'valid today only' terms; USD PDFs drop GST/TCS/PAN lines and use State-ID terms. Terms text is configuration, not code.

Product-card PDF

A single-product share card generated from any detail page.

Wishlist PDF

A per-customer, per-date wishlist document across channels for WhatsApp sharing.

Multi-product catalogue PDF

Select products in any admin browser (all five channels) and generate a combined catalogue PDF.

Excel exports

Streaming Excel exports of the catalog after each import run, downloadable per run.

Shared-bucket URLs

All documents upload to the shared object-storage bucket and return stable URLs — the contract both the dashboard and Android rely on for WhatsApp sharing.

Security & RBAC

Deny-by-default authorization

Every route passes through server-side RBAC middleware; unknown roles get nothing. No client-side ability arrays exist to forge.

One scoping helper

A single scopeProducts(user) policy is applied to list, detail, similar, count and aggregate queries alike — the aggregate and detail leaks of the legacy system are closed.

Validated boundaries

Every request body and query string is zod-validated; estimate rate overrides are bounded server-side and every edit is recorded.

Session hygiene

Short-lived web access tokens with rotating httpOnly refresh cookies; 5-day tokens are issued only to the Android facade; login is rate-limited.

Audit logging

Admin mutations (users, prices, settings, imports) are written to an audit trail.

Secrets & credentials

Secrets live only in environment variables; Synergics credentials are stored encrypted; no CORS wildcard; the repository is secret-clean by construction.

Real IP checks

Store IP allowlisting validates the server-derived connection IP, never a spoofable client header.

No anonymous data leaks

Legacy unauthenticated endpoints (full-inventory aggregates, product detail by slug, report Excel dumps) are gone — everything requires an authenticated, scoped session.