Repository navigation
Description
Problem
Customers are create-only. POST /api/v1/customers and the New-customer dialog on the Customers page are the only ways a customer row is ever written; there is no PUT/PATCH, no Customer.Update, and no edit affordance in the SPA. A typo'd phone number, a changed address, or a renamed business is uncorrectable — the farm's only workaround is to create a duplicate customer, which then splits that customer's order history and outstanding balance across two rows.
Current state (main, 2026-08-30):
src/Cluckwork.Domain/Sales/Customer.cs—Createonly; every property isprivate setwith no mutator.src/Cluckwork.Api/Endpoints/Customers/CustomerEndpoints.cs—POST /,GET /,GET /{id}. No update route.src/Cluckwork.Application/Features/Customers/—CreateCustomer/only.web/src/routes/CustomersPage.tsx— create dialog only; rows are not clickable for edit.
Scope
Add customer editing end to end, following the Catalog/UpdateProduct slice as the shape reference (command + validator + handler + audit + endpoint + SPA dialog).
Backend
Customer.Update(name, phone, email, address, note)returningResult, with the same trim/normalize and required-field rules asCreate.UpdateCustomerCommand/UpdateCustomerValidator/UpdateCustomerHandlerunderFeatures/Customers/UpdateCustomer/. The validator must mirrorCreateCustomerValidator's length and required rules — do not let the two drift.PUT /api/v1/customers/{id}onAuthPolicies.SalesFlow(matching the create/read gating on this group — the customer directory is PII), requiring anIdempotency-Keylike every other write.- 404 on an unknown id; the tenant query filter already fences cross-account reads, but add a mutation test that proves it.
Blocking prerequisite — Customer has no Version column. Customer derives from AggregateRoot<Guid>, which does not define Version; Product declares its own, and CustomerConfiguration (in SalesOrderConfiguration.cs) configures no concurrency token. Per AGENTS.md "Every aggregate mutation must bump Version", this slice must:
- add
VersiontoCustomerandIsConcurrencyToken()toCustomerConfiguration, - ship one new migration for the column (never fold it into
InitialCreate— feat(eggs): make cracked and dirty eggs sellable stock via condition grades (#396) #407), - regenerate
docs/schema/viatools/schema-docs/generate.shin the same PR (chore(schema): generate PostgreSQL schema documentation #417), - bump
VersioninsideCustomer.Update, and - carry a parallel-race integration test asserting the second concurrent writer gets a 409 rather than silently winning. This has shipped broken three times; the race test is not optional.
Audit — there is currently no Customer* constant in AuditActions. Add CustomerCreate and CustomerUpdate, wire CreateCustomerHandler (an existing gap this slice should close) and UpdateCustomerHandler to IAuditWriter. Note AuditVocabularyCoverageTests only accepts a literal AuditActions.X reference (or a ternary of two) as the action argument — do not forward it through a helper parameter. Find the guards by grepping the readers of AuditActions, not from memory.
Frontend
- Edit affordance on each row of
CustomersPage.tsx, reusing the existing createDialogandBusyButtonpattern, prefilled from the row. - Handle the 409 from a concurrent edit with a real message, not a generic failure toast.
- Prefill races: follow the
usePagedListintent-ticket pattern — a submit must not write a half-prefilled form. - Vitest tests in the same PR (standing rule), including the 409 path.
- i18n: new keys land in
en/es/tlinline (machine-drafted, native review pending) per the translate-now policy.
Out of scope
- Deleting or archiving/deactivating a customer.
Customerhas noIsActive/archived flag today, and soft-delete semantics against existing orders and balances is its own decision. File separately if wanted. - Merging duplicate customers created as a workaround for this gap. Also separate — it needs an order/payment re-parenting story.
Acceptance criteria
-
PUT /api/v1/customers/{id}updates name, phone, email, address and note; 404 unknown id; 422 on a validation failure with the same rules as create. -
CustomercarriesVersionas an EF concurrency token, added by its own migration, withdocs/schema/regenerated in the same PR. - A parallel-race integration test proves two concurrent updates produce one 200 and one 409.
- A cross-tenant mutation test proves account A cannot update account B's customer.
-
AuditActions.CustomerCreate/CustomerUpdateexist and both handlers write an audit row with a resolved actor (fix(seed): seeded audit events carry "(unresolved)" as the actor, now visible on record History columns #500). - The Customers page can edit an existing customer, with Vitest coverage including the 409 path.
-
specs/product/GLOSSARY.md§Sales Customer entry and the SPA Help page + in-app glossary state that customer details are editable.
Activity
feat(customers): edit existing customer details (#625) (#626)
mforce commentedon Aug 31, 2026
Shipped in PR #626 (squash
062a55c8). Final contract: full versioned PUT; validation 400; unknown/cross-tenant 404; stale/concurrent writes 409; success 204; audited create/update; SPA edit flow with paged-list and idempotent-retry protections.mforce commentedon Aug 31, 2026
Follow-up from #627 baseline verification: CI run https://github.com/mforce/cluckwork/actions/runs/33409006169 on merge commit
062a55c88a075fc181cb675c2bd3f8d3ff856e9dtimed out inCustomersPage paging (#511) > keeps the loaded window after a create instead of snapping back to page oneat the existing 5-second Vitest limit. A driver-local rerun ofnpm run test:coverageon the same commit passed all 2,052 tests, so this currently presents as load-sensitive flakiness rather than a deterministic regression. #627 will record it as pre-existing baseline behavior and will not repair or relax the test.chore(main): release 0.1.0 (#542)