Mariinox CRM & Scrap Operations Platform
Technical Architecture Document — Laravel 11 + React 18 + MySQL 8 + AWS ap-south-1
Table of Contents
1. Executive Summary & Scope
This document defines the complete technical architecture for the Mariinox CRM & Scrap Operations Platform, re-platformed from ERPNext/Frappe to a purpose-built Laravel 11 + React 18 stack. It is the primary reference for database schema design, API contracts, sprint planning, and infrastructure provisioning.
1.1 Scope — This Document
- Lead Capture (inbound call, web form, WhatsApp chatbot)
- Lead Assignment & Master Queue Engine
- Pipeline Management (Car/Truck 11-stage, Bike 10-stage)
- Deal Types & Customer Case Types
- Document Collection, OCR, Fraud Detection & Verification
- Pricing Engine (Scrap Value calculation, overrides, audit)
- Payment CRM Triggers (UTR, receipts, stage gating)
- SLA & Escalation Engine
- WhatsApp + SMS Notification Engine
- Dashboards — Agent, Admin, Leadboard, Analytics (8 dashboards)
- Role-Based Access Control — 6 pipeline roles + dynamic field-level RBAC
- Immutable Audit Logging + Aadhaar Compliance
- AWS Infrastructure (ap-south-1)
1.2 Out of Scope
- Customer self-service portal
- Logistics portal & driver app
- Government scrapping portal connector (Phase 2)
- Payment gateway disbursement execution (NEFT/IMPS)
- Native mobile app (iOS/Android)
1.3 Key Constraints
| Constraint | Requirement |
|---|---|
| Uptime | 99.5% — EC2 Auto Scaling + RDS Multi-AZ + ALB |
| Lead Volume | 50,000+ leads/month; 50+ concurrent users |
| Data Residency | AWS ap-south-1 (Mumbai); S3 replica in ap-south-2 (Hyderabad) |
| Encryption | AES-256 at rest (S3 SSE-KMS); TLS 1.2+ in transit |
| RPO / RTO | RPO 4h (RDS snapshots every 4h); RTO 8h (tested restore runbook) |
| Audit Retention | 3 years — immutable logs in DB + CloudWatch |
| Page Load | < 2s P95 — Redis cache + CDN + DB indexing |
2. Technology Stack Rationale
The platform is intentionally separated into a stateless API layer (Laravel) and a stateful UI layer (React SPA). This allows independent scaling and clear contract boundaries for future mobile apps or third-party integrations.
| Layer | Technology | Rationale |
|---|---|---|
| API Backend | Laravel 11 (PHP 8.3) | Mature ecosystem; Eloquent ORM; queues; Sanctum auth; policy-based RBAC |
| Frontend | React 18 + TypeScript | Component reuse for 8 dashboards; React Query for server state; Zustand for local state |
| Database | MySQL 8 (RDS Multi-AZ) | JSON columns for flexible metadata; full-text search; proven at scale |
| Cache / Queue | Redis (ElastiCache) | Session store; Laravel queues; dashboard cache invalidation; RBAC permission cache |
| File Storage | AWS S3 (SSE-KMS) | AES-256; versioning; pre-signed URLs; no direct public access |
| Search (Phase 2) | Laravel Scout + Meilisearch | Fast lead search across 50k+ records; self-hosted on EC2 |
| Scheduler | Laravel Scheduler + Horizon | SLA engine (hourly), assignment retry (15-min), advance payment trigger |
| CI/CD | GitHub Actions + Docker | Build → test → staging deploy → smoke test → prod deploy |
| Monitoring | AWS CloudWatch + Sentry | Infra metrics + application error tracking |
3. System Architecture Overview
3.1 Deployment Architecture
| Layer | Components |
|---|---|
| DNS + CDN | Route 53 → CloudFront (static assets) → ALB |
| Load Balancer | Application Load Balancer — HTTPS termination, health checks, sticky sessions |
| App Tier | EC2 Auto Scaling Group (min 2, max 10) — Laravel API + React SPA via Nginx + PHP-FPM |
| Queue Workers | Dedicated EC2 worker fleet — Laravel Horizon (Redis-backed); separate from web tier |
| Data Tier | RDS MariaDB 10.11 Multi-AZ (primary + standby); automated snapshots every 4h |
| Cache Tier | ElastiCache Redis 7 cluster mode; sessions + queue + dashboard cache + RBAC permission cache |
| Storage | S3 (ap-south-1) — private bucket; versioning; SSE-KMS; pre-signed URLs (15-min expiry) |
| Logging | CloudWatch Logs — all app logs + audit stream; 3-year retention |
3.2 Request Flow
Browser → CloudFront → ALB → Nginx (EC2) → PHP-FPM / Laravel Router → Controller → Service Layer → Repository → MySQL / Redis / S3
Background jobs: Laravel Horizon (Redis) → Job Class → External APIs (VAHAN, Textract, WhatsApp, SMS)
3.3 Laravel Application Structure
| Directory | Purpose |
|---|---|
app/Modules/Lead |
Lead capture, queue, pipeline — self-contained module |
app/Modules/Document |
Checklist engine, upload, OCR, fraud detection, verification workflow |
app/Modules/Pricing |
Scrap value calculation, override workflow, version snapshots |
app/Modules/Payment |
UTR capture, receipt storage, stage gating, advance trigger |
app/Modules/SLA |
Hourly scheduler, breach detection, auto-reassignment, escalation |
app/Modules/Notification |
WhatsApp / SMS hooks, template renderer, delivery tracking |
app/Modules/Dashboard |
Aggregation queries powering all 8 React dashboard pages |
app/Modules/Auth |
Sanctum tokens, RBAC policies, dynamic permission system, session tracking |
app/Jobs/ |
Async jobs: VahanFetch, TextractOCR, SendWhatsApp, SlaCheck, AssignmentRetry |
app/Services/ |
Shared: AuditLogger, S3Manager, FraudDetector, EligibilityScorer, PermissionService, FieldSyncService |
4. Module Breakdown
4.1 Lead Capture Module
| Feature | Delivery | Implementation Notes |
|---|---|---|
| Inbound Call Centre | Custom | Agent UI form → POST /api/leads; all Mariinox fields; VAHAN triggered on reg. no. |
| Website Form | Custom + 3P | Public React form → POST /api/leads/web; UTM + IP middleware; OTP pre-validated |
| WhatsApp Inbound | 3rd Party | Gupshup/Twilio webhook → POST /api/webhooks/whatsapp → LeadFactory |
| VAHAN API Auto-Fetch | 3rd Party | VahanFetchJob dispatched on lead save; auto-populates 11 vehicle fields; manual fallback |
| OTP Mobile Verify | Custom + 3P | POST /api/otp/send → SMS gateway; POST /api/otp/verify → mobile_verified = true |
| UTM / Attribution | Custom | UTM middleware parses query params + IP geolocation; stored on lead_attribution table |
| Preliminary Price Estimate | Custom | Triggered on assignment; Kerb Weight × Metal Rate × Condition Modifier stored on lead |
4.2 Assignment & Queue Module
Agent Status Model
| Status | Condition | Assignment Eligibility |
|---|---|---|
| Active | Bandwidth Available — active lead count < max_capacity | Eligible for auto-assignment |
| Busy | Bandwidth not available — at or above max_capacity | Skipped by auto-assignment; Supervisor can override manually |
Bandwidth (active lead count) = leads in stages: Fresh + Interested + Verbal Commitment assigned to that agent.
Agent Capacity Benchmarks (configurable per agent)
| Metric | Agent A | Agent B | Agent C |
|---|---|---|---|
| Calls / Day | 30 | 30 | 30 |
| Duration / Day (hrs) | 5 | 5 | 5 |
| Avg Duration / Call (min) | 10 | 10 | 10 |
| Current Pipeline (max_capacity) | 20 | 25 | 28 |
| ↳ Fresh leads | 14 | 18 | 20 |
| ↳ Non-Fresh (Interested + Verbal) | 6 | 7 | 8 |
| ↳ Follow Up | 3 | 4 | 3 |
| ↳ Repeat Attempt | 2 | 3 | 3 |
| ↳ Re-assigned | 1 | 0 | 2 |
5-Layer Assignment Rules Engine
Layers are evaluated in strict order. The engine stops at the first layer that produces a unique eligible agent.
| Layer | Rule Criterion | Logic & Notes |
|---|---|---|
| L1 | Duration / No. of Calls | Rank eligible (Active) agents by call performance score: total connected call duration ÷ number of genuine attempts in the scoring window (default last 30 days). Highest score assigned first. |
| L2 | Type of Call | Match lead call-type to agent specialisation: Fresh leads → agents with higher Fresh conversion; Follow-up → agents with higher Follow-up conversion. |
| L3 | Type of Vehicle | Filter agents whose vehicle expertise matches: 4W (car/truck) or 2W (bike). Measured by Lead-to-Pay conversion rate per vehicle type. Key makes tracked: Honda, Maruti, Skoda, TVS (Admin-configurable). |
| L4 | Language | Match lead's preferred language (from IP state / chatbot selection) to agent's language profile. Multi-value JSON array e.g. ["hi", "kn", "en"]. Currently confirmed: Hindi. |
| L5 | Manual Assignment | Supervisor/Admin manually picks from Active agents list only. Busy agents not shown. Admin can bypass from Escalation Queue view. |
Genuine Attempt Qualifier
| Condition | Classification & Action |
|---|---|
| Outgoing call rang for ≥ 30 seconds | ✅ Genuine Attempt — credited to agent's performance score and call metrics |
| Outgoing call rang for < 30 seconds | ❌ Fake Call — NOT credited; flagged in call_attempts.is_genuine = false; excluded from Leadboard and assignment scoring |
system_configs.genuine_call_min_seconds and is Admin-configurable without code change.4.3 Pipeline Module
- Car / Truck (11 stages): Fresh → Interested → Verbal Commitment → Document Initiated → Doc Verification Pending → Document Verified → Scrap Value Paid → Assigned to Logistic → Pickup Scheduled → Assigned to Agent → Pickup Done
- Bike / 2W (10 stages): Same but COD With Trade disabled; Scrap Value Paid occurs after Pickup Scheduled
Stage transitions are gated by: role permission check → mandatory field validation → business rule validation → audit log entry.
4.4 SLA Engine Module
| Stage | Default SLA | Breach Action | Job |
|---|---|---|---|
| Fresh | 24h | Auto-reassign | SlaBreachJob (hourly) |
| Interested | 48h | Auto-reassign | SlaBreachJob (hourly) |
| Document Initiated | 96h | Escalate to Supervisor | SlaBreachJob (hourly) |
5. Database Schema — Core Tables
5.1 users
| Column | Type | Constraints | Notes |
|---|---|---|---|
| id | CHAR(26) | PK | ULID |
| name | VARCHAR(120) | NOT NULL | Full name |
| VARCHAR(191) | UNIQUE, NOT NULL | Login identifier | |
| mobile | VARCHAR(15) | UNIQUE | E.164 format |
| password | VARCHAR(255) | NOT NULL | bcrypt hashed |
| role | ENUM | NOT NULL | agent | supervisor | verification | finance | logistics | admin |
| max_capacity | TINYINT | DEFAULT 10 | Max concurrent active leads for callers |
| is_active | BOOLEAN | DEFAULT true | Soft disable without delete |
| language | VARCHAR(20) | DEFAULT 'hi' | Preferred language for assignment matching |
| team_id | CHAR(26) | FK teams.id | Nullable — for team-based reporting |
Indexes: email (unique), role, is_active, team_id
5.2 leads
| Column | Type | Constraints | Notes |
|---|---|---|---|
| id | CHAR(26) | PK | ULID |
| lead_number | VARCHAR(20) | UNIQUE, NOT NULL | MRX-YYYYMMDD-NNNN format |
| source | ENUM | NOT NULL | call_centre | web | whatsapp | chatbot |
| stage | ENUM | NOT NULL | fresh | interested | verbal_commitment | doc_initiated | doc_verification_pending | doc_verified | scrap_value_paid | assigned_to_logistic | pickup_scheduled | assigned_to_agent | pickup_done |
| vehicle_type | ENUM | NOT NULL | car | truck | bike | other |
| deal_type | ENUM | NULLABLE | cod_with_trade | cod_without_trade | noc |
| case_type | ENUM | NULLABLE | individual | business | death | hypothecation_individual | hypothecation_company | previous_owner |
| assigned_to | CHAR(26) | FK users.id | Current assigned caller; nullable if in queue |
| stage_entered_at | TIMESTAMP | NOT NULL | Used for SLA aging calculation |
| sla_breached | BOOLEAN | DEFAULT false | Set true by SlaBreachJob |
| eligibility_score | TINYINT | NULLABLE | 0–100 numeric score |
| estimated_scrap_value | DECIMAL(12,2) | NULLABLE | Calculated on assignment |
| final_scrap_value | DECIMAL(12,2) | NULLABLE | Confirmed value (may be overridden) |
| cod_value | DECIMAL(12,2) | NULLABLE | Only for COD With Trade deal type |
| priority | TINYINT | DEFAULT 0 | 0=normal, 1=high, 2=urgent |
| exceptions_flag | JSON | NULLABLE | Array: hypothecation, duplicate_aadhaar, vahan_mismatch, etc. |
Indexes: lead_number, stage, vehicle_type, assigned_to, stage_entered_at, sla_breached, composite (stage+assigned_to)
5.3 lead_vehicle_details
One-to-one with leads. Stores all VAHAN + manual vehicle data. Pricing formula: Scrap Value = Kerb Weight (kg) × Metal Rate (₹/kg) × Condition Modifier
| Column | Type | Notes |
|---|---|---|
| lead_id | CHAR(26) | PK + FK leads.id (1:1) |
| registration_number | VARCHAR(20) | SHA-256 hash stored separately in fraud_detection_index |
| make | VARCHAR(60) | From VAHAN or manual |
| model | VARCHAR(80) | |
| fuel_type | ENUM | petrol | diesel | cng | electric | hybrid |
| manufacture_year | YEAR | |
| kerb_weight_kg | DECIMAL(8,2) | Critical for pricing formula |
| registration_date | DATE | |
| registration_expiry | DATE | RC validity for eligibility scoring |
| hypothecation_status | ENUM | none | hypothecated | cleared | unknown |
| engine_number | VARCHAR(40) | |
| chassis_number | VARCHAR(40) | |
| vahan_raw | JSON | Full raw VAHAN API response for audit |
5.4 lead_contacts & 5.5 lead_attribution
| Table | Key Columns | Purpose |
|---|---|---|
| lead_contacts | lead_id, name, mobile, alternate_mobile, email, pincode, city, state, full_address | Customer contact info; mobile = OTP-verified primary |
| lead_attribution | lead_id, utm_source, utm_medium, utm_campaign, utm_term, gclid, fbclid, ip_address, ip_city, ip_state, referrer_url | Marketing attribution; IP geolocation via MaxMind GeoLite2 |
6. Database Schema — Module Tables
Key module-specific tables. Full 22-table ERD finalized in Sprint 1, Week 2.
| Table | Purpose | Key Fields |
|---|---|---|
| master_queue | All leads pending assignment | lead_id, source, vehicle_type, priority, entered_at, assignment_status (pending | assigned | failed), wait_time_minutes |
| agent_capacity_config | Per-agent capacity settings | user_id, calls_per_day_target, max_pipeline_size, vehicle_type_expertise JSON, language_skills JSON, call_type_eligibility JSON |
| call_logs | All outbound call records | agent_id, lead_id, duration_seconds, is_genuine_attempt, call_type (fresh/followup/reassigned), called_at |
| lead_timeline | Immutable audit log (INSERT-only) | lead_id, event_type, from_stage, to_stage, changed_by, metadata JSON, created_at (no updated_at) |
| documents | All uploaded documents | lead_id, document_type, s3_key, file_hash_sha256, ocr_extracted JSON, ocr_corrections JSON, verification_status, tampering_flags JSON |
| document_checklist_configs | Required docs per case×deal matrix | deal_type, case_type, document_type, is_mandatory, is_active |
| fraud_detection_index | SHA-256 hashes for duplicate detection | hash_sha256, document_type, lead_id, first_seen_at |
| pricing_metal_rates | Metal rate history | metal_type, rate_per_kg, effective_date, source (manual/api), created_by |
| pricing_condition_rules | Condition modifier rules | vehicle_age_from_years, vehicle_age_to_years, condition, modifier_multiplier |
| pricing_snapshots | Versioned pricing ruleset snapshots | snapshot_data JSON, version, created_by, restored_from_id |
| payments | Payment records (CRM side only) | lead_id, payment_type, amount, utr_number UNIQUE, payment_mode, receipt_s3_key, receipt_hash_sha256, is_manual_review |
| sla_configurations | SLA thresholds per stage | stage, threshold_hours, breach_action (auto_reassign/escalate_supervisor), is_active |
| whatsapp_notifications | WhatsApp message log | lead_id, stage, template_id, message_id, status (sent/delivered/read/failed), delivery_at, fallback_triggered_at |
| agent_sessions | Session tracking | user_id, login_at, logout_at, active_minutes, idle_minutes, device_info |
| agent_daily_summaries | Daily aggregates for Leadboard | user_id, date, active_minutes, idle_minutes, leads_touched, genuine_call_count |
| system_configs | Admin-configurable parameters | key VARCHAR UNIQUE, value TEXT — genuine_call_min_seconds, queue_backlog_threshold, keyword_priority_list |
| customer_agent_mappings | Returning customer → original agent | mobile_hash SHA-256, agent_id, call_attempt_count, last_assigned_at |
7. API Design Conventions
7.1 Base URL & Standards
- Base URL:
/api/v1/ - Auth: Laravel Sanctum token in
Authorization: Bearer {token}header - Response envelope:
{ data, meta, errors } - Status codes: 200 / 201 / 422 / 401 / 403 / 404 / 500
7.2 Core API Endpoints
| Method | Endpoint | Auth / Role | Description |
|---|---|---|---|
| POST | /api/v1/auth/login |
Public | Returns Sanctum token + user profile |
| POST | /api/v1/auth/logout |
Auth | Revokes current token |
| POST | /api/v1/otp/send |
Public | Sends OTP to mobile via SMS gateway |
| POST | /api/v1/otp/verify |
Public | Validates OTP, returns verified=true |
| GET | /api/v1/leads |
Agent, Admin, Supervisor | Paginated list; scoped to own leads for agent role |
| POST | /api/v1/leads |
Agent, Admin, Call Centre | Create new lead; triggers VAHAN + pricing jobs |
| GET | /api/v1/leads/{id} |
Role-scoped | Full lead detail with timeline, documents, payments |
| PATCH | /api/v1/leads/{id}/stage |
Role-scoped per stage | Stage transition; gated by validation rules |
| PATCH | /api/v1/leads/{id}/assign |
Admin, Supervisor | Manual assignment/reassignment |
| POST | /api/v1/leads/bulk-assign |
Admin, Supervisor | Bulk assignment; array of lead IDs + caller_id |
| POST | /api/v1/leads/{id}/documents |
Agent, Admin | Upload document → S3; triggers TextractJob |
| GET | /api/v1/documents/{id}/url |
Role-scoped | Returns pre-signed S3 URL (15-min expiry); logs access |
| PATCH | /api/v1/documents/{id}/verify |
Verification Team | Mark verified | rejected | request_reupload |
| GET | /api/v1/queue |
Admin, Supervisor | Master queue with wait times, colour coding |
| POST | /api/v1/queue/auto-assign |
Admin | Triggers AssignmentEngine for all pending queue items |
| GET | /api/v1/pricing/calculate |
Agent, Admin | Returns calculated scrap value for given params |
| POST | /api/v1/pricing/override |
Manager, Admin | Request override → approval workflow |
| POST | /api/webhooks/whatsapp |
HMAC-signed | Gupshup/Twilio inbound message + delivery status |
| POST | /api/v1/rbac/permissions/batch |
Admin | Save permission matrix; busts Redis cache |
| POST | /api/v1/rbac/fields/sync |
Admin | FieldSyncService — sync DB columns to fields table |
8. Authentication & RBAC
8.1 Pipeline Role Definitions
| Role | Stages Accessible | Data Scope | Config Access | Sensitive Fields |
|---|---|---|---|---|
agent |
Fresh → Doc Verification Pending | Own leads only | None | Aadhaar ✗ PAN ✗ Bank ✗ |
supervisor |
All stages (view); Fresh→Doc Verif Pending (action) | Team leads | Reassign, escalation, priority | Aadhaar ✗ PAN ✗ Bank ✗ |
verification |
Doc Verification Pending, Doc Verified | Assigned verification queue | Document review only | Aadhaar ✓ PAN ✓ Bank ✓ |
finance |
Scrap Value Paid (action) | Payment fields + receipts | View pricing only | Aadhaar ✗ PAN ✗ Bank ✓ |
logistics |
Assigned to Logistic → Pickup Done | Assigned pickups only | None | Aadhaar ✗ PAN ✗ Bank ✗ |
admin |
All stages | All records | Full — users, SLA, pricing, RBAC | Aadhaar ✓ PAN ✓ Bank ✓ |
8.2 Dynamic RBAC Architecture — Overview
In addition to the 6 pipeline roles, Mariinox CRM implements a fully dynamic field-level permission system. Admin can configure which roles can view, edit, or are hidden from any field on any module — without code changes or redeployment.
| Layer | Description |
|---|---|
| Module | A feature area: customers, leads, orders, payments. Each maps to a DB table and a Laravel module folder. |
| Field | A column inside a module's table. Fields are auto-synced from Schema::getColumnListing() and stored in the fields table with a UI label, type, and is_active flag. |
| Permission | A per (role × module × field) record. Stores can_view and can_edit booleans. field_id = NULL means module-level permission. |
| PermissionService | Central service: canAccessField($user, $moduleSlug, $fieldName, $action). Called from controllers, API Resources, Blade directives. |
| Middleware | CheckModuleAccess: validates module-level can_view before the controller runs. Blocks 403 early. |
| Blade Directive | @canField('customers', 'credit_limit', 'view') — renders or hides Blade sections based on field-level permission. |
| Admin UI | Permission matrix table (role × field with View/Edit checkboxes). Saved via AJAX. No code deployment required. |
| Redis Cache | All permissions cached per (role_id × module_id) in Redis with 5-minute TTL. Cache busted on any permission save. |
8.3 Dynamic RBAC — Database Schema (Step 1)
| Table | Column | Type | Notes |
|---|---|---|---|
| roles | id | bigIncrements | PK |
| name | string(80) | e.g. 'Sales Agent' | |
| slug | string(80) unique | e.g. agent, finance, admin — lookup key | |
| modules | id | bigIncrements | PK |
| name | string(80) | e.g. 'Leads', 'Customers' | |
| slug | string(80) unique | maps to DB table name | |
| fields | id | bigIncrements | PK |
| module_id | FK → modules.id | CASCADE delete | |
| name | string(80) | DB column name e.g. credit_limit | |
| label | string(120) | UI display label e.g. 'Credit Limit (₹)' | |
| type | enum | text | number | select | date | boolean | file | |
| is_active | boolean default true | Soft-disable without deleting permissions | |
| permissions | id | bigIncrements | PK |
| role_id | FK → roles.id | CASCADE delete | |
| module_id | FK → modules.id | CASCADE delete | |
| field_id | FK nullable → fields.id | NULL = module-level permission | |
| can_view / can_edit | boolean default false | UNIQUE(role_id, module_id, field_id) |
// Migration snippet — permissions table
Schema::create('permissions', function (Blueprint $table) {
$table->id();
$table->foreignId('role_id')->constrained()->cascadeOnDelete();
$table->foreignId('module_id')->constrained()->cascadeOnDelete();
$table->foreignId('field_id')->nullable()->constrained()->cascadeOnDelete();
$table->boolean('can_view')->default(false);
$table->boolean('can_edit')->default(false);
$table->unique(['role_id', 'module_id', 'field_id']);
$table->index(['role_id', 'module_id']);
});
8.4 Eloquent Models & Relationships (Step 2)
// Role.php
class Role extends Model {
public function permissions(): HasMany {
return $this->hasMany(Permission::class);
}
}
// Module.php
class Module extends Model {
public function fields(): HasMany {
return $this->hasMany(Field::class);
}
}
// Field.php
class Field extends Model {
public function module(): BelongsTo {
return $this->belongsTo(Module::class);
}
}
// Permission.php
class Permission extends Model {
public function role(): BelongsTo { return $this->belongsTo(Role::class); }
public function module(): BelongsTo { return $this->belongsTo(Module::class); }
public function field(): BelongsTo { return $this->belongsTo(Field::class); }
}
8.5 FieldSyncService (Step 3)
// App\Services\FieldSyncService.php
class FieldSyncService {
public function sync(string $moduleSlug): array {
$module = Module::where('slug', $moduleSlug)->firstOrFail();
$columns = Schema::getColumnListing($moduleSlug);
$synced = 0; $skipped = 0;
foreach ($columns as $column) {
$existing = Field::where('module_id', $module->id)
->where('name', $column)->first();
if ($existing) {
// Preserve manual label/type overrides
$skipped++; continue;
}
Field::create([
'module_id' => $module->id,
'name' => $column,
'label' => Str::title(str_replace('_', ' ', $column)),
'type' => $this->inferType($column),
'is_active' => true,
]);
$synced++;
}
return ['synced' => $synced, 'skipped' => $skipped];
}
private function inferType(string $column): string {
if (str_ends_with($column, '_at')) return 'date';
if (str_ends_with($column, '_id')) return 'number';
if (str_contains($column, 'amount') || str_contains($column, 'value')) return 'number';
if (str_contains($column, 'is_') || str_contains($column, 'has_')) return 'boolean';
return 'text';
}
}
8.6 PermissionService (Step 4)
// App\Services\PermissionService.php
class PermissionService {
public function canAccessField(User $user, string $moduleSlug, string $fieldName, string $action = 'view'): bool {
$role = $user->role;
if (!$role) return false;
// Admin always has full access
if ($role->slug === 'admin') return true;
$map = $this->getPermissionMap($role->id, $moduleSlug);
// Check module-level permission first (field_id = null)
if (!($map['__module__']['can_view'] ?? false)) return false;
// Check field-level permission
$fieldPerms = $map[$fieldName] ?? null;
if (!$fieldPerms) return false;
return match($action) {
'view' => (bool) $fieldPerms['can_view'],
'edit' => (bool) $fieldPerms['can_edit'],
default => false,
};
}
private function getPermissionMap(int $roleId, string $moduleSlug): array {
$cacheKey = "rbac:role:{$roleId}:module:{$moduleSlug}";
return Cache::remember($cacheKey, now()->addMinutes(5), function () use ($roleId, $moduleSlug) {
$module = Module::where('slug', $moduleSlug)->firstOrFail();
$perms = Permission::with('field')
->where('role_id', $roleId)
->where('module_id', $module->id)
->get();
$map = [];
foreach ($perms as $perm) {
$key = $perm->field_id ? $perm->field->name : '__module__';
$map[$key] = ['can_view' => $perm->can_view, 'can_edit' => $perm->can_edit];
}
return $map;
});
}
public function bustCache(int $roleId, string $moduleSlug): void {
Cache::forget("rbac:role:{$roleId}:module:{$moduleSlug}");
}
// Bulk filter: removes inaccessible fields from an array
public function filterFields(User $user, string $moduleSlug, array $data): array {
return array_filter($data, function ($value, $key) use ($user, $moduleSlug) {
return $this->canAccessField($user, $moduleSlug, $key, 'view');
}, ARRAY_FILTER_USE_BOTH);
}
}
8.7 CheckModuleAccess Middleware (Step 5)
// App\Http\Middleware\CheckModuleAccess.php
class CheckModuleAccess {
public function handle(Request $request, Closure $next, string $moduleSlug): Response {
$user = $request->user();
if (!$user) return response()->json(['error' => 'Unauthenticated'], 401);
$service = app(PermissionService::class);
if (!$service->canAccessField($user, $moduleSlug, '__module__', 'view')) {
return response()->json(['error' => 'Access denied to module: ' . $moduleSlug], 403);
}
return $next($request);
}
}
// routes/api.php — usage
Route::middleware(['auth:sanctum', 'module:leads'])->group(function () {
Route::apiResource('leads', LeadController::class);
});
Route::middleware(['auth:sanctum', 'module:customers'])->group(function () {
Route::apiResource('customers', CustomerController::class);
});
8.8 Blade Directive (Step 6)
// AppServiceProvider.php — register Blade directive
Blade::if('canField', function (string $module, string $field, string $action = 'view') {
$user = auth()->user();
$service = app(PermissionService::class);
return $user && $service->canAccessField($user, $module, $field, $action);
});
// Global helper (Helpers.php)
function canField(string $module, string $field, string $action = 'view'): bool {
$user = auth()->user();
if (!$user) return false;
return app(PermissionService::class)->canAccessField($user, $module, $field, $action);
}
// Usage in Blade templates
@canField('customers', 'credit_limit', 'view')
Credit Limit: {{ $customer->credit_limit }}
@endcanField
@canField('leads', 'aadhaar_number', 'edit')
@else
**** **** ****
@endcanField
8.9 Admin Permission Matrix UI (Step 7)
React component inside Admin Command Centre. Role + Module selectors load the field matrix dynamically. All toggles save via AJAX to the permissions API.
| UI Component | Behaviour | API Call | Notes |
|---|---|---|---|
| Role Selector (dropdown) | Loads all roles | GET /rbac/roles | Default: first role alphabetically |
| Module Selector (dropdown) | Loads all modules | GET /rbac/modules | Triggers matrix refresh |
| Field Matrix Table (Field | View ☑ | Edit ☑) | Pre-filled from DB | GET /rbac/permissions?role_id=&module_id= | Inactive fields greyed out |
| View checkbox toggle | AJAX on change | POST /rbac/permissions | Unchecking View auto-unchecks Edit |
| Edit checkbox toggle | AJAX on change | POST /rbac/permissions | Checking Edit auto-checks View |
| Save All button | Batch save changed rows | PUT /rbac/permissions/batch | Busts Redis cache for role+module on save |
| Sync Fields button (per module) | Triggers FieldSyncService | POST /rbac/fields/sync | Shows new columns from DB; does not delete existing |
Example Permission Matrix (leads module)
| Field Name | Label | agent — View | agent — Edit | verification — View | admin — View |
|---|---|---|---|---|---|
| lead_number | Lead Number | ✅ | ❌ | ✅ | ✅ |
| customer_name | Customer Name | ✅ | ✅ | ✅ | ✅ |
| aadhaar_number | Aadhaar Number | ❌ | ❌ | ✅ | ✅ |
| pan_number | PAN Number | ❌ | ❌ | ✅ | ✅ |
| bank_account | Bank Account | ❌ | ❌ | ✅ | ✅ |
| credit_limit | Credit Limit (₹) | ✅ | ❌ | ❌ | ✅ |
| final_scrap_value | Final Scrap Value | ✅ | ✅ | ❌ | ✅ |
8.10 Controller Integration (Step 8)
// LeadController.php — show()
public function show(Lead $lead): JsonResponse {
$this->authorize('view', $lead); // Laravel Policy (pipeline role gate)
$data = $lead->toArray();
// Dynamic RBAC: strip fields user cannot view
$filtered = app(PermissionService::class)->filterFields(auth()->user(), 'leads', $data);
return response()->json(['data' => $filtered]);
}
// LeadController.php — update()
public function update(UpdateLeadRequest $request, Lead $lead): JsonResponse {
$this->authorize('update', $lead);
// Only allow fields where can_edit = true
$allowedFields = array_filter($request->all(), function ($key) {
return canField('leads', $key, 'edit');
}, ARRAY_FILTER_USE_KEY);
$lead->update($allowedFields);
return response()->json(['data' => $lead->fresh()]);
}
8.11 API Response Protection (Step 9)
// BaseApiController.php
protected function permissionFilteredResponse(User $user, string $module, array $data): array {
return app(PermissionService::class)->filterFields($user, $module, $data);
}
// For collection responses (e.g. GET /leads index)
protected function filterCollection(User $user, string $module, Collection $items): array {
return $items->map(fn($item) =>
$this->permissionFilteredResponse($user, $module, $item->toArray())
)->toArray();
}
// Pipeline: Controller → PermissionService::filterFields → JsonResponse
// No restricted field ever reaches the wire — server-side enforcement only
8.12 Test Cases (Step 10)
| Test ID | Role | Module | Field | Action | Expected |
|---|---|---|---|---|---|
| TC-RBAC-01 | admin | leads | aadhaar_number | view | true (admin bypass) |
| TC-RBAC-02 | agent | leads | aadhaar_number | view | false (masked) |
| TC-RBAC-03 | agent | leads | customer_name | view | true |
| TC-RBAC-04 | agent | leads | credit_limit | edit | false (view-only) |
| TC-RBAC-05 | finance | payments | bank_account | view | true |
| TC-RBAC-06 | logistics | payments | bank_account | view | false |
| TC-RBAC-07 | supervisor | leads | assigned_to | edit | true (reassign allowed) |
| TC-RBAC-08 | verification | leads | pan_number | view | true |
| TC-RBAC-09 | agent | customers | (module-level) | view | false (module blocked) |
| TC-RBAC-10 | admin | customers | credit_limit | edit | true (admin bypass) |
8.13 Advanced Enhancements
| Enhancement | Implementation Notes |
|---|---|
| Redis Permission Cache | Cache::remember() with 5-min TTL per role×module. Busted automatically on permission save. php artisan rbac:clear-cache for emergency flush. |
| Read-Only Field Mode | can_view=true, can_edit=false. API Resource returns value; Controller update() rejects writes with HTTP 403 + 'field_name is read-only for your role'. |
| Field Groups | Add field_group column to fields table (e.g. 'financial', 'contact', 'vehicle'). Admin grants/revokes entire group at once. FieldSyncService auto-assigns group by naming convention. |
| Dynamic Form Builder | GET /api/v1/rbac/form-schema?module=leads returns only fields current user can view/edit, with type + label + is_editable flag. React renders inputs dynamically from this schema. |
| Hidden vs Masked | Extend with display_mode enum: hidden (field removed), masked (returned as '****'), visible. Useful for showing agents data exists but is restricted. |
| RBAC Audit Trail | rbac_audit_logs table: who changed what permission, from/to values, timestamp. Streamed to CloudWatch. |
8.14 Laravel Policy Layer — Pipeline Stage Gates
// LeadPolicy.php
public function view(User $user, Lead $lead): bool {
if ($user->role->slug === 'agent') return $lead->assigned_to === $user->id;
if ($user->role->slug === 'supervisor') return $lead->team_id === $user->team_id;
return in_array($user->role->slug, ['admin', 'verification', 'finance', 'logistics']);
}
public function transition(User $user, Lead $lead, string $toStage): bool {
$allowedRoles = PipelineConfig::stageAllowedRoles($lead->vehicle_type, $toStage);
return in_array($user->role->slug, $allowedRoles);
}
8.15 Session & Token Management
- Laravel Sanctum SPA tokens — stored in HttpOnly cookies for web app; Bearer token for API clients
- Token expiry: 8 hours active session; 24 hours with remember-me
- Agent heartbeat ping: POST /api/v1/auth/heartbeat every 5 minutes — extends session + records active time in agent_sessions
- 10-minute inactivity = Idle status; 60-minute no-heartbeat = forced logout (configurable in system_configs)
- Admin can view active sessions in Admin Command Centre; terminate any session from the UI
9. Third-Party Integrations
| Service | Provider | Job / Hook | Implementation Notes |
|---|---|---|---|
| VAHAN API | MoRTH / NIC | VahanFetchJob | Dispatched async on lead save; 3 retries with 30s backoff; manual fallback if all fail |
| OCR | AWS Textract | TextractOCRJob | Triggered on S3 upload; confidence < 85% flags field amber; corrections logged |
| SMS Gateway | Gupshup / MSG91 | SendSmsJob | OTP delivery + WhatsApp fallback (5-min non-delivery trigger) |
| Gupshup / Twilio | SendWhatsAppJob | Template messages on every stage change; delivery status via webhook → lead_timeline | |
| Payment GW | API Team | N/A (CRM side) | CRM only stores UTR + receipt; disbursement handled by API Integration team |
| AI KYC (Phase 2) | IDfy / Signzy | KycVerifyJob | Name fuzzy-match, format validation; photo matching Phase 2+ |
| STT (Phase 2) | AWS Transcribe / GCP Speech | TranscribeCallJob | Confirmation required by Week 10; Hindi + Kannada + Hinglish support |
| Metal Rate (Phase 2) | LME / Metal Price API | MetalRateSyncJob | Scheduled daily; fallback to last known rate; Admin notified on failure |
| CSAT (Phase 3) | WhatsApp survey | CsatSurveyJob | Triggered 2h after Pickup Done; response captured via webhook |
10. Infrastructure & DevOps
10.1 AWS Resource Sizing — Phase 1 Launch
| Resource | Spec | Notes |
|---|---|---|
| EC2 Web (ASG) | t3.medium × 2 (min) | Scale to 10× on CPU > 60%; Nginx + PHP-FPM; 8 vCPU / 32GB at peak |
| EC2 Workers (ASG) | t3.small × 2 (min) | Laravel Horizon; separate from web tier; scales on queue depth |
| RDS MariaDB | db.t3.medium Multi-AZ | Primary + standby; automated snapshots every 4h; 7-day retention |
| ElastiCache Redis | cache.t3.small | Sessions + queue + RBAC cache; 2-node cluster for HA |
| S3 Buckets | Private + versioning | mariinox-crm-docs-prod (ap-south-1); CRR to ap-south-2; SSE-KMS; pre-signed URLs only |
| ALB | 1 ALB | HTTPS termination; ACM certificate; HTTP→HTTPS redirect; sticky sessions |
| CloudFront | 1 distribution | React SPA static assets; API calls bypass CDN |
| CloudWatch | Logs + Alarms | App logs + audit stream + infra metrics; 3yr log retention; SNS alerts on P1 alarms |
10.2 CI/CD Pipeline
GitHub Actions → Build PHP + React → Run PHPUnit + Jest → Build Docker image → Push to ECR → Deploy to Staging → Smoke test → Manual approval gate → Deploy to Production
10.3 Security Controls
- WAF: AWS WAF on ALB — rate limiting, SQL injection, XSS rules
- Secrets: AWS Secrets Manager for all API keys, DB credentials, S3 KMS key ARN
- VPC: App and DB tiers in private subnets; NAT Gateway for outbound-only internet access
- Aadhaar compliance: No raw Aadhaar in logs; AES-256 at rest; field-level API masking; legal review before go-live
- Penetration test: RBAC penetration test in Week 9 before Phase 1 UAT — all 6 roles × 47 endpoints
11. Sprint Plan — Phase 1 (Weeks 1–11)
11.0 Team Composition
| Team / Function | Head Count | Person-Days | Story Points |
|---|---|---|---|
| Product / BA / Project Management | 1 PM | 21 | 42 |
| DevOps / Infrastructure | 1 DevOps Engineer | 20 | 40 |
| Backend Team (Laravel) | 1 Tech Lead + 2 BEs | 272 | 544 |
| Frontend Team (React + TypeScript) | 2 Frontend Engineers | 210 | 420 |
| QA / Testing | 1 QA Engineer | 36 | 72 |
| UI/UX Design | 1 Designer | 18 | 36 |
| TOTAL | 8 people | 577 PD | 1,154 SP |
Sprint Schedule Overview
| Sprint | Weeks | Focus | Primary Modules | PD | SP |
|---|---|---|---|---|---|
| S1 | 1–2 | Foundation | Project Setup, DB Design, UI/UX Wireframes | 56 | 112 |
| S2 | 3–4 | Auth + Capture | Auth + RBAC, Lead Capture all channels, VAHAN, OTP | 40 | 80 |
| S3 | 5–6 | Assignment + Pipeline | Assignment Engine, Pipeline State Machine, Deal/Case/Checklist | 78 | 156 |
| S4 | 7–8 | Doc + Pricing | Document Engine, Verification Workflow, Pricing Engine, Payment Triggers | 82 | 164 |
| S5 | 9–10 | SLA + Notifs + Dashboards | SLA Engine, Notifications, 8 Dashboards, Session Tracking, Audit Logs, QA | 321 | 642 |
| S6 | 11 | UAT + Go-Live | UAT, bug fixes, production deployment, hypercare start | — | — |
| TOTAL | 577 PD | 1,154 SP |
Sprint 1 — Weeks 1–2: Foundation Setup | 56 PD | 112 SP
Module: Project Setup & Infrastructure | 20 PD | 40 SP | DevOps + Backend
| Task | Owner | PD | SP | Acceptance Criteria | Dependency |
|---|---|---|---|---|---|
| Provision AWS: EC2 ASG (min 2), RDS Multi-AZ, Redis, ALB, S3 (versioning+SSE-KMS), CloudWatch, ACM, WAF | DevOps | 4 | 8 | All services healthy; HTTPS reachable | AWS account access |
| VPC design: public (ALB), private (app+worker), isolated (RDS); NAT Gateway; security groups | DevOps | 2 | 5 | No public DB/app exposure | — |
| Dev + Staging envs: separate RDS instances, shared Redis with DB index isolation, S3 bucket per env | DevOps | 2 | 5 | Devs deploy to staging via CI in < 10 min | Row 1 |
| GitHub Actions CI/CD: build → PHPUnit → Jest → Docker → ECR push → staging deploy → smoke test → prod gate | DevOps+TL | 3 | 8 | Green pipeline on main branch merge | Row 3 |
| Laravel 11 skeleton: app/Modules/* structure, Sanctum config, Horizon config, base .env via AWS Secrets Manager | Tech Lead | 3 | 5 | php artisan route:list returns auth + health routes | Row 1 |
| React 18+TS+Vite scaffold: React Router v6, Tailwind, React Query v5, Zustand, auth context + sidebar layout | FE Lead | 3 | 5 | Login page renders; API base URL configurable per env | — |
| S3 integration: Laravel Filesystem S3 disk + local fallback; pre-signed URL helper with 15-min expiry | Tech Lead | 3 | 4 | Upload to S3 and retrieve via pre-signed URL on staging | Row 1 |
| Module Total | 20 | 40 |
Module: Database Design & Architecture | 18 PD | 36 SP | Backend
| Task | Owner | PD | SP | Acceptance Criteria | Dependency |
|---|---|---|---|---|---|
| Finalise ERD: all 22 tables including users, leads, lead_vehicle_details, lead_contacts, lead_attribution, master_queue, assignment_rules, agent_capacity_config, call_logs, customer_agent_mappings, lead_timeline, documents, document_checklist_configs, fraud_detection_index, pricing tables, payments, sla_configurations, whatsapp_notifications, agent_sessions, system_configs | Tech Lead | 4 | 8 | ERD signed off by Mariinox by Day 10 | Business rules locked |
| Write all Laravel migration files: column definitions, ULID PKs, FK constraints, nullable/default, ENUM types | BE1 | 5 | 8 | php artisan migrate:fresh runs without errors | ERD approval |
| Define all indexes: composite (stage, assigned_to), (assignment_status, priority, entered_at), (document_type, hash_sha256) | Tech Lead | 3 | 5 | EXPLAIN SELECT shows index on all dashboard queries | Migrations done |
| Eloquent models: relationships, casts, hidden fields (aadhaar/pan/bank masked), SoftDeletes, ULID trait | BE1 | 3 | 5 | Model factories + seeders for all 22 tables | Migrations done |
| DB-level immutability: REVOKE UPDATE, DELETE on lead_timeline FROM app_db_user; GRANT INSERT only; test assertion | Tech Lead | 2 | 5 | Direct UPDATE on lead_timeline returns DB error 1142 | — |
| DB seeding: system_configs, pipeline_stages (11+10 stage definitions), default SLA configs | BE1 | 1 | 5 | Fresh migrate + seed completes in < 2 min | Models done |
| Module Total | 18 | 36 |
Sprint 2 — Weeks 3–4: Auth + RBAC + Lead Capture | 40 PD | 80 SP
Module: Auth + RBAC | 16 PD | 32 SP | Backend
| Task | Owner | PD | SP | Acceptance Criteria | Dependency |
|---|---|---|---|---|---|
| Sanctum SPA auth: POST /login → HttpOnly token; POST /logout; POST /heartbeat (5-min extend); expiry 8h | BE1 | 2 | 3 | Token issued; logout clears cookie; 401 on expired token | Sprint 1 done |
| User management CRUD (Admin): create/edit, assign role, max_capacity, language_skills, vehicle_type_expertise JSON | BE1 | 2 | 5 | Admin creates all 6 role types; inactive user cannot login | DB migrations |
| Laravel Policy classes (6): LeadPolicy, DocumentPolicy, PricingPolicy, QueuePolicy, PaymentPolicy, DashboardPolicy | Tech Lead | 3 | 8 | 403 on all 47 endpoint × role mismatch combos (unit tested) | User model |
| Field-level masking: Aadhaar, PAN, bank_account hidden in API Resource transformers for agent + logistics roles | BE1 | 2 | 5 | Agent endpoint returns masked '****' for sensitive fields | Policies done |
| Agent session: heartbeat → active_minutes++; 10-min gap = idle; logout updates logout_at | BE1 | 2 | 3 | Session idle flag set after 10-min no-ping | DB migrations |
| Frontend: Login page, role-based sidebar, protected route HOC, auto-refresh on 401, session timeout warning at 7h | FE1 | 3 | 5 | Agent sees only agent sidebar items; admin sees all | Sanctum done |
| Admin user management UI: user list, create/edit form, role selector, capacity config, activate/deactivate toggle | FE1 | 2 | 3 | Admin manages users without backend console access | User CRUD API |
| Module Total | 16 | 32 |
Sprint 3 — Weeks 5–6: Assignment Engine + Pipeline + Checklist | 78 PD | 156 SP
Module: Assignment Engine | 32 PD | 64 SP | Backend (heavy logic)
| Task | Owner | PD | SP | Acceptance Criteria | Dependency |
|---|---|---|---|---|---|
| master_queue management: all leads enter on creation; priority sort (priority DESC, entered_at ASC); backlog alert | Tech Lead | 2 | 5 | New lead in queue within 1s; backlog alert fires | Lead model |
| Agent capacity engine: live active_lead_count per agent; compare vs max_capacity → Active/Busy; Redis cached | BE1 | 2 | 5 | Busy agent skipped by engine; cache updates < 500ms | Redis setup |
| 5-layer assignment rules engine (L1–L5): call score → call type specialisation → vehicle expertise → language match → manual fallback | Tech Lead | 5 | 13 | All 5 layers pass 20+ unit test cases; correct agent selected | agent_capacity_config seeded |
| Genuine attempt qualifier: call_logs.is_genuine_attempt = (duration_seconds >= genuine_call_min_seconds) | BE1 | 2 | 3 | Calls < 30s not counted in performance score | call_logs model |
| AssignmentRetryJob: Scheduler every 15 min; re-evaluates pending queue; triggered on stage change via Observer | BE1 | 2 | 5 | Queue re-evaluated < 1 min after stage change | Scheduler config |
| Customer-agent mapping: SHA-256(mobile) → preferential re-assign to original agent if Active | BE2 | 2 | 5 | Returning customer re-assigned to same agent if Active | Assignment engine |
| Manual assign/reassign API: only Active agents in eligibles; Admin can override Busy from Escalation Queue | BE1 | 2 | 5 | Reassignment entry has old agent + new agent + reason code | Timeline writer |
| Bulk assign: POST /api/v1/leads/bulk-assign; validates capacity per lead; partial success response | BE2 | 2 | 3 | 50 leads bulk-assigned in < 3s | Assign API |
| Frontend — Master Queue Dashboard: wait-time sorted list, red badge >2h, Assign button, Auto-Assign All, 60s refresh | FE1 | 3 | 8 | Queue shows correct wait times; badges accurate | Queue API |
| Frontend — Manual assign modal: status dot (Active/Busy), capacity bar, today's calls; Busy agents greyed-out | FE2 | 2 | 5 | Busy agents visible but not selectable | Queue dashboard |
| Module Total | 26 | 60 |
Sprint 4 — Weeks 7–8: Document Engine + Pricing + Payments | 82 PD | 164 SP
Module: Document Engine | 36 PD | 72 SP | Backend + Frontend
| Task | Owner | PD | SP | Acceptance Criteria | Dependency |
|---|---|---|---|---|---|
| S3 upload flow: pre-signed POST URL → client direct upload → S3 event → document record created; max 10MB; PDF/JPG/PNG/HEIC | BE1 | 3 | 8 | File in S3; document record with s3_key created | S3 setup |
| File integrity: SHA-256 hash server-side on upload; re-verified on every pre-signed GET URL; mismatch = tampering flag | BE1 | 2 | 5 | Modified file hash mismatch detected on next access | S3 upload |
| AWS Textract OCR: triggered post-upload; key-value pairs mapped to lead fields; confidence score per field; < 85% = amber | BE2 | 4 | 8 | RC upload populates vehicle fields with confidence scores | AWS Textract access |
| SHA-256 fraud detection: document number hash checked against fraud_detection_index; duplicate → Exceptions Queue | BE2 | 3 | 5 | Duplicate Aadhaar triggers exception within 1s | Fraud index table |
| Digital consent capture (immutable): timestamp + IP + agent ID + consent text version in lead_consents; REVOKE UPDATE/DELETE | BE1 | 2 | 3 | Consent record cannot be edited or deleted by any role | Timeline writer |
| Document Access Log: every pre-signed GET URL generation writes to document_access_logs; streamed to CloudWatch | BE1 | 1 | 2 | Log entry on every URL generation; admin can view log | S3 upload |
| Combined PDF: all verified docs merged into single PDF; stored in S3 combined/ prefix; link from lead detail | BE2 | 2 | 3 | Combined PDF downloadable from lead detail view | Verification done |
| Frontend — Document upload UI: drag-and-drop per doc type; HEIC preview; OCR result panel with amber fields; correction input | FE1 | 4 | 8 | Agent uploads, views OCR, corrects amber fields end-to-end | Upload + OCR BE done |
| Frontend — In-browser document viewer: PDF in iframe; image zoom; no download button; expired URL shows re-load prompt | FE2 | 2 | 5 | Document viewable in-browser; direct download prevented | Pre-signed URL API |
| Module Total | 27 | 55 |
Sprint 5 — Weeks 9–10: SLA + Notifications + Dashboards + QA | ~220 PD | ~430 SP
All 8 Dashboards Summary | 130 PD | 260 SP
| Dashboard | Owner | PD | SP | Key Components | Acceptance Criteria |
|---|---|---|---|---|---|
| Agent Day Briefing | FE1+BE1 | 18 | 36 | Day-at-a-glance strip; Priority Stack (5-item todo); Today's Schedule with Call buttons; personal conversion vs team avg | All tasks in correct priority order on login |
| Admin Command Centre | FE2+BE2 | 22 | 44 | Zone 1: Pipeline Health; Zone 2: Team Health (per-agent live card); Zone 3: Action Required (3 alert cards); 60s auto-refresh | All 3 zones populate with live data |
| Team Performance / Leadboard | FE1+BE1 | 18 | 36 | Date range filter; Call Metrics table; Pipeline Conversion table; sortable columns; top performer highlight | All columns sortable; top performer visually distinct |
| SLA & Escalations | FE2+BE1 | 14 | 28 | 3 tabs: Escalation Queue; Caller Capacity; SLA Rules Reference | Escalation queue shows leads with overage in hours |
| Master Queue Dashboard | FE1+BE2 | 14 | 28 | Wait-time sorted; red badge >2h; Assign button; Auto-Assign All; capacity panel; 60s refresh | Queue sorted correctly; modal shows Active agents only |
| Pipeline Analytics | FE2+BE2 | 18 | 36 | Source Conversion Quality table; Lead Ageing Distribution bar chart; weekly call volume chart | Charts render correctly for selected date range |
| Agent Personal Performance | FE1+BE1 | 16 | 32 | KPI strip; conversion funnel with drop-off %; incentive tracker (target vs actual, projected month-end) | Incentive estimate matches manual calculation |
| Notifications UI | FE2+BE2 | 10 | 20 | Bell icon with unread count; dropdown list; mark-as-read; toast alerts for SLA breach / new assignment | Unread count accurate; toast appears on SLA breach |
| Module Total | 130 | 260 |
Sprint 6 — Week 11: UAT & Production Go-Live
| Activity | Detail | Owner | Exit Criteria |
|---|---|---|---|
| Mariinox UAT | All 6 roles tested across 100+ test scenarios; test case library provided by PM in Week 10 | Mariinox + PM | 0 critical, 0 high bugs open |
| Bug triage + fixes | Critical/High fixed within 48h; Medium scheduled post go-live; all fixes verified on staging | TL + BE1 | UAT sign-off from Mariinox |
| Production deployment | DB migration on Mariinox AWS account; Docker image to prod EC2 ASG; CloudWatch alarms active; rollback plan ready | DevOps + TL | Smoke test passes on prod |
| Go-live sign-off | Written sign-off from Mariinox stakeholder; M2 payment milestone triggered (30% of contract) | PM + Mariinox | M2 invoice issued |
| Hypercare begins | Dedicated on-call engineer; business hours; critical SLA = 4h response + 24h fix; daily status call for 2 weeks | BE1 | Hypercare runbook active |
12. Sprint Plan — Phase 2 (Weeks 12–18) | 60 PD | 120 SP
| Weeks | Module | PD | SP | Prerequisite | Acceptance Criteria |
|---|---|---|---|---|---|
| 12–14 | Live metal rate feed (MetalRateSyncJob daily; admin-confirmed API source; fallback to last known rate) | 8 | 16 | Metal rate API provider + credentials by Week 12 | Rate auto-updates daily; fallback fires on failure |
| 12–14 | Logistics webhook receivers: Pickup Scheduled / Agent Assigned / Pickup Done → auto-stage progression in CRM | 12 | 24 | Logistics portal webhook event schema by Week 11 | Stage advances on webhook receipt within 5s |
| 12–14 | STT integration (AWS Transcribe / GCP Speech): Hindi + Kannada + Hinglish; call recording → transcript → note appended | 12 | 24 | STT scope decision locked by Week 10 | Transcript appended to lead note within 5 min |
| 15–17 | Government portal CRM layer: Ops UI to initiate scrapping application, poll status, capture CoD number; SLA escalation if overdue | 14 | 28 | Govt portal API docs by Week 12 | CoD number captured; SLA escalation fires if overdue |
| 17–18 | Phase 2 UAT + full regression (Phase 1+2) + production deployment + hypercare | 14 | 28 | Logistics + Govt portal teams available | M3 sign-off; 0 critical bugs open |
| TOTAL | 60 PD | 120 SP |
13. Sprint Plan — Phase 3 (Weeks 19–23) | 84 PD | 168 SP
| Weeks | Module | PD | SP | Owner | Acceptance Criteria |
|---|---|---|---|---|---|
| 19–22 | End-to-end conversion funnel (Lead → CoD); CPA per UTM source/campaign; source quality analytics dashboard | 22 | 44 | BE + FE | Funnel chart renders with correct drop-off per stage |
| 19–22 | Agent incentive calculation engine: configurable rules + exportable monthly report + projected month-end figure | 12 | 24 | BE | Incentive report matches manual calculation |
| 19–22 | CSAT: WhatsApp survey 2h post Pickup Done; response captured via webhook; aggregated score on Management KPI dashboard | 10 | 20 | BE + FE | CSAT survey fires 2h post Pickup Done; responses aggregated |
| 19–22 | Management KPI dashboard: conversion rate, turnaround, OCR accuracy %, on-time pickup %, CPA by channel, CSAT, uptime | 12 | 24 | FE + BE | All KPI tiles show live data from production |
| 19–22 | SLA adherence trend reporting; QA Phase 3 (regression + UAT re-run) | 12 | 24 | QA + BE | QA suite green; load test re-run passed |
| 23 | Final handover: Git source, API docs (OpenAPI 3.0), data dictionary, AWS infra runbook, role-wise user manuals (6 roles), all credentials transferred | 16 | 32 | PM + TL | All docs delivered; prod on Mariinox-owned AWS; M4 triggered |
| TOTAL | 84 PD | 168 SP |
Grand Total — All Phases: 577 + 60 + 84 = 721 Person-Days | 1,154 + 120 + 168 = 1,442 Story Points
14. Non-Functional Requirements
| Requirement | Target | Status | How Achieved |
|---|---|---|---|
| Uptime | 99.5% | Achievable | EC2 ASG + RDS Multi-AZ + ALB health checks |
| Page load P95 | < 2s | Achievable | Redis cache, DB indexing, CloudFront CDN, load tested Week 9 |
| Lead volume | 50,000+/month | Achievable | Horizontal scaling; separate web + worker + DB tiers |
| Concurrent users | 50+ min | Achievable | Stateless API + Redis sessions |
| RPO | 4 hours | Achievable | RDS automated snapshots every 4h |
| RTO | 8 hours | Achievable | Tested restore runbook delivered at handover |
| Data residency | India only | Fully met | All AWS resources in ap-south-1; S3 CRR to ap-south-2 only |
| Encryption at rest | AES-256 | Fully met | S3 SSE-KMS; RDS encryption at rest; Redis TLS |
| TLS | TLS 1.2+ | Fully met | ACM cert on ALB; HTTP → HTTPS redirect; HSTS |
| Aadhaar UIDAI | Compliant | Fully met | No raw Aadhaar in logs; field-level masking; legal review before go-live |
| Mobile responsive | Web + tablet | Partial | React Tailwind responsive; native iOS/Android separate scope |
| Audit retention | 3 years | Fully met | CloudWatch Logs 3-year policy; immutable DB table |
15. Risk Register
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Business rules not finalised by Week 2 | High | High | Vendor provides rules template in Week 1; every week delay = direct timeline slip |
| Laravel performance at 50K leads/month | Medium | High | Infra designed for horizontal scale from Week 1; load tested 2× peak in Week 9 |
| VAHAN API unavailability / rate limits | Medium | Medium | Manual entry fallback from day one; retry queue; CRM fully operable without VAHAN |
| Scope creep — new features mid-sprint | High | High | Formal change request process from contract signing; written approval + revised estimate required |
| Aadhaar UIDAI compliance gap | Medium | High | AES-256 at rest; no raw Aadhaar in logs; field-level masking; Mariinox legal review mandatory before Phase 1 go-live |
| OCR accuracy below 92% target | Medium | Medium | Manual correction workflow always available; AWS Textract performs well on Indian IDs; thresholds tuned post go-live |
| WhatsApp template pre-approval delay | Medium | Medium | Template submission begins Week 5 (Meta 1–7 days); SMS fallback active from Phase 1 go-live |
| Logistics / Portal team API delays | Medium | Medium | CRM logistics sync built against mock API in Phase 1; Phase 1 go-live not blocked |
| VAHAN credentials delayed past Week 2 | Medium | Medium | VAHAN deferred to Phase 2; manual vehicle entry only in Phase 1 |
| WhatsApp Business API account not ready by Week 7 | Medium | Medium | Notifications stubbed in Phase 1 dev; live testing and template submission delayed only |
Legend: Red = High Orange = Medium — All risks reviewed at each sprint retrospective.
Laravel 11 + React 18 + MySQL 8 + AWS ap-south-1 | 721 PD | 1,442 SP