# Sale Order (Tax Invoice) — Implementation Plan

## Problem Statement

Build a **Sale Order** module that produces a GST **Tax Invoice** PDF matching the
supplied reference invoice (Blue Waves Media / Ashutosh College sample). It must be
built as a **standalone, fully user-input** feature that mirrors the existing
**Purchase Order** module in structure (model, migration, controller, service,
Blade, routes, permissions, draft/final flow, emailing, terms, PDF download), but
its **entire data model matches the reference invoice** — not the PO's shipping /
delivery / freight fields.

The whole SO must round-trip the invoice's data faithfully: create → store → fetch → PDF.

## Requirements (confirmed)

- Everything is **user input**, standalone — exactly like Purchase Order.
- **Full parity** with PO capabilities: draft + final flow, list/detail, soft delete,
  email sending (attachments + history + resend), terms sub-resource, PDF download,
  auto-generated SO number, amount-in-words, new `sale_order_management` permission group.
- The **data model and PDF must match the reference image** (labels, layout, and
  computed values). Sample invoice values are used as the seed/test fixture.
- Drop PO-only fields absent from the invoice: shipping, freight, delivery method/location,
  packaging, expected delivery date, other-charges. **No `charges` table.**

## Reference Invoice — Exact Data (seed / test fixture)

### Seller (billing_from)
- Name: **Blue Waves Media**
- Address: `43A, Tollygunge Circular Road, Mahendra Tower, 3rd Floor, Suit 3B, Kolkata - 700053`
- URN: `UDYAM-WB-10-0062907`
- GSTIN/UIN: `19AYCPK7151N1ZY`
- State Name: `West Bengal`, Code: `19`
- E-Mail: `accounts@bluewavesmedia.in`
- PAN: `AYCPK7151N`

### Meta box
- Invoice No.: `SMS/367/2026-27`
- Dated: `14-Sep-26`
- Mode/Terms of Payment: _(blank)_
- Reference No. & Date: _(blank)_
- Other References: _(blank)_
- Terms of Delivery: _(blank)_

### Buyer (billing_to)
- Name: **Ashutosh College**
- Address: `92, Shyamaprasad Mukherjee Road, Kolkata - 700026`
- State Name: `West Bengal`, Code: `19`
- Place of Supply: `West Bengal`

### Line item
| Sl | Description of Services | HSN/SAC | GST Rate | Quantity | Rate | per | Amount |
|----|-------------------------|---------|----------|----------|------|-----|-----------|
| 1  | BULK SMS                | 998419  | 18 %     | _(blank)_| _(blank)_ | _(blank)_ | 14,500.00 |

- Output CGST: `1,305.00`
- Output SGST: `1,305.00`
- Total: **₹ 17,110.00**
- `E & O.E`

### Amount Chargeable (in words)
`Indian Rupees Seventeen Thousand One Hundred Ten Only`

### HSN / Tax summary
| HSN/SAC | Taxable Value | CGST Rate | CGST Amount | SGST/UTGST Rate | SGST Amount | Total Tax Amount |
|---------|---------------|-----------|-------------|-----------------|-------------|------------------|
| 998419  | 14,500.00     | 9%        | 1,305.00    | 9%              | 1,305.00    | 2,610.00         |
| **Total** | 14,500.00   |           | 1,305.00    |                 | 1,305.00    | 2,610.00         |

- Tax Amount (in words): `Indian Rupees Two Thousand Six Hundred Ten Only`

### Footer
- Company's PAN: `AYCPK7151N`
- Declaration:
  - We declare that this invoice shows the actual price of the goods described and that all particulars are true and correct.
  - 1. Goods once sold will not be taken back.
  - 2. Interest @ 18% p.a. will be charged if the payment is not made within 30 Days from the date of invoice.
  - 3. Rate of TDS under 194C is 1% as individual. ICICI BANK A/C NO : 127505000250
- IFS Code: `ICIC0001275, BRANCH - SANTOSHPUR`
- Company's Bank Details: Bank Name `HDFC BANK`, A/c No. `00142020003530`, Branch & IFS Code `PRINCE ANWAR SHAH ROAD & HDFC0001219`
- `for Blue Waves Media` / `Authorised Signatory`
- `SUBJECT TO WEST BENGAL JURISDICTION`
- `This is a Computer Generated Invoice`

## Data Model (authoritative — drives migration, model, validation, formatter, Blade)

> **Sourcing principle:** the reference Tax Invoice image is the single source of
> truth for **what data exists and its type**. The Sale Order reuses the PO module's
> *mechanics* (draft/final flow, permissions, email, DomPDF service pattern, decimal
> conventions) but **does not** inherit PO's column set. Every field below exists
> because it appears on the invoice; PO-only fields (shipping, freight, delivery
> method/location, packaging, expected-delivery, other-charges, `cin`,
> `contact_person`, `phone` on parties) are intentionally absent. Where the invoice
> and PO disagree on shape (e.g. GST is **per-line / HSN-grouped** on the invoice vs.
> header-level on PO), the invoice wins.

### Field → invoice-region map (data types)
| Region on invoice | Fields | Type |
|-------------------|--------|------|
| Meta box | `document_no`, `so_date`, `mode_terms_of_payment`, `reference_no_date`, `other_references`, `terms_of_delivery` | string / date / nullable strings |
| Seller header | `billing_from_*` (name, address, urn, gstin, gst_state, gst_state_code, email, pan) | strings |
| Buyer (Bill to) | `billing_to_*` (name, address, gst_state, gst_state_code), `place_of_supply` | strings |
| Line row | `description`, `hsn_sac`, `gst_rate`, `quantity`, `rate`, `per`, `amount` | qty/rate/per **nullable** (invoice line can show amount only) |
| Per-line GST split | `cgst_rate`, `cgst_amount`, `sgst_rate`, `sgst_amount`, `total_tax_amount` | decimal(15,4) |
| Totals | `taxable_amount`, `cgst`, `sgst`, `igst`, `round_off`, `tax_total`, `grand_total` | decimal(15,4) |
| Two "in words" rows | `amount_in_words` (grand_total), `tax_amount_in_words` (tax_total) | derived strings |
| Footer | `declaration`, `jurisdiction`, dual bank blocks | text / strings |


### `sale_orders` (header)
- **Identity / meta**: `so_number` (auto `SO-{year}-####`), `document_no` (Invoice No.),
  `so_date` (Dated), `mode_terms_of_payment`, `reference_no_date`, `other_references`,
  `terms_of_delivery`, `currency` (default INR)
- **Seller (billing_from)**: `billing_from_name`, `billing_from_address`, `billing_from_urn`,
  `billing_from_gstin`, `billing_from_gst_state`, `billing_from_gst_state_code`,
  `billing_from_email`, `billing_from_pan`
- **Buyer (billing_to)**: `billing_to_name`, `billing_to_address`, `billing_to_gst_state`,
  `billing_to_gst_state_code`, `place_of_supply`
- **Totals**: `taxable_amount`, `cgst`, `sgst`, `igst`, `round_off`, `tax_total`,
  `grand_total`, `amount_in_words` (derived), `tax_amount_in_words` (derived from tax_total)
- **Footer**: `declaration` (text), `jurisdiction`
- **Bank (two sets — HDFC box + ICICI declaration line)**: `bank_name`,
  `bank_account_number`, `bank_ifsc_code`, `bank_branch`, `secondary_bank_name`,
  `secondary_bank_account_number`, `secondary_bank_ifsc_code`, `secondary_bank_branch`
- **System**: `created_by`, `status` (enum draft/final), `is_deleted`, timestamps, softDeletes
- Indexes: `created_by`, `status`, `so_number`

### `sale_order_items` (line items)
- `sale_order_id`, `sl_no`, `description`, `hsn_sac`, `gst_rate`,
  `quantity` (nullable), `rate` (nullable), `per` (nullable), `amount`,
  `cgst_rate`, `cgst_amount`, `sgst_rate`, `sgst_amount`, `total_tax_amount`, timestamps
- The bottom **HSN tax-summary table is computed** by grouping items by `hsn_sac`
  and summing taxable value + CGST/SGST amounts.

### `sale_order_emails`, `sale_order_terms`
- Mirror `purchase_order_emails` / `purchase_order_terms`.

Decimal columns use `decimal(15,4)` to match PO conventions.

## Proposed Solution

Design the `SaleOrder` data model **from the invoice regions** (the field map above),
then borrow the PO module's proven *plumbing* to deliver it: draft/final flow,
permission group, soft delete, emailing, DomPDF service pattern, decimal(15,4)
conventions, and the `POST /{module}/{action}` routing style. The PO is a structural
reference for *how the pieces wire together*, not a source of columns — so the
migration, model `$fillable`, validation rules, `SaleOrderTotalVerifier` (per-line
CGST/SGST, HSN-grouped summary), `formatSaleOrderResponse()`, and
`exports/sale_order_pdf.blade.php` all follow the invoice shape (including the computed
HSN-grouped tax-summary table and both "in words" rows), never the PO shipping/delivery
cluster. Keeps consistency with the codebase and isolation from `LeadSale` / incentive logic.

```mermaid
flowchart LR
  R[routes/app.php\nprefix sale-order] --> C[SaleOrderController]
  R --> TC[SaleOrderTermsController]
  C --> M[SaleOrder model]
  M --> I[sale_order_items]
  M --> E[sale_order_emails]
  TC --> T[sale_order_terms]
  C --> V[SaleOrderTotalVerifier]
  C --> PDF[SaleOrderPdfService] --> B[exports/sale_order_pdf.blade.php]
  C --> EM[SaleOrderEmailService] --> PDF
  C --> F[FormatsEntityData::formatSaleOrderResponse]
  P[PermissionCatalog + PermissionSeeder] --> C
```

## Task Breakdown

### Task 1 — Migrations
Create `sale_orders`, `sale_order_items`, `sale_order_emails`, `sale_order_terms`
with the columns above (no charges table).
- **Test**: `php artisan migrate` clean on SQLite; `migrate:rollback` drops all four tables;
  Pest asserts `Schema::hasColumns` for the invoice fields.
- **Demo**: Tables exist with correct schema.

### Task 2 — Models
`SaleOrder`, `SaleOrderItem`, `SaleOrderEmail`, `SaleOrderTerm`. `SaleOrder` has
`generateSoNumber()` (`SO-{year}-####`), creating/updating/saving hooks deriving
`amount_in_words` + `tax_amount_in_words`, `not_deleted` global scope, relations.
- **Test**: creating a final SaleOrder auto-assigns sequential `SO-{year}-0001`;
  both in-words fields populated; scope hides `is_deleted=1`; relations resolve.
- **Demo**: Create SO + items and see number/words/relations.

### Task 3 — Totals Verifier
`App\Support\SaleOrderTotalVerifier`: per-line `amount` (when qty/rate present),
per-line cgst/sgst amounts = amount × rate / 100, `taxable_amount` = Σ line amounts,
`tax_total` = Σ line taxes, `grand_total` = taxable + tax_total + round_off (tol 0.01).
- **Test**: valid payload → no errors; tampered totals → keyed errors.
- **Demo**: Feed sample payload; show pass/fail arrays.

### Task 4 — Response Formatter
Add `formatSaleOrderResponse()` to `FormatsEntityData` returning the exact invoice
field set (seller, buyer, meta, items w/ per-line GST, totals, both in-words,
bank blocks, declaration, jurisdiction).
- **Test**: covered by controller feature tests (JSON shape).
- **Demo**: Detail endpoint returns fully shaped object.

### Task 5 — Permissions
Add `sale_order_management` (`sale_order => $full`, `terms => $crud`) to
`PermissionCatalog::hierarchy()`; ensure `PermissionSeeder` registers it.
- **Test**: seeding asserts `sale_order_management.sale_order.{create,read,update,delete,export,override}`.
- **Demo**: `php artisan db:seed --class=PermissionSeeder` creates them.

### Task 6 — SaleOrderController (CRUD + draft/final)
Mirror PO: create/update/delete, draft create/update/delete/finalize, all/fetch,
detail/fetch, draft variants. Uses `sale_order_management.*` perms,
`SaleOrderTotalVerifier`, header/party/line sync helpers, `RecordsActivity`,
`formatSaleOrderResponse`. Validation for all invoice fields.
- **Test**: Pest feature — create (final → number), draft create/update/finalize
  (guard: ≥1 item + seller name), update, soft delete, list pagination/search,
  detail; `403` without permission.
- **Demo**: create→list→detail→update→delete and draft→finalize.

### Task 7 — Routes
Add `Route::prefix('sale-order')` group mirroring PO routes (CRUD, draft/*,
send-email/email-history/resend-email, GET /pdf, term sub-group).
- **Test**: `/api/sale-order/all/fetch` with auth → 200 paginated envelope.
- **Demo**: Endpoints resolve; auth/permission enforced.

### Task 8 — Terms sub-resource
`SaleOrderTermsController` + `SaleOrderTerm` (create/update/delete/fetch) under
`sale_order_management.terms.*`.
- **Test**: term CRUD with permission checks.
- **Demo**: create/list/update/delete SO terms.

### Task 9 — PDF service + Tax-Invoice Blade
`SaleOrderPdfService` (loads SaleOrder + items + terms, renders
`exports/sale_order_pdf.blade.php`, stores under `sale_orders/`). Blade reproduces
the reference regions exactly: title "Tax Invoice", seller header block
(URN/UDYAM/GSTIN/State+Code/email), right meta box, Buyer (Bill To) + Place of Supply,
item table (Sl, Description, HSN/SAC, GST Rate, Quantity, Rate, per, Amount),
Output CGST/SGST rows, Total, `E & O.E`, Amount Chargeable in words, computed
HSN-grouped tax summary + Tax Amount in words, PAN, Declaration, dual Bank Details,
`for {seller}` / Authorised Signatory, jurisdiction line, computer-generated note.
Add `download_sale_order_pdf` (perm `export`) + GET `/sale-order/pdf`.
- **Test**: download returns `application/pdf`, non-empty body for seeded final SO.
- **Demo**: Download a tax invoice reproducing the reference values.

### Task 10 — Email service + endpoints
`SaleOrderEmailService` (uses `SaleOrderPdfService`) + `send_sale_order_to_email`,
`get_sale_order_email_history`, `resend_sale_order_email`, reusing
`QuotationAttachmentService` + `AccountDocument`. Log via `RecordsActivity`.
- **Test**: send persists `sale_order_emails` record; history returns it; resend
  updates status (mailer faked).
- **Demo**: Send invoice by email, view history, resend.

### Task 11 — Integration + cleanup
Run `composer test`, `./vendor/bin/pint` on new files, verify full flow
(draft → finalize → PDF → email → terms), ensure routes/permissions wired, no orphans.
- **Test**: green `composer test`; happy-path test covering create→finalize→pdf→email.
- **Demo**: One script exercising the full SO lifecycle.

## Notes / Decisions
- **Data model is invoice-driven, not PO-derived.** PO is copied only for plumbing
  (flow, permissions, email, PDF service pattern, decimal conventions); the field set,
  types, and GST shape come from the reference invoice image.
- No `charges` table — invoice has no other-charges concept.
- Per-item CGST/SGST rate + amount stored (invoice splits GST at line and HSN-summary level).
- Two bank-detail sets (HDFC box + ICICI declaration line).
- Reference invoice values are the canonical seed/test fixture so generated output matches the image.

## Pattern References (existing PO artifacts reused for *plumbing only* — not for data shape)
- `app/Models/PurchaseOrder.php` (+ items/charges/emails/terms)
- `app/Http/Controllers/PurchaseOrderController.php`, `PurchaseOrderTermsController.php`
- `app/Services/PurchaseOrderPdfService.php`, `PurchaseOrderEmailService.php`
- `app/Support/PurchaseOrderTotalVerifier.php`, `app/Support/AmountInWords.php`
- `app/Traits/FormatsEntityData.php::formatPurchaseOrderResponse()`
- `app/Support/PermissionCatalog.php` (`purchase_order_management`)
- `resources/views/exports/purchase_order_pdf.blade.php`
- `routes/app.php` (`Route::prefix('purchase-order')`)
- `database/migrations/2026_09_05_000001..000004`, `2026_09_09_000000` (PO tables)
