Skip to content

Edit an existing customer (name, phone, email, address, note) #625

Closed

Description

@mforce

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 — Create only; every property is private set with 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) returning Result, with the same trim/normalize and required-field rules as Create.
  • UpdateCustomerCommand / UpdateCustomerValidator / UpdateCustomerHandler under Features/Customers/UpdateCustomer/. The validator must mirror CreateCustomerValidator's length and required rules — do not let the two drift.
  • PUT /api/v1/customers/{id} on AuthPolicies.SalesFlow (matching the create/read gating on this group — the customer directory is PII), requiring an Idempotency-Key like 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:

  1. add Version to Customer and IsConcurrencyToken() to CustomerConfiguration,
  2. 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),
  3. regenerate docs/schema/ via tools/schema-docs/generate.sh in the same PR (chore(schema): generate PostgreSQL schema documentation #417),
  4. bump Version inside Customer.Update, and
  5. 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 create Dialog and BusyButton pattern, prefilled from the row.
  • Handle the 409 from a concurrent edit with a real message, not a generic failure toast.
  • Prefill races: follow the usePagedList intent-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/tl inline (machine-drafted, native review pending) per the translate-now policy.

Out of scope

  • Deleting or archiving/deactivating a customer. Customer has no IsActive/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.
  • Customer carries Version as an EF concurrency token, added by its own migration, with docs/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 / CustomerUpdate exist 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

  1. mforce commented on Aug 31, 2026

    @mforce
    OwnerAuthor

    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.

  2. mforce commented on Aug 31, 2026

    @mforce
    OwnerAuthor

    Follow-up from #627 baseline verification: CI run https://github.com/mforce/cluckwork/actions/runs/33409006169 on merge commit 062a55c88a075fc181cb675c2bd3f8d3ff856e9d timed out in CustomersPage paging (#511) > keeps the loaded window after a create instead of snapping back to page one at the existing 5-second Vitest limit. A driver-local rerun of npm run test:coverage on 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.

  3. added a commit that references this issue on Sep 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions