# Customer profile API repair

Scope: `/Users/imzee/projects/appengine`, `PUT /client-data/profile`. Local source repair completed 2026-09-18; renderer/API verification completed 2026-09-21 against the Cedar training customer. Production deployment remains outside scope.

## Defect and write-path decision

`ClientAccountService.updateClientProfile` contained only a commented-out `usersService.updateUser` call and returned `undefined`. The controller therefore completed successfully without saving the patch.

Traced alternatives before implementing:

- `UsersService.customerUpdate` merges arbitrary customer data, delegates to a full-record write and sends a customer notification. It is unsuitable for a restricted self-service patch.
- `UsersService.updateUser` replaces a full record and handles password/access-change behavior; it is unsuitable for this patch.
- `UsersService.updateUserMeta` demonstrates the established field-scoped `RepositoryService.updatePartial` followed by fresh `findOne` pattern. This repair uses that pattern on the customer collection.

## Files changed

- `src/client-account/client-account.service.ts`: replaces the empty method with the restricted profile updater.
- `src/client-account/customer-profile.ts`: validates identity and patch, writes explicit dotted paths, confirms persistence by a fresh read, and returns an allowlisted response.
- `src/client-account/client-account.profile.spec.ts`: isolated regression/security tests that exercise the actual service method with an in-memory repository and mocked unrelated services.

Existing changes in package.json, yarn.lock, .yarn/install-state.gz, and tools.domain.service.ts were preserved. No applicable AGENTS.md was found in the repository/ancestor paths inspected.

## API behavior

Accepts a nonempty flat JSON patch containing only `firstName`, `lastName`, and/or `phone`:

```json
{"firstName":"Ada","lastName":"Lovelace","phone":"+15551234567"}
```

Names are trimmed, must be nonempty, and have a 120-character limit. Phone accepts common numeric formatting, 7–15 digits and at most 40 characters; an empty string removes it. Non-string values, control characters, empty objects, arrays, unknown fields and nested record envelopes are rejected with HTTP 400. Email/username changes are outside this endpoint's supported patch and are rejected along with IDs, roles, permissions, credentials, balance and verification flags.

Requires an actual `customer` datatype; staff fallback from `CurrentCustomerOrUser` is rejected. The record ID comes exclusively from the authenticated customer. The repository reads within the requested/resolved organization, and its stored datatype/sk/pk must match the session customer before writing. It compares stored identities rather than synthesizing a partition key from a possibly aliased organization header, preserving middleware/repository alias resolution.

Only requested `data.firstName`, `data.lastName`, `data.phone` fields are written. A changed phone also sets `data.phoneVerified` to false. Credentials and unrelated fields are preserved; no customer notification method is invoked. The returned record contains only sk, datatype, the three safe profile fields, and phoneVerified if present. It contains no credentials, roles, balances, tokens or unfiltered stored data. No sensitive logging was added.

Repository exceptions propagate; a false update result or a fresh read that does not reflect the requested patch raises an error rather than claiming success. As with any write followed by readback, a read failure after a successful write can report failure even though the write persisted; no transaction or retry guarantee is added.

## Validation evidence

Before applying the repair, the new `persists a patch on fresh readback instead of returning empty success` test failed against the original service: expected `Ada`, received the stored `Before`. That directly reproduced the no-op.

After applying the repair:

- **44/44 isolated tests passed.** Covers fresh readback, exact safe response, preservation of credentials/other fields, unrelated concurrent changes, another customer remaining unchanged, client-supplied IDs/security fields rejected, staff/absent/invalid identity, cross-org identity mismatch, resolved org alias, missing customer, malformed/empty payloads, rejected/false/silent-no-op writes, and phone verification reset.
- **Full repository TypeScript check passed** with no diagnostics: `NODE_OPTIONS=--max-old-space-size=8192 node node_modules/typescript/bin/tsc --noEmit --incremental false --pretty false`.
- `git diff --check` passed.

Focused test command:

```sh
node node_modules/jest/bin/jest.js --runInBand --no-cache --cacheDirectory=/tmp/appmint-profile-jest --testPathPattern=client-account.profile.spec.ts --globals='{"ts-jest":{"isolatedModules":true,"diagnostics":false}}'
```

Jest emitted deprecation notices for its existing ts-jest configuration style; these were not failures. Runtime tests used transpilation-only isolation to avoid loading unrelated application integrations; the separate full TypeScript check covered semantic typing.

## Live local verification

The repaired local renderer 4106 and API 3311 were exercised with the existing Cedar customer: flat `PUT /client-data/profile` returned 200 and a fresh GET returned the phone with `phoneVerified:false`. The shared proxy repair is recorded in [appengine-profile-proxy.md](appengine-profile-proxy.md), with sanitized evidence in [profile-before-update-after.json](assets/appengine-operations/profile-before-update-after.json). Production deployment/retesting remains outside scope. This does not implement a separate profile UI TODO or change appmint.io's separate customer-update endpoint.
