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
| Repository | Tech | Role |
|---|---|---|
tenant/tenant-backend-crm | Laravel 12 + PHP 8.4 | Per-tenant CRM API |
tenant/tenant-frontend (CRM module) | Vue 3 + Vuetify + Pinia | UI |
mobile-app | React Native + Expo | Mobile 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.
| Phase | Theme | Status |
|---|---|---|
| F0 | Security KRITIKUS (tenant isolation, SQL injection, soft-deletes, file uploads, ChatBot rate-limit, identity transient failures) | done |
| F1 | Frontend cleanup (Vuexy bloat, hardcoded strings) | partial |
| F2 | Mock + test environment, OpenAPI regeneration | partial — npm run api:check added |
| F3 | Controller bontás | deferred |
| F4 | Menu + i18n teljesség | partial — de.json CRM-namespace filled, notification keys ported in 12+ stores |
| F5 | OpenAPI + SDK | done — Scramble + @hey-api/openapi-ts + drift-detect |
| F6 | Backend security & quality | mostly done — 30+ MAGAS fixed |
| F7 | Hiányzó funkciók (RabbitMQ event consumers, audit trail) | done — IdentityEventConsumer + ActivityLog publish events |
| F8 | Tesztek | scaffolded — vitest set up, first composable tests in src/composables/*.test.ts |
| F9 | Documentation | this doc + parallel sections |
| F10 | UI/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_iddiscriminator 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:
| Event | Handler | Action |
|---|---|---|
identity.user.scheduled_for_deletion | IdentityEventHandler::onUserScheduledForDeletion | TaskUser pivot drop, sole-assigned tasks → unassigned, TenantUser → is_active=false |
identity.user.updated | onUserUpdated | Mirror name/email/locale/timezone into local TenantUser cache |
identity.tenant.member_added | onTenantMemberAdded | Log only (TenantUser is lazy-loaded) |
identity.tenant.member_removed | onTenantMemberRemoved | TenantUser → is_active=false |
identity.policy.updated | onPolicyUpdated | Flush 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:permissionmiddleware gates every endpoint. - The permission name follows
tenant.{tenant_name}.crm.{namespace}.{action}afterCheckPermissionbuilds the FQN. - Policies inherit from
App\Policies\BasePolicy, child classes register anamespacePrefix(). Currently onlyTaskPolicyexists;Gate::authorize('view', $task)is the recommended controller entry-point for resource-bound checks (sample inTaskController::show / update / destroy). - Identity is the single source of policy truth; CRM never persists policy rows.
- Run
php artisan crm:list-permissions --jsonto dump every permission the routes reference (CI drift-detect against identity'sPolicySeeder).
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 onidentity.policy.updated.identity.user:{sha256(token)}— 60s TTL. Eliminates duplicate/api/userround-trips during XHR bursts.
Notable patterns
- Notification snackbar: stores call
useApiError(error)fromsrc/composables/useApiError.tsand emitnotifySuccess('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 incompanies/index.vueandcontacts/index.vueas reference. - Diacritic-insensitive search:
App\Support\Search::whereLike()wraps Postgresunaccent(lower(...))LIKE; sqlite/mysql fall back toilike. Used inContact,Company,Task,Document,KbArticlescopeSearch. - PolicyDTO + PolicyEvaluator: identity policies arrive as JSON,
App\DTO\PolicyDTO::fromArraynormalises the shape (action wildcards, resource scoping, conditions), andApp\Services\PolicyEvaluatorruns deny-overrides combining. See the unit tests pinned in testing.md. - Cross-tenant clone:
App\Support\TenantClonepowersPOST /api/system/clone-fromfor 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)addsactor_user_id+impersonator_idto 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