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
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
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
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 amountEditable 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.