Slice 2: Organizations + contact↔org linking - #1
Merged
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
crm_organizationtable (002_organizationsmigration);org_idcolumn + index added tocrm_contact.org_id, then remove) so a person is never destroyed because their company was.org_idvalidated to an existing org at write (soft ref, no hard FK);get()/all()LEFT JOIN the org name; hydrate exposesorg_id+organization.crm_organizations/_get/_set/_delete, gated on the same wildcard-immunenimbuscms.crmcapability;org_idadded tocontact_set.Tests
OrganizationsTest,OrganizationsAdminTest.ContactsTest(org linking, missing-org rejected, blank unlinks, org-delete-unlinks-but-keeps) andCrmToolsetTest(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.crmcapability — a content*:writetoken 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