Skip to content

Slice 2: Organizations + contact↔org linking - #1

Merged
DanMat merged 1 commit into
mainfrom
feat/organizations
Sep 4, 2026
Merged

DanMat merged 1 commit into
mainfrom
feat/organizations

Conversation

@DanMat

@DanMat DanMat commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Second CRM slice: organizations (companies) and the contact→org link. Same discipline as Slice 1 (contacts) — capability-gated on every surface, store-raw/escape-on-render, bound SQL, PII-safe.

What's here

  • Schema — crm_organization table (002_organizations migration); org_id column + index added to crm_contact.
  • Organizations service — allow-listed save (name required, length caps), bound + wildcard-escaped name search, and a delete that unlinks its contacts in one transaction (NULL org_id, then remove) so a person is never destroyed because their company was.
  • Contacts — org_id validated to an existing org at write (soft ref, no hard FK); get()/all() LEFT JOIN the org name; hydrate exposes org_id + organization.
  • MCP — crm_organizations / _get / _set / _delete, gated on the same wildcard-immune nimbuscms.crm capability; org_id added to contact_set.
  • Admin — Organizations page (search/list/create/edit/delete) + an org dropdown on the contact form and an Organization column in the list.
  • Guide — organizations section.

Tests

  • New: OrganizationsTest, OrganizationsAdminTest.
  • Extended: ContactsTest (org linking, missing-org rejected, blank unlinks, org-delete-unlinks-but-keeps) and CrmToolsetTest (org tools listed, content token can't reach them, read token can't write, round-trip, name-required-as-data).

Security posture (unchanged from Slice 1, extended to orgs)

Every org surface gates on the wildcard-immune nimbuscms.crm capability — a content *:write token can't see or call any org tool. Values stored raw, escaped on render (XSS-on-render tests). Bound + wildcard-escaped search. No public surface.

cs-fixer + PHPStan level 6 green locally (borrowed-vendor); phpunit runs in CI.

🤖 Generated with Claude Code

Adds the second CRM entity — organizations (companies) — and the soft
contact→org link, on the same discipline as contacts.

- Schema: crm_organization table (002_organizations migration); org_id
  column + index on crm_contact (added directly to Schema::contacts()
  since there are zero installs to migrate).
- Organizations service: allow-listed save (name required, length caps),
  bound + wildcard-escaped name search, and a delete that unlinks its
  contacts in one transaction (NULL org_id, then remove) — a person is
  never destroyed because their company was.
- Contacts: org_id validated to an existing org at write (soft ref, no
  hard FK); get()/all() LEFT JOIN the org name; hydrate exposes org_id +
  organization.
- MCP: crm_organizations / _get / _set / _delete tools, gated on the same
  wildcard-immune nimbuscms.crm capability; org_id added to contact_set.
- Admin: Organizations page (search/list/create/edit/delete) and an
  organization dropdown on the contact form + column in the list.
- Guide: organizations section.
- Tests: OrganizationsTest, OrganizationsAdminTest; ContactsTest and
  CrmToolsetTest extended for org linking, org-delete-unlinks, and the
  org tools' capability gating.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@DanMat
DanMat merged commit 6120a27 into main Sep 4, 2026
2 checks passed
@DanMat
DanMat deleted the feat/organizations branch September 4, 2026 15:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant