Skip to content

Contacts — Developer Guide

Mirror of the Leads developer guide / Tasks developer guide. Prefer copying those patterns over inventing new ones.

Backend layout

PiecePath
Modelsapp/Models/Contact.php, ContactNote, ContactActivity
Enumapp/Enums/Tenant/ContactActivityTypeEnum, ContactLifecycleStatusEnum
Serviceapp/Services/Tenant/ContactService.php (+ ScopesToAssignee)
Controllerapp/Http/Controllers/Tenant/Api/V1/ContactController.php
Requestsapp/Http/Requests/Tenant/Api/V1/Contact/*
Resourcesapp/Http/Resources/Tenant/Api/V1/Contact/*
Policyapp/Policies/ContactPolicy.php
Eventsapp/Events/Contact*.php
Subscriberapp/Listeners/ContactEventSubscriber.php (audit + assignment notification)
Notificationsapp/Notifications/Tenant/Contact/ContactAssignedNotification.php
Placeholdersapp/Services/Tenant/CommunicationTemplates/Providers/ContactPlaceholderProvider.php
Teststests/Feature/Tenant/Contact/ContactTest.php

Domain notes

  • lifecycle_status (on_boarded | off_boarded) is independent of soft-delete (deleted_at / trashed filters). SPA labels use On Boarded Clients / Off Boarded Clients.
  • Assignee scoping via ScopesToAssignee with contacts.assign; without it, users only see contacts assigned to them (view/update/list/stats).
  • contacts.force.delete is not granted to any default role — owner/superadmin only, matching Leads/Tasks.
  • Lead → Contact linkage: leads.contact_id (nullable FK). LeadService::convert() creates (or reuses) a Contact when the contacts module is entitled (requires contacts.create, preserves lead assignee, sets lifecycle on_boarded, transactional). When Companies is entitled and the lead has a company name, also creates/links a Company onto the Contact. Optional Opportunity creation uses the same convert endpoint (create_opportunity). Stub converts without contact_id can be completed after Contacts is installed. Otherwise conversion remains the earlier status-only placeholder for contacts (conversion_meta.stub = true).
  • Contact → Company linkage: contacts.company_id (nullable FK) when Companies is entitled. Writes sync the legacy company string from the linked Company name. Resources expose linked_company when loaded.
  • SPA: Contact create/edit can open create-company-dialog.tsx (companies.create) and auto-select the new company_id without navigating to Companies.
  • Sales prefill: Quotation / invoice / payment create forms accept ?contact= / ?company= on create only (src/lib/related-record-query.ts).
  • Party billing hub: CustomerPartyBillingPanel on contact view — summary strip + recent invoices/payments/credit notes + statement route. Backend: CustomerPartyBillingSummaryService, CustomerAccountStatementService (+ PDF). List deep links ?contact= / ?company= on invoices, payments, quotations, credit notes.
  • Statement JSON includes opening_balance (pre-from) and balance_due (as of to). Credits on statements are applied only.
  • Assignee eligibility mirrors Leads (EligibleContactAssignee / User::isEligibleLeadAssignee).

Permissions

config/tenant-permissions.php:

contacts.view | create | update | delete | restore | force.delete | assign

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

API (tenant)

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

MethodPathPermission
GET/contactsview
GET/contacts/statsview
GET/contacts/{contact}view
GET/contacts/{contact}/timelineview
POST/contactscreate
PUT/contacts/{contact}update
DELETE/contacts/{contact}delete
POST/contacts/{contact}/restorerestore
DELETE/contacts/{contact}/forceforce.delete
POST/contacts/{contact}/assignassign
POST/contacts/{contact}/notesupdate
GET/contacts/{contact}/billing-summaryview
GET/contacts/{contact}/statementview
GET/contacts/{contact}/statement.pdfview

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

Frontend

PiecePath
Pagesrc/pages/contacts/contacts-page.tsx (table + filters + KPIs)
Formcontact-form.tsx (+ create-company-dialog.tsx for inline company create)
Detailcontact-view-page.tsx (details, notes, activity; billing hub; related sales create actions)
Statementsrc/pages/crm/party-statement-page.tsx (ContactStatementPage)
ServicecontactService in src/api/services.ts (billingSummary, statement, downloadStatementPdf)
Navpermission: contacts.view, module: 'contacts' (between Leads and Tasks)
DashboardRecentContactsWidget (recent_contacts widget) + create_contact quick action in tenant-dashboard-widgets.tsx / tenant-dashboard-page.tsx
Lead linkLead record shows a View contact link when a converted lead has contact_id
Company linkContact form company picker when module:companies + companies.view; New when companies.create; list/detail prefer linked_company?.name over legacy company
Sales prefillQuotation / invoice / payment create forms accept ?contact= / ?company=
Party billingHub + deep links + statement; Pest tests/Feature/Tenant/PartyBilling/; Playwright e2e/tests/contacts/contacts.party-billing.spec.ts

Tests

bash
# Backend
php artisan test --compact tests/Feature/Tenant/Contact/ContactTest.php
php artisan test --compact tests/Feature/Tenant/PartyBilling/PartyBillingSummaryAndStatementTest.php

# Frontend
npm run typecheck && npm run lint && npm run build
npm run test:e2e:contacts
SuiteLocation
Pesttests/Feature/Tenant/Contact/ContactTest.php (+ Lead convert cases); PartyBilling suite
E2Ee2e/tests/contacts/, including contacts.party-billing.spec.ts

Logging

  • Spatie LogsActivity on Contact (log name contacts)
  • Domain contact_activities timeline
  • PlatformAuditService via ContactEventSubscriber

Intentional differences from Leads / Tasks

Leads / TasksContacts
Stages / status workflowNo workflow — directory record
Follow-upsNone
Board viewList/table only
export / convert / completeNone (Contacts is the conversion target, not source)

Official documentation for the EloSync SaaS Platform.