Skip to main content

Contributing to CRM

A short opinionated guide for anyone touching the CRM codebase. Pairs with quickstart (which covers the "how do I add an endpoint" flow) and troubleshooting (when something's wrong).

Coding standards

Backend (Laravel)

  • Strict types not required, but type-hint every public method signature.
  • Skinny controllers — validation, delegation, resource response. Anything else moves to a service or a sub-controller (see controller-split).
  • Models own state machinesLeaveRequest::TRANSITIONS, TaskHistory::ALLOWED_ACTIONS, etc. New states/types land here.
  • Use named permissions in routes — every Route::* should carry ->middleware('can:permission'). php artisan crm:list-permissions is the registry.
  • One assertion per migration intent — don't bundle unrelated schema changes.
  • Form Requests for body validation when the request has more than ~5 rules; inline $request->validate() for the rest.

Frontend (Vue 3 + Pinia)

  • Stores own snackbar dispatch via useApiError(error). Pages don't notify.success / notify.error.
  • List pages use the composable trio (useApiError in the store, useDeleteDialog + usePaginationDeepLink in the page).
  • Typed SDK only, never raw axios — call generated SDK methods with the matching crmClient (the typed client). Regenerate via npm run api:sync after a backend change.
  • i18n every string — even the small ones. useI18n is cheap.
  • <script setup lang="ts"> in every component; no Options API.

Adding a new feature

The walkthrough in quickstart is the canonical "add an endpoint" recipe. Don't skip:

  1. Route with explicit can: middleware.
  2. Controller method (thin).
  3. Resource (typed response).
  4. npm run api:sync to regenerate the SDK.
  5. Store action using useApiError.
  6. Component using the store.
  7. Test (Feature for the backend, vitest for the composable/store).

PR checklist

Before opening a PR:

# Backend
cd tenant/tenant-backend-crm
php artisan test
composer normalize
php -l app/**/*.php

# Frontend
cd tenant/tenant-frontend
npm run typecheck
npm run lint
npm run test # vitest unit tests
npm run api:check # CI gate for SDK staleness

CI runs these too; running locally just keeps the loop tight.

Commit messages

Short, imperative, no AI co-author tags. Example:

TaskController: extract assignment endpoints to TaskAssignmentController

F3 batch 15 split. 7 routes rebind, 4 methods move. Trait
EnsuresProjectMembership absorbs the shared guard. TaskController
shrinks 660 → 360 lines.

Skip "Co-Authored-By Claude" and similar — see project memory.

Migration safety

Forward-only by convention. If you need to roll back:

  • For a schema change: write a new "fix-forward" migration.
  • For an Eloquent change: revert in code and ship a patch release.

The 2026-05-12 tenant_id drop is the precedent — a coordinated forward-only schema + code change touching 46 tables.

When to split a controller

If a controller hits ~400 lines, look for an extract:

  • Sub-resource endpoints (/parent/{id}/sub-resource/*) usually move cleanly to a per-sub-resource controller.
  • Read-aggregate endpoints (stats, timeline) can split into a *StatsController.
  • Webhook / event-emit logic stays with the resource that owns the lifecycle event.

The Chat split (1 → 6 controllers) and Task split (1 → 4 controllers) are the references; see controller-split for the diagrams.

When to add a composable

If a Vue 3 pattern shows up in 3+ places:

  1. Extract into src/composables/use<Name>.ts.
  2. Co-locate a test (use<Name>.test.ts).
  3. Update the src/composables/README.md inventory.

Don't over-extract: a single-use helper stays in the calling component.

When to add an i18n key

  • Every UI string — even short ones — gets a key in hu.json + en.json. de.json is partial; CRM-namespace keys are filled, the broader Vuexy strings are deferred.
  • Notification keys live under <module>.notifications.{action}/{actionFailed}. Pattern reference in composables README.
  • Don't reuse common.* for module-specific copy — split when in doubt.

Audit-trail (ActivityLog)

When you add a compliance-relevant write endpoint:

ActivityLog::create([
'user_id' => (string) ($user['id'] ?? ''),
'action' => 'foo.event_verb',
'subject_type' => Foo::class,
'subject_id' => $foo->id,
'properties' => array_merge($this->auditActor($user), [
'previous_x' => $previous,
'new_x' => $new,
]),
'created_at' => now(),
]);

auditActor() on BaseController returns actor_user_id + the optional impersonator_id. Use it for everything that an admin might do on behalf of a user. Pattern reference: Blog/Page publish, ChatBot token regen, Project::destroy.

Test conventions

Test typeToolLocation
Pure model contractPHPUnittests/Unit/*Test.php
Container-dependentPHPUnit + Laravel TestCasetests/Feature/*Test.php
ComposableVitest + happy-domsrc/composables/*.test.ts
StoreVitest + Piniasrc/modules/*/stores/*.test.ts
E2E flowCypresscypress/e2e/

Pinning structural contracts of new controllers/traits in Unit tests catches the cheapest class of regressions — see ConcernsContractTest, ChatReactionControllerContractTest etc.

Where to ask

  • Architectural questions → architecture + the team channel
  • "How do I X" → quickstart
  • "Something's broken" → troubleshooting
  • "Why does this exist" → tenant/tenant-backend-crm/AUDIT_FINDINGS.md