# Purchase Pipeline — Design Spec **Date:** 2026-05-18 **Status:** Approved for implementation --- ## Overview Replace the disconnected purchase screens with a single end-to-end pipeline that tracks every purchase like a package delivery — each record shows exactly which stage it is at, who acted last, and what is needed next. The pipeline has 8 stages. 3 new database tables are added. All existing models (PurchaseRequest, PurchaseOrder, GRN, SupplierPayment) are reused with minor additions. --- ## The 8 Stages | # | Stage | Actor | Model | |---|-------|-------|-------| | 1 | Purchase Request created | Operations team | `purchase_requests` (existing) | | 2 | GM digital signature | GM | `purchase_signatures` (NEW) | | 3 | Suppliers selected + RFQ links sent | Purchasing team | `rfq_invitations` (NEW) | | 4 | Suppliers submit quotes via private link | Supplier (public portal) | `supplier_quotes` + `supplier_quote_items` (NEW) | | 5 | Quote comparison + winner selected | Purchasing team | `supplier_quotes` (awarded flag) | | 6 | LPO issued to winning supplier | Purchasing team | `purchase_orders` (existing, relabelled LPO) | | 7 | Materials received at site | Warehouse / Store Manager | `goods_receipt_notes` + `grn_items` (existing, + type field) | | 8 | Payment / cheque issued | Accounts | `supplier_payments` (existing) | The active stage is stored as a `stage` enum column on `purchase_requests`. It advances automatically when each step is completed. --- ## New Database Tables ### `purchase_signatures` ``` id purchase_request_id FK → purchase_requests signed_by FK → users signature_image TEXT (base64 PNG from canvas) signed_at TIMESTAMP ip_address STRING nullable ``` ### `rfq_invitations` ``` id purchase_request_id FK → purchase_requests supplier_id FK → suppliers token STRING(64) UNIQUE — cryptographically random hex channel ENUM: email | whatsapp | both sent_at TIMESTAMP nullable opened_at TIMESTAMP nullable expires_at TIMESTAMP (sent_at + 7 days) status ENUM: pending | sent | opened | submitted | declined timestamps ``` ### `supplier_quotes` ``` id rfq_invitation_id FK → rfq_invitations purchase_request_id FK → purchase_requests (denormalised for easy querying) supplier_id FK → suppliers submitted_at TIMESTAMP lead_time_days INTEGER nullable payment_terms STRING nullable notes TEXT nullable total_amount DECIMAL(12,3) is_awarded BOOLEAN default false award_reason TEXT nullable awarded_at TIMESTAMP nullable awarded_by FK → users nullable timestamps ``` ### `supplier_quote_items` ``` id supplier_quote_id FK → supplier_quotes description TEXT unit STRING(50) quantity DECIMAL(10,3) unit_price DECIMAL(12,3) total_price DECIMAL(12,3) timestamps ``` --- ## Existing Table Changes ### `purchase_requests` — add column ``` stage ENUM: draft | gm_approval | rfq | quoting | comparison | lpo | receiving | payment | complete DEFAULT: draft ``` ### `grn_items` — add column ``` type ENUM: inventory | consumable DEFAULT: inventory ``` No other existing tables are changed. --- ## Architecture ### Internal routes (auth-protected) All existing purchase routes remain. New routes added: ``` GET purchase/pipeline purchase.pipeline.index GET purchase/requests/{id}/sign purchase.requests.sign (GM signature page) POST purchase/requests/{id}/sign purchase.requests.sign.store GET purchase/requests/{id}/rfq purchase.requests.rfq (select suppliers) POST purchase/requests/{id}/rfq purchase.requests.rfq.store (send invitations) GET purchase/requests/{id}/quotes purchase.requests.quotes (view all quotes) POST purchase/requests/{id}/quotes/{quote}/award purchase.requests.quotes.award GET purchase/requests/{id}/compare purchase.requests.compare (comparison table) ``` ### Public routes (no auth) ``` GET /rfq/{token} RfqPortalController@show (supplier quote form) POST /rfq/{token} RfqPortalController@submit (supplier submits quote) ``` Token is 64 hex chars generated via `bin2hex(random_bytes(32))`. Validated on every request — expired, already-submitted, or unknown tokens show a clear error page. --- ## UI Components ### 1. Pipeline index page (`purchase/pipeline`) Replaces the current fragmented purchase list. Shows all active purchases as cards, each with: - Vertical timeline showing completed stages (checked, with date + actor) - Current stage highlighted in amber with action button - Upcoming stages grayed out ### 2. GM Signature modal - HTML5 Canvas — GM draws with mouse or touch - "Clear" and "Confirm Signature" buttons - On confirm: canvas serialised to base64 PNG, POSTed to server, stored in `purchase_signatures` - Signature image displayed on the request detail forever after ### 3. RFQ Invitation screen - Checklist of suppliers from the suppliers table (search/filter) - Per-supplier: toggle WhatsApp / Email / Both - "Send Invitations" generates tokens, logs `rfq_invitations`, sends messages - WhatsApp: deep link `https://wa.me/{number}?text=...` with the URL embedded - Email: Laravel `Mail` with the unique URL ### 4. Public supplier portal (`/rfq/{token}`) - No login, no navigation — clean standalone page - Shows: your company name, request reference, list of items needed (description + quantity) - Supplier fills: unit price per item, lead time (days), payment terms, notes - One submit only — redirects to a thank-you screen - After submission: `rfq_invitations.status` → submitted, `supplier_quotes` record created ### 5. Quote comparison table - Side-by-side: one column per supplier who submitted - Rows: each item, plus totals row, lead time, payment terms - Lowest price per item highlighted in green - "Award to this supplier" button per column — opens a modal to enter reason - Awarding locks the comparison and advances stage to `lpo` ### 6. Vertical timeline (on every purchase detail) Each completed stage shows: - Stage name + icon - Actor name (who did it) - Timestamp - Any key data (e.g. "Signed by Ahmed Al-Rashid", "3 quotes received", "LPO sent to Safety Chemical Trading") Current stage pulses amber. Future stages are gray. --- ## New Controllers / Services | File | Responsibility | |------|---------------| | `PurchasePipelineController` | Pipeline index — lists all purchases by stage | | `PurchaseSignatureController` | Store GM signature, advance stage | | `RfqController` | Select suppliers, generate tokens, send invites | | `SupplierQuoteController` | View quotes, award winner | | `RfqPortalController` | Public: show form, accept submission | | `RfqInvitationService` | Generate token, send WhatsApp link + email | | `PurchaseStageService` | Single place that advances `stage` and logs the event | --- ## New Models `PurchaseSignature`, `RfqInvitation`, `SupplierQuote`, `SupplierQuoteItem` --- ## Notification Channels **WhatsApp:** Generate a `wa.me` deep-link with the RFQ URL pre-filled in the message body. User clicks the link in the system → WhatsApp opens on their device → they send it to the supplier. (No WhatsApp API key needed.) **Email:** Standard Laravel `Mail::to($supplier->email)->send(new RfqInvitationMail($invitation))`. --- ## Constraints & Rules - A purchase cannot advance past stage 2 (GM sign) without a signature record - A purchase cannot advance past stage 4 (quoting) with fewer than 1 submitted quote (minimum 1, recommended 3) - A token expires 7 days after `sent_at`; expired tokens show an expiry message, not an error - A quote token can only be submitted once; re-visiting after submit shows the thank-you screen - Awarding a quote is irreversible — all other quotes are marked `declined` - GRN items default to `inventory`; the receiving form lets the user toggle each item to `consumable`