Contractor Portal — Business Guide Engineering Services API  |  Subcontractor Operations Reference
🏢

System Overview & Actor Model

CON-001

The 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.

ADMIN Full access EMPLOYEE Staff ops CONTRACTOR Subcontractor ops ● Active portal CLIENT Project visibility VIEWER Read-only Engineering Services API — Role-Scoped Portals

Critical Design: Identity Resolution

Every contractor request flows through a private resolver method in ContractorDashboardController:

// Identity resolution — called before every data query private function subcontractor(): ?Subcontractor { return Subcontractor::query() ->where('user_id', Auth::id()) ->first(); } // If no matching Subcontractor row exists: $sub = null → vouchers=[], ledger=[], subcontractor_id=null

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

FieldTypeDescription
idintegerPrimary key; used as subcontractor_id on related records
company_namestringRegistered name of the subcontracting firm (e.g. "RapidSteel Contractors")
trade_typestringSpecialisation (e.g. "Structural Steel", "Civil Works", "MEP")
statusenumactive | inactive | suspended — set by Admin
business_idintegerForeign key to the engineering business; controls materials scope
user_idinteger (nullable)Links to users.id — the contractor login account. Must be set by Admin.

Portal Capabilities

FeatureEndpointAccessRequires Subcontractor Link
Workspace / ProfileGET /contractor/workspacesReadPartial (blank record if unlinked)
Dashboard KPIsGET /contractor/dashboardReadYes (voucher_count = 0 if null)
Daily LogsGET/POST /contractor/daily-logsRead + WritePartial (subcontractor_id null if unlinked)
ExpensesGET/POST /contractor/expensesRead + WriteYes (subcontractor_id required)
Materials ViewGET /contractor/materialsRead-onlyYes (empty if unlinked)
GalleryGET /contractor/galleryRead + UploadNo (scoped to user_id)
VouchersGET /contractor/vouchersRead-onlyYes (empty if unlinked)
LedgerGET /contractor/ledgerRead-onlyYes (empty if unlinked)
NotificationsGET /contractor/notificationsReadNo (scoped to user_id)
🔑

Authentication & Workspace Link

CON-002

The 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

POST /api/login Content-Type: application/json { "email": "steel@rapidcontractors.com", "password": "••••••••", "user_type": "contractor" } // Response includes Bearer token — include in all subsequent requests: Authorization: Bearer <token>

Workspace Resolution

After login, the contractor calls GET /api/contractor/workspaces. The system resolves the Subcontractor record using Auth::id():

// Subcontractor resolver (called internally on every request) $sub = Subcontractor::where('user_id', Auth::id())->first(); // workspaces() endpoint behaviour: if ($sub) → return $sub // full profile elsereturn blank Subcontractor record // graceful empty

Onboarding Flow — 5 Steps

1
ADMIN
Creates a Subcontractor record via the Admin portal: sets company_name, trade_type, status='active', and business_id. The user_id field is initially null.
2
ADMIN
Creates (or assigns) a contractor user account via user management, then sets Subcontractor.user_id = contractor_user.id. This is the critical linking step — without it the contractor sees empty data.
3
SYSTEM
Contractor user receives login credentials (email + password). No further setup is required on the contractor side.
4
CONTRACTOR
Logs in with user_type: 'contractor'. Calls GET /api/contractor/workspaces to verify their profile — should see company_name, trade_type, and status.
5
CONTRACTOR
Begins daily operations: submitting daily logs, recording expenses, viewing vouchers, and monitoring ledger entries. All data flows through the resolved $sub identity.
⚠ Identity Link Warning: Without a matching 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

ScenarioHTTP ResponseReason
Valid contractor token, correct user_type200 or 201Normal operation
Expired or missing token401 UnauthenticatedToken invalid
Admin token used on contractor endpoint403 ForbiddenPortal guard rejects wrong user_type
Valid token, no Subcontractor link200 (empty data)Resolver returns null — graceful empty
📊

Dashboard KPIs

CON-003

The 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

GET /api/contractor/dashboard // Returns: { "voucher_count": 4 // number of vouchers issued to this contractor }

voucher_count — Query Logic

// When $sub exists: voucher_count = Voucher::where('subcontractor_id', $sub->id)->count(); // When $sub is null (no Subcontractor link): voucher_count = 0

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.

ℹ Design Intent: The contractor dashboard is kept minimal by design. Contractors primarily work from the daily log submission screen and the expense entry screen. Financial summaries (totals, balances, payment history) are visible in the Ledger section. The dashboard simply confirms portal connectivity and voucher activity at a glance.

Dashboard State Matrix

Conditionvoucher_countInterpretation
$sub exists, no vouchers issued0New contractor — no payment vouchers yet
$sub exists, vouchers existNActive contractor with N vouchers on record
$sub is null (no link)0Identity not resolved — check workspace link
📋

Daily Logs

CON-004

Daily 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

MethodPathDescription
GET/api/contractor/daily-logsList all logs for the authenticated contractor user. Ordered by log_date DESC.
POST/api/contractor/daily-logsSubmit a new daily log. Creates record with status='submitted'.

Request Fields

FieldRequiredValidationNotes
log_date✓ YesdateThe calendar date the work was performed
work_summary✓ YesstringFree-text description of work completed that day
project_idOptionalnullable integerAssociate log with a specific project for tracking
workers_countOptionalsometimes integer, min:0Number of workers on site. Defaults to 0 if omitted.

Auto-Set Fields (not in request)

FieldValue Set By System
business_idDerived from authenticated session context
user_idAuth::id() — the authenticated contractor user
subcontractor_id$sub->id (null if no Subcontractor link)
statusAlways 'submitted' on creation
workers_countDefaults to 0 if not provided

Daily Log Status State Machine

submitted
reviewed
 | 
rejected
StatusWho SetsMeaning
submittedSystem (on create)Log received, awaiting Admin review
reviewedAdminLog accepted as accurate record
rejectedAdminLog disputed — contractor should clarify or resubmit

Daily Log Submission Flow

1
CONTRACTOR
At end of working day, contractor fills the daily log form: enters log_date, work_summary (what was done), workers_count (team size), and optionally selects a project_id.
2
SYSTEM
Validates the request. Creates a DailyLog record with status='submitted', user_id=Auth::id(), and subcontractor_id=$sub->id. Returns the created log.
3
ADMIN
Reviews daily logs via the Admin portal. Checks work summary against project schedule and resource plan. Can approve or flag inconsistencies.
4
ADMIN
Sets log status to 'reviewed' (accepted) or 'rejected' (disputed). Reviewed logs become part of the project record and may inform voucher calculations.
⚠ workers_count Default: If 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.
Example — Daily Log Submission
Endpoint:POST /api/contractor/daily-logs
log_date:2024-03-01
work_summary:Column erection Level 1 — 4 columns installed, plumb checked
workers_count:8
project_id:12
status (auto):submitted
subcontractor_id (auto):$sub->id
💵

Expense Claims

CON-005

Expense 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

MethodPathDescription
GET/api/contractor/expensesList all expenses for the authenticated user. Ordered by expense_date DESC.
POST/api/contractor/expensesSubmit a new expense claim. Creates with status='pending'.

Request Fields

FieldRequiredValidationNotes
category✓ Yesstring, max:64Expense type. See category list below.
amount✓ Yesnumeric, min:0Amount in local currency. Cannot be negative.
expense_date✓ YesdateDate the expense was incurred
descriptionOptionalnullableAdditional detail about the expense
project_idOptionalnullableLinks expense to a specific project for cost tracking

Common Expense Categories

labour materials transport equipment accommodation tools subcontract other

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

pending
approved
 | 
rejected
StatusWho SetsFinancial Impact
pendingSystem (on create)Counts as pending exposure only — not in approved cost total
approvedAdminContributes to project cost tracking and expense totals
rejectedAdminExcluded from all cost calculations

Expense Aggregation Formulas

// Approved expense total (contributes to project costs) expense_total = SUM(amount) WHERE subcontractor_id = $sub->id AND status = 'approved' // Pending exposure (submitted but not yet approved) pending_exposure = SUM(amount) WHERE subcontractor_id = $sub->id AND status = 'pending' // Only approved expenses contribute to project cost tracking. // Pending expenses represent financial exposure until resolved.
Worked Example — RapidSteel Contractors
Transport (pending):PKR 12,000
Labour (pending):PKR 85,000
Materials (approved):PKR 45,500
approved_total:PKR 45,500
pending_exposure:PKR 97,000 (12,000 + 85,000)
total_submitted:PKR 142,500
🏭

Materials View

CON-006

The 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

GET /api/contractor/materials // Query executed when $sub exists: MaterialStock::where('business_id', $sub->business_id)->get(); // When $sub is null: return [] // empty array — no materials visible

Access Model

ActionContractor 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
ℹ Design Note: This endpoint gives contractors visibility into what materials are available on-site, enabling them to plan work and write accurate daily logs without needing to contact the Admin. The Admin manages the actual inventory — including stock receipts, consumption records, and adjustments. Contractors are consumers of this information, not managers of it.

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-007

Contractors 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

GET /api/contractor/gallery GalleryItem::where('uploaded_by', Auth::id())->get(); // Returns all gallery items uploaded by the authenticated contractor

Visibility Control

Visibility SettingWho Sees ItWho Sets It
internalAdmin and internal staff onlyAdmin
clientAdmin + Client portal usersAdmin
publicUnrestrictedAdmin

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

1
CONTRACTOR
Takes site photographs using the mobile app — progress shots, installed works, material deliveries, safety compliance evidence, or punch-list items.
2
CONTRACTOR
Uploads images via the gallery screen. Each image creates a GalleryItem record with uploaded_by=Auth::id(). Visibility defaults to internal until Admin changes it.
3
ADMIN
Reviews uploaded photos in the Admin portal. Sets visibility to internal, client, or public based on content sensitivity and client communication needs.
ℹ Note: Gallery items are scoped to the user who uploaded them — a contractor user only sees photos they personally uploaded. Admin can see all gallery items across all users. This scoping ensures contractors cannot view photos uploaded by other contractors or employees.
💰

Vouchers & Ledger

CON-008

Vouchers — 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.

GET /api/contractor/vouchers // When $sub exists: Voucher::where('subcontractor_id', $sub->id)->get(); // When $sub is null: return [] // empty array

Voucher Fields

FieldTypeDescription
voucher_nostringHuman-readable identifier (e.g. "VOC-001")
subcontractor_idintegerLinks to the Subcontractor record
project_idintegerProject the work relates to
amountdecimalVoucher value in local currency
week_endingdateLast day of the work period the voucher covers
statusenumSee state machine below

Voucher Status State Machine

draft
submitted
approved
 | 
rejected
StatusWho SetsMeaning
draftAdmin (initial)Voucher being prepared — not yet sent to contractor
submittedAdminVoucher issued to contractor for acknowledgement
approvedAdminPayment authorised — triggers ledger entry
rejectedAdminVoucher 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.

GET /api/contractor/ledger LedgerEntry::where('party_type', 'subcontractor') ->where('party_id', $sub->id) ->get(); // When $sub is null → return []

Ledger Entry Types

entry_typeDebitCreditMeaning
paymentamount0Direct payment from business to contractor
advanceamount0Advance payment issued ahead of work completion
deduction0amountAmount withheld — penalties, corrections, material recoveries
voucher_paymentamount0Payment issued against a specific approved voucher

Balance Calculation Formulas

// Amounts received (debit entries) total_received = SUM(debit) WHERE entry_type IN ('payment', 'advance', 'voucher_payment') // Amounts withheld (credit entries) total_deducted = SUM(credit) WHERE entry_type = 'deduction' // Net amount received by contractor net_received = total_received - total_deducted
Worked Example — RapidSteel Contractors
Advance payment:PKR 100,000 (debit)
Voucher VOC-001 payment:PKR 250,000 (debit)
Deduction (materials):PKR 15,000 (credit)
total_received:PKR 350,000 (100,000 + 250,000)
total_deducted:PKR 15,000
net_received:PKR 335,000
🔔

Notifications

CON-009

Contractors 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

MethodPathPurposeAuth guard
GET/api/contractor/notificationsBell dropdown — ≤20 most-recent unread notificationsuser_type ∈ {contractor, subcontractor}
PATCH/api/contractor/notifications/read-allMark every unread notification as read for the authenticated contractoruser_type ∈ {contractor, subcontractor}

Bell Dropdown Query

-- GET /api/contractor/notifications
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)

FieldTypeDescription
idintegerNotification PK — determines DESC sort order
typestringMachine-readable event type, e.g. voucher_approved, expense_approved
titlestringShort notification headline
bodystringFull notification message (empty string if null)
is_readbooleanAlways false in bell dropdown (only unread returned)
dataobjectArbitrary JSON — e.g. { "voucher_id": 88 }; empty object if null
created_atstring (ISO 8601)When the notification was created

Common Notification Triggers

Trigger Eventtype valueExample Title
Admin approves a vouchervoucher_approved"Your voucher #V-2026-088 has been approved"
Admin rejects a vouchervoucher_rejected"Your voucher #V-2026-087 was rejected — see notes"
Admin approves an expense claimexpense_approved"Expense claim approved: Tools & Equipment"
Admin reviews a daily logdaily_log_reviewed"Your daily log for 2026-07-02 has been reviewed"
Admin sends general announcementannouncement"Site access suspended — Block C scaffold inspection"
Work order assigned by adminwork_order_assigned"New work order: Electrical rough-in — Unit 4B"
ℹ Scoping note: Notifications are scoped solely by 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.
Example GET /api/contractor/notifications Response
successtrue
msg"Notifications"
data[0].id501
data[0].type"voucher_approved"
data[0].title"Your voucher #V-2026-088 has been approved"
data[0].body"Payment will be processed within 3 business days."
data[0].is_readfalse
data[0].data{ "voucher_id": 88 }
data[0].created_at"2026-07-03T09:22:11.000000Z"
🌠

End-to-End Contractor Journey

CON-010

This 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.

Scenario Context
Contractor:RapidSteel Contractors
Trade Type:Structural Steel
Project:Multi-Storey Commercial Block — Project ID 12
Work Period:Week ending 2024-03-07
1
ADMIN
Creates Subcontractor record: company_name='RapidSteel Contractors', trade_type='Structural Steel', status='active', business_id=1. The user_id field is null at this stage.
2
ADMIN
Creates contractor user account (email: steel@rapidcontractors.com, user_type: 'contractor'). Then sets Subcontractor.user_id = contractor_user.id to link the portal access to the Subcontractor record.
3
CONTRACTOR
RapidSteel logs in: 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'.
4
CONTRACTOR
Checks dashboard: GET /api/contractor/dashboardvoucher_count: 0. No vouchers have been issued yet — this is expected for a new engagement.
5
CONTRACTOR
Day 1 — submits daily log: 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'.
6
CONTRACTOR
Records a transport expense: POST /api/contractor/expenses with category: 'transport', amount: 12000, expense_date: '2024-03-01', project_id: 12. System creates with status='pending'.
7
ADMIN
Reviews daily log in Admin portal. Confirms 8 workers and column erection work is consistent with project schedule. Sets daily log status='reviewed'.
8
ADMIN
Creates voucher for week's work: 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.
9
CONTRACTOR
Calls GET /api/contractor/vouchers — sees VOC-001 with amount PKR 350,000 and status submitted. Dashboard now shows voucher_count: 1.
10
ADMIN
Approves voucher: sets VOC-001 status='approved'. Posts corresponding ledger entry: party_type='subcontractor', party_id=$sub->id, entry_type='voucher_payment', debit=350000.
11
CONTRACTOR
Calls GET /api/contractor/ledger — sees one entry: entry_type='voucher_payment', debit=350000. Balance: net_received = PKR 350,000 - PKR 0 = PKR 350,000.
12
ADMIN
Sends notification to contractor user: "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-011

The following table covers the most common error responses encountered in the Contractor Portal, their triggers, and the recommended remediation steps.

HTTP CodeError TypeTriggerFix
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.
⚠ Silent Empty Data: HTTP 200 with empty arrays is NOT an error response — it means the API is working correctly but either no data exists or the Subcontractor identity link is missing. Always check the workspaces endpoint first when debugging unexpected empty responses.

Validation Field Reference

EndpointRequired FieldsOptional 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

APPENDIX

This 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

FieldRuleError if violatedDefault
log_daterequired | date422 — The log date field is required / must be a valid date
work_summaryrequired | string422 — The work summary field is required
project_idnullable | integer422 — project_id must be an integer if providednull
workers_countsometimes | integer | min:0422 — workers_count must be a non-negative integer0

POST /api/contractor/expenses — Full Validation Rules

FieldRuleError if violatedDefault
categoryrequired | string | max:64422 — category is required; max 64 characters
amountrequired | numeric | min:0422 — amount is required; must be non-negative number
expense_daterequired | date422 — expense_date is required and must be a valid date
descriptionnullablenull
project_idnullablenull

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.

// Behaviour difference: sometimes vs nullable sometimes|integer|min:0 → field absent: OK (default applied) field present as null: 422 field present as -1: 422 nullable|integer → field absent: 422 (key required) field present as null: OK field present as integer: OK
🔍

Appendix B — Data Scoping Summary

APPENDIX

Every 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.

EndpointScope ColumnNotes
GET /contractor/daily-logsDailyLog.user_idShows logs the contractor submitted. subcontractor_id may be null if unlinked.
GET /contractor/expensesExpense.user_idShows expenses submitted by this user.
GET /contractor/galleryGalleryItem.uploaded_byShows items uploaded by this user.
GET /contractor/notificationsAppNotification.user_idShows 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.

EndpointScope ColumnFallback (no $sub)
GET /contractor/workspacesSubcontractor.user_idBlank Subcontractor record
GET /contractor/dashboardVoucher.subcontractor_idvoucher_count: 0
GET /contractor/materialsMaterialStock.business_id[]
GET /contractor/vouchersVoucher.subcontractor_id[]
GET /contractor/ledgerLedgerEntry.party_id (party_type='subcontractor')[]
ℹ Multi-Tenant Isolation: The 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

APPENDIX

Use this checklist when verifying a new contractor portal integration, testing a new deployment, or debugging a reported data issue.

Setup Verification

CheckExpected Result
POST /api/login with user_type: 'contractor'200 with Bearer token
GET /api/contractor/workspacesReturns company_name, trade_type, status (not blank)
Subcontractor.user_id matches authenticated user.idConfirmed in database
GET /api/contractor/dashboardReturns voucher_count (integer, 0 or more)

Daily Log Tests

Test CaseExpected
POST daily log with all fields201, status='submitted'
POST daily log without log_date422 validation error
POST daily log without work_summary422 validation error
POST daily log without workers_count201, workers_count=0 on record
GET daily-logsArray ordered by log_date DESC

Expense Tests

Test CaseExpected
POST expense with all required fields201, status='pending'
POST expense without category422 validation error
POST expense without amount422 validation error
POST expense with amount=-1422 — min:0 violated
POST expense with category longer than 64 chars422 — max:64 violated
GET expensesArray ordered by expense_date DESC

Identity & Scoping Tests

Test CaseExpected
GET /contractor/vouchers when Subcontractor.user_id is null200 with empty array []
GET /contractor/ledger when Subcontractor.user_id is null200 with empty array []
GET /contractor/materials when $sub existsReturns stock for $sub->business_id only
Admin token used on /contractor/* endpoint403 Forbidden
GET /contractor/notificationsReturns notifications for Auth::id() only
👥

Appendix D — Contractor vs Admin Responsibility Matrix

APPENDIX

The 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.

ActionContractorAdmin
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✓ YesN/A
Set Subcontractor.user_id (link)✗ No✓ Yes — critical setup step
View own profile (workspaces)✓ Yes✓ Yes
Edit Subcontractor profile✗ No✓ Yes
ℹ Design Philosophy: The contractor portal intentionally restricts write access to operational submissions (daily logs and expenses). Financial instruments (vouchers, ledger entries) and administrative configurations (profile, status, visibility) are Admin-only. This prevents contractors from self-authorising payments or modifying their own financial records — a standard segregation-of-duties control.

Appendix E — Common Scenarios & FAQs

APPENDIX

Q: A contractor logs in successfully but sees empty vouchers and ledger. What is wrong?

The most likely cause is a missing Subcontractor identity link. Have the Admin verify that 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?

The record will have 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.

Engineering Services API — Contractor Portal Business Guide — Generated for internal reference.
Sections: CON-001 through CON-011 — Identity resolution via Subcontractor.user_id