# Dava India ERP (internal name: Zota ERP / MySalesERP)

Laravel 9 application, developed **from scratch by GITCS**, for **Zota Healthcare's "Dava India"**
pharmacy retail chain. It started as a generic multi-tenant POS/ERP SaaS and has since been
extended into a **multi-tier pharmacy distribution chain ERP**: Retail Store → Regional Warehouse
→ Supplier Portal → Third-Party Manufacturer (TPM), all built on GITCS's own multi-`Business`
model (each store *and* each warehouse is its own `Business` row).

> Note: an earlier pass at this documentation mistakenly described this codebase as a fork of the
> third-party "UltimatePOS" product, based on surface-level structural resemblance (table/controller
> naming conventions). GITCS has confirmed this is incorrect — the application was built from
> scratch. That framing has been corrected throughout these docs.

See [`docs/zota_erp_system_diagram.md`](docs/zota_erp_system_diagram.md) for a full Mermaid system
map of every tier and layer described below.

## Where to look

| Doc | Covers |
|---|---|
| [`DATABASE_SCHEMA.md`](DATABASE_SCHEMA.md) | Every table (derived from all 365 `database/migrations/*.php` + module migrations), grouped by domain, with columns/FKs and 2026-dated pharmacy-specific additions called out |
| [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) | Routes, controllers (68 in `app/Http/Controllers`), middleware/auth stack, end-to-end workflows, scheduled console commands |
| [`docs/MODELS_AND_UTILS.md`](docs/MODELS_AND_UTILS.md) | Eloquent models (**live directly in `app/*.php`, not `app/Models/`**), `app/Utils/*.php` business-logic helpers, `app/Services/*.php` |
| [`docs/MODULES.md`](docs/MODULES.md) | The 7 `Modules/*` (nwidart/laravel-modules) add-ons: Accounting, AssetManagement, Connector, Essentials, Spreadsheet, Superadmin, SupportTicket |
| [`docs/PHARMACY_CUSTOMIZATIONS.md`](docs/PHARMACY_CUSTOMIZATIONS.md) | **Start here for "what's pharmacy/warehouse-specific vs. core generic ERP functionality"** — Doctor/Composition/Salt, the Supplier Portal WMS, warehouse bin/FEFO system, cycle counts, cold-chain, movement-tag velocity classification, plus the 2026-08 layer: batch/manufacturer traceability, PO follow-up + urgent replenishment, Compliance Management, Expiry & Disposal, bounce rate |
| [`docs/VIEWS_FRONTEND.md`](docs/VIEWS_FRONTEND.md) | `resources/views` layout (maps 1:1 to controllers by folder name), AdminLTE2+Tailwind hybrid frontend, i18n, PDF generation |

## Fastest orientation

1. **Identity**: this is not one tenant — it's a chain. A "business" row can be a retail store, a
   regional warehouse (`is_supplier_warehouse=1`), or (rarely) a template business. Products,
   variations, invoice schemes/layouts, and suppliers all use a **master/mirror** self-referential
   pattern (`master_product_id`, `master_variation_id`, `common_supplier_id`, etc.) to sync data
   from a chain-wide master down to every store.
2. **Core stock engine**: `app/Utils/ProductUtil.php` (qty mutations) and
   `app/Utils/TransactionUtil.php` (sell/purchase/payment lifecycle, ~7k lines, the biggest file
   in the app) are the two files nearly everything routes through. `TransactionUtil::mapPurchaseSell`
   is the FEFO/FIFO batch-consumption engine — pharmacy warehouses sort by `exp_date ASC` and
   exclude recalled/non-QC-passed batches.
3. **Pharmacy compliance layer**: `Doctor` (prescriber), `Composition`/`Salt` (drug formula, for
   generic substitution), `drug_schedule`/`prescription_required`/`hsn_code` on `products`, `mrp_inc_tax`
   on `variations`, Schedule H/H1/X narcotic register (`ScheduleRegisterUtil`), cold-chain
   (`TemperatureLog`), and a stubbed India GST e-Invoice/e-Way-Bill gateway
   (`App\Services\EInvoiceGateway` / `StubEInvoiceGateway` — **stub only, not wired to a real GSP
   yet**).
4. **Warehouse WMS layer**: hierarchical bin locations (`WarehouseBin`/`WarehouseBinStock`, zone →
   aisle → rack → shelf → bin), Goods QC quarantine on receipt, cycle counting (`StockCount`),
   consignment-based dispatch with POD, inter-warehouse rebalancing, and a whole family of
   `Warehouse*Util`/`*IntelligenceUtil` classes for demand forecasting and procurement — all
   built specifically for this pharmacy chain, on top of the app's core generic ERP functionality.
5. **Auth model**: three parallel identity tracks — normal store users (`user_type='user'`),
   supplier/warehouse portal logins (`user_type='user_supplier'`, scoped by `common_supplier_id`
   not `business_id`), and superadmin (config-file username whitelist, not a DB role). No global
   Eloquent `business_id` scope exists — every controller filters manually.
6. **Permissions**: `spatie/laravel-permission`, checked inline per controller action
   (`auth()->user()->can('resource.action')`), not via route middleware groups.
   Two gotchas: `Gate::before` (AuthServiceProvider) grants EVERY ability to any holder of
   `Admin#<their business>`, so `can()` is useless for gating a chain-wide screen — those use
   `hasPermissionTo()` instead; and role management is superadmin-only
   (`RoleController::__construct`).
7. **2026-08 layer** (see PHARMACY_CUSTOMIZATIONS §5-9): batch/manufacturer traceability down to
   the customer invoice; warehouse PO follow-up (T-15/T-7/daily chase ladder + escalation);
   urgent off-cycle store POs at 50% of min; Compliance Management (licence register);
   Expiry & Disposal (store → warehouse → certified destruction, plus a chain-wide analytics
   board); bounce rate. Bulk product import was also overhauled — 57 generated columns, dry-run,
   and `pos:syncMasterProducts` for large loads.

## Known open work

**The four multi-unit issues listed here previously (from `claudereply.md`) have all been
verified fixed as of 2026-08-06** — `resolveUnitForBusiness` now replicates `base_unit_id`,
`getDetailsFromVariation` selects `default_sell_sub_unit_id`, `products.sell_sub_unit_ids` /
`purchase_sub_unit_ids` provide the sell-vs-purchase split, and the PO→Purchase conversion
converts once (see the comment at `PurchaseController.php` ~L1312). `claudereply.md` is kept for
history but is no longer current.

Genuinely open, in rough priority order:

1. **Two variations have disagreeing stock ledgers.** `purchase_lines` and
   `variation_location_details` differ for Store 2 / variation 3 (batch says 9, location says 0)
   and Store 5 / variation 9 (batch 1, location 53). Anything reading one will disagree with
   anything reading the other. The expired-goods despatch guard refuses rather than driving stock
   negative, so those two products cannot be despatched until reconciled with a stock adjustment.
2. **Compositions were never synced to stores historically.** `resolveCompositionForBusiness` was
   find-only until 2026-08, so every store's synced product carries `composition_id = NULL` and
   generic substitution cannot work outside the master business. The resolver is now
   find-or-create (and replicates the salt set), but that only helps products synced from now on —
   **existing rows need a one-off backfill**, which has not been written.
3. **MRP is captured but never enforced as a price ceiling.** `variations.mrp_inc_tax` is stored
   and printed; nothing stops a sale above it. Also unpopulated across most of the catalogue,
   which is why expiry/disposal values at purchase cost rather than MRP.
4. **Purchase-unit data quality drives auto-PO size.** A product whose only purchase sub-unit is a
   large pack (e.g. `Baby Box(1/50)` = 2500 base units) will have auto-PO round a need of 7 up to
   2500. Now that urgent off-cycle POs fire automatically, a single sale can commit a very large
   order. The fix is data (give such products a smaller purchase unit), not code.
5. **E-invoice/e-way-bill is a stub.** `App\Services\StubEInvoiceGateway` returns obviously-fake
   `STUB`-prefixed values; no real GSP is wired.
6. **Trend/seasonality on the expiry board need history.** They compute correctly and report how
   many months/elapsed years they actually had, and say so when it is too thin — currently ~2
   months, so they read as indicative only.
7. **Permissions granted only by migration are fragile.** `RoleController::update()` rebuilds a
   role with `syncPermissions()` from the form's checkboxes, so any permission without a checkbox
   is silently revoked on the first save. That is how `pharmacy_expiry.view` was lost (store Expiry
   Alerts started 403-ing). It now has a checkbox; there is a `%custom_dashboard` preserve rule for
   another case. **Anything added by migration in future needs a checkbox or a preserve rule.**
8. **16 of 23 declared modules are absent** while `production_purchase`/`production_sell` hooks
   remain in the stock engine; ~20 translation keys reference uninstalled modules (cheque, crm,
   gym, hms, manufacturing, project, repair, zatca) and are correctly guarded at their call sites.
9. **No consolidated chain P&L** — warehouse→store margin is never eliminated on consolidation.
## Conventions worth knowing before editing

- Models are flat `app/*.php`, no `App\Models` namespace.
- No `app/Models` — don't create one; follow the existing flat convention.
- Core transactional tables (`Transaction`, `TransactionSellLine`, `PurchaseLine`, `Product`,
  `Business`, `BusinessLocation`) have **no** `SoftDeletes` — most reference/lookup tables do.
- The `transactions` table is polymorphic (`type` column) — there is no separate
  `stock_adjustments`/`stock_transfers` table; both are rows in `transactions`.
- Business-logic belongs in `app/Utils/*.php` (large static-heavy helper classes), not in
  controllers or models — follow that pattern for new pharmacy/warehouse logic.
- Internal code comments use a staged-delivery vocabulary ("Tier-A/A2", "Tier-B/B4", "Tier-C",
  "Tier-D/D1", "F-series") for the warehouse/pharmacy build-out — useful for grep'ing related work.
