System Overview & Actor Model
CON-001The Contractor Portal is one of five role-scoped portals in Engineering Services. It serves subcontracting companies that are engaged by the engineering business to deliver trade-specific work on site. A contractor user is identified by user_type = 'contractor' and must be linked to a Subcontractor record in the database before any meaningful data is visible.
Critical Design: Identity Resolution
Every contractor request flows through a private resolver method in ContractorDashboardController:
This single lookup controls the contractor's entire data scope. If the Subcontractor.user_id column has not been set to the authenticated user's ID, the portal returns graceful empty responses rather than errors — which can be mistaken for a working but empty account.
Subcontractor Profile Fields
| Field | Type | Description |
|---|---|---|
id | integer | Primary key; used as subcontractor_id on related records |
company_name | string | Registered name of the subcontracting firm (e.g. "RapidSteel Contractors") |
trade_type | string | Specialisation (e.g. "Structural Steel", "Civil Works", "MEP") |
status | enum | active | inactive | suspended — set by Admin |
business_id | integer | Foreign key to the engineering business; controls materials scope |
user_id | integer (nullable) | Links to users.id — the contractor login account. Must be set by Admin. |
Portal Capabilities
| Feature | Endpoint | Access | Requires Subcontractor Link |
|---|---|---|---|
| Workspace / Profile | GET /contractor/workspaces | Read | Partial (blank record if unlinked) |
| Dashboard KPIs | GET /contractor/dashboard | Read | Yes (voucher_count = 0 if null) |
| Daily Logs | GET/POST /contractor/daily-logs | Read + Write | Partial (subcontractor_id null if unlinked) |
| Expenses | GET/POST /contractor/expenses | Read + Write | Yes (subcontractor_id required) |
| Materials View | GET /contractor/materials | Read-only | Yes (empty if unlinked) |
| Gallery | GET /contractor/gallery | Read + Upload | No (scoped to user_id) |
| Vouchers | GET /contractor/vouchers | Read-only | Yes (empty if unlinked) |
| Ledger | GET /contractor/ledger | Read-only | Yes (empty if unlinked) |
| Notifications | GET /contractor/notifications | Read | No (scoped to user_id) |
Authentication & Workspace Link
CON-002The contractor portal uses the shared POST /api/login endpoint with user_type: 'contractor'. A portal guard (rejectUnlessPortalUserType('contractor')) ensures only contractor-type users can access these endpoints — even with a valid token from another portal role.
Login Request
Workspace Resolution
After login, the contractor calls GET /api/contractor/workspaces. The system resolves the Subcontractor record using Auth::id():
Onboarding Flow — 5 Steps
company_name, trade_type, status='active', and business_id. The user_id field is initially null.Subcontractor.user_id = contractor_user.id. This is the critical linking step — without it the contractor sees empty data.user_type: 'contractor'. Calls GET /api/contractor/workspaces to verify their profile — should see company_name, trade_type, and status.$sub identity.Subcontractor.user_id record, the contractor user will see: empty vouchers, empty ledger, null subcontractor_id on daily logs, and no expense subcontractor linkage. The API returns HTTP 200 with empty arrays — not an error — so missing data can be hard to diagnose. Always verify the workspace link with GET /api/contractor/workspaces immediately after login setup.
Portal Guard Behaviour
| Scenario | HTTP Response | Reason |
|---|---|---|
| Valid contractor token, correct user_type | 200 or 201 | Normal operation |
| Expired or missing token | 401 Unauthenticated | Token invalid |
| Admin token used on contractor endpoint | 403 Forbidden | Portal guard rejects wrong user_type |
| Valid token, no Subcontractor link | 200 (empty data) | Resolver returns null — graceful empty |
Dashboard KPIs
CON-003The contractor dashboard is intentionally minimal. It exposes a single aggregate metric designed to give quick operational awareness without overwhelming the contractor with administrative detail. Deep financial data is available through the Vouchers and Ledger sections.
Dashboard Endpoint
voucher_count — Query Logic
The voucher_count includes all voucher statuses (draft, submitted, approved, rejected). It is a total count, not a count of only pending or approved vouchers. Use the GET /api/contractor/vouchers endpoint for status-filtered detail.
Dashboard State Matrix
| Condition | voucher_count | Interpretation |
|---|---|---|
| $sub exists, no vouchers issued | 0 | New contractor — no payment vouchers yet |
| $sub exists, vouchers exist | N | Active contractor with N vouchers on record |
| $sub is null (no link) | 0 | Identity not resolved — check workspace link |
Daily Logs
CON-004Daily logs are the primary operational record for a contractor. They document work performed each day — what was accomplished, how many workers were on site, and which project the work relates to. Logs are created by the contractor and reviewed by the Admin.
Endpoints
| Method | Path | Description |
|---|---|---|
GET | /api/contractor/daily-logs | List all logs for the authenticated contractor user. Ordered by log_date DESC. |
POST | /api/contractor/daily-logs | Submit a new daily log. Creates record with status='submitted'. |
Request Fields
| Field | Required | Validation | Notes |
|---|---|---|---|
log_date | ✓ Yes | date | The calendar date the work was performed |
work_summary | ✓ Yes | string | Free-text description of work completed that day |
project_id | Optional | nullable integer | Associate log with a specific project for tracking |
workers_count | Optional | sometimes integer, min:0 | Number of workers on site. Defaults to 0 if omitted. |
Auto-Set Fields (not in request)
| Field | Value Set By System |
|---|---|
business_id | Derived from authenticated session context |
user_id | Auth::id() — the authenticated contractor user |
subcontractor_id | $sub->id (null if no Subcontractor link) |
status | Always 'submitted' on creation |
workers_count | Defaults to 0 if not provided |
Daily Log Status State Machine
| Status | Who Sets | Meaning |
|---|---|---|
submitted | System (on create) | Log received, awaiting Admin review |
reviewed | Admin | Log accepted as accurate record |
rejected | Admin | Log disputed — contractor should clarify or resubmit |
Daily Log Submission Flow
log_date, work_summary (what was done), workers_count (team size), and optionally selects a project_id.DailyLog record with status='submitted', user_id=Auth::id(), and subcontractor_id=$sub->id. Returns the created log.'reviewed' (accepted) or 'rejected' (disputed). Reviewed logs become part of the project record and may inform voucher calculations.workers_count is omitted from the POST request, it defaults to 0 on the created record. Always include an accurate count — this field is used for labour tracking, productivity analysis, and can affect cost calculations tied to the project.
Expense Claims
CON-005Expense claims allow contractors to submit reimbursable costs incurred during project work. All submitted expenses start with status='pending' and require Admin approval before contributing to project cost tracking or triggering any financial action.
Endpoints
| Method | Path | Description |
|---|---|---|
GET | /api/contractor/expenses | List all expenses for the authenticated user. Ordered by expense_date DESC. |
POST | /api/contractor/expenses | Submit a new expense claim. Creates with status='pending'. |
Request Fields
| Field | Required | Validation | Notes |
|---|---|---|---|
category | ✓ Yes | string, max:64 | Expense type. See category list below. |
amount | ✓ Yes | numeric, min:0 | Amount in local currency. Cannot be negative. |
expense_date | ✓ Yes | date | Date the expense was incurred |
description | Optional | nullable | Additional detail about the expense |
project_id | Optional | nullable | Links expense to a specific project for cost tracking |
Common Expense Categories
Categories are free-text strings (max 64 characters). Standardising on a consistent set of category names improves Admin filtering and reporting.
Expense Status State Machine
| Status | Who Sets | Financial Impact |
|---|---|---|
pending | System (on create) | Counts as pending exposure only — not in approved cost total |
approved | Admin | Contributes to project cost tracking and expense totals |
rejected | Admin | Excluded from all cost calculations |
Expense Aggregation Formulas
Materials View
CON-006The materials endpoint gives contractors a read-only view of the engineering business's material stock inventory. This allows contractors to check what materials are available on site before submitting daily logs or expense claims — reducing duplication and miscommunication.
Endpoint
Access Model
| Action | Contractor Access |
|---|---|
| View material list | ✓ Yes — scoped to their business_id |
| Create material record | ✗ No — Admin only |
| Update stock quantities | ✗ No — Admin only |
| Delete material record | ✗ No — Admin only |
Scoping Rule
Materials are scoped to $sub->business_id — the engineering business that the subcontractor works for. A contractor linked to Business A cannot see materials belonging to Business B, even if both businesses use the same API instance.
Gallery Upload
CON-007Contractors can upload site photographs through the gallery endpoint. These images are scoped to the uploading user (uploaded_by=Auth::id()) and are typically used to document progress, record site conditions, or capture quality assurance evidence.
Endpoint
Visibility Control
| Visibility Setting | Who Sees It | Who Sets It |
|---|---|---|
internal | Admin and internal staff only | Admin |
client | Admin + Client portal users | Admin |
public | Unrestricted | Admin |
Contractors upload images but do not control visibility settings. The Admin reviews uploaded photos and sets the appropriate visibility level before they can be seen by other portal users.
Gallery Upload Flow
GalleryItem record with uploaded_by=Auth::id(). Visibility defaults to internal until Admin changes it.internal, client, or public based on content sensitivity and client communication needs.Vouchers & Ledger
CON-008Vouchers — Overview
Vouchers are formal payment authorisation documents issued by the Admin to a contractor for work completed during a specific period (typically weekly). Contractors can view their vouchers but cannot create or modify them — voucher creation is an exclusive Admin function.
Voucher Fields
| Field | Type | Description |
|---|---|---|
voucher_no | string | Human-readable identifier (e.g. "VOC-001") |
subcontractor_id | integer | Links to the Subcontractor record |
project_id | integer | Project the work relates to |
amount | decimal | Voucher value in local currency |
week_ending | date | Last day of the work period the voucher covers |
status | enum | See state machine below |
Voucher Status State Machine
| Status | Who Sets | Meaning |
|---|---|---|
draft | Admin (initial) | Voucher being prepared — not yet sent to contractor |
submitted | Admin | Voucher issued to contractor for acknowledgement |
approved | Admin | Payment authorised — triggers ledger entry |
rejected | Admin | Voucher disputed or cancelled — no payment |
Ledger — Overview
The ledger is the complete financial record of all transactions between the engineering business and the contractor. It is written exclusively by the Admin; contractors have read-only access to their own entries.
Ledger Entry Types
| entry_type | Debit | Credit | Meaning |
|---|---|---|---|
payment | amount | 0 | Direct payment from business to contractor |
advance | amount | 0 | Advance payment issued ahead of work completion |
deduction | 0 | amount | Amount withheld — penalties, corrections, material recoveries |
voucher_payment | amount | 0 | Payment issued against a specific approved voucher |
Balance Calculation Formulas
Notifications
CON-009Contractors receive system notifications generated by Admin actions (voucher approvals, expense decisions, log reviews, general announcements). Two endpoints are available: a bell dropdown for recent unread notifications and a mark-all-read bulk action. Contractors cannot create or delete notifications.
Endpoints
| Method | Path | Purpose | Auth guard |
|---|---|---|---|
GET | /api/contractor/notifications | Bell dropdown — ≤20 most-recent unread notifications | user_type ∈ {contractor, subcontractor} |
PATCH | /api/contractor/notifications/read-all | Mark every unread notification as read for the authenticated contractor | user_type ∈ {contractor, subcontractor} |
Bell Dropdown Query
SELECT * FROM t_app_notifications
WHERE user_id = {auth_id}
AND is_read = false
ORDER BY id DESC
LIMIT 20;
-- No Subcontractor record link required — scoped solely by user_id
-- No pagination key in response
Response Row Shape (GET)
| Field | Type | Description |
|---|---|---|
id | integer | Notification PK — determines DESC sort order |
type | string | Machine-readable event type, e.g. voucher_approved, expense_approved |
title | string | Short notification headline |
body | string | Full notification message (empty string if null) |
is_read | boolean | Always false in bell dropdown (only unread returned) |
data | object | Arbitrary JSON — e.g. { "voucher_id": 88 }; empty object if null |
created_at | string (ISO 8601) | When the notification was created |
Common Notification Triggers
| Trigger Event | type value | Example Title |
|---|---|---|
| Admin approves a voucher | voucher_approved | "Your voucher #V-2026-088 has been approved" |
| Admin rejects a voucher | voucher_rejected | "Your voucher #V-2026-087 was rejected — see notes" |
| Admin approves an expense claim | expense_approved | "Expense claim approved: Tools & Equipment" |
| Admin reviews a daily log | daily_log_reviewed | "Your daily log for 2026-07-02 has been reviewed" |
| Admin sends general announcement | announcement | "Site access suspended — Block C scaffold inspection" |
| Work order assigned by admin | work_order_assigned | "New work order: Electrical rough-in — Unit 4B" |
user_id = Auth::id(). The Subcontractor record link (subcontractors.user_id) is not needed for notifications — a contractor user without a linked Subcontractor record can still receive notifications.
End-to-End Contractor Journey
CON-010This 12-step scenario traces the complete journey of RapidSteel Contractors — a structural steel subcontractor engaged for a multi-storey building project. The scenario demonstrates all major portal interactions from onboarding through to payment receipt.
company_name='RapidSteel Contractors', trade_type='Structural Steel', status='active', business_id=1. The user_id field is null at this stage.Subcontractor.user_id = contractor_user.id to link the portal access to the Subcontractor record.POST /api/login with user_type: 'contractor'. Receives Bearer token. Calls GET /api/contractor/workspaces — sees their profile: company_name, trade_type='Structural Steel', status='active'.GET /api/contractor/dashboard → voucher_count: 0. No vouchers have been issued yet — this is expected for a new engagement.POST /api/contractor/daily-logs with log_date: '2024-03-01', work_summary: 'Column erection Level 1 — 4 columns installed, plumb checked', workers_count: 8, project_id: 12. System creates record with status='submitted'.POST /api/contractor/expenses with category: 'transport', amount: 12000, expense_date: '2024-03-01', project_id: 12. System creates with status='pending'.status='reviewed'.voucher_no='VOC-001', amount=350000, week_ending='2024-03-07', subcontractor_id=$sub->id, project_id=12, status='submitted'. Voucher is now visible to contractor.GET /api/contractor/vouchers — sees VOC-001 with amount PKR 350,000 and status submitted. Dashboard now shows voucher_count: 1.status='approved'. Posts corresponding ledger entry: party_type='subcontractor', party_id=$sub->id, entry_type='voucher_payment', debit=350000.GET /api/contractor/ledger — sees one entry: entry_type='voucher_payment', debit=350000. Balance: net_received = PKR 350,000 - PKR 0 = PKR 350,000."Voucher VOC-001 approved — payment of PKR 350,000 is processing. Expected transfer: 2–3 business days." Contractor sees this in GET /api/contractor/notifications.Error Reference
CON-011The following table covers the most common error responses encountered in the Contractor Portal, their triggers, and the recommended remediation steps.
| HTTP Code | Error Type | Trigger | Fix |
|---|---|---|---|
401 |
Unauthenticated | Bearer token is missing, malformed, or expired | Call POST /api/login with correct credentials to obtain a fresh token |
403 |
Forbidden | user_type is not 'contractor' — portal guard rejects the request |
Authenticate using a contractor account (user_type: 'contractor'). Admin or Employee tokens cannot access contractor endpoints. |
422 |
Validation Error | Daily log submitted without log_date or work_summary (both required) |
Include both log_date (valid date string) and work_summary (non-empty string) in the POST body |
422 |
Validation Error | Expense submitted without category, amount, or expense_date |
All three fields are required. Ensure category is max 64 characters and amount is a non-negative numeric value. |
200 (empty) |
Silent empty response | Subcontractor identity link not established — $sub = null |
Admin must set Subcontractor.user_id to the contractor's user ID. Verify with GET /api/contractor/workspaces — should return profile fields, not a blank record. |
Validation Field Reference
| Endpoint | Required Fields | Optional Fields |
|---|---|---|
POST /contractor/daily-logs |
log_date, work_summary |
project_id, workers_count |
POST /contractor/expenses |
category, amount, expense_date |
description, project_id |
Appendix A — Full Field Validation Reference
APPENDIXThis appendix consolidates all field-level validation rules across the Contractor Portal write endpoints. Use this as a quick reference when building client-side validation or API integration tests.
POST /api/contractor/daily-logs — Full Validation Rules
| Field | Rule | Error if violated | Default |
|---|---|---|---|
log_date | required | date | 422 — The log date field is required / must be a valid date | — |
work_summary | required | string | 422 — The work summary field is required | — |
project_id | nullable | integer | 422 — project_id must be an integer if provided | null |
workers_count | sometimes | integer | min:0 | 422 — workers_count must be a non-negative integer | 0 |
POST /api/contractor/expenses — Full Validation Rules
| Field | Rule | Error if violated | Default |
|---|---|---|---|
category | required | string | max:64 | 422 — category is required; max 64 characters | — |
amount | required | numeric | min:0 | 422 — amount is required; must be non-negative number | — |
expense_date | required | date | 422 — expense_date is required and must be a valid date | — |
description | nullable | — | null |
project_id | nullable | — | null |
Validation Notes
Laravel's sometimes rule (used on workers_count) means the field is only validated if it is present in the request body. If omitted entirely, no validation error is thrown and the field defaults to 0. This differs from nullable, which requires the key to be present but allows a null value.
Appendix B — Data Scoping Summary
APPENDIXEvery contractor endpoint uses one of two scoping mechanisms to isolate data. Understanding which mechanism applies to each endpoint is critical for debugging data visibility issues and for understanding multi-tenant isolation guarantees.
Scope Type 1: user_id Scoped
These endpoints scope data to the authenticated user's ID directly — Auth::id(). No Subcontractor link is required for data to be visible.
| Endpoint | Scope Column | Notes |
|---|---|---|
GET /contractor/daily-logs | DailyLog.user_id | Shows logs the contractor submitted. subcontractor_id may be null if unlinked. |
GET /contractor/expenses | Expense.user_id | Shows expenses submitted by this user. |
GET /contractor/gallery | GalleryItem.uploaded_by | Shows items uploaded by this user. |
GET /contractor/notifications | AppNotification.user_id | Shows notifications sent to this user. |
Scope Type 2: Subcontractor Record Scoped
These endpoints require a resolved $sub (Subcontractor record). If the Subcontractor-to-user link is missing, they return empty arrays.
| Endpoint | Scope Column | Fallback (no $sub) |
|---|---|---|
GET /contractor/workspaces | Subcontractor.user_id | Blank Subcontractor record |
GET /contractor/dashboard | Voucher.subcontractor_id | voucher_count: 0 |
GET /contractor/materials | MaterialStock.business_id | [] |
GET /contractor/vouchers | Voucher.subcontractor_id | [] |
GET /contractor/ledger | LedgerEntry.party_id (party_type='subcontractor') | [] |
business_id on the Subcontractor record provides a second layer of scoping for materials — even within the same API instance, contractors can only see stock belonging to their specific engineering business. This supports scenarios where multiple engineering businesses share one API deployment.
Appendix C — Integration Testing Checklist
APPENDIXUse this checklist when verifying a new contractor portal integration, testing a new deployment, or debugging a reported data issue.
Setup Verification
| ▢ | Check | Expected Result |
|---|---|---|
| ▢ | POST /api/login with user_type: 'contractor' | 200 with Bearer token |
| ▢ | GET /api/contractor/workspaces | Returns company_name, trade_type, status (not blank) |
| ▢ | Subcontractor.user_id matches authenticated user.id | Confirmed in database |
| ▢ | GET /api/contractor/dashboard | Returns voucher_count (integer, 0 or more) |
Daily Log Tests
| ▢ | Test Case | Expected |
|---|---|---|
| ▢ | POST daily log with all fields | 201, status='submitted' |
| ▢ | POST daily log without log_date | 422 validation error |
| ▢ | POST daily log without work_summary | 422 validation error |
| ▢ | POST daily log without workers_count | 201, workers_count=0 on record |
| ▢ | GET daily-logs | Array ordered by log_date DESC |
Expense Tests
| ▢ | Test Case | Expected |
|---|---|---|
| ▢ | POST expense with all required fields | 201, status='pending' |
| ▢ | POST expense without category | 422 validation error |
| ▢ | POST expense without amount | 422 validation error |
| ▢ | POST expense with amount=-1 | 422 — min:0 violated |
| ▢ | POST expense with category longer than 64 chars | 422 — max:64 violated |
| ▢ | GET expenses | Array ordered by expense_date DESC |
Identity & Scoping Tests
| ▢ | Test Case | Expected |
|---|---|---|
| ▢ | GET /contractor/vouchers when Subcontractor.user_id is null | 200 with empty array [] |
| ▢ | GET /contractor/ledger when Subcontractor.user_id is null | 200 with empty array [] |
| ▢ | GET /contractor/materials when $sub exists | Returns stock for $sub->business_id only |
| ▢ | Admin token used on /contractor/* endpoint | 403 Forbidden |
| ▢ | GET /contractor/notifications | Returns notifications for Auth::id() only |
Appendix D — Contractor vs Admin Responsibility Matrix
APPENDIXThe Contractor Portal is deliberately limited to operational submissions and read-only financial visibility. Administrative actions — creating records, setting statuses, authorising payments — are exclusively handled by the Admin portal. This matrix clarifies ownership boundaries.
| Action | Contractor | Admin |
|---|---|---|
| Submit daily log | ✓ Yes | ✗ (Admin reviews, not submits) |
| Review / approve daily log | ✗ No | ✓ Yes |
| Submit expense claim | ✓ Yes | ✗ No |
| Approve / reject expense | ✗ No | ✓ Yes |
| Create voucher | ✗ No | ✓ Yes |
| View own vouchers | ✓ Yes (read-only) | ✓ Yes |
| Approve / reject voucher | ✗ No | ✓ Yes |
| Create ledger entry | ✗ No | ✓ Yes |
| View own ledger | ✓ Yes (read-only) | ✓ Yes |
| Upload gallery photos | ✓ Yes | ✓ Yes |
| Set gallery visibility | ✗ No | ✓ Yes |
| View material stock | ✓ Yes (read-only) | ✓ Yes |
| Create / update material stock | ✗ No | ✓ Yes |
| Send notifications | ✗ No | ✓ Yes |
| Receive notifications | ✓ Yes | N/A |
| Set Subcontractor.user_id (link) | ✗ No | ✓ Yes — critical setup step |
| View own profile (workspaces) | ✓ Yes | ✓ Yes |
| Edit Subcontractor profile | ✗ No | ✓ Yes |
Appendix E — Common Scenarios & FAQs
APPENDIXQ: A contractor logs in successfully but sees empty vouchers and ledger. What is wrong?
Subcontractor.user_id is set to the contractor user's ID. Call GET /api/contractor/workspaces — if it returns a blank/empty Subcontractor record (no company_name), the link is missing.
Q: A contractor submitted a daily log but did not include workers_count. Is the record wrong?
workers_count = 0. This is valid but potentially inaccurate. The contractor should contact Admin to manually correct the record, or (if supported) submit a correction via whatever amendment process the business uses. Preventing this: implement client-side validation to require workers_count >= 1 for any log where work was performed.
Q: Can a contractor submit expenses for a past date?
Yes — the expense_date field accepts any valid date string and is not constrained to be "today or earlier" at the API validation layer. Business-level controls (e.g. preventing submissions older than 30 days) would need to be implemented as additional validation rules if required.
Q: Can a contractor see vouchers from a previous engagement (different business)?
No. Vouchers are scoped by subcontractor_id = $sub->id. The $sub record is resolved from user_id = Auth::id() — a contractor account can only be linked to one Subcontractor record at a time. If a contractor works across multiple businesses, separate user accounts are required per Subcontractor/business combination.
Q: What happens if a contractor submits an expense with amount=0?
The validation rule is min:0, which means amount=0 passes validation and creates an expense record with zero value. This is technically valid but may indicate a data entry error. Consider adding client-side validation to warn when amount is zero.
Q: Are gallery uploads permanent?
Gallery items are stored as GalleryItem records. Deletion controls are managed by the Admin — contractors cannot delete their own uploaded items through the portal API. If an item needs to be removed, the contractor must contact Admin.
Q: Does the contractor portal support pagination?
The current implementation returns full result sets for all list endpoints. For contractors with large volumes of daily logs or expenses, client-side filtering and display limits should be implemented. Future API versions may introduce cursor-based pagination for high-volume endpoints.
Q: What is the difference between voucher_payment and payment ledger entries?
payment represents a general/ad-hoc direct payment to the contractor — not tied to a specific voucher. voucher_payment is a payment specifically against an approved voucher (identified by voucher_no/voucher_id). Both contribute to total_received in the balance calculation. The distinction allows the business to track whether payments are voucher-backed or discretionary.
Sections: CON-001 through CON-011 — Identity resolution via Subcontractor.user_id