spb/air Public MIT
AIR — The Language of Accounting.
Python 100%
1# =============================================================================2# Projet : AIR — Accounting Intermediate Representation3# Auteur : Simon-Pierre Boucher4# Contact : contact@spboucher.ai5# Fichier : backend.py6# Description : QuickBooks Online backend — optional target, fully testable offline.7# =============================================================================8"""QuickBooks Online backend.910QuickBooks is an OPTIONAL export target: AIR's books of record live in the11native ledger regardless. This backend lowers compiled journals to QBO12JournalEntry payloads and posts them through a pluggable transport — the13offline mock by default (no QuickBooks account needed anywhere in the14project), the HTTP transport only when the user supplies credentials.1516Idempotency: every create carries requestid = f"{key}:{DocNumber}" so retries17never double-post. Reversal: QBO has no native journal reversal, so reverse()18posts exact contra entries (never deletes).19"""20from __future__ import annotations2122from typing import Any2324from backends.base import (25 Backend,26 BackendCapabilities,27 PostingReceipt,28 ReversalReceipt,29 TargetPayload,30)31from backends.quickbooks.mapper import contra_body, map_journal32from backends.quickbooks.transport import MockQboTransport, QboTransport33from core.journal import CompiledJournal343536class QuickBooksBackend(Backend):37 def __init__(38 self,39 transport: QboTransport | None = None,40 account_map: dict[str, dict[str, str]] | None = None,41 ):42 self.transport = transport if transport is not None else MockQboTransport()43 self.account_map = account_map44 self._posted: dict[str, dict[str, Any]] = {} # DocNumber -> body posted4546 def capabilities(self) -> BackendCapabilities:47 return BackendCapabilities(48 name="quickbooks",49 posts_remotely=True,50 native_reversal=False, # contra-entry strategy51 multi_currency=True,52 idempotency="requestid",53 notes="QBO JournalEntry API, minorversion=75; optional target — "54 "native ledger remains the book of record",55 )5657 def compile(self, journal: CompiledJournal) -> TargetPayload:58 return TargetPayload(59 backend="quickbooks",60 format="qbo.journalentry",61 body=map_journal(journal, self.account_map),62 )6364 def post(self, payload: TargetPayload, idempotency_key: str) -> PostingReceipt:65 bodies = payload.body66 assert isinstance(bodies, list)67 details: dict[str, str] = {}68 for body in bodies:69 request_id = f"{idempotency_key}:{body['DocNumber']}"70 entity = self.transport.create_journal_entry(body, request_id)71 details[body["DocNumber"]] = entity["Id"]72 self._posted[body["DocNumber"]] = body73 return PostingReceipt(74 backend="quickbooks",75 reference=",".join(details.values()),76 idempotency_key=idempotency_key,77 details=details,78 )7980 def reverse(self, receipt: PostingReceipt) -> ReversalReceipt:81 reversed_ids: list[str] = []82 for doc_number in receipt.details:83 body = self._posted.get(doc_number)84 if body is None:85 continue86 contra = contra_body(body)87 request_id = f"rev:{receipt.idempotency_key}:{contra['DocNumber']}"88 entity = self.transport.create_journal_entry(contra, request_id)89 reversed_ids.append(entity["Id"])90 return ReversalReceipt(91 backend="quickbooks",92 reference=",".join(reversed_ids),93 reversed_reference=receipt.reference,94 )95