feat(contacts): reject duplicate company names per tenant #296

Merged
vas2000-work merged 2 commits from feat/PMS-400-reject-duplicate-company-names into main 2026-06-18 00:53:28 +02:00
Owner

Companies had no uniqueness on name within a tenant: the only indexes were the non-unique idx_companies_name and a trigram index, and ContactService::create_company / update_company performed no existence check. Two companies with the identical name could coexist in one tenant, producing ambiguous pickers and confusing list/search results.

Enforce tenant-scoped, case-insensitive, trimmed name uniqueness in two layers. ContactService::create_company runs SELECT EXISTS on lower(btrim(name)) inside its transaction before the INSERT and returns AppError::Conflict on a hit; update_company runs the same check (excluding the company's own id) only when the request sets a name, so re-saving an unchanged name still succeeds. Migration 054 adds the matching UNIQUE INDEX on (tenant_id, lower(btrim(name))) as the concurrency-safe backstop, and both mutations map a 23505 violation on that index to the same friendly 409 instead of a 500.

Contacts are intentionally out of scope: a real company routinely has multiple contacts sharing a name, so contact-name uniqueness is not a valid invariant. PMS-325 field validation is untouched.

#PMS-400

Companies had no uniqueness on name within a tenant: the only indexes were the non-unique idx_companies_name and a trigram index, and ContactService::create_company / update_company performed no existence check. Two companies with the identical name could coexist in one tenant, producing ambiguous pickers and confusing list/search results. Enforce tenant-scoped, case-insensitive, trimmed name uniqueness in two layers. ContactService::create_company runs SELECT EXISTS on lower(btrim(name)) inside its transaction before the INSERT and returns AppError::Conflict on a hit; update_company runs the same check (excluding the company's own id) only when the request sets a name, so re-saving an unchanged name still succeeds. Migration 054 adds the matching UNIQUE INDEX on (tenant_id, lower(btrim(name))) as the concurrency-safe backstop, and both mutations map a 23505 violation on that index to the same friendly 409 instead of a 500. Contacts are intentionally out of scope: a real company routinely has multiple contacts sharing a name, so contact-name uniqueness is not a valid invariant. PMS-325 field validation is untouched. #PMS-400
vas2000-work force-pushed feat/PMS-400-reject-duplicate-company-names from 159ce14d06
Some checks failed
E2E / Playwright against staging (pull_request) Successful in 58s
Check / fmt + clippy + compile + unit/doc tests (pull_request) Successful in 1m49s
Integration / integration tests (pull_request) Failing after 3m10s
to 11fafc0dfd
All checks were successful
E2E / Playwright against staging (pull_request) Successful in 40s
Check / fmt + clippy + compile + unit/doc tests (pull_request) Successful in 1m2s
Integration / integration tests (pull_request) Successful in 3m33s
Create release / Create release from merged PR (pull_request) Has been skipped
2026-06-18 00:44:22 +02:00
Compare
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/mokosh-server!296
No description provided.