* fix(customers): personnummer on the MCP path lands in personal_number, masked everywhere; MCP payment terms follow settings
Follow-up to #1724 (Discord kalletoxic): the fix reached the web form and
the v1 REST API, but not the MCP path, and the web customer list still
showed a personnummer raw when it sat in org_number.
Personnummer (MCP + every write path):
- gnubok_create_customer gets a personal_number input. Until now it had
none, so an agent creating a private person either dropped the number
or put it in org_number, which nothing masks. Encrypted at staging
(personal_number_encrypted + personal_number_masked; personal_number is
now a forbidden staging key in staging-pii-guard), the approval preview
shows ********-1234, commitCreateCustomer stores the ciphertext as-is.
Idempotency hashes the masked preview (new StageOptions.idempotencyParams)
because the random-IV ciphertext would make identical retries look like
payload changes.
- A personnummer-shaped org_number on customer_type=individual is the
personnummer in the wrong field: it is moved into personal_number
(encrypted) and org_number cleared, on CreateCustomerSchema (web POST,
v1 POST, v1 bulk), both PATCH routes, MCP staging, and commitCreateCustomer
for in-flight ops. Only a DIFFERENT personnummer next to personal_number
is refused (new CUSTOMER_PERSONAL_NUMBER_CONFLICT). The business-type
guard from #1724 is unchanged and now also fires at MCP staging, so the
user never approves an operation that fails at commit.
- Read side: the web customer list and gnubok_list_customers mask a legacy
individual row's org_number personnummer instead of showing it raw;
list_customers exposes personal_number_masked and never the ciphertext.
- scripts/repair-customer-personal-number-in-org-number.ts moves the
existing rows (dry run: 134 rows across 10 companies on prod); run by
hand with --confirm after deploy.
- customer-onboarding skill: EF customers follow the #1724 decision
(individual + personal_number); ROT/RUT section names the real field.
Payment terms (MCP):
- gnubok_create_customer staged `payment_terms || 30`, so
resolveDefaultPaymentTerms at commit always saw 30 and the company's
invoice_default_days never reached MCP customers. Resolved at staging
now, so the preview shows the value the row will get.
tools/list payload ceiling 59.75K to 59.85K (descriptions trimmed first,
rationale in payload-size.bench.test.ts). apiskill regenerated; no
migrations.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CbLqn9bgZ9NJ5qnZMeC1Bk
* fix(scripts): literal update payloads in the personnummer repair script
The no-phantom-columns scanner counts a runtime-built update payload as
unresolvable and the ceiling (379) had no headroom; two literal payloads
keep the guard able to resolve both branches.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CbLqn9bgZ9NJ5qnZMeC1Bk
---------
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>