# Dava India ERP — Model Layer & Business-Logic Utils Reference

> Models live directly under `app/*.php` — **not** `app/Models/`. See [`../CLAUDE.md`](../CLAUDE.md)
> for the project overview and [`../DATABASE_SCHEMA.md`](../DATABASE_SCHEMA.md) for the tables
> these models map to. Compiled 2026-07-30.

## 1. Architectural Summary

A **regional-warehouse → retail-store pharmacy chain model** sits on top of the core generic ERP:

- Each warehouse/distribution center is itself a `Business` record (`is_supplier_warehouse = 1`),
  linked to the stores it serves via `common_supplier_id` (on `Contact`) and
  `master_product_id`/`master_variation_id` bridges (on `Product`/`Variation`).
- A recurring **master/mirror (master/copy)** self-referential pattern threads through the schema:
  `Product.masterProduct/masterProductCopies`, `Variation.masterVariation/masterVariationCopies`,
  `Contact.masterSupplier`, `InvoiceLayout.master/mirrors`, `InvoiceScheme.master/mirrors`.
- On top of the core `ProductUtil`/`TransactionUtil` engines, a family of custom
  `Warehouse*Util` classes (plus `DemandForecastUtil`, `PredictiveInsightsUtil`,
  `ProcurementIntelligenceUtil`, `ManufacturerBridgeUtil`, `TpmPoNotificationUtil`,
  `AutoRequisitionUtil`, `ScheduleRegisterUtil`) implement batch/lot traceability, FEFO dispatch,
  GST tax splitting, cold-chain/narcotic bin compliance, demand forecasting, and manufacturer
  procurement intelligence.
- Code comments use an internal delivery-phase vocabulary ("Tier-A/A2", "Tier-B/B4", "Tier-C",
  "Tier-D/D1") — a staged build-out plan for the warehouse/pharmacy features.
- New pharmacy columns (`qc_status`, `recalled_at`, `lot_number`/`exp_date`, `drug_schedule`,
  `storage_condition`) were added to existing core tables rather than a schema rewrite.
- India-specific compliance: GST (CGST/SGST/IGST splitting), a stubbed **e-Invoice/e-Way-Bill**
  gateway (`App\Services\EInvoiceGateway`), Schedule H/H1/X narcotic movement registers.

## 2. Core Domain Models

### 2.1 Business / Location / User / Contact

| Model | Purpose | Key Relationships | Notes |
|---|---|---|---|
| `Business.php` | Tenant/business root; also encodes warehouse/TPM auto-PO cadence | `owner()`→User; `currency()`→Currency; `locations()`→BusinessLocation; `printers()`→Printer; `subscriptions()`→Superadmin\Subscription; `assignedCommonSuppliers()`/`assignedManufacturers()`→Contact | No SoftDeletes. Statics: `templateBusinessId`, `effectiveAutoPoFrequencyDays`, `effectiveTpmPoFrequencyDays` |
| `BusinessLocation.php` | Physical store/location | `price_group()`→SellingPriceGroup | Scope `scopeActive`; static `forDropdown` |
| `User.php` | Employee/auth account, incl. warehouse "supplier" operators | `business()`→Business; `contactAccess()`↔Contact (pivot `user_contact_access`); `documentsAndnote()` morphMany; `media()` morphOne | Traits: `HasRoles` (Spatie), `Notifiable`, `SoftDeletes`, `HasApiTokens` (Passport). `isWarehouseOperator()`, `hidePurchasePrice()`, `permitted_locations()` |
| `Contact.php` | Unified customer **and** supplier model; extends `Authenticatable` | `business()`→Business; `masterSupplier()` self (`common_supplier_id`) | Scopes: `scopeVisibleForBusiness`, `scopeMasterCustomers`, `scopeExcludeForeignStoreCustomers`. Methods: `cloneToBusinessAsSupplier`, `createStoreCustomer`, `unlinkStoreCustomer` |

### 2.2 Product & Variation Family

| Model | Purpose | Key Relationships |
|---|---|---|
| `Product.php` | Product/item master; multi-business "master product" sync | `product_variations()`→ProductVariation; `brand/manufacturer/division/unit/category`; `composition()`→Composition; `rack_details()`→ProductRack; `masterProduct()`/`masterProductCopies()` self |
| `Variation.php` | A specific sellable SKU/pack | `product_variation()`; `masterVariation()`/`masterVariationCopies()` self; `sell_lines()`→TransactionSellLine; `variation_location_details()`→VariationLocationDetails |
| `ProductVariation.php` | Attribute-group row (e.g. "Size") linking Product → Variations | `variations()`; `variation_template()` |
| `VariationLocationDetails.php` | Per-location `qty_available` — **the authoritative stock-quantity record** | bare model, manipulated via `ProductUtil` |
| `VariationGroupPrice.php` | Price-group-specific override | accessor `getCalculatedPriceAttribute()` |
| `VariationTemplate.php` / `VariationValueTemplate.php` | Reusable attribute template + allowed values | |

### 2.3 Transaction Family

| Model | Purpose | Key Relationships |
|---|---|---|
| `Transaction.php` | Central polymorphic record (sell/purchase/expense/stock_adjustment/transfer/return/order/payroll — discriminated by `type`) | `purchase_lines()`/`sell_lines()`; `contact()`; `payment_lines()`; `doctor()`→Doctor; `table()`→Restaurant\ResTable; `recurring_invoices()`/`recurring_parent()` self |
| `TransactionSellLine.php` | Sell line item | `product()`; `variations()`; `modifiers()` self; `sell_line_purchase_lines()`→TransactionSellLinesPurchaseLines; `lot_details()`→PurchaseLine; `so_line()` self |
| `TransactionPayment.php` | Payment against a Transaction; parent/child split payments & cheques | `payment_account()`→Account; `child_payments()` self; `denominations()` morphMany CashDenomination |
| `PurchaseLine.php` | Purchase line item — **the stock-lot/batch source of truth** (qty, qty_sold, qty_adjusted, qty_returned) | `transaction()`; `product()`; `variations()`; accessor `getQuantityRemainingAttribute` |
| `TransactionSellLinesPurchaseLines.php` | Pivot: which purchase-line batch(es) a sell line consumed (FIFO/FEFO) | `purchase_line()` |
| `StockAdjustmentLine.php` | Line of a `stock_adjustment` transaction | `variation()`; `lot_details()` |
| `CashRegister.php` / `CashRegisterTransaction.php` | POS register session + its transactions | |

### 2.4 Financial / Tax

| Model | Purpose |
|---|---|
| `Account.php` / `AccountTransaction.php` / `AccountType.php` | Money account, ledger entry, self-hierarchical account category |
| `TaxRate.php` / `GroupSubTax.php` | Tax rate or composite tax group (pivot `group_sub_taxes`) |

### 2.5 Reference / Lookup Tables

`Category`, `Brands`, `Unit` (base/sub/intermediate-unit chain), `Currency`, `CustomerGroup`,
`Discount`, `ExpenseCategory`, `SellingPriceGroup`, `Barcode`, `InvoiceLayout`/`InvoiceScheme`
(master/mirror across businesses), `Manufacturer`, `TypesOfService`.

**Cross-cutting**: SoftDeletes used broadly on lookup tables but **absent** from `Business`,
`BusinessLocation`, `Product`, `Transaction`, `TransactionSellLine`, `PurchaseLine` — core
transactional tables have no soft-delete. `media()` (morphMany → `App\Media`) is independently
implemented on `Product`, `Variation`, `Transaction`, `AccountTransaction`, `User`.

## 3. Pharmacy-Specific Custom Models

See [`PHARMACY_CUSTOMIZATIONS.md`](PHARMACY_CUSTOMIZATIONS.md) for full narrative detail. Summary:

| Model | Business Purpose |
|---|---|
| `Composition.php` | Drug composition (e.g. "Paracetamol + Ibuprofen"); `belongsToMany(Salt, 'composition_salt')`; `buildNameFromSaltNames()` auto-derives name |
| `Salt.php` | Master list of active pharmaceutical ingredients |
| `Doctor.php` | Prescribing doctors referenced on sales; `SoftDeletes`; `registration_number` |
| `Division.php` | Therapeutic category / product division lookup |
| `Consignment.php` | Dispatch trip/manifest grouping warehouse→store transfers; `hasMany(Transaction, 'consignment_id')` |
| `Warranty.php` | **Core generic ERP feature, not pharmacy-custom** — generic warranty duration |
| `TemperatureLog.php` | Cold-chain compliance; `syncBreach()` auto-flags out-of-range readings |
| `ProductRack.php` | **Core legacy feature (2018)** — simple free-text rack label per product; distinct from and much simpler than `WarehouseBin` |
| `WarehouseBin.php` | Hierarchical pharma location master (zone→aisle→rack→shelf→bin via `parent_id`); GDP/Schedule-H segregation zones |
| `WarehouseBinStock.php` | Per-bin × per-batch (`purchase_line_id`) quantity sub-ledger |
| `StockCount.php` / `StockCountLine.php` | Cycle-count header/line, expected vs. counted qty |
| `MovementTagConfig.php` | ABC/velocity classification (SFM/FM/NFM/SM) driving reorder sizing; 3-tier config fallback |
| `ReferenceCount.php` | Reference-number sequence counter |
| `CashDenomination.php` | Cash-drawer/register closing reconciliation |
| `DashboardConfiguration.php` | Per-user/business dashboard widget layout |
| `DocumentAndNote.php` | Generic polymorphic notes/attachments; `LogsActivity` trait (Spatie activitylog) |
| `UserContactAccess.php` | ACL pivot restricting which users can view which contacts |
| `Media.php` | Generic polymorphic file/media attachment, incl. base64 upload |
| `NotificationTemplate.php` | Email/SMS/WhatsApp templates, incl. `tpm_purchase_order` |
| `PaymentAccount.php` | Configured payment accounts for reconciliation |
| `System.php` | Generic key-value settings store (table `system`, no timestamps) |
| `Printer.php` | Receipt/label printer config (Epson TEP, Star SP2000 profiles) |

### 3.x Models added 2026-08

| Model | Table | Notes |
|---|---|---|
| `BusinessLicenseLog` | `business_license_logs` | Append-only compliance trail. Never edited or deleted — the register shown to an inspector |
| `ExpiredGoodsReturn` | `expired_goods_returns` | Store→warehouse despatch of expired stock. `draft → sent → received / rejected`; only `received` may be disposed of |
| `ExpiredGoodsReturnLine` | `expired_goods_return_lines` | One expired batch, with snapshot batch identity and `pendingDisposalQuantity()` |
| `DisposalRecord` | `disposal_records` | Destruction record + certificate. `draft → completed`; completed is immutable |
| `DisposalRecordLine` | `disposal_record_lines` | Batch destroyed, incl. `drug_schedule` and originating store |
| `ProductBounce` | `product_bounces` | A POS request the chain could not serve (`out_of_stock` / `not_listed`) |

All follow the flat `app/*.php` convention — there is no `App\Models` namespace.

## 4. `app/Restaurant/*.php`

`Booking.php`, `ResTable.php` — **core generic restaurant-mode scaffolding**, gated behind the
standard feature-toggle system (`enabled_modules` containing `tables`/`booking`/`kitchen`/
`service_staff`). No pharmacy-specific code references these. Dormant/vestigial for this chain.

## 5. `app/Utils/*.php` — Business-Logic Helper Layer

### 5.1 The "Big Three" stock engines (heavily customized)

**`TransactionUtil.php`** (~7,068 ln) — the largest, most central file in the app.
- Sell lifecycle: `createSellTransaction`/`updateSellTransaction`, `createOrUpdateSellLines`, `deleteSale`, `createSellReturnTransaction`
- Custom sales-order linkage (warehouse↔store): `so_line_id` tracking, `updateSalesOrderLine/Status`
- Payments: `createOrUpdatePaymentLines`, `payAtOnce`, `calculatePaymentStatus`
- **`mapPurchaseSell`** — the central batch-consumption/FEFO engine (~250 ln). Custom pharmacy
  logic: `$use_fefo` sorts candidate batches by `exp_date ASC` when `is_supplier_warehouse`;
  excludes `recalled_at` batches and non-`passed` `qc_status` batches
- Reporting: `getPurchaseTotals`/`getSellTotals`, `getInputTax`/`getOutputTax`, `getGrossProfit`
- Receipts/PDFs: `getReceiptDetails`, `getPdfContentsForGivenTransaction`

**`ProductUtil.php`** (~2,944 ln)
- Variation creation: `createSingleProductVariation`, `createVariableProductVariations`
- **Stock-quantity chokepoints**: `updateProductQuantity` / `decreaseProductQuantity` — the only
  two places `qty_available` changes; both call `AutoRequisitionUtil::syncForVariationLocation(...)`
  so every stock mutation keeps auto-purchase-requisitions live
- `createOrUpdatePurchaseLines` sets `qc_status='quarantine'` on new warehouse batches and stamps
  `source_purchase_line_id` linking a store's GRN batch back to its warehouse dispatch batch
- Stock lookups: `getCurrentStock`, `getVariationStockMisMatch`/`fixVariationStockMisMatch`

**`BusinessUtil.php`** (~701 ln) — stock. Business/tenant setup: `createNewBusiness`,
`cloneRolesFromTemplateBusiness`/`syncRolePermissionsToAllBusinesses`, `addLocation`,
`getCurrentFinancialYear`. No pharmacy-specific hooks.

### 5.2 Other core/general-purpose Utils (not pharmacy-specific)

| File | Size | Responsibilities |
|---|---|---|
| `Util.php` | ~1,940 ln | Base class: number/date formatting, reference-number generation, module/permission gating, `replaceTags` notification substitution, SMS/WhatsApp send, unit conversion |
| `ModuleUtil.php` | ~619 ln | Module/subscription-plan gating: `isModuleInstalled`, `isSubscribed`, quota checks |
| `CashRegisterUtil.php` | ~534 ln | Register open/close, `addSellPayments`/`refundSell` |
| `NotificationUtil.php` | ~383 ln | Email/SMS/WhatsApp dispatch, runtime SMTP config |
| `ContactUtil.php` | ~336 ln | Core contact CRUD + light overlay: `getWalkInCustomer` (chain-wide master walk-in customer), dedup by mobile |
| `RestaurantUtil.php` | ~290 ln | Restaurant/KOT helpers — unrelated to pharmacy |
| `InstallUtil.php` | ~175 ln | Legacy migration/install helpers |
| `AccountTransactionUtil.php` / `TaxUtil.php` | ~25 ln each | Tax-group total recompute (byte-identical duplicates — leftover from a stock refactor) |

### 5.3 Custom Warehouse/Pharmacy Utils (Dava India's core additions)

| File | Size | Responsibility |
|---|---|---|
| `WarehouseBinUtil.php` | ~363 ln | Bin/put-away engine: `putAway`/`moveBin` (row-locked), `conditionCheck` (cold-chain + narcotic rules), `pickPlan` (FEFO) |
| `WarehouseCountUtil.php` | ~104 ln | Posts a StockCount's variances to stock via `decreaseProductQuantity` + `stock_adjustment` |
| `WarehouseCustomerUtil.php` | ~110 ln | Syncs each served store as a private Customer Contact inside its warehouse (GST-correct B2B invoicing) |
| `WarehouseExpiryUtil.php` | ~75 ln | Near-expiry batch surfacing (`nearExpiryBatches`, `summary`) |
| `WarehouseRebalanceUtil.php` | ~227 ln | Inter-warehouse stock rebalancing modeled as a purchase+sell pair |
| `WarehouseTraceUtil.php` | ~100 ln | Batch traceability + recall (`searchBatches`, `traceBatch`, `markRecalled`) |
| `WarehouseTaxUtil.php` | ~141 ln | GST place-of-supply: CGST+SGST vs. IGST |
| `WarehouseStockUtil.php` | ~174 ln | Store↔warehouse mapping via `master_product_id`/`master_variation_id` |
| `WarehouseDispatchUtil.php` | ~146 ln | Resolves which batch(es) a warehouse dispatched (FEFO) for a store PO |
| `WarehouseReturnUtil.php` | ~201 ln | Reverse logistics: `returnBatchToManufacturer`, `rejectBatchAtQc` |
| `DemandForecastUtil.php` | ~209 ln | Holt's linear trend + weekday seasonality demand forecasting |
| `ManufacturerBridgeUtil.php` | ~129 ln | Bridges a Manufacturer to a login-less supplier Contact inside a warehouse |
| `PredictiveInsightsUtil.php` | ~233 ln | `atRiskBatches`, `rebalanceRecommendations` |
| `ProcurementIntelligenceUtil.php` | ~300 ln | `supplierScorecards`, `abcXyz`, `reorderSuggestions`/`applyReorderPoints` |
| `TpmPoNotificationUtil.php` | ~253 ln | Emails warehouse→TPM POs, idempotent send |
| `AutoRequisitionUtil.php` | ~256 ln | Maintains each store's/warehouse's live open auto-purchase-requisition |
| `ScheduleRegisterUtil.php` | ~124 ln | Statutory movement register for Schedule H/H1/X narcotics |
| `PoFollowupUtil.php` | ~887 ln | Warehouse→manufacturer PO chase ladder: lead-time resolution, T-15/T-7/daily rungs, escalation, reminder rungs, per-PO trail, fulfilment/receipt reconciliation |
| `UrgentPurchaseOrderUtil.php` | ~267 ln | Off-cycle single-product PO when a store hits the urgent threshold (default 50% of min). Reuses the scheduled producer's PO builder via `RaisesAutoPurchaseOrders` |
| `ComplianceUtil.php` | ~392 ln | Chain-wide licence register: licence-type registry, 30/60/90-day expiry buckets, per-business rollup, renewal recording (writes back to `business`) |
| `ExpiryUtil.php` | ~380 ln | Chain-wide expired/near-expiry batches with store/state/warehouse/tier filters, valued at purchase cost; ageing buckets, forward pipeline, store→warehouse link resolution |
| `ExpiredGoodsReturnUtil.php` | ~524 ln | Store→warehouse movement of expired stock: draft/send/receive/reject, dual-ledger guard, `qc_status='expired'` on receipt |
| `DisposalUtil.php` | ~506 ln | Destruction: pending-disposal list (store returns + warehouse own stock), certificate data, write-off via `stock_adjustment`/`abnormal`, destroyed-batch rows for the analytics drilldown |
| `ExpiryIntelligenceUtil.php` | ~456 ln | Expiry BI: expiry ratio per product, monthly trend, seasonality, over-ordering signals, site comparison, generated plain-language insights |
| `BounceRateUtil.php` | ~757 ln | Captures unfulfilled POS product searches and aggregates the chain-wide bounce rate, splits and forward projection |

## 6. `app/Services/*.php`

| File | Purpose |
|---|---|
| `EInvoiceGateway.php` | Interface contract for a GST e-Invoice/e-Way-Bill provider (GSP). Methods: `generateForSell`, `generateEwayBill`, `cancelIrn`. Bound via container in `AppServiceProvider` |
| `StubEInvoiceGateway.php` | Default local impl — deterministic, obviously-fake `STUB`-prefixed IRN/QR/e-way values, no external call. **Explicitly documented as dev/UAT-only**; real GSP integration replaces it via the binding |

## 7. Spatie Package Wiring

- **laravel-permission**: only `App\User` uses `HasRoles`. Roles business-scoped by naming
  convention (`"Admin#{business_id}"`). `BusinessUtil::cloneRolesFromTemplateBusiness`/
  `syncRolePermissionsToAllBusinesses` propagate role/permission sets across businesses.
- **laravel-activitylog**: only `App\DocumentAndNote` uses `LogsActivity` as a model trait.
  Broader auditing is done imperatively via `Util::activityLog()` calls, not trait-driven.
- **laravel-backup**: infrastructure-level (`config/backup.php`), not wired into any model.
