Skip to content

Companies — Developer Guide ​

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

Backend layout ​

PiecePath
Modelsapp/Models/Company.php, CompanyNote, CompanyActivity, CompanyFollowUp, CompanyImport
Enumapp/Enums/Tenant/CompanyActivityTypeEnum, FollowUpStatusEnum (shared with Contacts), CompanyImportStatusEnum
Serviceapp/Services/Tenant/CompanyService.php (+ ScopesToAssignee), CompanyIdentityMatcher, CompanyMergeService
Exportapp/Exports/CompaniesExport.php
Import frameworkapp/Import/* (shared with Leads/Contacts)
Company import handlerapp/Import/Company/CompanyImportHandler.php, CompanyImportValidator, CompanyImportMapper, app/Import/CompanyImportHistory.php, app/Import/CompanyImportManager.php
Import jobapp/Jobs/ProcessCompanyImportJob.php → queue imports
Controllerapp/Http/Controllers/Tenant/Api/V1/CompanyController.php, CompanyImportController.php
Requestsapp/Http/Requests/Tenant/Api/V1/Company/* (incl. Store/UpdateCompanyFollowUpRequest, Upload/UpdateCompanyImport*Request)
Resourcesapp/Http/Resources/Tenant/Api/V1/Company/* (incl. CompanyFollowUpResource, CompanyImportResource)
Policyapp/Policies/CompanyPolicy.php
Eventsapp/Events/Company*.php (incl. CompanyFollowUpCreated, CompanyFollowUpCompleted)
Subscriberapp/Listeners/CompanyEventSubscriber.php (audit + assignment/follow-up notification)
Notificationsapp/Notifications/Tenant/Company/CompanyAssignedNotification.php, CompanyFollowUpCreatedNotification.php, CompanyFollowUpDueNotification.php
Placeholdersapp/Services/Tenant/CommunicationTemplates/Providers/CompanyPlaceholderProvider.php
Due remindersapp/Console/Commands/SendCrmDueNotificationsCommand.php::notifyCompanyFollowUps() (crm:send-due-notifications, daily; entitlement-gated)
Factoriesdatabase/factories/CompanyFollowUpFactory.php, CompanyImportFactory.php
Teststests/Feature/Tenant/Company/CompanyTest.php, CompanyFollowUpTest.php, CompanyImportTest.php, CompanyExportTest.php; tests/Helpers/CompanyImport.php

Domain notes ​

  • Assignee scoping via ScopesToAssignee with companies.assign; without it, users only see companies assigned to them (view/update/list/stats).
  • companies.force.delete is not granted to any default role — owner/superadmin only, matching Leads/Tasks/Contacts.
  • Contact → Company linkage: contacts.company_id (nullable FK). ContactService resolves writes so that when company_id is set, the legacy company string is synced to the linked Company name. List/detail resources expose linked_company (id, uuid, name) when loaded.
  • Assignee eligibility mirrors Leads/Contacts (EligibleCompanyAssignee / User::isEligibleLeadAssignee).
  • Soft delete only — no stage/status workflow.
  • Party billing hub: same pattern as Contacts (CustomerPartyBillingPanel with partyKind: 'company'). Backend reuses customer statement services scoped by company_id. List deep links ?company= on invoices, payments, quotations, credit notes. Statement includes opening_balance + balance_due; PDF available.
  • Related records hub (1.3.0): CustomerPartyRelatedHub with partyKind: 'company' — opportunities / help-desk / projects / documents + ?company= list deep links.
  • Follow-ups (1.4.0): company_follow_ups (company_id, assigned_to nullable FK → users, title, notes, due_at, completed_at, status). Shared FollowUpStatusEnum (pending | completed | cancelled) — same simple lifecycle as Contacts; no tag-driven auto/force-follow-up. CompanyService::createFollowUp() / updateFollowUp() / completeFollowUp() default assigned_to to the company's assignee, denormalize companies.next_follow_up_at (earliest pending due_at), and fire CompanyFollowUpCreated / CompanyFollowUpCompleted (audit + assignee notification on create, audit-only on complete).
  • Calendar projection: CompanyService::projectCalendar() upserts a CalendarEvent (source_type='company', source_id, CalendarEventSourceEnum::Company) titled after the company, 1 hour starting at next_follow_up_at, organizer = assignee (falls back to actor). Entitlement-gated on calendar; clears the projection when next_follow_up_at is null / no organizer, and on company delete.
  • Due/overdue reminders: SendCrmDueNotificationsCommand::notifyCompanyFollowUps() (per-tenant, gated by EntitlementService::hasModule('companies')) mirrors the Contact reminder path; notifies via CompanyFollowUpDueNotification, idempotent per user/day (due|overdue:company_follow_up:{id}:{date}).
  • Import (1.4.0, identity match 1.5.0): CompanyImportHandler fields: name (required), email, phone, website, industry, source, assigned_to (resolved by email, validated via EligibleCompanyAssignee). Optional duplicate detection still matches email or phone from unique_fields; modes skip | update | keep (ImportDuplicateModeEnum). Before that, CompanyIdentityMatcher reuses a company with the same normalized_name or the same website/email domain (consumer inboxes such as gmail.com are ignored). Preview adds matched_existing_companies and new_companies. Contact import find-or-creates one company per normalized name when Companies is installed and the actor has companies.create. Lead conversion uses the same key. Every created row still goes through CompanyService::create() / update(). No assignment_mode. Single table company_imports; async via ProcessCompanyImportJob::dispatch(...)->onQueue('imports'). Platform audit: company_import_completed / company_import_failed.
  • Merge (1.5.0): CompanyMergeService behind POST /companies/{id}/merge-preview and POST /companies/{id}/merge, gated by companies.delete. One transaction moves company_id foreign keys (contacts, notes, follow-ups, company activity, CRM activities, opportunities, quotations, estimates, invoices, payments, credit notes, projects, help desk), document and email morphs, calendar source_id, and Spatie activity subjects. Leads and unlinked contacts whose company text normalizes to a duplicate are rewritten to the primary name. Primary field values win; only blank email/phone/website/industry/address/source are filled. Activity type merged. Duplicates are soft-deleted. normalized_name is indexed with tenant_id and is not unique until operators merge existing duplicates. List rows include possible_duplicates_count; show includes possible_duplicates.
  • Export (1.4.0): CompaniesExport streams CSV or a minimal hand-rolled XLSX of the current filtered/scoped query — id, name, email, phone, website, industry, source, assignee, creator, contacts count, next_follow_up_at, created_at.

Permissions ​

config/tenant-permissions.php:

companies.view | create | update | delete | restore | force.delete | assign | export | import

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

API (tenant) ​

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

MethodPathPermission
GET/companiesview
GET/companies/statsview
GET/companies/{company}view
GET/companies/{company}/timelineview
POST/companiescreate
PUT/companies/{company}update
DELETE/companies/{company}delete
POST/companies/{company}/restorerestore
DELETE/companies/{company}/forceforce.delete
POST/companies/{company}/assignassign
POST/companies/{company}/notesupdate
GET/companies/{company}/billing-summaryview
GET/companies/{company}/statementview
GET/companies/{company}/statement.pdfview
POST/companies/{company}/follow-upsupdate
PUT/companies/{company}/follow-ups/{followUp}update
POST/companies/{company}/follow-ups/{followUp}/completeupdate
GET/companies/exportexport
GET/companies/import/templateimport
GET/POST/companies/importsimport
GET/PUT/companies/imports/{import}import
PUT/companies/imports/{import}/optionsimport
POST/companies/imports/{import}/previewimport
POST/companies/imports/{import}/runimport
GET/companies/imports/{import}/fileimport
GET/companies/imports/{import}/failed-recordsimport
GET/companies/imports/{import}/error-reportimport

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

Frontend ​

PiecePath
Pagesrc/pages/companies/companies-page.tsx (table + filters + KPIs)
Formcompany-form-dialog.tsx
Detailcompany-view-page.tsx (details, notes, activity, follow-ups; billing hub; related hub)
List sheetcompany-record-sheet.tsx + shared ModuleRecordSheet (peek overview incl. next follow-up)
Statementsrc/pages/crm/party-statement-page.tsx (CompanyStatementPage)
Import wizardcompany-import-dialog.tsx (5-step: upload → map → options → preview → run)
Import historycompany-import-history-dialog.tsx
ServicecompanyService in src/api/services.ts (billingSummary, statement, downloadStatementPdf, export, importTemplate, listImports, uploadImport, getImport, updateImportMapping, updateImportOptions, createFollowUp, updateFollowUp, completeFollowUp)
Notification registrysrc/notifications/modules/companies.ts (follow-up created/due)
Navpermission: companies.view, module: 'companies' (between Leads and Contacts)
DashboardRecentCompaniesWidget (recent_companies widget) + create_company quick action in tenant-dashboard-widgets.tsx / tenant-dashboard-page.tsx
Contact linkcontact-form-dialog.tsx company picker when module:companies + companies.view; list/detail show linked_company?.name || company
Party billingHub + deep links + statement; shared Pest PartyBilling suite; Playwright company case in contacts.party-billing.spec.ts
Catalog1.4.0 (follow-ups + import/export)

Tests ​

bash
# Backend
php artisan test --compact tests/Feature/Tenant/Company/CompanyTest.php
php artisan test --compact tests/Feature/Tenant/Company/CompanyFollowUpTest.php
php artisan test --compact tests/Feature/Tenant/Company/CompanyDuplicateTest.php
php artisan test --compact tests/Feature/Tenant/Company/CompanyImportTest.php
php artisan test --compact tests/Feature/Tenant/Company/CompanyExportTest.php
php artisan test --compact tests/Feature/Tenant/PartyBilling/PartyBillingSummaryAndStatementTest.php

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

# Frontend
npm run typecheck && npm run lint && npm run build
npm run test:e2e:companies
npm run test:e2e:companies:headed
SuiteLocation
Pesttests/Feature/Tenant/Company/CompanyTest.php, CompanyFollowUpTest.php, CompanyImportTest.php, CompanyDuplicateTest.php, CompanyExportTest.php; PartyBilling suite
E2Ee2e/tests/companies/, including companies.duplicate-merge.headed.spec.ts, companies.follow-ups.spec.ts, companies.import.spec.ts; company hub smoke in contacts.party-billing.spec.ts

Logging ​

  • Spatie LogsActivity on Company (log name companies)
  • Domain company_activities timeline
  • PlatformAuditService via CompanyEventSubscriber

Intentional differences from Leads / Tasks ​

Leads / TasksCompanies
Stages / status workflowNo workflow — directory record
Follow-ups: tag-driven auto/force-follow-up, LeadFollowUpStatusEnumFollow-ups (1.4.0) are plain create/reschedule/complete on shared FollowUpStatusEnum; no tags, no forced follow-up
Import assignment_mode (none | equal department distribute)Import (1.4.0) has no assignment_mode — assigned_to column or manual-create default only
Board viewList/table only
convert / completeNone

Deferred ​

  • Legacy contact company string → Company backfill job
  • Meta invent Companies

Official documentation for the EloSync SaaS Platform.