Skip to content

CAP-CRM-ENT-001Establish a person’s CRM record

Markdown

CAP-CRM-ENT-001

Establish a person’s CRM record

Markdown
  • DocumentApproved
  • Product intentConfirmed
  • ImplementationObserved

KindCapabilityProduct areaCRMModuleEntitiesRevision1Approved byAndrewApproved at2026-10-22

Read atservices-crm 7ea48a41react-console da630758

The Formal Contract

The narrative above explains; the sections below are authoritative — the rules, scenarios and tests this document binds.

Outcomes
A new contact exists with its computed names, number and id, and every value entered was saved exactly once.A rejected create writes nothing and names every problem.A retried create never makes a second person.
Surfaces
consoleapi
Preconditions
The caller holds crm.contacts.create in the tenant of the path.
Postconditions
After completion, the contact can be retrieved with the values it was created with, and its number is never reused.
Intent sources
services-crm:docs/milestones/CRM-M1-contacts-and-organizations.md#M1-D1services-crm:docs/milestones/CRM-M1-contacts-and-organizations.md#M1-D2services-crm:docs/milestones/CRM-M1-contacts-and-organizations.md#M1-D3services-crm:docs/milestones/CRM-M1-contacts-and-organizations.md#M1-D4services-crm:docs/milestones/CRM-M1-contacts-and-organizations.md#PLAT-M10 D3services-crm:docs/milestones/CRM-M1-contacts-and-organizations.plan.md#A8services-crm:docs/milestones/CRM-M1-contacts-and-organizations.plan.md#A10

Controls

18 controls apply, from the corpus control catalogue.

TC-AZ-001 TC-AZ-002 TC-AZ-003 TC-AZ-004 TC-AZ-006 TC-DAT-001 TC-DAT-002 TC-DAT-003 TC-LOG-004 TC-LOG-005 TC-PI-001 TC-PI-002 TC-PI-003 TC-PRV-001 TC-PRV-002 TC-TEN-001 TC-TEN-002 TC-TEN-003

Repositories Read

RepositoryPaths
undefined:modules/entities/
undefined:modules/entities/tests/e2e/contacts-names.e2e.spec.js
undefined:modules/entities/tests/e2e/contacts.e2e.spec.js
undefined:modules/entities/tests/integration/db/contacts.table.integration.spec.js
undefined:modules/entities/tests/integration/services/contacts/contact-create.integration.spec.js
undefined:modules/entities/tests/integration/services/persistence/contacts.field-rules.integration.spec.js
undefined:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js
undefined:modules/entities/tests/integration/services/persistence/contacts.spouse.integration.spec.js
undefined:app/(console)/console/[tenantId]/crm/contacts/
undefined:acceptance/crm/playwright/journeys/contacts/CRM-ACC-0001.spec.ts
undefined:acceptance/crm/playwright/journeys/contacts/CRM-ACC-0042.spec.ts
undefined:acceptance/crm/playwright/journeys/contacts/CRM-ACC-0065.spec.ts

Where It Lives

Permissions

KeyChecked atScope
crm.contacts.createapitenant
crm.contacts.create-despite-duplicate-blockapitenant
crm.contact-types.readapitenant

Business Rules

13 rules, 13 to be proved before approval. A rule's product intent is derived from its origin: 13 confirmed by a product decision, the rest inheriting this capability's confirmed.

IdRuleKindEnforced atProvenance
01must prove
Confirmed
A contact requires a nonblank first or last name; other name components do not satisfy this requirement.
✓ Accepted example: last name "Hopper" alone is saved and sorted as "Hopper".✗ Rejected example: first name of spaces and no last name is rejected 400 naming first_name.
validationapi, service, database, consoletest
services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#answers 400 for a contact with neither a first nor a last name, naming the rule
02must prove
Confirmed
Each contact write derives its display name from first name, middle initial, quoted nickname, last name, suffix and credentials, adds the prefix for the full name, and builds the sort name from last name followed by the given names and nickname without a trailing comma.
✓ Accepted example: dr./Grace/Brewster/Amazing/Hopper/Jr/PhD gives "Grace B. "Amazing" Hopper Jr, PhD".✗ Rejected example: a last name only gives the sort name "Solo", never "Solo,".
calculationservicetest
services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#builds the full name from every part in order, trimmed and singly spaced
03must prove
Confirmed
Contact creation rejects supplied computed names and never extracts name components from a full name.
validationapitest
services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#answers 400 for a full name sent instead of name parts, and never splits it into parts
04must prove
Confirmed
Saving moves an honorific salutation into the prefix, standardizes recognized prefixes and removes a nickname that repeats the first name; Review Changes identifies those adjustments before the console saves.
✓ Accepted example: salutation "mr" is stored as prefix "Mr." with no salutation.✗ Rejected example: salutation "Dear colleague" is preserved as typed.
calculationservice, consoletest
services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#moves an honorific salutation to the prefix, start-cased and dotted (RB-2367)
05must prove
Confirmed
The service limits prefix, first, middle and last names, suffix and nickname to 200 characters, notes to 10,000 and e-mail addresses to 254, while credentials, salutation and maiden name have no equivalent service limit.
✓ Accepted example: a first name of exactly 200 characters is accepted.✗ Rejected example: notes of 10,001 characters are rejected naming notes.
validationservicetest
06must prove
Confirmed
A valid client-supplied contact id is retained if unused in the tenant; otherwise creation generates a time-ordered UUID, and another tenant may use the same id independently.
✓ Accepted example: a create naming no id returns a UUIDv7 id.✗ Rejected example: a create naming an id the tenant already uses is rejected 400.
validationapi, service, databasetest
services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#keeps the id the body named, which is no UUIDv7
07must prove
Confirmed
Contact numbers come from the tenant’s contact counter, cannot be selected by a client or reused, may have gaps after refusal, and recover collisions by taking one more than the tenant’s highest contact number.
✓ Accepted example: two creates in a row answer consecutive numbers.✗ Rejected example: a body naming number 5 returns the tenant’s next number.
calculationservice, databasetest
services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#keeps the number consumed when the transaction rolls back: a gap, never given back
08must prove
Confirmed
Repeating a contact create with the same UUIDv7 id and request returns the original contact, while reuse of that id for a different request produces a conflict.
policyservicetest
09must prove
Confirmed
Contacts may have no type, but a supplied type must belong to the tenant regardless of its active flag; an unknown type causes a conflict without saving the contact.
✓ Accepted example: a contact with no type is saved and listed as "No type".✗ Rejected example: a type id the tenant lacks is rejected 409.
validationapi, databasetest
services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 409 for a type the tenant does not have, and saves no contact
10must prove
Confirmed
Contact status defaults to active and accepts only active, inactive, deceased or duplicate; archive is a separate lifecycle action.
✓ Accepted example: status "deceased" is saved.✗ Rejected example: status "archived" is rejected 400.
validationapi, databasetest
services-crm:modules/entities/tests/integration/db/contacts.table.integration.spec.js#refuses a status outside the closed list by contacts_status_check
11must prove
Confirmed
A spouse link must identify another contact in the tenant who is not already a third contact’s spouse, is written reciprocally, and is removed from both sides when either link is cleared or either contact is deleted.
✓ Accepted example: a names B as spouse; B retrieves A as spouse with its version advanced once.✗ Rejected example: a spouse of another tenant is rejected 409.
invariantservice, databasetest
12must prove
Confirmed
A blocked contact create requires duplicate-block override authority and a reason, and a confirmation flag alone cannot authorize it.
origin: D-32 (cited in services-crm:aws/crm.service.ts:duplicateBlockOverrideAction)
policyapi, servicetest
13must prove
Confirmed
A contact photo is made permanent from a tenant upload, an existing tenant file or a public web image before creation writes the contact; failure rejects creation and identifies the photo field.
validationservicetest
services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 400 for a photo whose upload is not there, naming the field that failed

Scenarios

IdGivenWhenThenProvenance
01A tenant with a member contact type and two custom fields.A staff member creates a member with names, three e-mails, two phones, two addresses and opt-outs.The 360 and an API read agree on every value, with the computed names and the server number.test
02A request whose create timed out on the network.The client retries with the same id.It receives the contact the first attempt made and no second contact exists.test
03A contact type deactivated while a staff member is choosing it.The staff member saves.The contact is created with the now-inactive type and the chooser hides it on the next visit.test

Edge Cases

IdGivenWhenThenProvenance
04Two racing creates that meet a contact number another row holds.Both commit.Each takes a distinct number after the highest and no number is retained twice.test

Decision History

Product decisions recorded in this module's findings that bear on this specification, as recorded.

  1. OQ-CRM-ENT-003DecisionAndrew2026-10-22

    B: a rule's product intent is derived from its origin: an origin that names a product decision (a milestone decision id or an answered open question) makes the rule confirmed; otherwise the rule inherits its capability's intent. No rule-level field.

    Applied to: Structured decision update record

The Markdown files under specs/ in rhythmsoftware/rhythm-v2-docs are the canonical source of truth. This reader is a generated, read-only projection of them.