sevDesk Integration
sevDesk is the external accounting system behind the Billing System. R1 creates and manages invoices internally; sevDesk handles the bookkeeping side — invoice publication, email dispatch, PDF rendering, payment state, and DATEV export. The integration is one-directional per concern: R1 pushes invoices out, and pulls payment status back on a schedule.
Architecture
| Component | Path | Purpose |
|---|---|---|
| API client | backend/src/services/sevdesk/client.ts | Typed HTTP client for sevDesk REST API v1; retries with exponential backoff (max 3, 1–30 s) |
| Invoice bridge | backend/src/services/sevdesk/invoice-bridge.ts | Lifecycle orchestration: publish → finalize → PDF → email → payment sync → cancel → DATEV export; contains syncPaymentStatus() |
| Types | backend/src/services/sevdesk/types.ts | sevDesk API interfaces (Contact, Invoice, Payment, DatevExport, …) |
| Service impl | backend/src/services/billing/sevdesk-service-impl.ts | DI-registered service consumed by routes |
| Routes | backend/src/routes/sevdesk.ts | /api/sevdesk/* endpoints (see below) |
The boundary inside billing: billing-bridge.ts handles internal task → invoice creation, while invoice-bridge.ts owns everything that crosses into sevDesk.
Configuration
SEVDESK_API_TOKEN— required secret (Infisical, per environment). Never persisted to DB and never sent to the client; the frontend only sees atokenConfiguredboolean. If the token is missing, the client lazy-init skips registering the payment-sync cron, but operator workflows keep working.SEVDESK_BASE_URL— optional, defaults to the sevDesk API v1 base URL.
Operational settings live in the singleton table sevdesk_config: account name/ID (from the connection test), template mappings per deal type and document kind (templateEbAr, templateEbSr, templateKopAr, templateKopSr), paymentSyncIntervalMinutes (default 10), datevChartOfAccounts (SKR03/SKR04), autoTransferInvoices, isConnected, lastSyncAt, and connectedBy (FK → contacts.id, per the Human FK Rule). Template/config UI lives in the billing System tab.
API Routes
All routes require auth + internal-user RBAC, rate-limited at 20 req/60 s. Admin-only unless noted.
| Method | Path | Purpose |
|---|---|---|
| GET / PUT | /api/sevdesk/config | Read/update operational settings (token deliberately absent — env-only) |
| POST | /api/sevdesk/test-connection | Verify API connectivity; updates sevdesk_config |
| GET | /api/sevdesk/templates | List sevDesk templates for the mapping dropdowns |
| POST | /api/sevdesk/invoices/:invoiceId/publish | Publish R1 invoice to sevDesk (non-admin allowed) |
| POST | /api/sevdesk/invoices/:invoiceId/send | Send invoice email via sevDesk (non-admin allowed) |
| POST | /api/sevdesk/invoices/:invoiceId/cancel | Cancel invoice in sevDesk (non-admin allowed) |
| GET | /api/sevdesk/invoices/:invoiceId/pdf | Download invoice PDF from sevDesk (non-admin allowed) |
| POST | /api/sevdesk/sync/payments | Manual out-of-cycle payment sync |
| POST / GET | /api/sevdesk/export/datev + /:jobId/status + /:jobId/download | DATEV export batch: trigger, poll, download ZIP |
Payment Sync (BullMQ)
Job sevdesk-payment-sync on the operations queue, scheduled every 10 minutes (*/10 * * * *), processed by processSevdeskPaymentSyncJob() in backend/src/lib/jobs/processors/billing-scheduled.ts, which calls syncPaymentStatus():
- Select invoices with
accountingProvider = 'sevdesk',externalIdset,documentStatus = 'sent',claimStatus != 'paid'. - Fetch sevDesk payment state; compare
paidAmountagainstgrossAmount. - Update
claimStatustopaid(sevDesk status 1000 or fully paid; setspaidAt) orpartially_paid. - Log a
payment_syncedevent tobilling_invoice_event_log.
A manual trigger exists in the UI (payment-refresh button in the invoice detail panel) backed by useTriggerSevdeskPaymentSyncMutation().
Link to Billing Invoices
billing_invoices carries the handoff state: accountingProvider, externalId (sevDesk invoice ID — gates the preview/download buttons in the UI), externalInvoiceNumber, accountingSyncStatus, accountingSyncedAt. billing_document_templates.sevdeskTemplateId maps R1 document templates to sevDesk templates. Invoice recipients resolve polymorphically (contact = natural person, company = legal entity) via the HubSpot association infrastructure with fallback to invoice.contactId.
Frontend
frontend/src/lib/queries/sevdeskPaymentSyncQueries.ts—useTriggerSevdeskPaymentSyncMutation()(invalidates the billing query tree on success).frontend/src/lib/queries/billingQueries.ts—useSevdeskTemplatesQuery()(5-min stale time).frontend/src/lib/api/sevdesk-settings.ts— config get/save, test-connection, templates, manual sync.- UI:
BillingSettingsProvider.tsx(System tab: config, templates, connection test) andBillingSettingsApiMapping.tsx(R1 ↔ sevDesk field mapping display).
History: landed with the billing V3 rebase PR#1674 (foundation PR#1397), refined in PR#1751.