PLT-001

System Overview & Roles

Engineering Services is a multi-tenant SaaS platform for construction and engineering project management. A single deployment serves many independent business tenants, each isolated in their own workspace.

User Types

user_typeRoleScopeAPI prefix
super_adminPlatform administratorGlobal — sees all tenants/api/platform/*
adminBusiness adminScoped to own business_id/api/admin/*
staffBusiness staff memberScoped to own business_id/api/admin/*
employeeField employeeOwn assignments only/api/employee/*
contractorSubcontractorOwn workspace only/api/contractor/*
clientClient (project owner)Own projects only/api/client/*
Platform vs Business scope: The super_admin operates at platform level — no business_id filter applies. All other users are scoped and cannot cross tenant boundaries.

System Setup Order

1 · Platform sets up subscription plans
2 · Platform invites tenant (business)
3 · Business admin logs in, configures staff & settings
4 · Admin creates employees & subcontractors
5 · Admin creates projects & assigns clients
6 · Clients log in to view their project portal
PLT-002

Authentication & Super Admin Access

The platform uses Laravel Sanctum token auth. Every API call (except login) requires Authorization: Bearer {token}.

Super Admin Guard Logic

Request arrives at /api/platform/*
Is user authenticated? (Sanctum token valid?)
Is user_type == "super_admin"?
Does user have required Spatie permission slug?
Request proceeds ✓
Failing any check returns the appropriate HTTP error (401/403). The checks are sequential — an unauthenticated request never reaches the permission check.

Seed Credentials

UsernameEmailPasswordRole
superadminsuperadmin@engineering.testTest@1234Platform super admin
adminadmin@engineering.testTest@1234Business admin (tenant 1)
staffstaff@engineering.testTest@1234Business staff (tenant 1)
client1client1@engineering.testTest@1234Client portal
contractor1contractor1@engineering.testTest@1234Contractor portal
employee1employee1@engineering.testTest@1234Employee portal
PLT-003

Subscription Plan Management

Plans define what a tenant business is allowed to do. They must be created before any tenant is invited, because tenant invite requires a valid active subscription_plan_id.

Plan Fields

FieldTypeMeaning
namestringHuman-readable plan tier name (e.g. "Pro")
slugstringURL-safe identifier; auto-generated from name if omitted
monthly_pricedecimalBilling amount per month in local currency
max_projectsintegerMax concurrent active projects; -1 = unlimited
max_locationsintegerMax business locations; -1 = unlimited
max_employeesintegerMax employee user accounts; -1 = unlimited
has_client_portalbooleanWhether client portal access is included in plan
has_offline_syncbooleanWhether offline data sync is included
is_activebooleanInactive plans cannot be assigned to new tenants

Unlimited Limits Encoding

API input -1 → DB stored as 0 → API output -1
API input 5 → DB stored as 5 → API output 5

Transform: planLimitFromApi(n) = (n == -1) ? 0 : n
Reverse: planLimitToApi(n) = (n == 0) ? -1 : n
This means a value of 0 in the database always means "unlimited" — you cannot create a plan that allows exactly 0 projects. Use 1 as the minimum real limit.

Slug Auto-generation

slug = Str::slug(name)
if slug is taken → slug = base + "-1", "-2", … until unique

Deletion Rules

  • If no tenants assigned: plan is hard-deleted.
  • If tenants are assigned: plan is deactivated (is_active=false), and a 422 is returned with "error":"tenants_assigned". This protects existing tenants.
  • Deactivated plans are still shown in list but can no longer be assigned to new tenants.
PLT-004

Tenant Onboarding (Invite Flow)

The platform admin invites a new business tenant. Everything runs in a single database transaction.

Invite Flow

POST /api/platform/tenants with business details
Validate: plan exists and is_active?
Generate unique subdomain_slug from business_name
Create Business record (subscription_status=active)
create_admin_user == true? (default)
Generate temp password (12 chars, Str::password)
Create User (user_type=admin, business_id=new, bcrypt hash)
Write tenant_created audit event
Return 201 with tenant data + admin_invite.temporary_password

Subdomain Slug Generation

base = Str::slug(business_name) // "BuildCorp Pakistan" → "buildcorp-pakistan"
if base == "" → base = "tenant"
slug = base
while Business.exists(subdomain_slug=slug): slug = base + "-" + suffix++

Admin Username Generation

base = subdomain_slug.replace("-","") + "_admin"
// "buildcorp-pakistan" → "buildcorppakistan_admin"
while User.exists(username=base): base = base + suffix++
temporary_password is displayed exactly once in the API response. It is stored as a bcrypt hash in the DB — the plaintext is never stored. Communicate it to the tenant securely (e.g. email, secure link). There is no "resend" endpoint — reset their password if lost.

Required Validation

FieldRule
business_namerequired, string, max:255
owner_namerequired, string, max:255
contact_emailrequired, valid email, max:255
subscription_plan_idrequired, integer, must exist + be active
contact_phoneoptional, max:64
create_admin_useroptional boolean, default true
PLT-005

Tenant Lifecycle (Update & Suspension)

A tenant's status can be managed post-invite. No hard-delete is supported — suspension preserves all business data.

Status Values

subscription_statusis_activeMeaning
activetrueNormal operating state
suspendedfalseManually suspended by platform admin
past_duetruePayment overdue; access may be limited
cancelledfalseSubscription cancelled

Audit Events on Update

  • Any field change → writes tenant_updated event listing changed fields
  • Transitioning is_active from true → false additionally writes tenant_suspended
  • DELETE endpoint → always writes tenant_suspended (soft delete)

Plan Change Rules

  • New plan must exist and be is_active=true
  • Downgrading a plan does not immediately enforce new limits — enforcement is the business admin's responsibility
  • Upgrading is always allowed
PLT-006

Audit Event System

Every significant platform action writes an immutable row to t_platform_audit_events. These are exposed via /api/platform/notifications (the naming is UI-oriented, not email/push notifications).

Event Categories

categoryseverityTriggered bymetadata fields
tenant_createdinfoPOST /tenantsbusiness_name, plan_name
tenant_updatedinfoPUT /tenants/{id} (any field change)summary of changed fields
tenant_suspendedaction_takenPUT (is_active false) or DELETEbusiness_name, suspension reason
plan_createdinfoPOST /subscription-plansplan_name
plan_updatedinfoPUT /subscription-plans/{id} or soft-delete on tenant-assigned planplan_name, change summary

Event Code Format

event_code = "EVT-" + zero_padded_id(5 digits)
// id=1 → "EVT-00001" | id=10047 → "EVT-10047"

Read / Unread

  • Events start as is_read: false
  • Clients poll ?is_read=false to show a notification badge count
  • Mark individual or all read via PATCH endpoints
  • Events can be permanently deleted (hard delete)
PLT-007

Dashboard KPIs

The GET /api/platform/dashboard response contains platform-wide aggregate metrics computed in real time.

KPI Formulas

tenant_count = COUNT(*) FROM t_businesses
active_tenant_count = COUNT(*) WHERE is_active = true
suspended_tenant_count = tenant_count - active_tenant_count
subscription_plan_count = COUNT(*) FROM t_subscription_plans WHERE is_active = true

plan_breakdown = SELECT plan_id, plan_name, COUNT(tenants) as tenant_count, is_active
FROM t_subscription_plans LEFT JOIN t_businesses
ORDER BY monthly_price ASC

recent_tenants = SELECT top 5 FROM t_businesses ORDER BY created_at DESC

Notes

  • subscription_plan_count counts only active plans (inactive/deleted plans excluded)
  • plan_breakdown includes ALL plans (including inactive) to show full picture
  • recent_tenants is limited to 5, ordered by signup date descending
  • No caching — values are live DB queries. Cache at the HTTP client for dashboards with high poll rates.
PLT-008

Permission Matrix

The super_admin role receives all platform permissions automatically. Business admin and lower roles do not receive any platform permission.

Permission slugGrantsEndpoints
tenants.viewRead tenant list and detailGET /tenants, GET /tenants/{id}
tenants.manageInvite, update, suspend tenantsPOST, PUT, DELETE /tenants
subscription_plans.viewRead plan list and detailGET /subscription-plans, GET /subscription-plans/{id}
subscription_plans.manageCreate, update, delete plansPOST, PUT, DELETE /subscription-plans
platform_notifications.viewRead audit event logGET /notifications, GET /notifications/{id}
platform_notifications.manageMark read, delete eventsPATCH .../read, PATCH .../read-all, DELETE /notifications/{id}
Even with tenants.manage, a request fails if the user's user_type is not super_admin. Both checks are always applied. The permission check is secondary — the super_admin check comes first.
PLT-009

Error Reference

HTTPerror fieldMeaningTypical cause
401UnauthenticatedMissing, expired, or revoked Sanctum token
403Forbiddenuser_type != super_admin, or missing permission slug
404Not foundResource ID does not exist
422Validation errors objectValidation failedMissing required field, type mismatch, unique violation
422"tenants_assigned"Plan has tenantsDELETE /subscription-plans/{id} when tenants exist (plan deactivated instead)
422"invalid_id"ID ≤ 0GET /notifications/0 or negative ID
503OpenAPI spec not generatedGET /openapi.json before artisan openapi:generate has been run

Validation Error Shape

"success": false,
"msg": "Validation failed",
"error": {
  "contact_email": ["The contact email field must be a valid email address."]
}