Files
accounted/extensions/general/mcp-server/skills/customer-onboarding.ts
T
13b69a2056 fix(customers): personnummer via MCP lands in personal_number, masked everywhere; MCP payment terms follow settings (#1788)
* 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>
2026-08-21 18:32:17 +02:00

128 lines
6.8 KiB
TypeScript

import type { Skill } from './types'
const body = `# Customer Onboarding: Accounted
Adding a customer correctly the first time avoids a long tail of wrong-VAT
invoices. This skill covers the decision tree for picking \`customer_type\`,
when to run VIES validation, and which fields are required for each path.
## When to use
- "Lägg till en ny kund / Add a customer"
- "Skapa kund: Acme AB"
- "How do I invoice an EU customer?"
- Before \`gnubok_create_invoice\` whenever the customer isn't found in \`gnubok_list_customers\`
## Decision tree: which customer_type?
The customer_type drives **the VAT treatment** on every invoice you send them.
Getting it wrong means re-issuing invoices and amending VAT declarations
later: much cheaper to ask the customer once at onboarding.
| Customer | customer_type | Required fields | VAT treatment |
|---|---|---|---|
| Swedish private person | \`individual\` | name, personal_number (optional; encrypted, masked on read) | 25/12/6 % standard, no reverse charge |
| Swedish AB / HB / KB | \`swedish_business\` | name, org_number | 25/12/6 % standard |
| Swedish enskild firma (EF) | \`individual\` | name, personal_number (an EF's org number IS the owner's personnummer; a business type refuses it) | 25/12/6 % standard |
| EU company (VAT-registered) | \`eu_business\` | name, vat_number (validated) | Reverse charge (varor + tjänster): invoice has 0 % VAT, customer pays VAT in their country |
| EU company (no VAT number) | \`eu_business\` | name, country | Treated as individual: 25 % charged, no reverse charge |
| Non-EU company | \`non_eu_business\` | name, country | Export 0 % (varor) or reverse charge per service rules |
| Non-EU private person | \`individual\` | name, country | 25 % standard for goods; export rules can apply for services |
**Special cases:**
- **B2G (Swedish municipality/agency)**: still \`swedish_business\`. If the customer requires Peppol or another e-invoice format, deliver the invoice through an external e-invoice provider, then use \`gnubok_mark_invoice_as_sent\` to record delivery. Accounted currently creates PDF/email invoices and does not generate or send Peppol invoices.
- **Consumer customer in another EU country (B2C distance sale)**: \`eu_business\` does NOT apply. Charge Swedish VAT (25/12/6 %) below the OSS threshold; above the threshold the company must register for OSS. This is rare for sole traders: flag the user if turnover suggests they're approaching the threshold.
## Workflow
### Step 1: Check if the customer already exists
\`gnubok_list_customers\` returns all customers. Search by name client-side.
If a customer with the same org_number or vat_number exists, USE THAT ONE.
Duplicate customers fragment the AR ledger and confuse aging reports.
### Step 2: Decide customer_type
Ask the user the minimum questions needed for the decision tree above. Don't
ask "which customer_type?"; ask "is this a private person or a company?
Swedish or EU? Do they have a VAT number?". Map the answers to a
customer_type yourself.
### Step 3: Validate VAT number (EU business only)
For \`eu_business\` customers, **VIES validation is critical**. If VIES says
the VAT number is invalid (or unverifiable), reverse charge does NOT apply:
you must charge Swedish VAT instead. The wrong call here means you under-collect
VAT and Skatteverket charges interest + penalty later.
Accounted's \`gnubok_create_customer\` runs VIES automatically when \`vat_number\`
is provided. The result is stored in \`vat_number_validated\` on the customer.
Subsequent invoices read this field via \`getPermittedVatRates\` to determine
which VAT rates are legal.
If VIES is temporarily down (intermittent), Accounted marks the customer as
\`vat_number_validated = false\` and you'll see standard 25 % rates instead
of reverse charge. Re-validate by updating the customer once VIES is back.
### Step 4: Stage the customer
\`gnubok_create_customer({
name: 'Acme GmbH',
customer_type: 'eu_business',
vat_number: 'DE123456789',
email: 'invoicing@acme.de',
country: 'Germany',
payment_terms: 30,
})\`
Stages a pending operation for user approval. After approval, the customer
appears in \`gnubok_list_customers\` and can be invoiced via \`gnubok_create_invoice\`.
### Step 5: Sanity-check before first invoice
\`gnubok_list_customers\`: confirm the customer appears, customer_type is right.
Read \`vat_number_validated\` (for EU); if false, double-check the VAT number.
When you eventually call \`gnubok_create_invoice\`, Accounted uses the
\`getPermittedVatRates\` helper to constrain the legal rates per customer type.
The accepted set is 0/25/12/6 % for every customer type, so "VAT rate X% is not
allowed for customer type Y" means X is not a Swedish VAT rate at all: fix the
rate, and check the customer_type while you are there.
## Common errors
- *A validated \`eu_business\` invoiced at 25 %*: not refused, but almost always wrong. Reverse charge is the DEFAULT for a validated EU VAT number: leave \`vat_rate\` off (or 0) and Accounted adds the "Omvänd betalningsskyldighet" notation automatically. A Swedish rate is lawful here only for a supply taxed where it is performed (hotel/restaurang 12 %, persontransport and event admission 6 %, fastighetstjänst and korttidsuthyrning 25 %); read the \`Accounted://settings/vat-treatments\` resource before using one.
- *A \`swedish_business\` invoiced at 0 %*: also accepted (momsfritt supplies exist), but the normal domestic rate is 25/12/6 %. Do not use 0 % to avoid picking a rate.
- *"Customer not found"* on \`gnubok_create_invoice\`: the customer wasn't created (or its pending_operation wasn't approved). Run \`gnubok_list_customers\` to confirm.
## ROT/RUT-eligible customers (Swedish individuals)
ROT/RUT-avdrag applies to physical persons receiving home-services
(byggtjänster, städ, snöskottning, etc.). The customer's personnummer is
required on the invoice. Update the customer record with:
- \`fastighetsbeteckning\` (property identifier, for ROT)
- \`personal_number\` (set at creation via \`gnubok_create_customer\` or in the web form; encrypted at rest, masked on read)
Then create the invoice with ROT/RUT flag set. See the \`invoicing-rules\` skill
for the booking details (BAS accounts 1513 + 3740).
## Tools
- \`gnubok_list_customers\`: check for existing customers
- \`gnubok_create_customer\`: stage the new customer
- \`gnubok_create_invoice\`: uses the customer_type for VAT-rate validation
- (Helper, not a tool but referenced) \`getPermittedVatRates(customer_type, vat_number_validated)\`: drives the legal-rates check
`
export const customerOnboardingSkill: Skill = {
slug: 'customer-onboarding',
name: 'Customer Onboarding',
summary: 'Decision tree for customer_type (individual/swedish_business/eu_business/non_eu_business), VIES validation, ROT/RUT setup.',
tags: ['onboarding', 'customer', 'vat', 'eu', 'rot-rut'],
body,
tier: 'workflow',
applicability: { entity_type: 'both' },
}