Changelog
Notable changes by milestone. ScaiControl uses migration numbers (001–022+) as its versioning anchor; release tags follow the migration that bumped the schema.
v1.x (current)#
Outbound webhooks (migration 022)#
Two-table durable event-outbox pattern. 14 lifecycle topics ship in v1.0:
tenant.billing_linked.v1,tenant.billing_updated.v1partner.billing_linked.v1,partner.billing_updated.v1subscription.activated.v1,subscription.changed.v1,subscription.cancelled.v1,subscription.suspended.v1,subscription.resumed.v1,subscription.trial_ending.v1,subscription.payment_failed.v1pack_subscription.activated.v1,pack_subscription.changed.v1(reserved),pack_subscription.cancelled.v1
HMAC-SHA256 signing, 1m/5m/30m/2h/12h/24h backoff schedule, (subscription_id, idempotency_key) uniqueness, admin UI at /admin/webhook-subscriptions. See Concepts: webhooks.
Email template system (migration 020)#
Operator-customisable email templates per (name, document_type, language) triple. GrapesJS designer reused from invoices; Jinja syntax encoded as HTML comments (<!--JS:if-->) so the designer doesn't choke on {% %} tags. Variables differ per document_type (invoice-sent vs trial-ending vs payment-failed). Validated by scaicontrol admin templates validate.
Service packs (migration 014)#
Bundled subscriptions modeled as a parent PackSubscription linked to child Subscription rows. Pricing can be fixed_price (override the sum) or percentage (discount off the catalog sum). Lifecycle propagates parent → children atomically. Pack-level events fire instead of per-child events. See Concepts: service packs.
Identity sync (migration 013)#
scaicontrol sync pulls users + groups from ScaiKey, merging with local role assignments. Resolved the "super-admin downgrade after JWT refresh" issue: ScaiControl now re-fetches the canonical permission set from /auth/me on each token refresh.
Template designer (migration 012)#
GrapesJS-based WYSIWYG editor for invoice + credit-note templates, including a live preview pane that renders the same WeasyPrint pipeline used for production PDFs. Templates support partner-specific overrides AND language fallbacks. Blogger Sans is embedded as base64 so WeasyPrint doesn't need to fetch fonts at render time.
Accounting integrations (migration 011)#
Plugin architecture for outbound accounting sync. First implementation: Visma e-Accounting (Sweden/Norway/Finland). Hooks fire on invoice finalize → mirrored to the external accounting system. Failures don't roll back the invoice — they queue for retry.
EU-compliant invoicing (migration 008)#
The biggest milestone. Adds:
tenant_billing_profiles— buyer EU address + VAT.partner_configurationextensions — seller EU address + VAT + bank.invoice_templatestable with partner-specific and language-specific overrides.invoices.document_type(invoice|credit_note) andinvoices.referenced_invoice_id.- Buyer + seller + VAT-details snapshots frozen on finalize.
- VAT determination (
services/billing/vat.py): 4 rules covering domestic, intra-EU B2B reverse-charge per Art. 196 EU VAT Directive, intra-EU B2C, and non-EU export. - WeasyPrint-rendered PDFs in S3 keyed by invoice ID;
template_html_snapshotfor byte-stable reprints. CreditNoteSequencefor separate gap-freeSCAI-CN-YYYY-NNNNNNnumbering.- E-invoicing in migration 009 (UBL / XRechnung / CII / ZUGFeRD / Factur-X).
- Per-line tax in migration 010 (replacing single invoice-level rate).
See Concepts: invoice lifecycle, Concepts: VAT & reverse charge.
Cost-based metering (migration 007)#
Cost-derived plans where the unit price is computed from a per-unit cost + margin instead of fixed. Subscriptions inherit a billable rate updated on plan refresh; usage records reference the rate at ingestion time.
Service registry (migration 006)#
Parent-slug relationships for service hierarchy (e.g. scaicontrol-billing is a sub-component of scaicontrol). Health monitoring split from approval state — health_status is now derived continuously from heartbeats independent of registration_status.
Users + groups (migrations 003–005)#
Identity reflection from ScaiKey: users, groups, and user_groups populated by scaicontrol sync. Role bundles defined locally on groups; effective permissions = union of (user.role scopes ∪ groups.role scopes).
Marketplace (migration 002)#
Service catalog: services, plans, foundational subscription tables.
Initial schema (migration 001)#
Tenancy hierarchy (partners, tenants, users), audit log, basic ORM-layer tenant filter via do_orm_execute.
Forward-looking#
Topics deferred past v1.x — these features exist conceptually but have no producer wired up yet:
- Invoice events —
invoice.issued,invoice.paid,invoice.overdue. Will land when ScaiCRM needs them for AR automation. - Dunning escalation events — surfacing the past_due → suspended path to subscribers.
- Usage aggregate events — daily and monthly usage roll-ups as topic streams.
- Catalog product events — when a plan price changes, when a service is added/retired.
- Provisioning rejection events — explicit signal when a DAG step's compensating rollback completes.
- Plan-change service tokens — service-to-service tokens with a
scaicontrol:adminscope for CRM-driven plan changes.
Versioning policy#
- ScaiControl's external API is versioned at the URL (
/api/v1/). Within v1, only additive changes ship — no field removals, no semantics shifts. - Outbound event topics are versioned at the topic name (
subscription.activated.v1). Breaking shape changes ship a new major (v2); both versions coexist for at least one full quarter beforev1retires. - Migrations are numbered sequentially and never re-numbered. Once a migration is in
main, it's append-only — corrections ship as later migrations.