# Dava India ERP — Routing, Controllers & Middleware Reference

> Laravel 9 app, built from scratch by GITCS. See [`../CLAUDE.md`](../CLAUDE.md) for the project overview,
> [`../DATABASE_SCHEMA.md`](../DATABASE_SCHEMA.md) for the DB, [`MODELS_AND_UTILS.md`](MODELS_AND_UTILS.md)
> for models/business logic, [`MODULES.md`](MODULES.md) for `Modules/*`, and
> [`PHARMACY_CUSTOMIZATIONS.md`](PHARMACY_CUSTOMIZATIONS.md) for the pharmacy/warehouse-specific layer.
> Compiled 2026-07-30 by reading the actual route/controller/middleware source.

## 1. Route Files

| File | Purpose |
|---|---|
| `routes/web.php` (~390 route defs) | Main authenticated web app — all POS/pharmacy/ERP screens |
| `routes/api.php` | Almost empty — just an `auth:api` `/user` stub. The app is server-rendered web + AJAX, not API-driven (see Connector module for the real API) |
| `routes/install_r.php` | Installer wizard routes, included into `web.php` |
| `routes/channels.php` | One broadcast channel: `App.User.{id}` |
| `routes/console.php` | Stock `inspire` Artisan closure only |
| `Modules/*/Routes/{web,api}.php` | Each module owns its own route files (see MODULES.md) |

## 2. Middleware / Auth Structure

Registered in `app/Http/Kernel.php`:

| Middleware | Role |
|---|---|
| `setData` / `authh` → `IsInstalled` | Redirects to installer if app isn't installed yet |
| `auth` | Standard Laravel session auth |
| `SetSessionData` | Caches `user`/`business`/`currency`/`financial_year` into session on first request; re-fetches `session('business')` fresh from DB on every later request so chain-wide setting changes propagate without re-login |
| `language`, `timezone` | Locale/timezone from user prefs |
| `AdminSidebarMenu` | Builds the sidebar via `Menu::create(...)` per request (skipped on AJAX), gated per item by `auth()->user()->can(...)` — this is where the permission tree becomes navigation |
| `CheckUserLogin` | Restricts most ERP routes to `user_type == 'user'` or a warehouse operator (`isWarehouseOperator()`); blocks disabled accounts (`allow_login != 1`) |
| `CheckSupplierLogin` | Gates `/supplier/*`: only `user_type == 'user_supplier'` with `common_supplier_id` set, or a superadmin who picked a warehouse via Warehouse Oversight (`session('sa_warehouse_id')`) |
| `BlockSupplierPortalUser` | Inverse — keeps plain supplier logins out of the main ERP routes, except warehouse operators |
| `superadmin` | Whitelist check against `config('constants.administrator_usernames')` — a config-file gate, **not** a DB role |
| `EcomApi` | Guards a (currently commented-out) e-commerce API block |

**Permissions**: `spatie/laravel-permission` (`^5.5`). Every controller action does an explicit
`auth()->user()->can('resource.action')` check — there's no route-level `can:` middleware group;
authorization is inline per controller method. Roles are business-scoped by naming convention
(e.g. `"Admin#{business_id}"`).

**Tenant isolation**: no global Eloquent scope on `business_id` — every controller manually adds
`->where('business_id', session('user.business_id'))`. The Supplier Portal is the deliberate
exception: it scopes by `common_supplier_id` across businesses instead, since a supplier
legitimately spans stores.

**Standard route-group pattern in `web.php`**:
- Guest/public: `middleware(['setData'])` — landing page, business self-registration, public invoice/quote/pay links
- Main authenticated group: `['setData','auth','SetSessionData','language','timezone','AdminSidebarMenu','CheckUserLogin','BlockSupplierPortalUser']`
- Lighter authenticated group (no sidebar/CheckUserLogin) for print/PDF/show endpoints reachable by both stores and suppliers
- Supplier portal: `['setData','auth','SetSessionData','language','timezone','AdminSidebarMenu','CheckSupplierLogin']->prefix('supplier')`
- Superadmin: `['web','SetSessionData','auth','language','AdminSidebarMenu','superadmin']->prefix('superadmin')`

## 3. Auth / Install / Restaurant Subfolders

- **`app/Http/Controllers/Auth/`** — stock Laravel `make:auth` scaffolding (Login/Register/ForgotPassword/ResetPassword/ConfirmPassword/Verification), wired via `Auth::routes()`.
- **`app/Http/Controllers/Install/`** — `InstallController` (server check → DB details → install/upgrade wizard), `ModulesController` (upload/enable/disable/regenerate nwidart modules from the UI).
- **`app/Http/Controllers/Restaurant/`** — `TableController`, `ModifierSetsController`, `ProductModifierSetController`, `KitchenController`, `OrderController`, `BookingController`, `DataController`. Core generic restaurant-mode feature (table/order management); dormant for a pure pharmacy chain (see MODELS_AND_UTILS.md §4).

## 4. Controller Inventory (68 controllers in `app/Http/Controllers/`)

### Catalog / Product Setup
| Controller | Responsibility |
|---|---|
| `ProductController` (~3000 ln) | Product/variation CRUD, bulk edit/deactivate, selling-price groups, stock history, combo products, WooCommerce sync toggle |
| `BrandController`, `ManufacturerController`, `DivisionController`, `UnitController` | Simple lookup-table CRUD |
| `TaxonomyController` | Category/sub-category tree |
| `VariationTemplateController` | Reusable variation templates (Size/Color sets) |
| `CompositionController` | **Pharmacy-specific** — see PHARMACY_CUSTOMIZATIONS.md |
| `SellingPriceGroupController` | Price tiers per product, activate/deactivate, import/export |
| `WarrantyController`, `DiscountController`, `GroupTaxController`, `TaxRateController` | Pricing/compliance lookups |
| `LabelsController` | Barcode label sheet designer + PDF print |
| `BarcodeController` | Barcode sticker formats |
| `InvoiceLayoutController`, `InvoiceSchemeController` | Invoice PDF template + numbering scheme |

### Doctors & Pharmacy
| Controller | Responsibility |
|---|---|
| `DoctorController` | CRUD of prescribing doctors, `DOC-` ref numbers, dropdown for POS |
| `CompositionController` | Named salt combinations, auto-derived names, free-text salt entry |
| `PharmacyController` | Single action `expiryAlerts()` — store-side near-expiry/expired batch view |

Full detail on these three in [`PHARMACY_CUSTOMIZATIONS.md`](PHARMACY_CUSTOMIZATIONS.md).

### Selling (POS / Invoicing)
| Controller | Responsibility |
|---|---|
| `SellPosController` (~3000 ln) | POS screen: rows, payments, invoice show/print/PDF/quotation/packing-list PDF, payment gateway callbacks, recurring-invoice subscriptions, service-staff timer, sales-order → POS conversion, e-commerce "place order" stub |
| `SellController` | Non-POS sell listing/edit/show, duplicate sale, drafts, quotations, shipping edit |
| `SellReturnController` | Sales returns |
| `SalesOrderController` | Sales-order index + status workflow |
| `TypesOfServiceController` | Service catalog |
| `SalesCommissionAgentController` | Commission agent CRUD |
| `CashRegisterController` | Register open/close |

### Buying (Purchases / Supply Chain)
| Controller | Responsibility |
|---|---|
| `PurchaseController` (~1660 ln) | Purchase entry CRUD, GRN show/PDF, status update |
| `PurchaseOrderController` (~1000 ln) | PO lifecycle: create → status transitions → PDF |
| `PurchaseRequisitionController` | Internal requisition before a PO; ties into `AutoRequisitionUtil` |
| `PurchaseReturnController` / `CombinedPurchaseReturnController` | Return-to-supplier (combined spans multiple purchases) |
| `OpeningStockController` | One-off opening-stock entry |
| `StockAdjustmentController` | Write-off/adjustment + "remove expired stock" |
| `StockTransferController` (~970 ln) | Inter-location transfer CRUD + status workflow |

### Import Tools
`ImportProductsController`, `ImportPurchasesController`, `ImportPurchaseOrdersController`,
`ImportSalesController`, `ImportOpeningStockController`, `ImportStockSettingsController` — bulk
spreadsheet import with preview/revert-by-batch.

### Contacts / CRM
| Controller | Responsibility |
|---|---|
| `ContactController` (~1870 ln) | Suppliers + customers unified CRUD, due balances, ledger, supplier stock report |
| `CustomerGroupController` | Customer pricing-tier CRUD |
| `LedgerDiscountController` | Per-contact ledger discount config |

### Accounting / Payments
`AccountController`, `AccountReportsController`, `AccountTypeController`,
`TransactionPaymentController`, `ExpenseController`, `ExpenseCategoryController`,
`MyFatoorahController`, `PesaPalController`.

### Business / Location / Users / Settings
`BusinessController`, `BusinessLocationController`, `LocationSettingsController`,
`UserController`, `ManageUserController` (incl. `sign-in-as-user` impersonation), `RoleController`,
`PrinterController`, `NotificationController`, `NotificationTemplateController`,
`DocumentAndNoteController` (generic polymorphic notes widget), `BackUpController`,
`DashboardController`, `DashboardConfiguratorController`, `HomeController`.

### Reporting
`ReportController` — **~4775 lines, the largest controller in the app.** ~40 report endpoints:
GST purchase/sales, P&L, stock report/details/expiry/value, tax, trending products, expense
report, register report, sales-rep performance & commission, lot report, activity log, product
movement, etc.

### Expiry & Disposal (2026-08)
`ExpiryDisposalController` — one controller for both ends of the same document: the store
despatches (`storeIndex`, `storeSendReturn`), the warehouse receives and destroys
(`warehouseReturns`, `receiveForm`/`receiveReturn`, `rejectReturn`, `disposals`,
`storeDisposal`, `showDisposal`). Every warehouse action re-derives the acting warehouse from
the session rather than trusting a posted id.

### Import Tools (extended 2026-08)
`ImportManufacturersController` — bulk manufacturer load (name/description/email/mobile/address)
with dry-run, case-insensitive duplicate detection and an optional update-existing mode.
`ImportProductsController` gained a generated 57-column template (`templateColumns()` is the
single source of truth for both the download and the parser), a dry-run switch, MRP, mfg lead
time, the full multi-unit set and multi-manufacturer columns.

### Custom Warehouse / Supplier Portal Layer
`SupplierPortalController` (~1600 ln) — see [`PHARMACY_CUSTOMIZATIONS.md`](PHARMACY_CUSTOMIZATIONS.md) §2 for full detail.

## 5. Superadmin Module — HQ Command Center

See [`MODULES.md`](MODULES.md#superadmin) for the full breakdown. In short: still the core
SaaS admin panel (packages/subscriptions/billing/CMS), but heavily extended into a
chain-HQ console (master product catalog, supplier/manufacturer assignment, warehouse oversight,
schedule-drug register, drug-license tracking, batch traceability/recall, centralized invoice/GST
numbering, support-ticket oversight).

## 6. Main End-to-End Workflows

| Workflow | Route(s) | Controller/Method | Flow |
|---|---|---|---|
| POS Sale | `Route::resource('pos', SellPosController)` | `create/store` | Ajax product/payment rows → `store()` creates a `sell` transaction → invoice printed/PDF'd |
| Purchase Entry | `Route::resource('purchases', ...)` | `store/update` | Manual entry or PO conversion → GRN generated → stock added |
| PO → GRN → Payment | `purchase-order` resource | `PurchaseOrderController` + `PurchaseController` | PO raised (manual or `AutoRaisePurchaseOrders`/`AutoRaiseWarehousePurchaseOrders` cron) → status progressed → converted to Purchase on receipt → payment recorded |
| Stock Transfer | `Route::resource('stock-transfers', ...)` | `StockTransferController` | Transfer between locations, status workflow, printable note |
| Stock Adjustment | `Route::resource('stock-adjustments', ...)` | `StockAdjustmentController` | Ad-hoc write-off or one-click expired-stock removal |
| Sales/Purchase Return | `sell-return`, `purchase-return` resources | respective controllers | Validate original invoice, build return lines, adjust stock/ledger |
| Expense Tracking | `expenses`, `expense-categories` | `ExpenseController` | Manual or bulk import; recurring expenses via `pos:generateRecurringExpense` cron |
| Warehouse Replenishment (custom) | `/supplier/*`, Superadmin Warehouse Oversight | `SupplierPortalController` | Store demand → auto-requisition → daily combined store PO to supplier → mirrored as warehouse sales order → pick/pack/dispatch → store GRN. Warehouse's own restock to TPM follows the same pattern one tier up |
| Expiry Management | `/pharmacy/expiry-alerts`, `/supplier/expiry-alerts` | `PharmacyController`, `SupplierPortalController` | Both reuse `WarehouseExpiryUtil::nearExpiryBatches()`; warehouse side also swept nightly |
| Recurring Invoices | `/toggle-subscription/{id}`, `/sells/subscriptions` | `SellPosController` | Nightly `pos:generateSubscriptionInvoices` cron turns active subscriptions into new sales |
| Doctor-linked Sale | `/doctors/dropdown` | `DoctorController::getDoctorsDropdown` | Prescribing doctor attached to a sale line |
| Composition Setup | `compositions` resource | `CompositionController` | Salts combined into a named Composition, attached to a Product |
| PO Follow-up (2026-08) | `/supplier/po-followups` | `SupplierPortalController::poFollowups` | Every open warehouse PO gets ETA = PO date + product lead time. Chase rungs T-15/T-7 fire once, T-6…T-0 and overdue fire daily. Recording a follow-up (with optional revised ETA) writes to the activity log; the per-PO trail merges chases, reminder emails, escalations and goods receipts into one timeline |
| Urgent off-cycle PO (2026-08) | — (real time + cron) | `UrgentPurchaseOrderUtil` | A store product reaching 50% of min is flagged on its requisition line and gets its own PO immediately (fired on `DB::afterCommit` so a rolled-back sale leaves no PO). Every other requisition line still waits for the cadence day; the cadence timer is deliberately NOT stamped |
| Compliance Management (2026-08) | `/superadmin/compliance` | `ComplianceController` | Chain-wide licence register over the existing `business` licence columns, bucketed expired / not-on-record / 30 / 60 / 90 / valid, with a status+explanation trail. Marking a licence renewed writes the new number/expiry back to `business` |
| Expired goods → disposal (2026-08) | `/expiry-disposal`, `/supplier/expired-goods`, `/supplier/disposals` | `ExpiryDisposalController` | Store despatches expired batches to its linked warehouse (`purchase_return`) → warehouse verifies and receives them (`purchase_transfer`, `qc_status='expired'`, unsellable) → warehouse destroys them (`stock_adjustment`/`abnormal`) and issues a printable Certificate of Destruction |
| Expiry analytics (2026-08) | `/superadmin/expiry-analytics` | `ExpiryAnalyticsController` | Chain-wide expired value at purchase cost, ageing, forward pipeline, expiry ratio per product, trend, seasonality, over-ordering signals, site comparison, generated insights. Batch drilldown shows on-hand AND already-destroyed batches so the ratio reconciles |
| Bounce rate | `/superadmin/bounce-rate` | `BounceRateController` | POS searches that could not be served (`out_of_stock` / `not_listed`) captured at the till and aggregated chain-wide |

## 7. Console Commands (`app/Console/Commands`, scheduled in `app/Console/Kernel.php`)

| Command | Schedule | Purpose |
|---|---|---|
| `backup:clean` / `backup:run` | 01:00 / 01:30 daily | Backup rotation |
| `pos:generateSubscriptionInvoices` | 23:30 daily | Recurring invoices → new sales |
| `pos:updateRewardPoints` | 23:45 daily | Recompute customer reward points |
| `pos:autoSendPaymentReminder` | 08:00 daily | Due-payment reminders |
| `pos:generateRecurringExpense` | 02:00 daily | Materialize recurring expenses |
| `pos:seedWarehouseMinMax` | 02:45 daily | Recompute warehouse min/max = Σ served stores' mins |
| `pos:updateMovementTags` | 03:00 daily | Recompute movement tag + min/max from sales/dispatch history |
| `pos:autoRaisePurchaseOrders` | 03:30 daily | Combine store's open auto-requisitions into one PO to its supplier |
| `pos:autoRaiseWarehousePurchaseOrders` | 04:00 daily | Same, warehouse → Third-Party Manufacturer, emailed |
| `supportticket:flag-delayed` | every 30 min | Flags tickets past TAT as Delayed |
| `pos:refreshAutoRequisitions` / `pos:refreshWarehouseAutoRequisitions` | hourly | Safety-net resync of open requisitions vs. current stock |
| `pos:syncWarehouseStoreCustomers` | 02:30 daily | Keeps warehouse's store-customer list current |
| `pos:syncWarehouseSalesOrders` | hourly | Mirrors incoming store POs as warehouse sales orders |
| `pos:warehouseExpiryAlert` | 05:00 daily | Near-expiry/expired batch sweep per warehouse |
| `pos:dummyBusiness` | every 3h, demo only | Wipes/reseeds a dummy demo business |
| `pos:flagPoFollowups` | 05:30 daily | Advances the warehouse→manufacturer PO chase ladder (T-15/T-7/daily), escalates POs past ETA to a delivery-delay support ticket, and emails the manufacturer at the reminder rungs |
| `pos:autoRaiseUrgentPurchaseOrders` | every 15 min | Safety net for off-cycle store POs: raises one for any product flagged critical (≤50% of min) whose real-time raise did not happen. Idempotent |
| `pos:syncMasterProducts` | manual | Pushes master products into every business, chunked and re-runnable. Used after a bulk import above `ImportProductsController::INLINE_SYNC_LIMIT` (100), where an inline sync would outlive the web request |
| `MapPurchaseSell`, `RepairStoreUnitHierarchy`, `SyncTaxToStores`, `CreateDummyBusiness` | manual/ad-hoc | Data-repair/one-off tools |

## Key Files
`routes/web.php` · `Modules/Superadmin/Routes/web.php` · `app/Http/Kernel.php` ·
`app/Http/Middleware/{SetSessionData,CheckUserLogin,CheckSupplierLogin,BlockSupplierPortalUser,Superadmin,AdminSidebarMenu}.php` ·
`app/Http/Controllers/{PharmacyController,DoctorController,CompositionController,SupplierPortalController}.php` ·
`app/Console/Kernel.php`
