Skip to content

CAP-CRM-ENT-004Save corrections without overwriting another change

Markdown

CAP-CRM-ENT-004

Save corrections without overwriting another change

Markdown
  • DocumentApproved
  • Product intentProposed
  • ImplementationInconsistent

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 stale save is rejected and says who changed the record.A save that changes nothing leaves no trace.A rejected save leaves every row as it was.
Surfaces
consoleapi
Intent sources
services-crm:docs/milestones/CRM-M1-contacts-and-organizations.md#D-05services-crm:docs/milestones/CRM-M1-contacts-and-organizations.md#M1-D5services-crm:docs/milestones/CRM-M1-contacts-and-organizations.md#M1-D6services-crm:docs/milestones/CRM-M1-contacts-and-organizations.plan.md#A4services-crm:docs/milestones/CRM-M1-contacts-and-organizations.plan.md#A5services-crm:docs/milestones/CRM-M1-contacts-and-organizations.plan.md#A7services-crm:docs/milestones/CRM-M1-contacts-and-organizations.plan.md#A9services-crm:docs/milestones/CRM-M1-contacts-and-organizations.plan.md#A14services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.md#D-09

Controls

17 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-003 TC-PRV-001 TC-PRV-002 TC-TEN-001 TC-TEN-002 TC-TEN-003

Repositories Read

RepositoryPaths
undefined:docs/entities-api.md
undefined:docs/milestones/
undefined:modules/entities/
undefined:acceptance/crm/

Where It Lives

Permissions

KeyChecked atScope
crm.contacts.updateapitenant
crm.organizations.updateapitenant
crm.contacts.unlockservicetenant
crm.organizations.unlockservicetenant

Business Rules

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

IdRuleKindEnforced atProvenance
01must prove
Confirmed
A replacement supplies the read version and a patch begins by testing it; omission is rejected at sys_version, and a stale version produces a conflict identifying the latest person or system change.
✓ Accepted example: a patch led by test /sys_version at the current version is applied and advances it once.✗ Rejected example: two patches at the same version: the second is rejected 409 and the first writer's value is preserved.
validationapi, servicetest
services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 400 naming sys_version to a patch that tests no version
02must prove
Confirmed
An unchanged save preserves version, modification time and history, but still rejects a stale version.
invariantservicetest
03must prove
Proposed
A patch modifies the stored record before validating the whole result, preserves unnamed properties and clears nullable values when explicitly set to null.
policyapi, servicetest
04must prove
Confirmed
Replacement clears omitted editable scalar values and omitted e-mail, phone, address and communication-preference collections while retaining server-owned data and collections governed by explicit preservation rules.
✓ Accepted example: a replacement without job_title clears it.✗ Rejected example: a replacement without addresses removes every address.
origin: Behaviour at the pinned commit; superseded by BR-CAP-CRM-ENT-004-13 (OQ-CRM-ENT-001 decided 2026-10-22: A)
policyservicetest
05must prove
Proposed
An explicit collection preserves and versions changed elements by their own ids, creates idless elements, removes omitted elements, rejects foreign or duplicate ids and allows elements to exchange types or preferred flags in one save.
invariantapi, service, databasetest
services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#moves the second address into the type the first left, and the preferred flag with it
06must prove
Confirmed
Round-trip writes accept unchanged contact names, numbers, attribution, locks and origin values but reject changes to those read-only values, ignore derived organization ancestry and compliance values, and reject a conflicting tenant id.
validationapi, servicetest
services-crm:modules/entities/tests/integration/services/persistence/contacts.field-rules.integration.spec.js#refuses a replacement whose contact number differs from the stored one, naming number
07must prove
Proposed
A failed contact or organization save leaves the record, its elements, custom values and affiliations at their previous state regardless of the failing validation, constraint, file, lock or version check.
invariantservice, databasetest
08must prove
Proposed
Validation refusals collect the problems detected in one response and identify each problem’s field path.
validationservicetest
09must prove
Confirmed
Input strings are trimmed, blank optional values become empty, blank required values are rejected and blank collection elements are omitted.
calculationapi, databasetest
services-crm:modules/entities/tests/unit/api/contact-api-mapper.spec.js#drops a blank e-mail element rather than refusing the null the string standard made of it
10must prove
Confirmed
System changes to spouse links, regions or compliance advance the record version and make an open editor’s next save conflict with attribution to the system.
policyservice, consoletest
services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 409 naming the system, not a colleague, when the system cleared the spouse link while the contact was edited (M1-D6)
11must prove
Proposed
The contact editor reviews changes before sending a version-tested patch of changed fields and offers reload or comparison after a conflict without silently accepting a newer version through Keep Editing.
policyconsoletest
12
Proposed
The product should keep unsaved changes during session refresh and validate the original version when restoring an editor draft.
policydocumented
13must prove
Confirmed
A whole-record save of a contact or organization must include every replaceable property. A save that omits one is refused and names the missing property, null clears a value, and a read states its empty properties as null so that a record read back can be saved unchanged.
✓ A replacement that names every replaceable property, with null for the ones to clear, is saved.✗ A replacement that omits job_title is refused naming job_title; nothing is written.
origin: OQ-CRM-ENT-001 decided 2026-10-22 by Andrew: A (require complete payloads, as Activities already do); not yet implemented for these records, see FND-CRM-ENT-004
policyapi, servicedocumented
services-crm:docs/entities-api.md#Not yet under the complete-payload rule

Scenarios

IdGivenWhenThenProvenance
01Two staff members open the same contact.The second saves after the first.The second is told the contact changed, reloads and re-applies without losing the first change.test
02An API client retrieves a contact and sends it back unmodified.The replacement is saved.The version, stamp and history are unmodified.test

Edge Cases

IdGivenWhenThenProvenance
03A contact edited while its spouse is deleted.The editor saves.409 naming the system change.test
services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 409 naming the system, not a colleague, when the system cleared the spouse link while the contact was edited (M1-D6)

Decision History

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

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

    A: require complete payloads for whole-record saves of contacts, organizations and classification records, as Activities already do (reads state empty properties as null; an omitted property is refused; null clears). Affiliation and custom-field exceptions stay.

    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.