Mappings and master data contract¶
Mappings convert validated source identity into approved Accountify policy and Odoo targets. They are business configuration, not connector transformation code. A worker never searches Odoo and silently selects the first plausible record.
Mapping types¶
| Type | Source key | Target |
|---|---|---|
company |
source connection + market/legal entity | allowed Odoo company |
product |
source platform + variant ID and/or SKU | Odoo product and unit of measure |
customer |
source customer or B2C strategy key | Odoo partner or approved anonymous partner strategy |
tax |
jurisdiction + tax code/title + rate + inclusive flag + supply class | Odoo tax and fiscal position |
shipping |
source shipping/service code | Odoo service product and tax treatment |
discount |
discount type/funding party | allocation rule or Odoo discount product |
payment |
provider + transaction type + currency | Odoo payment journal, method, clearing account |
fee |
provider + fee/adjustment type + currency | account, tax, analytic dimensions |
warehouse |
source location/fulfillment service | Odoo warehouse, picking type, locations |
return_disposition |
source location + reason/disposition | Odoo return destination and handling rule |
dispute |
provider + dispute type/stage | clearing/expense account and review rule |
Data model¶
create table mapping_sets (
tenant_id uuid not null,
id uuid not null default gen_random_uuid(),
version integer not null,
status text not null check (status in ('draft','approved','retired')),
effective_from timestamptz not null,
effective_to timestamptz,
approved_by uuid,
approved_at timestamptz,
created_at timestamptz not null default now(),
primary key (tenant_id, id),
unique (tenant_id, version),
check (effective_to is null or effective_to > effective_from)
);
create table mapping_rules (
tenant_id uuid not null,
id uuid not null default gen_random_uuid(),
mapping_set_id uuid not null,
mapping_type text not null,
source_connection_id uuid,
company_id bigint not null,
precedence integer not null check (precedence > 0),
source_key jsonb not null,
source_key_sha256 char(64) not null,
target jsonb not null,
evidence jsonb not null,
status text not null check (status in ('active','blocked','retired')),
created_at timestamptz not null default now(),
primary key (tenant_id, id),
unique (tenant_id, mapping_set_id, mapping_type, source_key_sha256),
foreign key (tenant_id, mapping_set_id)
references mapping_sets(tenant_id, id)
);
create table mapping_usage (
tenant_id uuid not null,
mapping_rule_id uuid not null,
posting_intent_id uuid not null,
resolved_at timestamptz not null,
target_fingerprint char(64) not null,
primary key (tenant_id, mapping_rule_id, posting_intent_id)
);
Approved mapping sets and rules are immutable. A correction creates a new set version. Effective ranges for approved sets may not overlap for the same tenant and posting profile.
Resolution algorithm¶
- Select the approved mapping set captured by the tenant posting policy and operation date.
- Restrict rules to the tenant, source connection where applicable, allowed Odoo company, mapping type, and active status.
- Canonicalize the source key and compute its hash.
- Match exact keys before any approved pattern rule.
- Apply the lowest numeric precedence only when exactly one rule remains.
- Validate the target through current source/Odoo capability evidence.
- Persist the rule ID, set version, target fingerprint, and evidence in the posting intent.
- Return a stable exception when no rule or multiple rules remain.
Resolution is deterministic: the same canonical inputs, policy version, mapping set, and capability evidence produce the same result.
Matching constraints¶
- Product mapping prefers immutable source variant ID. SKU may be an additional exact key but is not assumed globally unique.
- Customer matching may use a verified source customer ID. Email or name alone cannot auto-link a financial partner.
- Tax mapping requires jurisdiction/scope and inclusive behavior; rate alone is insufficient.
- Account, journal, tax, product, warehouse, and location targets must be active and company-correct.
- Currency-specific mappings never apply to another currency.
- A generic fallback product, customer, tax, account, or warehouse requires explicit accounting approval and a visible exception marker.
- Fuzzy matching can rank suggestions but cannot create an active rule without reviewer approval.
Approval and separation of duties¶
| Mapping | Preparer | Approver |
|---|---|---|
| Product/customer | Integration preparer | Integration reviewer |
| Tax/fiscal position | Accounting preparer | Accounting approver |
| Journal/account/payment/fee/dispute | Accounting preparer | Accounting approver |
| Warehouse/return disposition | Operations preparer | Inventory/accounting approver |
The same user cannot approve a mapping set containing their own accounting-sensitive rule changes. Approval records the diff, reason, evidence, affected tenants/workflows, and effective time.
Drift¶
Target fingerprints include relevant Odoo write_date, active state, company, currency, account type, tax use/type, journal type, and warehouse/location identity. Source fingerprints include stable external identity and required semantic fields.
Drift marks the rule blocked or the mapping set degraded when:
- the Odoo target is archived, deleted, moved to another company, or materially reconfigured;
- source identity becomes ambiguous or a duplicate SKU appears;
- a tax rate/scope or fiscal position changes;
- a journal/account/payment method no longer supports the policy;
- a warehouse/location route or valuation configuration changes;
- the required capability evidence expires.
New posting intents fail with MAPPING_TARGET_DRIFTED. Confirmed historical operations keep their immutable mapping evidence.
API¶
GET /api/v1/tenants/{tenant_id}/mapping-sets
POST /api/v1/tenants/{tenant_id}/mapping-sets
GET /api/v1/tenants/{tenant_id}/mapping-sets/{mapping_set_id}
POST /api/v1/tenants/{tenant_id}/mapping-sets/{mapping_set_id}/rules
POST /api/v1/tenants/{tenant_id}/mapping-sets/{mapping_set_id}/validate
POST /api/v1/tenants/{tenant_id}/mapping-sets/{mapping_set_id}/approve
POST /api/v1/tenants/{tenant_id}/mapping-sets/{mapping_set_id}/retire
GET /api/v1/tenants/{tenant_id}/mapping-exceptions
POST /api/v1/tenants/{tenant_id}/mapping-exceptions/{exception_id}/resolve
Writes use If-Match, reject unknown properties, and require Idempotency-Key for approval/retirement commands. Validation returns a complete diff and does not mutate Odoo.
Stable errors¶
| Code | Meaning |
|---|---|
MAPPING_REQUIRED |
No approved rule matches |
MAPPING_AMBIGUOUS |
Multiple rules have equal effective precedence |
MAPPING_SET_NOT_APPROVED |
Policy references a draft/retired set |
MAPPING_SET_OUTSIDE_EFFECTIVE_RANGE |
No approved set applies to the operation date |
MAPPING_TARGET_INACTIVE |
Target is archived/deleted |
MAPPING_TARGET_COMPANY_MISMATCH |
Target is outside the allowed Odoo company |
MAPPING_TARGET_DRIFTED |
Target fingerprint differs from approved evidence |
MAPPING_CAPABILITY_EXPIRED |
Required source/Odoo evidence is stale |
Acceptance criteria¶
- Database constraints prevent overlapping approved effective ranges and duplicate canonical keys.
- Property tests prove deterministic resolution independent of database row order.
- Cross-company, cross-currency, inactive, ambiguous, and stale targets fail closed.
- A mapping change cannot alter a previously approved posting intent.
- Approval separation, audit, rollback-of-release, and drift detection are tested.