Skip to content

Leads — Developer Guide

Reference implementation. Copy this layout for Tasks and later modules.

Backend layout

PiecePath
Modelsapp/Models/Lead.php, LeadStage, LeadNote, LeadFollowUp, LeadActivity, LeadAssignmentHistory
Enumsapp/Enums/Tenant/LeadStatusEnum, LeadPriorityEnum, LeadFollowUpStatusEnum, LeadActivityTypeEnum
Serviceapp/Services/Tenant/LeadService.php (+ ScopesToAssignee)
Exportapp/Exports/LeadsExport.php
Import frameworkapp/Import/* (ImportManager, ImportFile, ImportColumnMapper, ImportErrorWriter, ImportTemplateGenerator, ImportHistory, ImportJob)
Lead import handlerapp/Import/Lead/LeadImportHandler.php, LeadImportValidator, LeadImportMapper
Import modelapp/Models/LeadImport.php (lead_imports table)
Import jobapp/Jobs/ProcessLeadImportJob.php → queue imports
ControllersLeadController.php, LeadImportController.php
Requestsapp/Http/Requests/Tenant/Api/V1/Lead/*
Resourcesapp/Http/Resources/Tenant/Api/V1/Lead/*
Policyapp/Policies/LeadPolicy.php
Eventsapp/Events/Lead*.php
Subscriberapp/Listeners/LeadEventSubscriber.php (audit + notifications)
Notificationsapp/Notifications/Tenant/Lead/* (mail + database)
Seederdatabase/seeders/Tenant/LeadStageSeeder.php
Teststests/Feature/Tenant/Lead/LeadTest.php, LeadValidationTest.php, LeadImportTest.php

Domain notes

  • lead_value replaced estimated_value (migration rename). Store/update requests still accept estimated_value as a write alias.
  • Status is independent of stage flags (is_won / is_lost). Stage change does not sync status.
  • Convert stub: converted_at, conversion_meta (includes stub: true), status closed, activity type converted. Contacts deferred.
  • Assignee scoping via ScopesToAssignee with leads.assign (superadmin always org-wide).
  • Lead assignee eligibility (User::eligibleLeadAssignees / EligibleLeadAssignee rule): excludes suspended users, workspace owners (superadmin), and users with exclude_from_lead_auto_assign. Used by assign / create / update / bulk-assign / import column mapping, and by LeadBulkAssignmentService::eligibleAssignees for equal distribute.

Permissions

config/tenant-permissions.php:

leads.view | create | update | delete | assign | export | import | convert

Routes use module:leads then can:leads.* / policies.

Import architecture

  • Reusable package: Maatwebsite Laravel Excel for CSV/XLSX read + templates
  • Entity-agnostic app/Import framework; Lead is the first handler (future modules add their own handlers)
  • Every imported lead row uses LeadService::create() / LeadService::update() — never bypasses business rules
  • All runs are async: ProcessLeadImportJob::dispatch(...)->onQueue('imports')
  • Uploads stored on the configured uploads disk under imports/{tenant_uuid}/
  • Single table lead_imports holds file metadata, mapping, options, status, stats, and report paths
  • Platform audit: lead_import_completed / lead_import_failed

Inbound ingestion (shipped): Custom Webhook + Meta Lead Ads use the Lead Source Driver Architecture. See Custom Lead Webhook, Meta App Setup, and Meta Lead Ads. Drivers normalize only; LeadDuplicateService + LeadService remain the sole write path.

API (tenant)

Base: /api/tenant/v1 — full reference tenant-v1-leads.md.

MethodPathPermission
GET/lead-stagesview
GET/leadsview
GET/leads/statsview
GET/leads/boardview
GET/leads/exportexport
GET/leads/import/templateimport
GET/POST/leads/importsimport
GET/PUT/leads/imports/{import}import
PUT/leads/imports/{import}/optionsimport
POST/leads/imports/{import}/previewimport
POST/leads/imports/{import}/runimport
GET/leads/imports/{import}/fileimport
GET/leads/imports/{import}/failed-recordsimport
GET/leads/imports/{import}/error-reportimport
POST/leadscreate
GET/leads/{lead}view
PUT/leads/{lead}update
DELETE/leads/{lead}delete
POST/leads/{lead}/assignassign
POST/leads/{lead}/convertconvert
POST/leads/{lead}/stageupdate
POST/leads/{lead}/notesupdate
POST/leads/{lead}/follow-upsupdate
PUT/leads/{lead}/follow-ups/{followUp}update
POST/leads/{lead}/follow-ups/{followUp}/completeupdate
GET/leads/{lead}/timelineview
GET/leads/{lead}/assignment-historyview

Auth login/me include modules: string[] for SPA gating.

Frontend

PiecePath
Pagesrc/pages/leads/leads-page.tsx (board default + table)
Formlead-form-dialog.tsx
Detaillead-detail-sheet.tsx (DnD stage pending until Save)
Import wizardlead-import-dialog.tsx (5-step)
Import historylead-import-history-dialog.tsx
Shared boardsrc/components/crm/kanban-board.tsx
ServiceleadService in src/api/services.ts
Navpermission: leads.view, module: 'leads'

Tests

bash
# Backend
php artisan test --compact tests/Feature/Tenant/Lead
php artisan test --compact tests/Feature/Tenant/Lead/LeadImportTest.php

# Worker (import jobs)
php artisan queue:work --queue=imports,default

# Frontend E2E
npm run test:e2e:leads

Logging

  • Spatie LogsActivity on Lead (log name leads)
  • Domain lead_activities timeline
  • lead_assignment_histories for assignee changes
  • PlatformAuditService via LeadEventSubscriber (+ lead_import_completed / lead_import_failed)

Official documentation for the SaleOS SaaS Platform.