Skip to content

Accountify platform specification

Attribute Value
Document version 0.4.1
Status Draft for pilot implementation
Replaces 0.4.0
Last updated July 7, 2026
Initial scale target 300 client Odoo databases plus one Accountify HQ Odoo database
Runtime basis Odoo 19 snapshot with 764 models and 15,789 fields
First production slices Bank reconciliation, Accountify client billing, German B2B e-invoicing readiness, and bounded Shopify ecommerce pilot

This document is the normative implementation contract for the first Accountify platform release. The Accountify platform PRD is authoritative for product goals, users, workflows, feature priorities, KPIs, UX requirements, and roadmap. This specification defines system ownership, deployment profiles, tenant isolation, persistence, Odoo mappings, HTTP contracts, asynchronous execution, authorization, state machines, reliability controls, and acceptance criteria. The ecommerce contract registry is the normative domain extension for the bounded Shopify pilot and adopts every platform invariant defined here.

It complements the generated Odoo model reference. Generated model pages are authoritative only for metadata visible to the exporting identity. This specification is authoritative for Accountify behavior. A capability test in a representative Odoo clone remains required before relying on defaults, constraints, record rules, computed fields, or non-standard methods.

Odoo hosting profiles have different guarantees

Accountify HQ runs self-hosted Odoo 19 Enterprise and can install a custom module with database constraints. Client databases run Odoo Online and cannot install custom Python modules. The platform must not claim the same mutation guarantee for both profiles.

Specification map

A developer should read this page first. It provides the complete platform context and identifies the authoritative detail rather than repeating every domain schema and workflow.

Concern Authoritative contract
Product goals, target users, user journeys, feature priority, KPIs, UX requirements, and roadmap Accountify platform PRD
Platform ownership, tenancy, identity, persistence rules, common API, operations, authorization, resilience, acceptance, rollout This specification
Complete bounded Shopify pilot, contract precedence, owners, readiness boundary Ecommerce contract registry
Ecommerce runtime flow and component ownership Ecommerce architecture
Source capability, ingestion, canonical data, mappings, accounting, API/events, Odoo posting, settlements, returns/inventory, marts, governance, operations, runbooks EC-01 through EC-13 in the registry
Executable post-Odoo readback, outbox, warehouse, mart, reporting, control, and monitoring baseline Post-Odoo implementation artifacts
Exact Odoo models and fields visible to the exporter identity Generated Odoo model reference
Runtime API transport and standard ORM behavior JSON-2 guide and standard methods
German B2B E-Rechnung applicability, Odoo surface, receive/issue workflows, evidence, and rollout German B2B e-invoicing

Precedence is: approved legal/regulatory obligation, this platform specification, the ecommerce registry/architecture, the applicable domain contract, then source-specific notes/examples/generated metadata. A conflict blocks implementation until all affected contracts are updated.

1. Terms and normative language

The words must, must not, required, and shall are normative. Should describes a recommended default that may be changed through an architecture decision record. May is optional.

Term Definition
Tenant One Accountify client workspace and its authorized users
Client connection A connection to one client Odoo Online database and one or more allowed companies
HQ connection The shared connection to Accountify's self-hosted Odoo database
Operation A durable Accountify command that may cause an Odoo mutation
Projection Rebuildable, tenant-scoped data copied from Odoo for workflow or reporting
Receipt Odoo-side evidence that a unique operation was accepted and applied
Single-flight At most one Accountify worker may execute a given mutation key at a time
Authoritative readback A fresh Odoo read used to classify the final business outcome
Source connection A tenant-bound ecommerce, marketplace, or payment-provider account and its managed ingestion configuration
Canonical commerce Accountify's versioned source-independent representation of orders, payments, fulfillment, refunds, returns, disputes, settlements, and payouts
Posting intent Immutable, policy- and mapping-resolved evidence for one proposed Odoo business command
Warehouse mart Rebuildable tenant-scoped reporting product derived from canonical, Odoo readback, and reconciliation facts
Capability evidence Versioned proof that a specific connection/configuration supports a named workflow requirement
E-invoice document An invoice whose structured electronic format is authoritative and enables electronic processing under the applicable contract

Every cross-boundary payload carries a contract_version. Every durable command carries an operation_id, idempotency_key, request_hash, correlation_id, and tenant_id.

2. Objectives and non-goals

Objectives

  • Give clients and Accountify staff one web application for reporting, document exchange, work queues, review, and approved accounting actions.
  • Preserve each client Odoo database as that client's accounting system of record.
  • Use Accountify HQ Odoo for Accountify CRM, customer invoices, receivables, expenses, and corporate accounting.
  • Provide cross-client operational reporting without synchronous fan-out to hundreds of Odoo databases during page loads.
  • Execute accounting mutations through explicit business commands, durable operation state, and authoritative readback.
  • Make each in-scope German client company demonstrably capable of receiving structured B2B E-Rechnungen and separately prove issuance before enabling it.
  • Automate tenant provisioning, capability checks, credential rotation, Studio wrapper deployment, and drift detection.
  • Ingest bounded ecommerce source evidence, produce deterministic canonical commerce, and reconcile source activity through payouts, bank transactions, and Odoo clearing accounts.
  • Serve governed operational and analytical reporting from rebuildable warehouse marts without live source/Odoo fan-out.
  • Scale to 300 tenants without tenant-specific application deployments or permanently assigned workers.

Non-goals for the first production release

  • Reimplementing Odoo tax, posting, reconciliation, inventory, payment, or exchange-difference logic in Accountify.
  • Maintaining a complete replica of every client ledger.
  • Exposing arbitrary Odoo models, domains, fields, or methods to a browser.
  • Automatically posting complex accounting decisions without explicit policy and approval.
  • Supporting reconciliation write-offs, bank fees, transfers, netting, chargebacks, or multi-currency adjustments before separate policy contracts exist.
  • Using Accountify HQ Odoo as a cross-client data warehouse.
  • Claiming unconditional exactly-once mutation in Odoo Online.
  • Allowing Airbyte, source webhooks, dashboards, or warehouse models to call Odoo or decide accounting treatment directly.
  • Treating canonical commerce, projections, or marts as a second legal ledger.
  • Issuing or transporting a client invoice from Accountify when client Odoo is the approved legal invoice endpoint.

Ecommerce ingestion and posting are a first-class, independently enabled platform subsystem governed by the ecommerce architecture and complete contract registry. The subsystem adopts the tenant, identity, operation, Odoo execution-profile, authorization, resilience, and audit requirements in this specification and adds source capability, ingestion, canonical commerce, mappings, accounting policy, settlement reconciliation, warehouse reporting, governance, and runbook contracts. Documentation completeness does not bypass source evidence, Odoo clone proof, accounting/security approval, shadow runs, or production enablement gates.

3. System context and ownership

flowchart LR subgraph Users["Users"] Client["Client users"] Staff["Accountify staff"] Admin["Platform administrators"] end subgraph Platform["Accountify platform"] Web["Web application"] API["Backend API"] Ingest["Source ingestion and canonicalization"] DB[("Platform PostgreSQL")] Queue[("Managed job queue")] Workers["Shared workers"] Marts[("Warehouse marts")] Files[("Object storage")] Secrets["Secrets manager"] end subgraph Commerce["Commerce sources"] Shops["Shopify and future approved sources"] Airbyte["Airbyte replication"] end subgraph Estate["Odoo estate"] Clients["300 client Odoo Online databases"] HQ["Self-hosted Accountify Odoo Enterprise"] end Client --> Web Staff --> Web Admin --> Web Web --> API API --> DB API --> Files API --> Queue Shops --> Airbyte Shops -->|verified webhooks| Ingest Airbyte --> Ingest Ingest --> DB DB --> Marts Queue --> Workers Workers --> Secrets Workers --> Clients Workers --> HQ
System Owns Must not own
Client Odoo Client invoices, bills, structured invoice artifacts, e-invoice endpoints/transport status, payments, bank transactions, journal entries, taxes, reconciliation, statutory accounting Accountify staff assignment, cross-client workflow, Accountify billing
Ecommerce source Original commercial/financial source events and source account state Accountify accounting policy, Odoo posting decisions, cross-source identity
Airbyte Scheduled extraction and raw destination delivery Canonical identity, accounting treatment, Odoo mutation, workflow completion
Accountify platform Tenant registry, identities, assignments, tasks, documents, suggestions, e-invoice readiness/status projections, billing facts, canonical commerce, mappings, posting intents, reconciliation, operation state, audit Final client ledger state, parallel client invoice issuance, or duplicated Odoo accounting rules
Accountify HQ Odoo Accountify customers, service products, customer invoices, receivables, expenses, corporate ledger, HQ operation receipts Replicated client ledgers or cross-client operational reporting
Warehouse marts Rebuildable operational and analytical views with lineage/freshness Transactional workflow, approvals, source evidence, legal accounting authority
Object storage Uploaded evidence, encrypted raw source payloads, large readback/evidence snapshots, generated exports, document versions Credentials or authorization policy
Secrets manager Odoo/source/Airbyte/payment-provider API keys, webhook credentials, key versions, rotation metadata Business records and reporting data

Ownership rule: Accountify may read, rank, validate, approve, and orchestrate. Odoo performs the accounting mutation. Accountify reports success only after authoritative Odoo readback.

4. Runtime and deployment profiles

Use a modular monolith with separately scalable API, scheduler, and worker processes. All modules share one versioned codebase and one PostgreSQL cluster in the first release. Module boundaries are enforced through application interfaces, database ownership, and queue contracts.

src/
  identity/
  tenants/
  odoo/
  synchronization/
  reconciliation/
  billing/
  source_connections/
  ingestion/
  commerce/
  mappings/
  settlements/
  close_management/
  documents/
  work_management/
  reporting/
  warehouse/
  audit/
  operations/

Odoo execution profiles

Profile Hosting Allowed extension Mutation guarantee
hq_managed Self-hosted Odoo 19 Enterprise Required accountify_integration custom module Transactionally deduplicated by a unique Odoo receipt plus readback
client_online Odoo Online/SaaS Studio fields, Studio automation, incoming webhook Platform single-flight, atomic wrapper attempt where proven, and mandatory readback; no unconditional exactly-once claim
client_managed Future Odoo.sh/self-hosted client Accountify custom module Same receipt pattern as HQ after tenant-specific acceptance testing

The custom HQ module does not protect mutations in a separate client database. Client operations must use the controls defined for client_online unless that client is moved to a managed hosting profile.

5. Tenant, identity, and connection model

A tenant maps to one client Odoo database and one or more explicitly allowed Odoo companies. An ecommerce-enabled tenant also has one or more independently provisioned source connections. The shared HQ connection may be bound to every billable tenant. Odoo record IDs are not globally unique. Every Odoo record reference uses:

(tenant_id, connection_id, company_id, odoo_model, odoo_record_id)

Every ecommerce source reference uses:

(tenant_id, source_connection_id, source_object_type, source_object_id)

Connection rules:

  • The browser submits an Accountify tenant_id, never an Odoo URL, database name, company context, model, or method.
  • The backend resolves connections through an authorized tenant-to-connection binding.
  • Secret values remain in the secrets manager; PostgreSQL stores only secret references and rotation metadata.
  • Every client database uses a dedicated operating identity and API key.
  • Bootstrap credentials are separate, short-lived, and unavailable to normal workers.
  • Every Odoo request sets allowed_company_ids, includes a company domain where the model supports it, and rejects records returned for any other company.
  • Context alone is not a tenant boundary. Response records and relational targets must be company-validated.
  • A disabled, unhealthy, drifted, or capability-expired connection cannot execute mutations.
  • Source content never selects its tenant, source connection, Odoo connection, company, adapter, policy, or mapping set. The backend resolves all bindings from trusted control-plane configuration.
  • Each source connection has a dedicated secret reference, external-account fingerprint, configured-catalog hash, adapter version, status, and capability evidence.
  • Source and Odoo enablement are independent. Ecommerce posting requires both the source profile and every required Odoo capability to be supported and current.
  • Interactive routes operate on one tenant per database transaction.
  • Workers set one tenant context per job and clear it when the transaction ends.

6. Normative PostgreSQL design

The SQL below is a logical contract. Migrations may add implementation details, but may not weaken tenant keys, foreign keys, uniqueness, or row-level security without a specification change.

Foundation and connection binding

create table tenants (
  id uuid primary key default gen_random_uuid(),
  slug text not null unique,
  legal_name text not null,
  status text not null check (status in ('provisioning','active','suspended','offboarded')),
  reporting_timezone text not null,
  base_currency_code char(3) not null,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  unique (id, status)
);

create table users (
  id uuid primary key default gen_random_uuid(),
  external_identity_id text not null unique,
  email text not null,
  display_name text not null,
  user_type text not null check (user_type in ('client','staff','service','platform_admin')),
  status text not null check (status in ('invited','active','disabled')),
  created_at timestamptz not null default now()
);

create table tenant_memberships (
  tenant_id uuid not null references tenants(id),
  user_id uuid not null references users(id),
  role text not null check (role in (
    'client_viewer','client_contributor','staff_preparer','staff_reviewer',
    'billing_preparer','billing_approver','operations_admin'
  )),
  active boolean not null default true,
  created_at timestamptz not null default now(),
  primary key (tenant_id, user_id, role)
);

create table odoo_connections (
  id uuid primary key default gen_random_uuid(),
  execution_profile text not null check (execution_profile in ('hq_managed','client_online','client_managed')),
  base_url text not null,
  database_name text not null,
  operating_secret_ref text not null,
  bootstrap_secret_ref text,
  webhook_secret_ref text,
  odoo_major_version integer not null check (odoo_major_version = 19),
  wrapper_version text,
  wrapper_code_hash text,
  status text not null check (status in ('pending','healthy','degraded','disabled')),
  last_health_check_at timestamptz,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  unique (base_url, database_name)
);

create table tenant_odoo_connections (
  tenant_id uuid not null references tenants(id),
  connection_id uuid not null references odoo_connections(id),
  connection_role text not null check (connection_role in ('client_ledger','hq_billing')),
  allowed_company_ids bigint[] not null,
  active boolean not null default true,
  created_at timestamptz not null default now(),
  primary key (tenant_id, connection_id),
  unique (tenant_id, connection_role),
  check (cardinality(allowed_company_ids) > 0)
);

create table tenant_capabilities (
  tenant_id uuid not null,
  connection_id uuid not null,
  capability text not null,
  status text not null check (status in ('unknown','supported','unsupported','degraded')),
  evidence jsonb not null default '{}'::jsonb,
  checked_at timestamptz not null,
  expires_at timestamptz,
  primary key (tenant_id, connection_id, capability),
  foreign key (tenant_id, connection_id)
    references tenant_odoo_connections(tenant_id, connection_id)
);

The schema must also enforce these invariants:

  • A connection with profile hq_managed may be bound only as hq_billing.
  • A client profile may be bound only as client_ledger.
  • Each tenant has exactly one active client_ledger binding before activation.
  • One shared HQ connection is active per environment.
  • Changes to connection bindings, companies, and secret references are audited.

Tenant isolation and database roles

Every tenant-owned table, including child and join tables, contains tenant_id uuid not null. Every parent table exposes unique (tenant_id, id). Every tenant-owned foreign key includes tenant_id.

Example:

foreign key (tenant_id, contract_id)
  references client_contracts(tenant_id, id)

Required PostgreSQL controls:

alter table billing_runs enable row level security;
alter table billing_runs force row level security;

create policy billing_runs_tenant_policy on billing_runs
  using (tenant_id = current_setting('app.tenant_id', true)::uuid)
  with check (tenant_id = current_setting('app.tenant_id', true)::uuid);
  • Apply equivalent policies to every tenant-owned table.
  • Runtime API and worker roles must not own tables and must not have BYPASSRLS.
  • Set app.tenant_id transaction-locally after application authorization and before any tenant query.
  • Reject a transaction when tenant context is missing.
  • Connection pools must reset all session state before reuse.
  • Cross-client portfolio endpoints query an authorized portfolio projection keyed by staff identity; they do not bypass RLS on operational tables.
  • Maintenance access uses a separate, time-limited role and produces an enhanced audit event.

Operations, attempts, outbox, and inbox

create table integration_operations (
  tenant_id uuid not null,
  id uuid not null default gen_random_uuid(),
  connection_id uuid not null,
  operation_type text not null,
  contract_version text not null,
  idempotency_key text not null,
  request_hash text not null,
  correlation_id text not null,
  status text not null check (status in (
    'created','queued','executing','verifying','confirmed','rejected',
    'unknown','repair_required','cancelled'
  )),
  safe_request_summary jsonb not null,
  result_payload jsonb,
  lease_owner text,
  lease_expires_at timestamptz,
  attempt_count integer not null default 0,
  last_error_code text,
  last_error_safe_message text,
  started_at timestamptz,
  completed_at timestamptz,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  primary key (tenant_id, id),
  unique (tenant_id, connection_id, operation_type, idempotency_key),
  foreign key (tenant_id, connection_id)
    references tenant_odoo_connections(tenant_id, connection_id)
);

create table operation_attempts (
  tenant_id uuid not null,
  id uuid not null default gen_random_uuid(),
  operation_id uuid not null,
  attempt_number integer not null,
  delivery_id text not null,
  started_at timestamptz not null,
  finished_at timestamptz,
  outcome text not null check (outcome in ('running','response','timeout','transport_error','rejected')),
  response_hash text,
  safe_error jsonb,
  primary key (tenant_id, id),
  unique (tenant_id, operation_id, attempt_number),
  foreign key (tenant_id, operation_id)
    references integration_operations(tenant_id, id)
);

create table outbox_events (
  tenant_id uuid not null,
  id uuid not null default gen_random_uuid(),
  event_type text not null,
  contract_version text not null,
  aggregate_type text not null,
  aggregate_id uuid not null,
  deduplication_key text not null,
  payload jsonb not null,
  available_at timestamptz not null default now(),
  published_at timestamptz,
  attempt_count integer not null default 0,
  lease_owner text,
  lease_expires_at timestamptz,
  created_at timestamptz not null default now(),
  primary key (tenant_id, id),
  unique (tenant_id, deduplication_key)
);

create table inbox_messages (
  tenant_id uuid not null,
  consumer text not null,
  message_id text not null,
  received_at timestamptz not null default now(),
  completed_at timestamptz,
  payload_hash text not null,
  primary key (tenant_id, consumer, message_id)
);

Domain state and its outbox event are committed in one PostgreSQL transaction. Consumers claim work with a lease, record inbox deduplication before side effects, and tolerate redelivery. Broker acknowledgement is not evidence that an Odoo mutation succeeded.

Synchronization and projections

create table sync_cursors (
  tenant_id uuid not null,
  connection_id uuid not null,
  model text not null,
  cursor_write_date timestamptz,
  cursor_record_id bigint,
  run_high_watermark timestamptz,
  overlap_seconds integer not null default 300,
  generation bigint not null default 0,
  lease_owner text,
  lease_expires_at timestamptz,
  last_success_at timestamptz,
  last_membership_sweep_at timestamptz,
  status text not null check (status in ('idle','running','failed','paused')),
  primary key (tenant_id, connection_id, model),
  foreign key (tenant_id, connection_id)
    references tenant_odoo_connections(tenant_id, connection_id)
);

Required projections:

Table Required unique key Purpose
bank_transactions (tenant_id, connection_id, odoo_record_id) Bank and card reconciliation queue
open_account_items (tenant_id, connection_id, odoo_record_id) Open AR/AP candidates
client_invoice_summaries (tenant_id, connection_id, odoo_record_id) Client invoice and payment reporting
einvoice_document_summaries (tenant_id, connection_id, company_id, odoo_record_id) Rebuildable structured-document, transport-status, validation, and work-item projection
hq_invoice_summaries (tenant_id, connection_id, odoo_record_id) Accountify billing status
staff_portfolio_items (tenant_id, work_type, local_record_id) Authorized cross-client operations view

Each projection includes tenant_id, connection_id, company_id, source write_date, synced_at, source_hash, and a local resource_version. Store only fields required by Accountify. Detailed Odoo records remain on demand.

Ecommerce domain persistence

The ecommerce contract registry is authoritative for domain schemas and invariants. The platform database owns these tenant-scoped table families:

Family Representative tables Authority
Source control source_connections, source_capability_evidence, ingestion_runs, ingestion_inbox Source capabilities and ingestion
Canonical commerce commerce_events, commerce_orders, lines, payments, fulfillments, refunds, returns, disputes, settlements, payouts Canonical model
Mapping and policy mapping_sets, mapping_rules, mapping_usage, tenant_posting_policies, posting intents Mappings and accounting policy
Odoo synchronization Odoo object mappings, operation/readback evidence, inventory reconciliation cases Odoo posting and returns/inventory
Settlement control Settlement lines, allocations, payout matches, clearing evidence Settlements and payouts
Reporting and governance Fact/dimension/mart models, refresh state, export/audit/restriction metadata Warehouse marts and data governance

Every ecommerce table follows the same composite tenant foreign-key and forced-RLS rules as platform tables. In addition:

  • Source/Odoo/canonical identifiers use their full scoped composite identity; no external numeric/string ID is globally unique.
  • Raw payloads are referenced through opaque encrypted object-storage keys and hashes, not copied through operational tables.
  • Approved policy and mapping versions are immutable and captured by every posting intent and operation.
  • Canonical facts and events retain adapter/schema lineage; corrections append evidence rather than rewriting confirmed operations.
  • Warehouse facts and marts are rebuildable and cannot be foreign-key authority for transactional workflows.
  • Retention/deletion cannot remove inputs required to reproduce retained financial evidence or published controls without an approved disposition.

Reconciliation data

create table reconciliation_candidates (
  tenant_id uuid not null,
  id uuid not null default gen_random_uuid(),
  bank_transaction_id uuid not null,
  candidate_move_line_ids jsonb not null,
  proposed_allocations jsonb not null,
  score numeric(8,5) not null check (score between 0 and 1),
  explanation jsonb not null,
  source_fingerprint text not null,
  scoring_version text not null,
  status text not null check (status in ('suggested','accepted','rejected','expired')),
  created_at timestamptz not null default now(),
  expires_at timestamptz not null,
  primary key (tenant_id, id),
  foreign key (tenant_id, bank_transaction_id)
    references bank_transactions(tenant_id, id)
);

create table reconciliation_operations (
  tenant_id uuid not null,
  id uuid not null,
  bank_transaction_id uuid not null,
  candidate_id uuid,
  action text not null check (action in ('exact_match','partial_match','mark_to_check')),
  selected_move_line_ids jsonb not null,
  allocations jsonb not null,
  expected_source_fingerprint text not null,
  requested_by uuid not null references users(id),
  approved_by uuid references users(id),
  verified_at timestamptz,
  primary key (tenant_id, id),
  foreign key (tenant_id, id)
    references integration_operations(tenant_id, id),
  foreign key (tenant_id, bank_transaction_id)
    references bank_transactions(tenant_id, id),
  foreign key (tenant_id, candidate_id)
    references reconciliation_candidates(tenant_id, id),
  check (approved_by is null or approved_by <> requested_by)
);

Candidate JSON is allowed because selected Odoo move-line IDs are external references, but its shape is versioned and schema-validated before persistence.

Billing data

Every billing child table includes tenant_id and a composite tenant foreign key.

create table client_contracts (
  tenant_id uuid not null,
  id uuid not null default gen_random_uuid(),
  hq_partner_id bigint not null,
  contract_code text not null,
  currency_code char(3) not null,
  billing_frequency text not null check (billing_frequency in ('monthly','quarterly','annual')),
  starts_on date not null,
  ends_on date,
  status text not null check (status in ('draft','active','suspended','ended')),
  primary key (tenant_id, id),
  unique (tenant_id, contract_code),
  check (ends_on is null or ends_on >= starts_on)
);

create table pricing_versions (
  tenant_id uuid not null,
  id uuid not null default gen_random_uuid(),
  contract_id uuid not null,
  version integer not null,
  effective_from date not null,
  effective_to date,
  status text not null check (status in ('draft','active','retired')),
  primary key (tenant_id, id),
  unique (tenant_id, contract_id, version),
  foreign key (tenant_id, contract_id)
    references client_contracts(tenant_id, id),
  check (effective_to is null or effective_to >= effective_from)
);

create table pricing_rules (
  tenant_id uuid not null,
  id uuid not null default gen_random_uuid(),
  pricing_version_id uuid not null,
  service_code text not null,
  rule_type text not null check (rule_type in ('fixed','per_unit','tiered','manual')),
  included_quantity numeric(20,6),
  unit_price numeric(20,6),
  tiers jsonb,
  hq_product_id bigint not null,
  hq_tax_ids jsonb not null default '[]'::jsonb,
  hq_analytic_account_id bigint,
  primary key (tenant_id, id),
  unique (tenant_id, pricing_version_id, service_code),
  foreign key (tenant_id, pricing_version_id)
    references pricing_versions(tenant_id, id)
);

create table billing_usage_events (
  tenant_id uuid not null,
  id uuid not null default gen_random_uuid(),
  service_code text not null,
  occurred_at timestamptz not null,
  quantity numeric(20,6) not null,
  source_type text not null,
  source_key text not null,
  source_evidence jsonb not null,
  reversal_of_event_id uuid,
  status text not null check (status in ('pending','included','excluded','reversed')),
  primary key (tenant_id, id),
  unique (tenant_id, service_code, source_type, source_key),
  foreign key (tenant_id, reversal_of_event_id)
    references billing_usage_events(tenant_id, id)
);

create table billing_runs (
  tenant_id uuid not null,
  id uuid not null default gen_random_uuid(),
  contract_id uuid not null,
  pricing_version_id uuid not null,
  revision integer not null,
  supersedes_run_id uuid,
  period_start date not null,
  period_end date not null,
  currency_code char(3) not null,
  status text not null check (status in (
    'draft','calculating','ready_for_review','approved','exporting','exported',
    'verification_failed','post_approved','posting','posted','partially_paid',
    'paid','cancelled','reversed','repair_required'
  )),
  calculation_hash text,
  approved_by uuid references users(id),
  approved_at timestamptz,
  hq_invoice_id bigint,
  created_by uuid not null references users(id),
  last_material_editor_id uuid not null references users(id),
  primary key (tenant_id, id),
  unique (tenant_id, contract_id, period_start, period_end, revision),
  foreign key (tenant_id, contract_id)
    references client_contracts(tenant_id, id),
  foreign key (tenant_id, pricing_version_id)
    references pricing_versions(tenant_id, id),
  foreign key (tenant_id, supersedes_run_id)
    references billing_runs(tenant_id, id),
  check (period_end > period_start),
  check (approved_by is null or approved_by <> last_material_editor_id)
);

create table billing_run_usage_events (
  tenant_id uuid not null,
  billing_run_id uuid not null,
  usage_event_id uuid not null,
  quantity_snapshot numeric(20,6) not null,
  evidence_hash text not null,
  primary key (tenant_id, billing_run_id, usage_event_id),
  foreign key (tenant_id, billing_run_id)
    references billing_runs(tenant_id, id),
  foreign key (tenant_id, usage_event_id)
    references billing_usage_events(tenant_id, id)
);

billing_run_lines also contains tenant_id, immutable product/tax/analytic snapshots, quantity, unit price, net amount, evidence, and a stable line number. Approved runs and their usage-event links are immutable. Corrections create a new revision.

Migrations must enforce that active pricing-version effective ranges do not overlap for the same (tenant_id, contract_id), using a PostgreSQL exclusion constraint or an equivalently serialized database invariant. An application-only pre-check is insufficient.

Work, documents, audit, and close

Table Normative requirement
accounting_periods Tenant, company, period dates, state, checklist version, owner, reviewer, due/completion timestamps, reopen reason
close_checklist_versions Immutable versioned task definitions and dependency graph
period_close_tasks Tenant, period, checklist item, state, assignee, reviewer, evidence hash, completion timestamps
period_transitions Append-only actor, from/to state, reason, evidence, timestamp
work_items Tenant, period, type, state, priority, assignee, reviewer, due date, source link
document_requests Tenant, work item, requester, recipient, state, due date, object key, checksum
audit_events Tenant, actor, action, entity, correlation ID, before/after hashes, safe metadata, timestamp

Audit and period transitions are append-only. Do not store bearer tokens, webhook URLs, full bank payloads, or unnecessary personal data in audit metadata.

Required indexes

  • Queue tables: (tenant_id, status, available_at).
  • Projections: (tenant_id, source_write_date, odoo_record_id) plus relevant business date.
  • Due work: (tenant_id, due_at) with a partial index for incomplete states.
  • Operations: (tenant_id, status, created_at) and (connection_id, operation_type, idempotency_key).
  • Outbox: partial index on (available_at) where published_at is null.
  • Leases: partial indexes on lease_expires_at for claimable rows.

7. Odoo integration contracts

Client Odoo Online model mapping

Accountify use Odoo model Required fields Policy
Tenant/company res.company id, name, currency_id Read and validate allowed company
Contacts res.partner Identity, commercial partner, email, VAT, company, write_date Read; controlled draft create/write only
Invoice summaries account.move Type, state, payment state, partner, company, currency, dates, totals, residual, reference, write_date Read; guarded AP drafts where enabled
E-invoice documents account.move ubl_cii_xml_id, ubl_cii_xml_filename, peppol_message_uuid, peppol_move_state, type, state, partner, company, totals, write_date Read and project; retain structured evidence under approved policy
E-invoice company/routing res.company, account.journal Proxy state, endpoint scheme/ID, send capability, reception mode/journal, Peppol-journal status Read for per-company readiness; authorized Odoo administration owns configuration
E-invoice counterparty res.partner invoice_edi_format, invoice_sending_method, endpoint scheme/ID, verification state Read; controlled configuration only after counterparty verification
E-invoice proxy identity account_edi_proxy_client.user Identification, mode, registration state Operations-only read; never project private keys or secrets
Open items account.move.line Move/account/partner IDs, dates, balance, residuals, currency, reconcile state, write_date Read; reconcile only through wrapper
Bank/card queue account.bank.statement.line Move, company, journal, date, payment reference, partner, amounts, currencies, is_reconciled, write_date Read; mutation only through wrapper
Verification account.partial.reconcile, account.full.reconcile Linked lines, amounts, currencies, full-reconcile link Read
Configuration account.journal, account.account, account.tax, res.currency Active same-company configuration and precision Read

Optional task, time, and document mirrors remain adapters. Accountify remains workflow authority and object storage remains portal document authority.

Accountify HQ Odoo model mapping

Purpose Odoo model Contract Policy
Customer res.partner Customer identity, VAT, addresses, payment terms, fiscal position Stable HQ partner mapping
Service product product.product Product, income account, taxes Read and capability-check configuration
Draft invoice account.move move_type = out_invoice, partner, company, currency, dates, terms, lines, reference Create through HQ command module
Invoice lines account.move.line Product, description, quantity, price, taxes, account, analytics Nested through invoice_line_ids
Posting account.move Call the action_post() method on the approved invoice record Separate approved command plus readback
Payment status account.move State, payment state, residual, write_date Incremental sync

The current runtime snapshot exposes no Subscriptions model. Accountify owns billing schedules. Installing Subscriptions later requires an architecture decision establishing one authoritative scheduler.

HQ custom module

The self-hosted HQ database must install a versioned accountify_integration module. The module provides an accountify.operation.receipt model with:

  • Unique operation_uuid enforced by an Odoo database constraint.
  • operation_type, contract_version, request_hash, state, safe error code, target model, target ID, and timestamps.
  • A restricted integration group and access rules.
  • A business method for billing export and a separate method for posting.
  • No generic proxy for arbitrary model or method invocation.

HQ billing execution:

  1. Begin one Odoo request transaction.
  2. Insert the operation receipt.
  3. On unique conflict, load the existing receipt. Return it only when request_hash matches; otherwise reject the command.
  4. Validate partner, company, currency, product, taxes, accounts, analytic configuration, period, and lock dates.
  5. Create the complete draft invoice using Odoo ORM logic.
  6. Link the invoice to the receipt and set the receipt result.
  7. Commit receipt and invoice together, or roll both back.
  8. Return identifiers and verification fields. Accountify still performs independent readback.

Prefer an authenticated model business method over a custom HTTP controller. Add a controller only when a protocol requirement cannot be satisfied through the supported Odoo API, and threat-model it separately.

Client Odoo Online Studio wrapper

Odoo Online cannot install the HQ module. Each enabled client database uses a versioned Studio wrapper that has been proven in a clone.

Required Studio fields:

Model Field Purpose
account.bank.statement.line x_accountify_last_operation_id Replay and recovery evidence for the most recent accepted operation
account.bank.statement.line x_accountify_wrapper_version Evidence of the wrapper contract used

Required platform controls:

  • Acquire a durable single-flight lease keyed by (tenant_id, bank_statement_line_id) before delivery.
  • Allow only one non-terminal reconciliation operation for a statement line.
  • Include operation ID, contract version, expected fingerprint, expected write dates, and a per-attempt delivery ID.
  • Validate body secret, action, company, records, state, residuals, accounts, currency, lock dates, and marker before mutation.
  • Execute validation, native reconciliation, and marker update in one webhook request where clone evidence proves transaction behavior.
  • Treat timeout or lost response as unknown. Read Odoo before any redelivery.
  • Never use the marker as the only success proof.

The Studio URL and body secret are bearer credentials. Rotate them, redact them from logs, and do not describe the request as HMAC-signed unless HMAC has been implemented and proven in the Studio environment.

Credential separation is mandatory. The bootstrap credential that may create or update base.automation and ir.actions.server records is distinct from the operating credential used at runtime, and the operating identity must not be able to write server-action code. The June 2026 deployment smoke test ran under an administrator-scoped identity; production tenants require a restricted integration user whose minimum group set is determined and recorded during capability testing.

The delivery transport is under a recorded evaluation gate. The webhook trigger is the baseline. Invoking the same versioned server action through JSON-2 ir.actions.server.run() โ€” standard API-key authentication, Odoo access-rights enforcement, structured error responses โ€” is the preferred candidate if clone evidence proves it under the restricted operating identity. Whichever transport is adopted, the wrapper code, validation, and readback contract are identical, and the decision must be recorded before production enablement.

Approved Odoo surface

Method or route Allowed use
search_read, read Bounded projection reads and verification
default_get Capability tests and context-dependent defaults
create, write Explicitly mapped draft objects and metadata only
fields_get Capability discovery, never normal page rendering
read_group Bounded aggregates after tenant and company validation
HQ custom business methods Billing receipt, invoice export, posting
Client Studio webhook Reconciliation transaction for the configured statement line
Odoo e-invoice send/receive Odoo-managed inbound processing and staff-driven outbound send in the first release; automated send only through a separately approved clone-proven business command

No browser or user-provided payload may select an Odoo model, method, field list, context, or domain dynamically.

All Odoo API calls use the JSON-2 API. The legacy /xmlrpc, /xmlrpc/2, and /jsonrpc endpoints are prohibited for new code: Odoo schedules their removal in Odoo Online 21.1 (winter 2027) and Odoo 22 (fall 2028), and client databases upgrade on Odoo's schedule.

8. Mutation and idempotency guarantees

Common guarantees

  • The API persists an operation before publishing work.
  • The idempotency key is unique within (tenant_id, connection_id, operation_type).
  • The request body is canonicalized and stored as request_hash.
  • Reusing a key with the same hash returns the existing operation.
  • Reusing a key with a different hash returns 409 IDEMPOTENCY_KEY_REUSED.
  • Delivery is at least once. Workers and consumers must tolerate redelivery.
  • No timed-out mutation is blindly retried.
  • confirmed requires authoritative readback, not an HTTP 2xx response.

Profile-specific guarantee

Profile Accepted guarantee
HQ managed One Odoo receipt per operation UUID, receipt and invoice committed atomically, then verified by readback
Client Online One active Accountify command per contract-defined business key, serialized delivery, replay/search evidence, and verified classification; no unconditional exactly-once guarantee against arbitrary concurrent external actors

Any documentation or UI text using "exactly once" must name the profile and the enforced boundary.

9. HTTP API contract

The normative machine-readable API must be published as OpenAPI before implementation begins. This section defines conventions that OpenAPI may not weaken.

Common conventions

  • Base path: /api/v1.
  • OAuth/OIDC platform authentication only; Odoo credentials never reach the browser.
  • Every tenant resource path includes /tenants/{tenant_id}.
  • Tenant membership, role, assignment, and resource state are authorized server-side.
  • List endpoints use opaque cursor pagination with deterministic ordering.
  • Money is serialized as decimal strings and currency as an ISO 4217 code.
  • Timestamps are UTC ISO 8601. Business dates are YYYY-MM-DD.
  • Mutable resources include resource_version and support If-Match where a user edits reviewed state.
  • Commands require Idempotency-Key and return 202 Accepted with an operation resource.
  • Responses include contract_version and correlation_id.
  • Unknown JSON properties are rejected on command payloads.

Standard command response:

{
  "contract_version": "1.0",
  "operation_id": "0190...",
  "status": "queued",
  "correlation_id": "corr_01J...",
  "links": {"self": "/api/v1/tenants/018f.../operations/0190..."}
}

Standard error:

{
  "error": {
    "code": "ODOO_CONCURRENCY_CONFLICT",
    "message": "The accounting record changed after it was reviewed.",
    "correlation_id": "corr_01J...",
    "retryable": false,
    "details": {"action": "reload_and_review"}
  }
}

Required status semantics:

Status Use
400 Malformed request or unsupported property
401 Missing or invalid platform identity
403 Valid identity without tenant/role permission
404 Resource absent or deliberately concealed by tenant isolation
409 Idempotency mismatch, stale resource version, conflicting active operation
422 Valid shape but unsupported accounting scenario or failed business invariant
429 Tenant or user rate limit
502 Safe upstream rejection before mutation
503 Dependency unavailable before mutation
202 Durable asynchronous command accepted

Never expose Odoo tracebacks, SQL, credentials, webhook URLs, or raw financial payloads in errors.

Reconciliation endpoints

GET  /api/v1/tenants/{tenant_id}/bank-transactions
GET  /api/v1/tenants/{tenant_id}/bank-transactions/{id}
GET  /api/v1/tenants/{tenant_id}/bank-transactions/{id}/candidates
POST /api/v1/tenants/{tenant_id}/reconciliation-operations
GET  /api/v1/tenants/{tenant_id}/operations/{operation_id}

Verification is an internal job. An operations administrator may request verification through a separate audited operations endpoint; normal users cannot force state transitions.

Billing endpoints

GET  /api/v1/tenants/{tenant_id}/billing/contracts
POST /api/v1/tenants/{tenant_id}/billing/runs
GET  /api/v1/tenants/{tenant_id}/billing/runs/{billing_run_id}
POST /api/v1/tenants/{tenant_id}/billing/runs/{billing_run_id}/recalculate
POST /api/v1/tenants/{tenant_id}/billing/runs/{billing_run_id}/approve
POST /api/v1/tenants/{tenant_id}/billing/runs/{billing_run_id}/export
POST /api/v1/tenants/{tenant_id}/billing/runs/{billing_run_id}/approve-posting

Ecommerce endpoints and events

The ecommerce API and event contract is normative for source operations, commerce resources, mappings, posting intents, settlements, reports, event names, consumer deduplication, and compatibility. Its routes use the same identity, tenant, authorization, pagination, money, versioning, error, idempotency, and asynchronous-command rules defined above.

Route groups:

/api/v1/operations/tenants/{tenant_id}/source-capabilities
/api/v1/operations/tenants/{tenant_id}/ingestion-runs
/api/v1/tenants/{tenant_id}/commerce/*
/api/v1/tenants/{tenant_id}/mapping-sets/*
/api/v1/tenants/{tenant_id}/settlements/*
/api/v1/tenants/{tenant_id}/payout-matches/*
/api/v1/tenants/{tenant_id}/reports/*

No ecommerce request or event authorizes arbitrary source execution, SQL, Odoo model/method selection, or accounting mutation. Domain events are facts; an authorized, durable posting command remains required before Odoo mutation.

E-invoicing endpoints

GET  /api/v1/tenants/{tenant_id}/e-invoicing/capabilities
GET  /api/v1/tenants/{tenant_id}/e-invoicing/documents
GET  /api/v1/tenants/{tenant_id}/e-invoicing/documents/{document_id}
POST /api/v1/operations/tenants/{tenant_id}/e-invoicing/capability-scans
POST /api/v1/operations/tenants/{tenant_id}/e-invoicing/documents/{document_id}/refresh

The first implementation exposes readiness, document/status projection, and operations refresh. It does not expose arbitrary send or retry. A future outbound command requires a versioned OpenAPI operation, fixed Odoo method/inputs, authorization and separation of duties, durable idempotency, clone evidence, and authoritative XML/message/status readback.

The German B2B e-invoicing guide is normative for legal scope, separate receive/issue capability gates, Odoo fields, structured-evidence precedence, deduplication, failure classification, retention, fixtures, and rollout.

Operations endpoints

GET  /api/v1/operations/tenants
GET  /api/v1/operations/tenants/{tenant_id}/health
POST /api/v1/operations/tenants/{tenant_id}/capability-scans
POST /api/v1/operations/tenants/{tenant_id}/sync-runs
POST /api/v1/operations/tenants/{tenant_id}/wrapper-deployments
POST /api/v1/operations/tenants/{tenant_id}/wrapper-verifications
POST /api/v1/operations/{operation_id}/verification-runs
GET  /api/v1/operations/incidents

Administrative mutations require step-up authentication, a reason, and enhanced audit.

10. Reconciliation contract

Supported first-release actions

  • exact_match: same company and supported currency; selected open residuals settle the bank amount within Odoo currency rounding.
  • partial_match: same company and supported currency; a reviewer explicitly approves every positive allocation amount.
  • mark_to_check: no reconciliation mutation; records a work decision only.

Fees, write-offs, transfers, netting, chargebacks, exchange differences, cash-basis tax exceptions, and multi-currency adjustments are detected and routed to manual Odoo handling until their own policy contracts are approved.

Monetary conventions

  • Decimal values are strings at API boundaries and fixed-precision decimals internally.
  • Allocation amounts are positive settlement magnitudes. Direction is derived from the signed Odoo balances and bank amount.
  • Currency precision and rounding come from the active Odoo res.currency configuration captured during capability checks.
  • The sum of allocations must equal the supported settlement amount within currency rounding.
  • No implicit write-off or tolerance is allowed.
  • All selected lines must be posted, unreconciled, reconcilable, in the allowed company, and compatible with the statement line.
  • A scenario requiring an exchange rate or exchange-difference entry returns 422 RECONCILIATION_SCENARIO_UNSUPPORTED in the first release.

Canonical source fingerprint

The fingerprint is SHA-256 of canonical UTF-8 JSON with sorted object keys, fixed decimal strings, and move lines sorted by numeric ID. It includes:

  • Statement line ID, write_date, move ID, company, journal, date, signed amount, currencies, partner, and reconciliation state.
  • Selected move-line IDs, write_date, move state, company, account, partner, balance, residuals, currencies, and reconciliation links.
  • Currency precision, lock-date evidence, wrapper version, and scoring policy version.

Changing any included value invalidates the candidate and requires a new review.

Command payload

{
  "contract_version": "1.0",
  "bank_transaction_id": "018f...",
  "candidate_id": "0190...",
  "action": "partial_match",
  "selected_move_line_ids": [456, 789],
  "allocations": [
    {"move_line_id": 456, "amount": "100.00", "currency": "EUR"},
    {"move_line_id": 789, "amount": "25.00", "currency": "EUR"}
  ],
  "expected_source_fingerprint": "sha256:...",
  "resource_version": 7
}

Execution and verification

  1. Authorize tenant, assignment, role, action, and separation of duties.
  2. Recompute the request hash and enforce idempotency semantics.
  3. Lock the platform statement resource and reject another active operation.
  4. Refresh the statement, move lines, configuration, and lock-date evidence from Odoo.
  5. Recompute the fingerprint and reject stale review.
  6. Persist and queue the operation before delivery.
  7. Acquire the single-flight delivery lease.
  8. Invoke the versioned Studio wrapper.
  9. Move every response, timeout, or transport error to verifying or unknown; never directly to confirmed.
  10. Read statement reconciliation state, residuals, partial/full reconcile records, company, and operation marker.
  11. Classify the operation as confirmed, rejected, unknown, or repair_required.
stateDiagram-v2 [*] --> Created Created --> Queued Queued --> Executing Executing --> Verifying: response received Executing --> Unknown: timeout or lost response Executing --> Rejected: proven pre-mutation rejection Unknown --> Verifying: readback scheduled Verifying --> Confirmed: authoritative evidence matches Verifying --> Rejected: no mutation and safe rejection Verifying --> RepairRequired: ambiguous or divergent evidence RepairRequired --> Verifying: repaired and rechecked Confirmed --> [*] Rejected --> [*]

unknown is not failure. Only readback can determine whether redelivery is safe.

11. Billing contract

Period and usage semantics

  • Billing periods use half-open intervals: period_start <= occurred_at < period_end in the contract timezone.
  • Exactly one active pricing version may cover a billing instant.
  • Usage events are append-only. Corrections use reversal events; source events are never edited after inclusion.
  • Calculation selects usage events in a repeatable-read transaction and records them in billing_run_usage_events.
  • The calculation hash covers contract, pricing version, rule snapshots, usage IDs and quantities, lines, products, taxes, analytics, currency, and period.
  • Approval freezes the run, lines, evidence, pricing snapshot, and usage-event links.
  • Material correction creates a new run revision and reapproval.

HQ invoice export

  1. Billing preparer calculates and reviews commercial lines and evidence.
  2. A different billing approver approves the immutable run.
  3. The worker sends the operation UUID, request hash, tenant mapping, invoice header, and line snapshots to the HQ custom module.
  4. The HQ module transactionally creates or returns the unique receipt and draft invoice.
  5. Odoo applies its product, account, fiscal-position, tax, currency, and rounding logic.
  6. Accountify reads back invoice lines, taxes, totals, company, currency, state, and receipt.
  7. Any difference from the approved calculation outside currency rounding sets verification_failed and blocks posting.
  8. Posting requires a separate approval and HQ command receipt.
  9. Posted, partial-payment, paid, cancelled, and reversal states synchronize back from Odoo.
stateDiagram-v2 [*] --> Draft Draft --> Calculating Calculating --> ReadyForReview ReadyForReview --> Calculating: edit or recalculate ReadyForReview --> Approved: independent approval Approved --> Exporting Exporting --> Exported: receipt and draft verified Exporting --> VerificationFailed: divergent evidence Exporting --> RepairRequired: ambiguous outcome VerificationFailed --> Calculating: new revision RepairRequired --> Exporting: repaired and safe retry Exported --> PostApproved PostApproved --> Posting Posting --> Posted: post receipt and readback Posting --> RepairRequired Posted --> PartiallyPaid Posted --> Paid PartiallyPaid --> Paid Draft --> Cancelled ReadyForReview --> Cancelled Exported --> Cancelled: cancel draft in Odoo Posted --> Reversed: Odoo credit/reversal flow

12. Synchronization contract

Incremental algorithm

  1. Acquire a fenced lease on (tenant_id, connection_id, model).
  2. Resolve allowed companies, model capability, field contract, cursor, and a fixed run high watermark.
  3. Query a bounded window ordered by (write_date, id) using a tuple predicate, not offset pagination.
  4. Start from cursor_write_date - overlap_seconds; deduplicate by external key and source version.
  5. Require write_date <= run_high_watermark so a moving source cannot starve completion.
  6. Request only versioned projection fields and include company identifiers.
  7. Reject records outside allowed companies.
  8. Upsert one page and its source hashes in one platform transaction.
  9. Advance (cursor_write_date, cursor_record_id) only after commit.
  10. Release the lease only when the generation/fencing token still matches.

Deletions and domain exits

Incremental write_date does not prove membership completeness. Each projection defines a membership sweep:

  • Re-read source IDs for the bounded company/domain and compare them to active projection IDs.
  • Mark missing records as tombstoned before physical deletion.
  • Treat record-rule or company-scope changes as a new projection generation requiring a full sweep.
  • Reconciliation queues ingest changed records even after they leave the "unreconciled" domain so a newly reconciled line is removed promptly.
  • A count mismatch alone is diagnostic, not deletion authority.

Freshness targets

Data Target while Odoo is healthy Requirement
Bank transactions and open items 99% within 15 minutes Explicit refresh before review
Client invoice/payment summaries 99% within 60 minutes Live read for sensitive drill-down
E-invoice receipt/delivery status 99% within 15 minutes after Odoo state change Explicit refresh for pending/error drill-down; age always visible
HQ invoice status 99% within 15 minutes Event or polling adapter
Historical aggregates Daily Rebuildable generation
Capabilities and wrapper drift Daily and on change Expired critical capability disables mutation

Dashboards query PostgreSQL, not 300 live databases. Every freshness-sensitive view shows synced_at, source status, and stale-data state.

13. Asynchronous jobs

Job Concurrency key Retry policy Success evidence
sync_model_incremental connection:model Retry bounded transport/5xx; pause auth/schema errors Cursor advanced after committed upserts
sync_model_membership connection:model:generation Bounded Membership sweep and tombstones committed
score_reconciliation_candidates tenant:statement Recompute deterministically Candidate and fingerprint stored
execute_reconciliation tenant:statement Never blindly retry unknown mutation Readback classifies result
verify_reconciliation operation Bounded polling with incident threshold Residual and reconcile evidence classified
generate_billing_run tenant:contract:period Deterministic recalculation Immutable hash and usage links
export_billing_invoice billing_run HQ receipt lookup before redelivery Receipt and draft invoice verified
post_billing_invoice billing_run HQ receipt lookup before redelivery Posted state read back
sync_hq_invoice_status hq:move Bounded read retry State, payment state, residual updated
scan_einvoice_capability tenant:connection:company No retry for unsupported configuration Versioned receive/issue evidence stored separately
sync_einvoice_documents connection:company:einvoice Bounded read retry; pause auth/schema errors Structured-document/status projection committed
refresh_einvoice_status tenant:einvoice_document Bounded polling; never blind resend XML/message/status evidence classified
tenant_capability_scan tenant:connection No retry for unsupported capability Versioned evidence stored
studio_wrapper_drift_check tenant:connection Bounded read retry Version/hash/no-op probe passes
credential_expiry_scan connection Alerting job Rotation task created
outbox_dispatch outbox:shard Jittered retry and lease recovery Broker acknowledgement stored
source_capability_scan tenant:source_connection No retry for unsupported schema/permission Versioned source evidence stored
source_ingestion_monitor tenant:source_connection:provider_job Bounded provider polling Run counts, catalog hash, cursor/checkpoint stored
canonicalize_source_record tenant:inbox_record:adapter_version Deterministic replay; quarantine permanent contract errors Events, lineage, and projections committed
source_completeness_check tenant:source_connection:stream:period Daily retry then incident IDs, counts, amounts, and gaps classified
build_posting_intent tenant:canonical_aggregate:policy_version Retry only transient reads Immutable ready/blocked intent stored
execute_ecommerce_command Contract-defined business key Never blindly retry unknown mutation Odoo readback confirms or classifies result
verify_ecommerce_command operation Increasing bounded verification Confirmed, safe-to-retry, or repair-required
allocate_settlement tenant:settlement Deterministic replay Every line allocated or exception classified
match_payout tenant:payout Retry while bank matching window is open Approved match or explicit exception
refresh_warehouse_mart tenant:mart:partition Safe shadow rebuild Quality gates pass and partition publishes atomically

Queue requirements:

  • Separate interactive accounting, verification, synchronization, ingestion/canonicalization, settlement, warehouse, billing, and maintenance queues.
  • Interactive verification outranks bulk synchronization.
  • Apply per-tenant rate and concurrency limits.
  • Use weighted fair scheduling so one degraded or large tenant cannot starve others.
  • Open one circuit breaker per tenant connection, not one global Odoo breaker.
  • Exhausted jobs enter a dead-letter queue and create an integration incident.
  • Lease expiry permits recovery, but fencing tokens prevent stale workers from committing cursor or operation state.

14. Authorization and separation of duties

Roles

Role Scope Allowed decisions
Client viewer Own tenant Reports, billing evidence, document status
Client contributor Own tenant Upload documents and respond to requests
Staff preparer Assigned tenants Prepare work, candidates, drafts; cannot approve own high-risk work
Staff reviewer Assigned tenants Approve supported accounting commands and period close
Commerce preparer Assigned ecommerce tenants Resolve source/mapping exceptions and prepare posting/settlement work; cannot approve own accounting-sensitive work
Commerce approver Assigned ecommerce tenants Approve supported mapping sets, posting intents, settlement/payout actions not prepared or materially edited by self
Data operations Connection and warehouse scope Source capability scans, bounded backfills, mart refresh/repair; no accounting approval or unrestricted raw-payload access
Billing preparer Contract-assigned tenants Calculate, adjust, and explain billing runs
Billing approver Contract-assigned tenants Approve runs/posting not prepared or adjusted by self
Operations admin Connection scope Health, sync, credential rotation, wrapper deployment; no accounting approval
Platform admin Platform Configuration and audited break-glass only

Normative rules:

  • Assignment restricts staff access in addition to role.
  • A preparer or last material editor cannot approve the same reconciliation, billing run, or close transition.
  • A mapping/policy preparer or last material editor cannot approve the same accounting-sensitive ecommerce configuration or posting intent.
  • Odoo posting and reconciliation require both platform permission and Odoo integration-identity permission.
  • Source backfill, mart repair, or capability administration does not grant accounting-posting authority.
  • Platform administrators do not receive routine accounting authority.
  • Break-glass access is time-limited, requires step-up authentication and a reason, alerts security, and cannot silently bypass readback.
  • Disabled identities and memberships take effect on the next request and revoke active sessions according to identity-provider policy.
  • Authorization is evaluated again when a queued command executes, not only when it is requested.

15. Accounting period close

Period close is a platform workflow over authoritative Odoo evidence. Completing a platform period does not itself change an Odoo lock date.

stateDiagram-v2 [*] --> NotStarted NotStarted --> Open Open --> Review: required tasks and evidence complete Review --> Open: reviewer requests changes Review --> Complete: independent reviewer approval and final Odoo checks Complete --> Reopened: authorized exception with reason Reopened --> Review: affected tasks repeated

Required close controls:

  • Freeze one checklist version when a period opens.
  • Enforce task dependencies and required evidence.
  • Read Odoo lock dates, unreconciled bank items, draft entries, exceptions, and integration health before review and completion.
  • For ecommerce-enabled tenants, require settlement/payout allocation status, unresolved refund/dispute/return exceptions, source completeness, mart control status, and Odoo provider-clearing residual evidence for the close period.
  • Record the exact Odoo evidence hash used for completion.
  • Require a different reviewer from the period owner for completion.
  • Reopening requires step-up authorization, reason, impacted-task selection, and an enhanced audit event.
  • Any later Odoo change that invalidates completion creates a close-drift incident; it does not silently rewrite history.
  • Changing Odoo lock dates is a separate future command with its own approval and receipt contract.

16. Security, privacy, and resilience

Required security controls

The ecommerce data governance contract is normative for ecommerce classification, minimization, raw payload access, retention, deletion/restriction, exports, lineage, non-production data, and tenant offboarding. It may impose stricter controls than this summary.

  • Store Odoo keys and webhook credentials in a managed secrets service and rotate before expiry.
  • Store ecommerce source, Airbyte, and payment-provider credentials in the same managed secret boundary with independent versions and rotation.
  • Verify source webhook signatures against the unmodified body, enforce payload limits/replay controls, and acknowledge only after durable receipt.
  • Use TLS and an allowlist of provisioned Odoo hostnames; block redirects to untrusted hosts.
  • Prevent SSRF by resolving connections only from the tenant registry.
  • Redact authorization headers, webhook URLs, secrets, and sensitive payloads from logs and traces.
  • Encrypt PostgreSQL, queue, backups, and object storage at rest.
  • Use tenant-prefixed object keys and short-lived signed URLs.
  • Malware-scan uploads and verify content type, size, extension, and checksum.
  • Record document version, checksum, uploader, tenant, and access history.
  • Apply data minimization and a documented retention/offboarding policy approved by legal and security owners.
  • Treat raw source payloads and customer/financial evidence as restricted; ordinary support, reporting, logs, and traces use IDs/hashes and pseudonymous dimensions.
  • Prohibit intentional collection/storage of CVV, authentication passwords, magnetic-stripe data, and unnecessary full card PAN; detection quarantines data and triggers the security runbook.
  • Enforce tenant isolation and authorization in warehouse facts, marts, report caches, drill-through, and asynchronous exports as well as operational stores.
  • Separate production, staging, and test credentials and data.
  • Test RLS, object storage, caches, search indexes, exports, and logs for cross-tenant leakage.
  • Review the HQ custom module and Studio wrapper as security-sensitive code.

Backup and disaster recovery targets

Initial production targets, subject to business approval:

Control Target
Platform PostgreSQL RPO 5 minutes or better
Platform service RTO 4 hours or better
Object storage durability Managed versioning and lifecycle policy
Restore test Quarterly and before major migration
Secret recovery Documented rotation without database restore
Odoo outage behavior Queue safely, show stale state, disable mutation when preconditions cannot be refreshed

Restore tests must prove tenant isolation, operation uniqueness, outbox/inbox consistency, source cursor replay with overlap/deduplication, unknown Odoo-operation verification, mart control-hash rebuilds, retention/hold preservation, and the ability to resume without duplicate Odoo mutation.

17. Observability and service objectives

Required telemetry:

  • API rate, latency, error, tenant, route, actor class, and correlation ID.
  • Odoo profile, model/business method, safe record identifier, duration, response class, and retry class.
  • Queue depth, oldest age, attempts, dead letters, lease recovery, and tenant circuit state.
  • Sync lag, cursor generation, membership sweep age, and tombstone count by connection/model.
  • Reconciliation outcome and repair rate by wrapper version.
  • Per-tenant concurrency_conflict rate as an external-mutation policy signal, and changes to accounting-capable users or reconcile-model auto-validate configuration in client databases.
  • Billing receipt reuse, duplicate-prevention recovery, verification failure, and posting status.
  • Capability expiry, credential expiry, wrapper drift, and close drift.
  • Source webhook/Airbyte freshness, configured-catalog drift, cursor age, completeness differences, and quarantined canonical records.
  • Ecommerce posting-intent, mapping exception, operation/unknown/repair, refund/return/dispute, settlement/payout, and Odoo clearing outcomes.
  • Warehouse mart refresh/freshness, failed quality gates, source-to-fact-to-mart control differences, metric version, and stale-report status.
  • Restricted-data access, export generation/download/expiry, retention/hold actions, and tenant-offboarding progress.

Never use raw financial payloads, names, emails, bank labels, or document identifiers as metric labels.

Initial service objectives:

Objective Target
Platform-only API availability 99.9% monthly
Platform-only API latency p95 below 500 ms for non-export list/detail routes
Dashboard latency p95 below 2 seconds at the 300-tenant load profile
Accepted command durability Operation committed before 202; zero acknowledged commands lost
Interactive verification start p95 within 60 seconds while dependencies are healthy
Valid ecommerce webhook to canonical projection p95 within 5 minutes
Approved ecommerce command readback p95 within 2 minutes; p99 within 15 minutes while Odoo is healthy
Operational warehouse marts 95% within their contract-defined freshness targets
Tenant 90-day reporting query p95 below 2 seconds without source/Odoo network calls
Unknown ecommerce mutation classification 99% within 30 minutes; all remaining cases incidented before 24 hours
Cross-tenant security Zero successful negative-isolation probes
Duplicate Odoo mutation Zero in the defined replay and timeout acceptance suite

External Odoo downtime is reported separately from platform availability, but degraded behavior and safe recovery remain Accountify responsibilities.

18. Testing and acceptance

Test layers

Layer Required coverage
Unit Pricing, scoring, canonical hashes, transitions, authorization, monetary invariants
Database Composite tenant FKs, RLS, idempotency, outbox/inbox, leases, immutable approvals, concurrency
Contract OpenAPI, JSON schema, unknown properties, errors, idempotency replay, webhook versions
Odoo HQ integration Receipt uniqueness, invoice creation, tax/default behavior, posting, timeout recovery
Odoo Online integration Clone-based exact/partial reconciliation, marker behavior, readback, timeout classification
Odoo e-invoicing Per-company receive/issue capability, XRechnung/ZUGFeRD fixtures, endpoint verification, inbound routing, XML integrity, delivery states, duplicate/correction/error/timeout behavior
End-to-end Browser to platform to each Odoo profile and back for pilot tenants
Load 300 schedules, representative projection volume, queue fairness, Odoo degradation, dashboard queries
Security RLS, role escalation, company escape, SSRF, replay, redaction, uploads, signed URLs
Recovery Worker crash, lease expiry, queue redelivery, database restore, cursor rebuild, Odoo outage
Source/adapter Exact connector catalog/version, scopes, cursor/lookback, webhook signature/duplicate/order, schema drift, bounded backfill
Ecommerce accounting Orders, fulfillment, invoices, payments, refunds, returns, disputes, settlements, payouts, clearing, unsupported cases
Warehouse Fact/mart grain and uniqueness, lineage, late corrections, metric fixtures, freshness, tenant/date/currency controls
Governance Classification, prohibited-data detection, retention/hold, subject restriction, export, offboarding
Runbook Ingestion lag, Odoo unknown outcome, settlement imbalance, credentials, drift, stale mart, isolation incident, restore

Mandatory reconciliation cases include exact match, approved partial match, multiple invoices, stale fingerprint, double-click, duplicate delivery, timeout before commit, timeout after commit, manual concurrent Odoo change, wrong company, draft move, non-reconcilable account, lock date, missing suspense configuration, unsupported fee/write-off/transfer/FX scenario, and wrapper drift.

Mandatory billing cases include repeated idempotency key with same and different payloads, concurrent export, timeout after receipt creation, product/tax/fiscal-position change, rounding difference, corrected usage, run revision, posting approval, partial payment, cancellation, reversal, and HQ module upgrade.

Mandatory e-invoicing cases include valid and invalid XRechnung/ZUGFeRD, unsupported profile, missing structured requirement, standalone PDF classification, Peppol and approved alternative intake, duplicate delivery, correction and credit-note chains, structured/rendered mismatch, wrong company/endpoint, unregistered receiver, recipient format mismatch, queued/delayed/error/unknown delivery, original XML retrieval/hash, role and attachment isolation, and clone/provider migration without duplicate production registration.

Mandatory ecommerce cases are defined by EC-01 through EC-13 in the ecommerce contract registry. They include duplicate/out-of-order delivery, late source correction, connector/catalog drift, ambiguous mapping, partial fulfillment, refund without return, return without refund, dispute won/lost, settlement/payout/bank mismatch, unknown Odoo outcome, mart replay/late rebuild, prohibited data, subject restriction, offboarding, and cross-tenant negative tests.

Pilot acceptance criteria

  1. A user can access only an authorized tenant and never receives Odoo credentials.
  2. Negative RLS, API, cache, object-storage, and export tests cannot retrieve another tenant's data.
  3. The UI displays synchronized unreconciled transactions with freshness and source-health metadata.
  4. The platform ranks supported candidates and explains the scoring inputs.
  5. Stale fingerprints and resource versions are rejected before mutation.
  6. One approved click creates one durable operation before returning 202.
  7. Same-key replay returns the operation; different-payload replay returns 409.
  8. Client Online delivery is single-flight per statement line.
  9. The Studio wrapper validates and performs only the supported mutation.
  10. Accountify verifies residuals and reconcile records before success.
  11. Timeout-after-commit is classified through readback without duplicate mutation.
  12. Unsupported accounting cases are routed to manual handling with 422, not approximated.
  13. Billing calculation freezes pricing, usage links, lines, and evidence.
  14. HQ export creates one unique receipt and one verified draft invoice under concurrent replay.
  15. Posting requires separate approval and receipt.
  16. Invoice posting, partial payment, payment, cancellation, and reversal synchronize back.
  17. Period completion requires checklist evidence, fresh Odoo checks, and an independent reviewer.
  18. Every sensitive action is attributable through append-only audit without secret leakage.
  19. An ecommerce workflow cannot enable until its source and Odoo capability profiles are current and fully supported.
  20. Source delivery, canonicalization, mappings, policy, posting intent, operation attempts, Odoo readback, reconciliation, mart lineage, and exports are end-to-end traceable.
  21. Duplicate and out-of-order source delivery produces one canonical outcome and no duplicate confirmed Odoo mutation.
  22. Settlement components balance to provider payout, matched bank evidence, and authoritative Odoo clearing residual or an approved explicit exception.
  23. Every governed mart passes grain, uniqueness, lineage, freshness, and source-to-fact-to-mart controls; failed refreshes retain the last valid partition and expose stale state.
  24. Ecommerce classification, retention/hold, restricted access, export expiry, subject restriction, and tenant offboarding pass storage-wide tests.
  25. All required ecommerce runbooks pass tabletop/failure-injection exercises with recorded exit evidence.
  26. Every enabled German company has separate, current receive and issue capability evidence; one capability never implies the other.
  27. Inbound structured invoices preserve the original structured part, route to the expected Odoo/AP state, and classify invalid, duplicate, correction, and mismatch cases explicitly.
  28. Outbound delivery is completed only from generated structured evidence and authoritative Odoo transport-status readback; pending, error, and unknown states cannot be presented as delivered.

Scale acceptance criteria

  • Execute the agreed 300-tenant synthetic profile with representative record and schedule volumes documented in the test report.
  • Meet API, dashboard, sync-freshness, and verification-start objectives at p95.
  • Demonstrate weighted queue fairness with at least 10% of tenant connections degraded.
  • Recover killed workers and expired leases without duplicate mutation or cursor regression.
  • Restore PostgreSQL within RTO and resume outbox and sync processing without cross-tenant or idempotency violations.
  • Rebuild representative ecommerce tenant-year mart partitions without blocking other tenants and reconcile source/canonical/Odoo/settlement controls.
  • Demonstrate fair scheduling when source ingestion, Odoo, and mart workloads are simultaneously degraded for a tenant cohort.

19. Implementation order

Milestone 0: contract proof

  • Refresh runtime snapshots for HQ and pilot clients.
  • Create controlled Odoo fixtures.
  • Prove HQ module receipt uniqueness, invoice creation, posting, and readback.
  • Prove exact and partial reconciliation through Studio, including timeout and concurrent-change tests.
  • Publish OpenAPI and webhook JSON schemas.
  • Publish the e-invoicing projection/operations OpenAPI and prove receive/issue fixtures for one representative German client company.
  • Approve the ecommerce contract registry and publish ecommerce OpenAPI/event schemas, source capability fixtures, canonical/database contracts, policy schemas, mart models/tests, governance metadata, and runbook exercise plan.

Milestone 1: secure foundation

  • Implement identity, tenant registry, Odoo/source connection registries, composite tenant schema, forced RLS, Odoo gateway, secrets, restricted object storage, operation log, outbox/inbox, queue, leases, warehouse-serving roles, and audit.
  • Complete automated cross-tenant negative tests before feature work merges.

Milestone 2: synchronization and reconciliation

  • Implement projections, tuple cursors, membership sweeps, candidate scoring, staff UI, command execution, verification, and repair workflows.
  • Implement e-invoice readiness/document projections, AP routing, status refresh, exception handling, and drift monitoring without automated outbound send.

Milestone 3: billing and HQ module

  • Implement contracts, pricing, usage events, immutable approvals, HQ receipts, invoice export, posting approval, and status synchronization.

Milestone 4: ecommerce shadow and controlled pilot

  • Implement source capability scans, ingestion, canonicalization, mappings/policies, posting intents, Odoo adapters/readback, settlement/payout reconciliation, warehouse marts, governance controls, and runbook automation.
  • Complete a no-mutation shadow period with source/canonical/mart controls, then Odoo clone mutation tests, then one tenant with manual approval for every command.
  • Enable policy automation only after all EC-12 pilot criteria and EC-13 runbook exercises pass and accounting/security owners sign off.

Milestone 5: close and client operations

  • Implement close checklists, document requests, staff queues, review controls, client portal, capacity, and profitability views.

Milestone 6: 300-tenant readiness

  • Automate Odoo/source onboarding, rotation, wrapper/webhook deployment, capability/catalog/mapping drift checks, fair scheduling, load tests, mart partition rebuilds, cohorts, canaries, restore/offboarding tests, and incident runbooks.

20. Versioning and change control

  • Patch: clarification with no changed behavior, persistence, or payload.
  • Minor: backward-compatible endpoint, state, job, field, or policy addition.
  • Major: incompatible persistence, authorization, webhook, API, or accounting behavior.
  • API, webhook, queue, outbox, and Odoo receipt payloads carry explicit versions.
  • Consumers support the current and immediately previous compatible version during rollout.
  • Production migrations are forward-only and include repair and rollback-of-release procedures.
  • Accounting mutation changes require an architecture decision record, clone evidence, security review, canary tenant, and staged rollout.
  • A specification change that weakens isolation, idempotency, approval, or readback requires explicit security and accounting-owner approval.
  • Ecommerce domain contracts are independently versioned, but any change to platform tenancy, identity, authorization, operation semantics, Odoo execution profiles, governance minimums, SLOs, or acceptance gates must update this specification in the same change set.
  • The ecommerce registry records affected contract IDs, owners, compatibility, migration, evidence, and approvals; documentation and executable schemas must not drift.

Version 0.3.0 promotes the bounded Shopify ecommerce pilot from a linked optional note to a first-class platform subsystem and aligns platform runtime, persistence ownership, APIs/events, jobs, roles, period close, governance, observability, acceptance, recovery, and rollout with EC-00 through EC-13.

Version 0.4.0 makes German B2B e-invoicing readiness a first-class platform contract, assigns legal invoice transport to each client Odoo database, and aligns projections, Odoo mappings, APIs, jobs, tests, acceptance, and rollout with the dedicated guide.