Skip to main content

CRM Service

Vecton's CRM backend owns the per-tenant operational data: contacts, companies, projects, tasks, time entries, knowledge base, blog/page content, internal chat, document folders, SLA policies, shifts, and leave requests. Every CRM instance is bound to one tenant via the TENANT_ID env (per-tenant deployment); there is no tenant_id column on CRM tables — see the multi-tenant note below.

Service surface

RepositoryTechRole
tenant/tenant-backend-crmLaravel 12 + PHP 8.4Per-tenant CRM API
tenant/tenant-frontend (CRM module)Vue 3 + Vuetify + PiniaUI
mobile-appReact Native + ExpoMobile entry points

The CRM does NOT issue tokens — it relies on the identity backend's OAuth and validates tokens via VerifyOAuthToken middleware. Per-action authorisation runs through CheckPermission (route-level can: middleware) plus the per-resource policies in App\Policies\Crm\*.

Refactor state (2026-05-15)

The audit + refactor was scoped as F0 through F10 (mirroring identity). F0–F9 are landed (F9 polish pass added permissions, webhook-events, and tenant-clone docs on 2026-05-15); F10 UI/UX deep review remains deferred.

PhaseThemeStatus
F0Security KRITIKUS (tenant isolation, SQL injection, soft-deletes, file uploads, ChatBot rate-limit, identity transient failures)done
F1Frontend cleanup (Vuexy bloat, hardcoded strings)partial
F2Mock + test environment, OpenAPI regenerationpartial — npm run api:check added
F3Controller bontásdeferred
F4Menu + i18n teljességpartial — de.json CRM-namespace filled, notification keys ported in 12+ stores
F5OpenAPI + SDKdone — Scramble + @hey-api/openapi-ts + drift-detect
F6Backend security & qualitymostly done — 30+ MAGAS fixed
F7Hiányzó funkciók (RabbitMQ event consumers, audit trail)done — IdentityEventConsumer + ActivityLog publish events
F8Tesztekscaffolded — vitest set up, first composable tests in src/composables/*.test.ts
F9Documentationthis doc + parallel sections
F10UI/UX deep review (21 CRM oldal)deferred

The full finding list and per-batch close log lives in tenant/tenant-backend-crm/AUDIT_FINDINGS.md.

Multi-tenant model

CRM is deployed once per tenant. Each deployment:

  • Is bound to one tenant via TENANT_ID (env, never request-derived)
  • Has its own database (no tenant_id discriminator column on CRM tables — that column was dropped on 2026-05-12 as redundant under the per-deployment model)
  • Talks to the central identity service over HTTP for token validation + policy fetch
  • Subscribes to a per-tenant RabbitMQ queue crm.identity-events.{tenant_id} for cross-cluster identity lifecycle events

config('app.tenant_id') is the source of truth on the backend, and the identity-side tenants[] JWT claim (post F-future-6) is what CheckPermission cross-checks against. A token whose tenants[] does not include the bound tenant is rejected with 403 tenant_not_a_member.

Identity event integration

The CRM subscribes to identity's cross-cluster events on the vecton.events exchange (RabbitMQ). Five event types are consumed:

EventHandlerAction
identity.user.scheduled_for_deletionIdentityEventHandler::onUserScheduledForDeletionTaskUser pivot drop, sole-assigned tasks → unassigned, TenantUser → is_active=false
identity.user.updatedonUserUpdatedMirror name/email/locale/timezone into local TenantUser cache
identity.tenant.member_addedonTenantMemberAddedLog only (TenantUser is lazy-loaded)
identity.tenant.member_removedonTenantMemberRemovedTenantUser → is_active=false
identity.policy.updatedonPolicyUpdatedFlush tagged policy cache (crm.policies + tenant:{tid})

Worker: php artisan crm:consume-identity-events. Each handler is idempotent (RabbitMQ retries are allowed).

Permission model

  • Route-level can:permission middleware gates every endpoint.
  • The permission name follows tenant.{tenant_name}.crm.{namespace}.{action} after CheckPermission builds the FQN.
  • Policies inherit from App\Policies\BasePolicy, child classes register a namespacePrefix(). Currently only TaskPolicy exists; Gate::authorize('view', $task) is the recommended controller entry-point for resource-bound checks (sample in TaskController::show / update / destroy).
  • Identity is the single source of policy truth; CRM never persists policy rows.
  • Run php artisan crm:list-permissions --json to dump every permission the routes reference (CI drift-detect against identity's PolicySeeder).

Cache strategy

  • policies:{tenantId}:{userId}:{sha256(token)[:12]} — 1h TTL, tagged (crm.policies, tenant:{tid}). Token-keyed suffix means token rotation lands on a fresh slot; tag flush on identity.policy.updated.
  • identity.user:{sha256(token)} — 60s TTL. Eliminates duplicate /api/user round-trips during XHR bursts.

Notable patterns

  • Notification snackbar: stores call useApiError(error) from src/composables/useApiError.ts and emit notifySuccess('module.notifications.created') on success / notifyError(err, 'module.notifications.createFailed') on failure. The composable centralises i18n fallback + Pinia notification dispatch.
  • Delete dialog: useDeleteDialog<T>(deleter) from the same composables dir collapses the open/close/confirm triad. Adopted in companies/index.vue and contacts/index.vue as reference.
  • Diacritic-insensitive search: App\Support\Search::whereLike() wraps Postgres unaccent(lower(...)) LIKE; sqlite/mysql fall back to ilike. Used in Contact, Company, Task, Document, KbArticle scopeSearch.
  • PolicyDTO + PolicyEvaluator: identity policies arrive as JSON, App\DTO\PolicyDTO::fromArray normalises the shape (action wildcards, resource scoping, conditions), and App\Services\PolicyEvaluator runs deny-overrides combining. See the unit tests pinned in testing.md.
  • Cross-tenant clone: App\Support\TenantClone powers POST /api/system/clone-from for fresh-tenant seeding. Idempotent (24h replay window), audited, system-token only. See tenant-clone.md.
  • Audit trail: ActivityLog::create([...]) for compliance events (Blog/Page publish/unpublish so far). BaseController::auditActor($authUser) adds actor_user_id + impersonator_id to the properties blob.
  • State machines: LeaveRequest::TRANSITIONS + canTransitionTo() instead of ad-hoc === 'pending' checks.

Where to look next

  • Audit findings → tenant/tenant-backend-crm/AUDIT_FINDINGS.md
  • Identity↔CRM event contract → documentations/docs/identity/events.md
  • Cross-cutting tenant policy pattern → documentations/docs/identity/tenant-policies.md