Skip to content

Book 3, Chapter 14: Supplier Portal

1. Purpose

The Supplier Portal is how Bizwiz opens a door to the outside — to the third-party suppliers who sell goods to the retailer. Everything else in Books 1–3 is internal staff using the ERP. This module is the one place where a non-employee (a supplier's own staff) logs in and interacts with the business.

Two sides fit together:

  • The supplier-facing apps. A supplier can log in (on a phone app or a web portal) and: see the Purchase Orders / LPOs the retailer has raised against them, accept or reject an order, book a delivery slot, upload their invoice and delivery documents, confirm returns, and check payment status (which invoices are paid, which are pending, what bank file the payment went out on). They can also see statements, credit notes, trade discounts / incentives owed to them, sales/stock analytics for their products, request new SKUs, book meetings with the buying team, and top up a wallet.

  • The admin-side management module. Internal staff (buyers, procurement admins) use the "Supplier Portal" menu inside Bizwiz to invite suppliers to the portal, watch pending invites turn into onboarded suppliers, suspend a supplier, impersonate a supplier account (with an approval workflow) to troubleshoot, approve the changes suppliers request on LPOs, approve bank deposits, run billing for the portal subscription, and manage marketing campaigns suppliers can buy into.

The most important architectural fact: the Supplier Portal is not one codebase. It is a distributed system with three tiers, and this chapter's job is to keep them straight.

┌────────────────────────┐     ┌──────────────────────────────┐     ┌───────────────────────────┐
│  Supplier front-ends   │     │  Supplier Portal backend     │     │  bizwiz tenant (this repo)│
│  • Flutter app         │────▶│  (SEPARATE service at        │────▶│  • supplier-center /      │
│    bwsupplierportalapp │     │   env SUPPLIER_PORTAL_URI)   │     │    supplier-portal APIs   │
│  • Web portal          │     │  identity, onboarding, ERP   │◀────│  • admin management UI    │
│    (not in either repo)│◀────│   selection, wallet, billing │     │  • the actual PO/AP data  │
└────────────────────────┘     └──────────────────────────────┘     └───────────────────────────┘
        picks an ERP →         issues per-ERP JWT, proxies →         reads/writes wa_* tables

The Supplier Portal backend is a distinct application (Bizwiz calls it over HTTP via env('SUPPLIER_PORTAL_URI'), e.g. /api/onboarding-check, /api/impersonation-url, /api/top-up-requests/pending). Its source is not in this repo and not in the Flutter repo — treat it as a black box that owns supplier identity and multi-tenant routing. Each retailer runs its own bizwiz ("ERP"); the portal backend fans a single supplier login out across all the ERPs that supplier deals with.


2. Users & roles

2a. Supplier users (external)

Suppliers authenticate through the portal backend, not through Bizwiz's normal AdminLoggedIn staff auth.

  • Flutter app auth (bwsupplierportalapp): phone-number + OTP login. The app posts to /api/mobile/login → /api/mobile/verify-otp (endPointLogIn, endPointValidateOTP in lib/utils/constants/api_endpoints.dart:12-14). The AuthModel returned carries role, user_id, requires_password_set, phone_number, otp_active (lib/models/user.dart). The only app role is "Supplier Admin" (roleDefSupplierAdmin, lib/utils/constants/app_roles.dart:8) — there is a single role enum AppRoles.supplierAdmin.
  • Multi-ERP selection: after login the supplier picks which retailer/ERP to work in (/api/mobile/erps → /api/mobile/erps/select, api_endpoints.dart:43-46). Selecting an ERP returns a fresh JWT scoped to that tenant (change_erp_viewmodel.dart onSelectErp() reads data.authorization.token and stores it), and the app's appBaseURL then points at that tenant's bizwiz. So a supplier logs in once but holds N tenant tokens.
  • Supplier identity to bizwiz: when the portal backend calls a bizwiz data endpoint, the supplier is resolved by supplier_code, falling back to KRA PIN (+ optional email) via the ResolveSupplierByCodeOrPin middleware (app/Http/Middleware/ResolveSupplierByCodeOrPin.php:18-66).

2b. Portal API auth (service-to-service)

The supplier-center / supplier-portal data endpoints in bizwiz are protected by the isr_request middleware (ValidateIncomingSupplierPortalHmacSignature):

  • Primary: HMAC-SHA256 signature over the request using X-Signature, X-Timestamp, X-Nonce headers (app/Http/Middleware/ValidateIncomingSupplierPortalHmacSignature.php:27-29).
  • Fallback: legacy Bearer token compared to ERP_ISR_KEY env (same file, ~lines 114-176).

This is a trust boundary between the portal backend and each bizwiz tenant — not a per-supplier auth. The supplier's identity rides in the request body/params and is resolved by ResolveSupplierByCodeOrPin.

2c. Internal admins (staff)

Managed by the normal staff permission system. role_id == 1 is superadmin (sees everything). Otherwise the sidebar gates each item on a $my_permissions[...] flag. Permission slugs (from sidebar_includes/supplier_portal.blade.php and controller can() checks):

Menu item Permission slug Controller can()
Onboarded Suppliers supplier-maintain-suppliers___view can("view","supplier-maintain-suppliers")
Pending Invites pending-suppliers___view can("view","pending-suppliers")
Invite supplier — can("invite","pending-suppliers")
Suspend supplier supplier-portal___* can("suspend","supplier-portal")
Impersonation Requests impersonation-requests___view can("view","impersonation-requests") / can("manage",…)
Do impersonation — can("impersonate","supplier-maintain-suppliers")
Supplier staff mgmt — can("staff","supplier-maintain-suppliers")
Logs supplier-portal___logs can("logs","supplier-portal")
Approve LPO Changes lpo-portal-req-approval___view (LpoPortalReqApproval)
New SKU Requests request-new-sku___view (RequestNewSkuController)
Billing Note billing-description_view can("view","billing-description")
Top Up Requests top_up_requests___view / pending_top_up_requests___view (SupplierTopUpRequest)
Bank Deposits (initial/final) supplier-bank-deposits-initial-approval___view, …final… —
Billing (initial/final) billing-submitted___view, billing-submitted-final___view —
Schedule Meetings supplier-schedule-meetings___view, supplier-slot-bookings___view (SupplierScheduleMeeting)
Marketing Campaigns marketing-campaigns___view (+ applications/configs/banner/whatsapp) (Marketing* controllers)
Incentives Dashboard salesman_incentives_dashboard___view (SalesmanIncentivesDashboard)
Delivery Slots / Offloading / Vehicle Type order-delivery-slots___view, supplier-vehicle-type___view —
API Call Logs api-call-logs___view —

Note a couple of items are hard-coded to user IDs, not permissions: "Billed Suppliers" is gated on in_array($logged_user_info->id,[1,1186]) (sidebar_includes/supplier_portal.blade.php).


3. Processes

3.0 Route map (where each side lives)

Concern File
Admin management UI (invite/suspend/impersonate/logs/billing-note/meetings) routes/web.php:5390-5430+ (supplier-portal. prefix)
Admin management UI (top-up, incentives, billing, marketing) routes/modules/supplier_portal.php
Supplier data APIs (app/web portal → bizwiz) routes/api_includes/supplier_portal.php (supplier-center. / supplier-portal. under isr_request)
Supplier LPO actions (accept/change/return) routes/api.php:174-176 + api_includes/supplier_portal.php:37
Flutter app endpoints (/api/mobile/*, /api/management-reports/*) served by the portal backend, not in this repo

3.1 Onboarding a supplier (admin invites → supplier joins)

Trigger: buyer clicks Invite on the Pending Invites screen.

The source of truth for "in portal vs not" is trade_agreements.linked_to_portal (SupplierPortalController.php:56). A supplier appears under Pending Invites when they have an Approved trade agreement whose linked_to_portal is false/null; they move to Onboarded Suppliers once linked_to_portal = 1 (set by the portal callback at Api\TradeAgreementController@store, then ordered by linked_at desc).

sequenceDiagram
    participant Buyer as Buyer (bizwiz admin)
    participant BW as bizwiz SupplierPortalController
    participant Portal as Portal backend (SUPPLIER_PORTAL_URI)
    participant Supp as Supplier
    Buyer->>BW: inviteSupplier() (can invite pending-suppliers)
    Note over BW: validate supplier_id in wa_suppliers,<br/>kra_pin required
    BW->>Portal: POST /api/onboarding-check<br/>{supplier_code,name,email,kra_pin,source}
    Portal-->>BW: result=1 (created) / 0 (validation) / -1 (error)
    BW->>Supp: email SupplierNotification / SupplierLinkedNotification
    Supp->>Portal: accepts, sets password, logs in
    Portal->>BW: TradeAgreementController@store callback
    Note over BW: trade_agreements.linked_to_portal = 1, linked_at = now()
    Note over Buyer: supplier now shows under "Onboarded Suppliers"
  • getPendingSuppliers() SupplierPortalController.php:43-95 — TradeAgreement where status='Approved' AND (linked_to_portal false/null). If the admin lacks can("can-view-all-suppliers","maintain-suppliers"), results are restricted to suppliers linked to them via wa_user_suppliers (lines 66-70).
  • inviteSupplier() :477-557 — POSTs to portal /api/onboarding-check, emails the supplier.
  • get_all_supplier_from_portal() :289-332 — TradeAgreement where linked_to_portal=true, enriched with portal data via ApiService->get_portal_suppliers().
  • suspend_supplier() :414-475 — calls portal suspend_supplier then toggles wa_suppliers.is_suspended locally. (Flutter SupplierModel.suspended reflects this.)

3.2 Supplier acts on an LPO (the Ch12 seam)

Trigger: retailer raises an LPO/PO against the supplier (Ch12 Purchases). It surfaces to the supplier as wa_purchase_orders with supplier_accepted = false.

The Flutter app fetches LPOs from /api/mobile/lpo-listing (endPointGetLPOListing, lpo_listing_viewmodel.dart:fetchLpoList) with type = pending|fulfilled|rejected, cursor pagination (next_cursor), and a search_query. LpoModel carries lpo_number, status, supplier_code, ship_to, lpo_type, for_approval, supplier_own, reject_reason, credit_note_amount, plus LpoItemModel lines (quantity, unit_price, vat_amount, discount_*, return_quantity, free_qualified_stock) (lib/models/lpo_models.dart).

Supplier actions map to bizwiz endpoints (all write wa_purchase_orders):

stateDiagram-v2
    [*] --> Sent: HQ raises LPO (supplier_accepted=false)
    Sent --> Accepted: acceptLpo() → supplier_accepted=true
    Sent --> Rejected: rejectLpo() → supplier_accepted=2
    Accepted --> Sent: reverseLpo() → supplier_accepted=false
    Accepted --> SlotBooked: slotBooked() (OwnCollection) → slot_booked=true
    SlotBooked --> GoodsReleased: goodsReleased() → goods_released=true
    Sent --> ChangeRequested: receive_lpo_for_approval() → wa_lpo_portal_req_approval (Pending)
    ChangeRequested --> Sent: HQ approves/rejects (Ch12 "Approve LPO Changes")
    Accepted --> Discarded: discardLpoFromPortal() → is_hide='Yes'
    Accepted --> DetailsUpdated: receive_order_details_update() → invoice/vehicle/docs

Key handlers (app/Http/Controllers/Api/PurchaseOrderController.php unless noted): - Accept / reject / reverse / slot / goods-released: acceptLpo():578, rejectLpo():596, reverseLpo():614, slotBooked():632, goodsReleased():650 — set the corresponding wa_purchase_orders boolean/enum columns. - Request changes receive_lpo_for_approval():29-114 (route api.php:175) — creates a wa_lpo_portal_req_approval (status Pending) with wa_lpo_portal_req_approval_items lines; auto-rejects prior pending requests for the same item. Notifies the PO creator (LpoChangesApprovalNotification). Admin reviews under Approve LPO Changes (lpo-portal-req-approval.index). - Submit delivery details / invoice receive_order_details_update():179-204 (route api.php:174) — writes cu_invoice_number, vehicle_reg_no, driver_name, driver_phone, receive_note_doc_no, supplier_invoice_no, invoice_control_amount, and a documents JSON (delivery_note / supplier_invoice / other_documents, from_portal flag). - Confirm/reject a return lpo_return_accepted():116-177 (route api.php:176) — sets wa_receive_purchase_orders.return_status = Accepted|Rejected (+ return_comment, credit_note_doc) and writes a reverse wa_stock_moves row for the returned qty. - Discard an LPO discardLpoFromPortal() (Admin PurchaseOrderController:214-229, route supplier_portal.php:37) — wa_purchase_orders.is_hide='Yes' and archives related inbound_deliveries (status='ARCHIVED').

3.3 Supplier views invoices & payment status (the Ch13 AP seam)

Trigger: supplier opens the "Vendor Centre" / payments area in the portal.

Served by VendorCentreController under the supplier-center prefix (api_includes/supplier_portal.php:44-51):

  • payables() :115-202 — the supplier's invoices/statement: reads wa_supp_trans (invoice header — trans_date, cu_invoice_number, suppreference, total_amount_inc_vat, vat_amount, withholding_amount, grn_number), nets off payment_voucher_items / payment_vouchers, financial_notes (credit notes), and advance_payment_allocations to show payable_amount / paid_amount per invoice.
  • grn() :507-597 — GRNs the supplier delivered (wa_grns + linked wa_purchase_orders), split pending (no invoice) vs completed.
  • payments() :599-681 — payment vouchers with status badge Pending / Approved / Processed / Bounced, plus the wa_bank_files / wa_bank_accounts the payment went out on.
  • paymentsWithItems() :683-746 — each voucher's line items resolved polymorphically to the underlying wa_supp_trans invoice or supplier_bills bill.

Trade discounts, incentive demands, price-drop/promotion credit notes are exposed via TradeDiscountController, TradeDiscountDemandController, PriceDropDemandController, PromotionDemandController, IncentiveDemandController, FinancialNoteController, and ReturnToSupplierController (see the long supplier-center route list). Payment terms & bank details come from SupplierPortalMiscellaneousRequestController::getSupplierPaymentDetails() (wa_suppliers->getPaymentTerm).

3.4 Admin impersonates a supplier (approval workflow)

Trigger: support needs to see exactly what a supplier sees.

sequenceDiagram
    participant A as Admin (requester)
    participant BW as SupplierPortalController
    participant TC as TaskCenter
    participant Approver
    participant Portal as Portal backend
    A->>BW: storeImpersonationRequest() {supplier_id,user_id,reason}
    Note over BW: no duplicate Pending for same supplier+user
    BW->>TC: register task, SMS approvers
    Approver->>BW: approveImpersonationRequest() (can manage impersonation-requests)
    Note over BW: status=Approved, approved_at=now(), approved_by
    BW->>TC: complete, SMS requester
    A->>BW: SupplierImpersonationController@show(supplier)
    Note over BW: request Approved AND approved_at >= now()-Xh, mark used=1
    BW->>Portal: POST /api/impersonation-url {supplier_code,email,kra_pin,is_admin}
    Portal-->>BW: {url}
    BW-->>A: redirect to portal url (logged in as supplier)
  • impersonation_requests table: supplier_id, requested_by, reason, status (Pending/Approved/Rejected), approved_at/by, rejected_at/by, rejection_reason, approval_reason, used (bool). Migration 2025_09_09_142750_create_impersonation_requests_table.php.
  • Validity window = settings slug number-of-hours-for-impersonation-requests (SupplierPortalController:343,375; SupplierImpersonationController:23).
  • show() (SupplierImpersonationController:17-71): checks Approved + within window, marks used=1, calls portal /api/impersonation-url, redirects.

3.5 Other admin-side processes (brief)

  • Top-up requests: admin lists/approves supplier wallet top-ups by proxying the portal backend (SupplierTopUpRequestController → SUPPLIER_PORTAL_URI/api/top-up-requests/*).
  • Portal subscription billing: SupplierPortalSubscriptionBillController (current bill / history to suppliers via billing/current-bill, billing/bill-history; admin "Billed Suppliers"). Two-stage bank-deposit and billing approvals (initial/final) are separate menu items.
  • Schedule meetings: SupplierScheduleMeetingController + SupplierBookingController — suppliers book slots with buyers (supplier-portal/book-meeting, /scheduled-meetings, /cancel-meeting); admin manages slots, exceptions, reschedules, arrival notifications.
  • Marketing campaigns: suppliers apply to promote products; admin approves per-product, configures pricing, runs bulk WhatsApp, manages banner slots (routes/modules/supplier_portal.php).
  • Analytics for suppliers (Flutter Home/Reports): summary stats, turnover, total sales, stock counts, and stock reports (location/dead/overstock/missing/item-sales) via /api/mobile/* and /api/mobile/procurement-reports/* (api_endpoints.dart:17-35, home_viewmodel.dart).

4. Tables touched & key data

Supplier identity / onboarding: - wa_suppliers — supplier_code, name, slug, email, kra_pin, is_suspended, bank fields, payment term. Migration 2023_09_08_134414_create_wa_suppliers_table.php. - trade_agreements — reference, status (Approved/Pending), wa_supplier_id, linked_to_portal, linked_at. This is the pending↔onboarded gate. (…create_trade_agreements_table.php, …add_linked_at_column…). - wa_user_suppliers (WaUserSupplier) — links internal user_id ↔ wa_supplier_id (scopes which buyer sees which suppliers). - impersonation_requests — approval workflow (see 3.4). - settings — SUPPLIER_PORTAL_BILLING_DESCRIPTION (billing note), number-of-hours-for-impersonation-requests.

Purchase / LPO (Ch12 seam): - wa_purchase_orders — purchase_no, supplier_accepted (false / true / 2=rejected), slot_booked, goods_released, is_hide, documents (JSON), cu_invoice_number, vehicle_reg_no, driver_name, driver_phone. - wa_lpo_portal_req_approval / wa_lpo_portal_req_approval_items — supplier-requested LPO changes awaiting HQ (status Pending/Rejected, rejection_message, item qty/price/reason). - wa_receive_purchase_orders — return_status, return_comment, credit_note_doc. - wa_stock_moves — reverse movement rows for accepted returns. - inbound_deliveries — SUPPLIER_COLLECTION docs, archived when an LPO is discarded. - wa_grns — grn_number, wa_supplier_id, delivery_date, wa_purchase_order_id.

AP / payments (Ch13 seam): - wa_supp_trans — supplier invoices/statement lines. - payment_vouchers / payment_voucher_items — payments + allocations (polymorphic to wa_supp_trans or supplier_bills). - financial_notes — credit/debit notes. advance_payment_allocations. - wa_bank_files / wa_bank_file_items / wa_bank_accounts — payment execution.

Supplier items / incentives: - wa_inventory_item_suppliers, wa_inventory_items, pack_sizes. - wa_inventory_location_transfers / _items / _item_returns, restaurants (branches), routes, wa_inventory_assigned_items (parent/child).

Owned by the portal backend, not bizwiz: the supplier login identity, per-ERP JWTs, wallet balances, top-up requests, subscription billing ledgers, and the portal activity log feed all live in the SUPPLIER_PORTAL_URI service. Bizwiz only mirrors/reads them via ApiService.


5. Interactions with other modules

  • Ch12 Purchases (POs/LPOs) — the core seam. LPOs originate in Purchases; the portal is the supplier's window to accept/reject/change/deliver against them. Supplier writes land back on wa_purchase_orders, wa_lpo_portal_req_approval*, wa_receive_purchase_orders, wa_stock_moves, wa_grns. The admin Approve LPO Changes screen (lpo-portal-req-approval) closes the loop. GRN generation and stock impact are Ch12/inventory.
  • Ch13 Accounts Payable — the "payment status / statement / credit notes" the supplier sees are AP artifacts: wa_supp_trans (invoices), payment_vouchers (+bank files), financial_notes (CNs), advance_payment_allocations. The portal is a read view onto AP; it does not post GL. Supplier-submitted invoices (receive_order_details_update) feed the AP invoicing pipeline.
  • Inventory / Reports — supplier analytics (turnover, dead/overstock/missing stock, item-sales) reuse the same report controllers as internal reporting, filtered to the supplier.
  • Trade Agreements — trade_agreements.linked_to_portal is the onboarding gate; new-SKU and trade-discount demands flow through TradeAgreement* / TradeDiscount* controllers.
  • Notifications / SMS / TaskCenter — invites, impersonation, and LPO-change approvals use the email + AlertService/SmsScenarios + TaskCenterService infrastructure.

6. Alternatives & variants

  • Two supplier front-ends. (1) The Flutter app bwsupplierportalapp — phone+OTP, multi-ERP, focused on LPO listing + analytics + profile (bottom-nav Home / LPOs / Reports / Profile, single "Supplier Admin" role). (2) A web portal (source not in either repo) that drives the richer supplier-center surface (invoices, GRNs, payments, discounts, returns, meetings, marketing). Both talk to the same bizwiz APIs via the portal backend.
  • Distributed vs monolith. Unlike every other chapter, the "portal" logic is split across the external SUPPLIER_PORTAL_URI service and this bizwiz repo. Bizwiz is the data/tenant tier; the portal backend is the identity/tenant-router tier.
  • Auth variants. Service auth to bizwiz has a modern HMAC path and a legacy Bearer (ERP_ISR_KEY) fallback. Supplier resolution tries supplier_code first, then KRA PIN.
  • supplier_accepted tri-state (false / true / 2) rather than a clean enum — legacy boolean overloaded to carry "rejected". Confirm downstream readers treat 2 as rejected.
  • Permission vs hard-coded gates. Most menu items use permission slugs; a few (Billed Suppliers) are gated on literal user IDs [1,1186].
  • Marketing / meetings / top-ups / bank-deposits are optional sub-features layered on the core portal; a tenant may not use all of them.

7. Open questions to confirm

CODE-PROVEN

  • Onboarding gate is trade_agreements.linked_to_portal set by portal callback (SupplierPortalController.php:56, 62-65, 303).
  • Invite calls portal /api/onboarding-check; suspend calls portal then toggles wa_suppliers.is_suspended (:508-514, 433-450).
  • Impersonation is a Pending→Approved→used workflow with an expiry window from settings; the actual login URL is minted by the portal /api/impersonation-url (SupplierPortalController:853-953, SupplierImpersonationController:17-71).
  • Supplier LPO actions write specific wa_purchase_orders columns; change-requests create wa_lpo_portal_req_approval*; returns write wa_receive_purchase_orders + wa_stock_moves (Api\PurchaseOrderController.php:29-204, 578-666).
  • Invoices/payments/GRNs come from wa_supp_trans, payment_vouchers(+items), financial_notes, wa_grns, wa_bank_files (VendorCentreController.php:115-746).
  • Data APIs are guarded by HMAC (isr_request) + ResolveSupplierByCodeOrPin.
  • Flutter app: phone+OTP, single "Supplier Admin" role, multi-ERP token swap (bwsupplierportalapp/lib/…).

INFERENCE (verify)

  • The /api/mobile/* and /api/management-reports/* endpoints the Flutter app calls are served by the portal backend (or a per-ERP bizwiz not in this repo), not by any route file in this bizwiz repo. No matching routes exist here — inferred from the dynamic appBaseURL + ERP-selection JWT swap. Confirm which service actually hosts /api/mobile/login, /api/mobile/lpo-listing, /api/mobile/erps.
  • The web portal front-end (invoices/discounts/meetings UI) is a separate app; its repo location is unknown. Inferred from the supplier-center API surface having no bizwiz Blade views.
  • Exact ownership boundary of wallet / top-up / subscription-billing ledgers (portal backend vs bizwiz mirror) — bizwiz only proxies; the authoritative store is assumed portal-side.
  • Whether supplier_accepted = 2 is universally interpreted as "rejected" across all readers.
  • Full permission-seeder definitions for the slugs in §2c (values inferred from sidebar + controller can() calls, not from a seeder read).

8. Source references

bizwiz — nav & routes - resources/views/admin/includes/sidebar_includes/supplier_portal.blade.php — module boundary, all menu items + permission gates. - routes/web.php:5390-5430+ — admin supplier-portal. management routes (pending, onboarded, impersonation, suspend, invite, staff, billing-description, meetings). - routes/modules/supplier_portal.php — top-up, salesman-incentives, billing, marketing campaigns/ applications/configs/banner/whatsapp. - routes/api_includes/supplier_portal.php — supplier data APIs (supplier-portal. + supplier-center. under isr_request): items, incentives, meetings, discounts, invoices, GRNs, payments, credit notes, returns, billing. - routes/api.php:174-176 — supplier LPO portal actions (details-update, lpo-for-approval, return-status).

bizwiz — controllers - app/Http/Controllers/Admin/SupplierPortalController.php — onboarding, invite (:477), suspend (:414), impersonation workflow (:853-953), logs (:97-257), billing-description (:629-676), staff (:559-627). - app/Http/Controllers/Admin/SupplierImpersonationController.php:17-71 — mint impersonation URL. - app/Http/Controllers/Api/PurchaseOrderController.php:29-204, 578-666 — supplier LPO accept/reject/change/return/slot/goods-released. - app/Http/Controllers/Admin/PurchaseOrderController.php:214-229 — discardLpoFromPortal. - app/Http/Controllers/Admin/VendorCentreController.php:115-746 — payables/grn/payments. - app/Http/Controllers/Api/SupplierPortalMiscellaneousRequestController.php — items, branches, payment details, incentives. - app/Http/Controllers/SupplierTopUpRequestController.php, SupplierPortalSubscriptionBillController.php, SupplierScheduleMeetingController.php, SalesmanIncentivesDashboardController.php, Marketing*Controller.php.

bizwiz — middleware / services / models / migrations - app/Http/Middleware/ValidateIncomingSupplierPortalHmacSignature.php (isr_request). - app/Http/Middleware/ResolveSupplierByCodeOrPin.php:18-66. - app/Services/ApiService.php + app/helpers.php:3114 (SUPPLIER_PORTAL_URI). - Model/WaUserSupplier.php; migrations 2023_09_08_134414_create_wa_suppliers_table.php, 2024_04_06_134419_create_trade_agreements_table.php, 2024_08_31_110218_add_linked_at_column_in_trade_agreements_table.php, 2025_09_09_142750_create_impersonation_requests_table.php.

bwsupplierportalapp (Flutter) - lib/utils/constants/api_endpoints.dart — all /api/mobile/* endpoints + dynamic baseURL. - lib/services/auth_service.dart — supplier profile, ERP, token; BaseAuthService (OTP). - lib/ui/views/change_erp/change_erp_viewmodel.dart — ERP selection → per-ERP JWT swap. - lib/ui/views/lpo_listing/lpo_listing_viewmodel.dart — LPO listing (pending/fulfilled/rejected, cursor paging). - lib/ui/views/home/home_viewmodel.dart — dashboard analytics. - lib/models/lpo_models.dart, lib/models/user.dart (SupplierModel, AuthModel, ErpModel). - lib/utils/constants/app_roles.dart — single "Supplier Admin" role. - lib/utils/constants/app_variables.dart — app IDs, prefs keys.