Peppol sending has been live since #1780 behind a per-company access grant, but the MCP skills, the swedish-invoice-compliance atom, docs/PEPPOL_FOUNDATION.md and the v1 :send / :mark-sent descriptions still told agents it did not exist. Every text now says gated per company (requested under Installningar > Fakturering) and keeps the restrictions explicit: aktiebolag senders, standard invoices only, Swedish org-number buyers, no MCP or v1 Peppol send verb yet, :mark-sent as the recovery step when a network-accepted send fails issuance. The skills guard test pins the truthful claim across all surfaces. Includes the regenerated agent_atom_registry seeds and skills/accounted-api references. Refs #546
This commit is contained in:
@@ -140,23 +140,112 @@ describe('Skills registry', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('does not advertise unsupported built-in e-invoice delivery', () => {
|
||||
it('describes Peppol sending truthfully: gated per company, never absent (#546)', () => {
|
||||
const invoiceComplianceAtom = readFileSync(
|
||||
join(process.cwd(), '.claude/skills/swedish-invoice-compliance/SKILL.md'),
|
||||
'utf8',
|
||||
)
|
||||
const allBodies = [...skills.map((skill) => skill.body), invoiceComplianceAtom].join('\n')
|
||||
|
||||
expect(allBodies).not.toMatch(/Accounted\s+(?:renders|generates|produces)[^.\n]*EN\s*16931/i)
|
||||
expect(allBodies).not.toMatch(/Accounted\s+(?:handles|sends|delivers)[^.\n]*Peppol/i)
|
||||
// The release-pinned EN 16931 validation stack is still open (docs/PEPPOL_FOUNDATION.md),
|
||||
// so no text may claim Accounted validates against it.
|
||||
expect(allBodies).not.toMatch(/Accounted\s+(?:renders|generates|produces|validates)[^.\n]*EN\s*16931/i)
|
||||
// Peppol send is live behind a per-company access grant (app/api/invoices/[id]/peppol/send).
|
||||
// A text that claims the capability is absent sends users to a competitor; say gated instead.
|
||||
// The patterns take the capability as subject (active, passive and adjective forms) so that
|
||||
// the true statement "a v1 or MCP Peppol send action is not yet available" stays legal.
|
||||
const capabilityAbsentPatterns = [
|
||||
/(?:does not|doesn't|cannot|can't)[^.\n]*(?:send|deliver|generate)[^.\n]*Peppol/i,
|
||||
/Peppol\s+(?:invoices?|e-invoices?|documents?|send(?:ing)?|delivery)\s+(?:is|are|has|have)\s+(?:not|never)\b[^.\n]*\b(?:sent|delivered|generated|built|implemented|available|supported|possible)\b/i,
|
||||
]
|
||||
for (const pattern of capabilityAbsentPatterns) {
|
||||
expect(allBodies).not.toMatch(pattern)
|
||||
// Forbidden: the pre-#546 wording in its active, passive and adjective forms.
|
||||
expect(
|
||||
[
|
||||
'It does not generate e-invoice XML or deliver invoices through Peppol.',
|
||||
'Peppol invoices are not generated or sent by Accounted.',
|
||||
'Peppol sending has not been built.',
|
||||
'Peppol sending is not yet available.',
|
||||
].some((claim) => pattern.test(claim)),
|
||||
).toBe(true)
|
||||
// Legal: the v1 :send/:mark-sent descriptions say the agent verb is missing, not the capability.
|
||||
expect('a v1 or MCP Peppol send action is not yet available').not.toMatch(pattern)
|
||||
}
|
||||
// No MCP tool or v1 action sends via Peppol yet: no text may hand an agent a Peppol send verb.
|
||||
expect(allBodies).not.toMatch(/gnubok_send_invoice[^.\n]*Peppol/i)
|
||||
expect(allBodies).not.toMatch(/gnubok_send_peppol|gnubok_peppol_send/i)
|
||||
|
||||
for (const slug of ['invoicing-rules', 'customer-onboarding']) {
|
||||
const truthfulSkills = ['invoicing-rules', 'customer-onboarding'].map((slug) => {
|
||||
const skill = skills.find((candidate) => candidate.slug === slug)
|
||||
expect(skill?.body).toMatch(/external e-invoice provider/i)
|
||||
expect(skill?.body).toContain('gnubok_mark_invoice_as_sent')
|
||||
expect(skill, `skill ${slug}`).toBeTruthy()
|
||||
return skill!
|
||||
})
|
||||
// The discovery surface (gnubok_list_skills) must not frame e-invoicing as external either.
|
||||
for (const skill of truthfulSkills) {
|
||||
expect(skill.summary).not.toMatch(/external e-invoic/i)
|
||||
}
|
||||
expect(truthfulSkills[0].summary).toMatch(/Peppol/)
|
||||
|
||||
const truthfulTexts = [...truthfulSkills.map((skill) => skill.body), invoiceComplianceAtom]
|
||||
for (const text of truthfulTexts) {
|
||||
// Where it lives, and that it is gated per company (with the English label for en-locale users).
|
||||
expect(text).toMatch(/invoice page in the dashboard/i)
|
||||
expect(text).toMatch(/(?:gated|access)[^.\n]*per company|per[- ]company[^.\n]*(?:access|gated)/i)
|
||||
expect(text).toContain('Inställningar > Fakturering (Settings > Invoicing)')
|
||||
expect(text).toMatch(/send cap/i)
|
||||
// The restrictions agents must not over-promise past (lib/invoices/peppol-bis-billing.ts).
|
||||
expect(text).toMatch(/aktiebolag/i)
|
||||
expect(text).toMatch(/enskild firma/i)
|
||||
expect(text).toMatch(/standard invoices only|no credit notes/i)
|
||||
expect(text).toMatch(/SEK/)
|
||||
expect(text).toMatch(/6, 12 or 25 %/)
|
||||
expect(text).toMatch(/no reverse charge/i)
|
||||
expect(text).toMatch(/no ROT\/RUT deductions/i)
|
||||
expect(text).toMatch(/Er referens/)
|
||||
// No agent-callable send verb yet.
|
||||
expect(text).toMatch(/no MCP tool[^.\n]*Peppol|MCP tool[^.\n]*not (?:yet )?available/i)
|
||||
// A successful dashboard send issues the invoice; mark-sent is only the issuance-failure recovery.
|
||||
expect(text).toMatch(/successful dashboard Peppol send issues the invoice itself/i)
|
||||
expect(text).toMatch(/could not be marked as sent/i)
|
||||
// The fallback path stays documented for companies without access.
|
||||
expect(text).toMatch(/external e-invoice provider/i)
|
||||
expect(text).toContain('gnubok_mark_invoice_as_sent')
|
||||
// The exporter refuses personnummer-based BUYER identifiers too (prepareParty('buyer') in
|
||||
// peppol-bis-billing.ts), so an enskild firma customer must not be promised a send.
|
||||
expect(text).toMatch(/(?:buyer|customer)[^.\n]*personnummer/i)
|
||||
// The mark-sent recovery applies to the still-draft invoice only: INVOICE_MARK_SENT_REPAIR_REQUIRED
|
||||
// leaves the invoice sent with the verifikat posted, and a second mark-sent returns 409.
|
||||
expect(text).toMatch(/still-draft invoice/i)
|
||||
}
|
||||
|
||||
// The numbered workflow must route an e-invoice customer to the Peppol section from Step 4
|
||||
// itself, so an agent reading top-down never reaches the external-provider fallback first.
|
||||
const invoicingRules = truthfulSkills[0].body
|
||||
expect(invoicingRules).toMatch(/### Step 4: Send[\s\S]*?Peppol[\s\S]*?### Step 5/)
|
||||
// Kontantmetod and defer_invoice_booking companies get no verifikat at issue (Step 3 says the same).
|
||||
expect(invoicingRules).toMatch(/issues the invoice itself \(number, status, and the verifikat under faktureringsmetoden\)/)
|
||||
|
||||
// The v1 :send / :mark-sent descriptions (source of skills/accounted-api/references/invoices.md)
|
||||
// are the fourth and fifth corrected surfaces; apiskill:check only detects generated-vs-source
|
||||
// drift, so the truthful claim is pinned here on the route source itself.
|
||||
const v1RouteTexts = [
|
||||
'app/api/v1/companies/[companyId]/invoices/[id]/send/route.ts',
|
||||
'app/api/v1/companies/[companyId]/invoices/[id]/mark-sent/route.ts',
|
||||
].map((relativePath) => readFileSync(join(process.cwd(), relativePath), 'utf8'))
|
||||
for (const text of v1RouteTexts) {
|
||||
for (const pattern of capabilityAbsentPatterns) {
|
||||
expect(text).not.toMatch(pattern)
|
||||
}
|
||||
// The pre-#546 framing listed Peppol as an external channel next to postal mail.
|
||||
expect(text).not.toMatch(/\(Peppol, postal/)
|
||||
expect(text).toMatch(/a v1 or MCP Peppol send action is not yet available/)
|
||||
expect(text).toMatch(/per-company access grant/)
|
||||
expect(text).toContain('Inställningar > Fakturering (Settings > Invoicing)')
|
||||
expect(text).toMatch(/aktiebolag senders, standard invoices only/)
|
||||
expect(text).toMatch(/buyers whose org number is not a personnummer/)
|
||||
expect(text).toMatch(/could not be marked as sent/)
|
||||
}
|
||||
expect(invoiceComplianceAtom).toMatch(/external e-invoice provider/i)
|
||||
expect(invoiceComplianceAtom).toContain('gnubok_mark_invoice_as_sent')
|
||||
})
|
||||
|
||||
it('findSkill resolves the workflow skill or null (sync workflow lookup)', async () => {
|
||||
|
||||
@@ -31,7 +31,7 @@ later: much cheaper to ask the customer once at onboarding.
|
||||
|
||||
**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.
|
||||
- **B2G (Swedish municipality/agency)**: still \`swedish_business\`. Public-sector buyers require Peppol e-invoices. Accounted sends Peppol BIS Billing 3 invoices from the invoice page in the dashboard, gated per company: request access under Inställningar > Fakturering (Settings > Invoicing), Accounted's operators enable it, and a grant may carry a send cap that support raises. Restrictions: aktiebolag senders only, standard invoices only (no credit notes), a buyer whose org number is not a personnummer (an enskild firma customer is refused until GLN identifiers are supported), SEK invoices with taxable Swedish VAT at 6, 12 or 25 % only (no reverse charge, no ROT/RUT deductions), and Er referens set. No MCP tool sends via Peppol yet, so the user sends from the dashboard. A successful dashboard Peppol send issues the invoice itself, so do not mark it as sent afterwards; if the dashboard reports that the invoice was sent via Peppol but could not be marked as sent, complete the issuance with \`gnubok_mark_invoice_as_sent\` on the still-draft invoice (if it already shows as sent, the verifikat exists and only its link to the invoice needs repair: do not mark it as sent again, leave the repair to support). If Peppol access is not granted, or the company is an enskild firma, deliver the invoice through an external e-invoice provider, then use \`gnubok_mark_invoice_as_sent\` to record delivery.
|
||||
- **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
|
||||
|
||||
@@ -83,6 +83,8 @@ Returns staged operation. User approves in web app → invoice number is allocat
|
||||
|
||||
If the user delivered the invoice manually (printed or sent through an external e-invoice provider), use \`gnubok_mark_invoice_as_sent\` instead: same booking effect, no email.
|
||||
|
||||
For a customer that requires an e-invoice (B2G, or a buyer that asks for Peppol), see "E-invoicing via Peppol" below: the user sends from the invoice page in the dashboard, gated per company; no MCP tool sends via Peppol yet.
|
||||
|
||||
### Step 5: Record payment
|
||||
|
||||
When money arrives in 1930:
|
||||
@@ -112,9 +114,9 @@ For consumer-targeted services (RUT: städning, RUT) or construction (ROT):
|
||||
|
||||
This data goes on the invoice; Accounted's invoice template renders it automatically when set on the customer.
|
||||
|
||||
## External e-invoicing (including B2G)
|
||||
## E-invoicing via Peppol (including B2G)
|
||||
|
||||
Accounted currently creates PDF invoices and can send them by email. It does not generate e-invoice XML or deliver invoices through Peppol. If the customer requires an e-invoice, deliver it through an external e-invoice provider, then use \`gnubok_mark_invoice_as_sent\` to record the delivery and apply the same booking effect without sending another email.
|
||||
Accounted sends Peppol BIS Billing 3 e-invoices from the invoice page in the dashboard. Peppol sending is gated per company: the user requests access under Inställningar > Fakturering (Settings > Invoicing) and Accounted's operators enable it; a grant may carry a send cap, and when it is used up the dashboard says so and support raises it. Restrictions: the sending company must be an aktiebolag (enskild firma is refused until GLN identifiers are supported), standard invoices only (no credit notes, no self-billed invoices), the customer must be a Swedish business or organization whose org number is not a personnummer (an enskild firma customer is refused until GLN identifiers are supported), and the invoice must be in SEK with taxable Swedish VAT at 6, 12 or 25 % (no reverse charge, no VAT-exempt sales, no ROT/RUT deductions) and carry Er referens. There is no MCP tool and no v1 API action for Peppol sending yet, so an agent cannot trigger it: tell the user to send from the invoice page. Never tell a user that Accounted lacks Peppol sending; say it is gated per company. A successful dashboard Peppol send issues the invoice itself (number, status, and the verifikat under faktureringsmetoden), so do not mark it as sent afterwards. If the dashboard reports that the invoice was sent via Peppol but could not be marked as sent, call \`gnubok_mark_invoice_as_sent\` on the still-draft invoice to complete the issuance; a number already allocated is reused, never consumed twice. If that invoice already shows as sent, the number and the verifikat exist and only the invoice's link to the verifikat needs repair: do not mark it as sent again (it returns 409), leave the repair to support. If the company has no Peppol access, or is an enskild firma, and the customer requires an e-invoice, deliver it through an external e-invoice provider, then use \`gnubok_mark_invoice_as_sent\` to record the delivery and apply the same booking effect without sending another email.
|
||||
|
||||
## Critical rules
|
||||
|
||||
@@ -133,7 +135,7 @@ Accounted currently creates PDF invoices and can send them by email. It does not
|
||||
- \`gnubok_list_customers\` / \`gnubok_create_customer\`: customer setup
|
||||
- \`gnubok_create_invoice\`: stage new invoice
|
||||
- \`gnubok_send_invoice\`: email PDF
|
||||
- \`gnubok_mark_invoice_as_sent\`: manual delivery
|
||||
- \`gnubok_mark_invoice_as_sent\`: manual delivery (also the recovery when a dashboard Peppol send could not mark the invoice as sent)
|
||||
- \`gnubok_mark_invoice_as_paid\`: manual payment
|
||||
- \`gnubok_match_transaction_to_invoice\`: link bank payment
|
||||
- \`gnubok_credit_invoice\`: kreditfaktura (legal undo)
|
||||
@@ -146,7 +148,7 @@ Accounted currently creates PDF invoices and can send them by email. It does not
|
||||
export const invoicingRulesSkill: Skill = {
|
||||
slug: 'invoicing-rules',
|
||||
name: 'Invoicing Rules',
|
||||
summary: 'Mandatory invoice fields (ML 17 kap. 24 §), VAT treatment, ROT/RUT, external e-invoicing, kreditfaktura.',
|
||||
summary: 'Mandatory invoice fields (ML 17 kap. 24 §), VAT treatment, ROT/RUT, Peppol e-invoicing (gated per company), kreditfaktura.',
|
||||
tags: ['invoicing', 'vat', 'compliance', 'eu', 'rot-rut'],
|
||||
body,
|
||||
tier: 'workflow',
|
||||
|
||||
Reference in New Issue
Block a user