MiknasTrading/docs/superpowers/specs/2026-05-18-purchase-pipeline-design.md

8.0 KiB

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