Skip to content

Accounting Placeholder Audit — features that exist in the model but aren't 100% functional

Date: 2026-06-15 Scope: accounting-related GraphQL domains only Trigger: TaxRate.account_payable_id / account_receivable_id / withholding_account_id turned out to be stored-and-echoed placeholders that no posting logic ever reads. This document sweeps the rest of the accounting surface for the same pattern.


Re-verification & remediation — 2026-06-27

All 42 findings were re-checked against HEAD and the still-pending ones triaged into cleanup now / fix now / feature (deferred). Original finding text below is preserved as the 2026-06-15 record.

Resolved since the audit

  • #1 — TaxRate GL-account links: now fully functional and tested. The tax-rate GL wiring feature renamed the columns (output_tax_account_id / input_tax_account_id / withholding_asset_account_id) and they are consumed at posting time by invoice_ledger_service and supplier_invoice_ledger_service (covered by tests/graphql/ledger/test_tax_account_sourcing.py).

Verified NOT a bug (no change needed)

  • #12 (adjacent double-post risk): false alarm. OpeningInvoiceService / OpeningSupplierInvoiceService create the AR/AP invoice as an aging-only subledger record via session.add() — they post no ledger entry of their own (confirmed by the class docstring and the absence of any ledger_service call). The single OPENING_BALANCES entry is the only GL posting, so AR/AP are posted exactly once.
  • #34 (recurring-journal idempotency): the scheduler is already idempotent — materialize_due advances next_run_date past as_of, so list_due never re-selects a journal in the same period. last_run_at is an informational write-only field, not a missing guard.

Fixed now (with tests)

  • #27 — depreciation double-post: FixedAssetService.run_period now refuses a period that already has a POSTED run (tests/graphql/fixed_assets/test_depreciation_idempotency.py).
  • #11 — account landing-page balance: AccountQueryRepository.paginated_stmt now computes each account's real posted GL balance (debit-normal vs credit-normal) instead of literal(0) (tests/graphql/accounts/test_account_balance.py).

Dead code removed now

  • #7 stale pac_transactions/ bytecode tree; #8 SupplierDefault + client_defaults.py; #9 AgeInvoice enum; #10 orphan validate_supplier_defaults_accounts; #22 last_closed() stub; #32 unused LedgerEntry.reversal_of/reversed_by ORM relationships (scalar FK columns kept); #41 dead RecurringInvoiceRunStatus enum.

Shipped as feature builds — 2026-06-27

  • #16 bank-reconciliation settlement → feature (2.9.0).
  • #36–#39 supplier-credit application → feature (2.9.1).
  • #24 depreciation-run reversal, #25 units-of-production rejected at registration, #26 DRAFT runs retired → feature (2.10.0).
  • #31 is_posted made real via unpost/repost of manual journal entries → feature (2.11.0). (The audit's "headline" claim that is_posted is never False no longer holds.)

Still deferred — schema change or real feature (need product review)

  • Dead enum members #14 SPLIT / #15 COMPLETED / #21 IN_PROGRESS / #33 EDITED_MEMO: verified inert. Removing them changes the GraphQL schema; #21 and #33 are auto() members whose removal would reindex later persisted values (data corruption). Retained pending a dedicated enum-cleanup with a data migration. (#24 REVERSED is now produced — no longer dead.)
  • #35 unused Account.journal_details / User.journals back-refs (removal needs matching back_populates edits); #42 Payment.qb_sync_enabled column (drop needs a migration).
  • Remaining features: #17/#19 bank matching/detection (embeddings + auto-detect); #28 budget approval gating; #30 budget project/office variance scope (the ledger has no project/office dimension — needs that added first); #40 sales-credit due_amount semantics; #2/#3 declaration/certificate PDF persistence; #12 journal-link field (OpeningBalanceCommitResponse.journal is typed JournalResponse but the entry is a LedgerEntry — needs a response-type change); units-of-production depreciation (build once per-period usage is captured).

Still-none-resolved informational findings (#4, #5, #6, #13, #18, #20, #23, #29) are inert-but-acceptable model fields/labels; left as-is.


What counts as a finding

A field, enum value, relationship, mutation, or service method that is declared (and often written) but never consumed by business logic — never read by any ledger-posting, calculation, matching, report, query-filter, or state-transition path. Echoing a value back in a GraphQL *_response serializer does not count as "used."

Three severity tiers:

Tier Meaning
placeholder Stored/echoed but never consumed anywhere. Pure dead data.
partial Consumed in some paths, but a specific state/branch/dimension is dead or ignored.
stub A method/mutation that runs but does nothing meaningful (no-op, returns constant, or raises).

Verification method: each finding was checked by grepping the whole app/ for real reads of the field, then classifying every hit as "real use" vs "serialization echo." Citations are file:line. The headline claims (is_posted never False, account balance = literal(0), bank matches never post to the ledger) were additionally spot-checked by hand.


Summary

# Domain Feature Severity
1 tax_filings TaxRate.account_payable_id / account_receivable_id / withholding_account_id placeholder
2 tax_filings TaxPeriod.declaration_file_id placeholder
3 tax_filings RetentionCertificate.pdf_file_id placeholder
4 tax_filings TaxRate.rate (never used in a tax calc) partial
5 tax_filings TaxRateRepository.find_effective() + TaxRate.valid_to partial
6 tax_filings TaxPeriodType (no behavioral branching) partial
7 pac_transactions Entire domain removed — only stale .pyc remain stub
8 accounts SupplierDefault model (+ client_defaults.py duplicate) placeholder
9 accounts AgeInvoice enum + age_invoices_by (account aging) placeholder
10 accounts AccountValidationService.validate_supplier_defaults_accounts placeholder
11 accounts Account landing-page balance hardcoded to 0 placeholder
12 opening_balances OpeningBalanceCommitResponse.journal always None placeholder
13 bank_statements TransactionKind enum + transaction_kind placeholder
14 bank_statements TransactionStatus.SPLIT (never set) placeholder
15 bank_statements BankStatementStatus.COMPLETED (unreachable) placeholder
16 bank_statements confirm_match / ignore_transaction never settle AR/AP partial
17 bank_statements normalized_counterparty + reference_number (matching inputs unused) partial
18 bank_statements balance_after (never reconciled against) placeholder
19 bank_statements bank_account_id auto-detection (detection path doesn't exist) partial
20 bank_reconciliations statement_id + statement relationship placeholder
21 bank_reconciliations BankReconciliationStatus.IN_PROGRESS (never produced) partial
22 bank_reconciliations BankReconciliationRepository.last_closed() stub
23 bank_accounts BankAccount.client / supplier / company forward relationships partial
24 fixed_assets DepreciationRunStatus.REVERSED (never produced) placeholder
25 fixed_assets DepreciationMethod.UNITS_OF_PRODUCTION (always computes 0) stub
26 fixed_assets DepreciationRunStatus.DRAFT (vestigial) partial
27 fixed_assets DepreciationRunRepository.list_for_period() → no duplicate-period guard stub
28 budgets BudgetStatus.APPROVED / ARCHIVED (inert lifecycle) placeholder
29 budgets Budget.approved_at / approved_by_id (write-only) placeholder
30 budgets Budget.project_id / office_id (ignored in variance) partial
31 ledger LedgerEntry.is_posted (never False) placeholder
32 ledger LedgerEntry.reversal_of / reversed_by relationships partial
33 ledger_audit LedgerAuditOperation.EDITED_MEMO (no producer) placeholder
34 recurring_journals last_ledger_entry_id / last_run_at (write-only) partial
35 journals Account.journal_details / User.journals back-refs partial
36 credits SupplierCredit.supplier_credit_status (never transitioned) placeholder
37 credits SupplierCreditRepository.cancel_supplier_credit (no-op) stub
38 credits SupplierCreditBalance.due_amount (hardcoded 0) placeholder
39 credits SupplierCreditDetail.supplier_invoice_detail_id (line link unused) placeholder
40 credits CreditBalance.due_amount — sales (hardcoded 0) placeholder
41 recurring_invoices RecurringInvoiceRunStatus enum (entirely dead) placeholder
42 payments Payment.qb_sync_enabled (never read for payments) partial

Headline themes

  • Tax-rate → GL automation was never built. The TaxRate GL-account links and rate exist so the system could derive tax journal lines automatically, but fileTaxPeriod takes payment lines from the frontend and worksheets sum pre-computed tax_amount. (#1, #4)
  • PDF persistence was never wired. Tax declarations and retention certificates generate ephemeral presigned URLs; the *_file_id columns meant to persist them are never written. (#2, #3)
  • Bank reconciliation records matches but posts nothing. Confirming a bank-statement match links IDs but never settles the invoice/expense or posts a ledger entry — the accounting consequence is missing. (#16, #20)
  • Supplier credit application is a shell. A supplier credit can be created but never applied, never has its status/balance maintained, and cannot be meaningfully canceled — unlike the fully-wired sales-credit flow. (#36–#39)
  • Several lifecycle states are decorative. Budget approval, ledger is_posted, depreciation REVERSED/DRAFT, bank IN_PROGRESS/COMPLETED/SPLIT are stored or queryable but drive no behavior. (#28, #31, #24, #26, #21, #15, #14)

tax_filings

  • Where: app/graphql/tax_filings/models/tax_rate.py:30-47
  • Intent: Tie each fiscal rate code to the GL accounts where output-tax liability, input-tax credit, and withholding payable accumulate — so tax journal entries could be posted automatically.
  • Why it's not functional: The three FKs are written by create/update (tax_rate_service.py:48-50, 75-77) and echoed in tax_rate_response. No posting code reads them. fileTaxPeriod posts a LedgerEntry built from paymentLines supplied by the frontend, not from the rate's accounts. Compare with Invoice.account_receivable_id and SupplierInvoice.account_payable_id, which ledger services do consume (invoice_ledger_service.py:77, payment_ledger_service.py:75).

2. TaxPeriod.declaration_file_idplaceholder

  • Where: app/graphql/tax_filings/models/tax_period.py:46-51
  • Intent: Link a filed period to the persisted declaration PDF File.
  • Why: No code assigns it. file_period() sets status/filed_at/filed_by_id/payment_ledger_entry_id but not this. TaxDeclarationPdfService.generate() uploads to ephemeral tmp/ and returns a presigned URL — it never creates a File row. The declaration_file resolver therefore always returns None.

3. RetentionCertificate.pdf_file_idplaceholder

4. TaxRate.ratepartial

  • Where: app/graphql/tax_filings/models/tax_rate.py:23-27
  • Intent: The decimal multiplier (e.g. 0.0700) used to compute ITBMS.
  • Why: The only consumer of tax_filings.TaxRate is build_itbms_worksheet(), which reads code only and sums already-stored InvoiceDetail.tax_amount / sub_total_amount — it never multiplies by rate. Actual invoice tax is computed from a different model, items.ITBMSTaxRate. Here rate is write-and-echo only.

5. TaxRateRepository.find_effective() + TaxRate.valid_topartial

  • Where: app/graphql/tax_filings/repositories/tax_rate_repository.py:23-38
  • Intent: Resolve the rate effective on a given date via valid_from/valid_to windows.
  • Why: Zero callers. The one place needing date-effective resolution — the backfill script — re-implements the logic inline (scripts/backfill_tax_rate_id_invoice_details.py:85, comment "Inline replication of TaxRateRepository.find_effective for script use"). So valid_to and the windowing logic are never consumed by a live read path.

6. TaxPeriodType (ITBMS_MONTHLY / RETENTION_MONTHLY / ISR_ANNUAL) — partial

  • Where: app/graphql/tax_filings/models/enums.py:7-11
  • Intent: Distinguish the kind of period and drive the right worksheet/declaration.
  • Why: Never branched on. build_itbms_worksheet, build_retention_summary, build_isr_annual_worksheet, and both PDF services run against any period_id regardless of its type — nothing stops you building an ITBMS worksheet for an ISR_ANNUAL period. The enum is only a display label.

Verified functional (not flagged): TaxPeriodStatus.AMENDED + amendTaxPeriod, filed_at/filed_by_id/payment_ledger_entry_id, ITBMS_UNCLASSIFIED bucketing, the retention TaxRateCodes, and InvoiceDetail.tax_rate_id / SupplierInvoiceDetail.tax_rate_id worksheet joins.


pac_transactions

7. Entire domain removed — only orphaned bytecode remains — stub

  • Where: app/graphql/pac_transactions/ (only __pycache__/*.pyc; no .py files)
  • Intent: A PacTransaction model logging each PAC call (transaction_type, source_id, document_number, pac_document_id, success, error_message) for billing/audit.
  • Why: Committed historically (a448a29d, 62f35685) but does not exist in HEAD. grep -rn "PacTransaction" app --include=*.py → zero hits. The working tree contains only stale .pyc. The PAC-call audit/billing logging it promised is absent at runtime.
  • Cleanup: delete the stray __pycache__ directory.

Note: the live PAC/DGI e-invoicing integration (wrappers/pac, dgi/, pac_service.py, common/mutations/pac_mutations.py) is a real, non-stubbed HTTP integration — not a placeholder. The dgi/ domain has no findings.


accounts

8. SupplierDefault model (and client_defaults.py duplicate) — placeholder

  • Where: app/graphql/accounts/models/supplier_defaults.py:21-56, app/graphql/accounts/models/client_defaults.py:21-56
  • Intent: Per-supplier posting defaults (aging basis + default expense_account_id / discount_account_id).
  • Why: The class is never imported, so SQLAlchemy never registers it; no migration creates a supplier_defaults table; it's not exported from the models __init__. Real defaults live on Supplier / ClientInvoiceDefault. client_defaults.py is a copy-paste that also defines class SupplierDefault with __tablename__ = "supplier_defaults" — there is no ClientDefault class anywhere, and its back_populates="supplier_default" has no matching relationship on Account, so it would fail to map if ever imported.

9. AgeInvoice enum + age_invoices_by (account aging) — placeholder

  • Where: app/graphql/accounts/models/aging_invoice.py:8-10
  • Intent: Configure whether AR/AP aging is computed by invoice date vs. due date.
  • Why: Referenced only by the two dead default models (#8); never imported into any service/schema/report. A ## TODO: Account Aging Aging Categories comment at supplier_defaults.py:44 confirms it was never finished.

10. AccountValidationService.validate_supplier_defaults_accountsplaceholder

11. Account landing-page balance hardcoded to 0placeholder


opening_balances

12. OpeningBalanceCommitResponse.journal always Noneplaceholder

  • Where: app/graphql/opening_balances/strawberry/opening_balance_commit_response.py:11
  • Intent: Return the journal/GL entry created by a successful opening-balance commit so the UI can link to it.
  • Why: commit() returns journal=None on both the failure and success paths; the ledger entry it creates at opening_balance_import_service.py:112 is discarded (_ = await self.ledger_service.create_entry(...)). The field is a permanent None.

⚠️ Adjacent correctness risk (not a placeholder): in commit(), AR/AP rows appear to post twice — once via _post_ar_row/_post_ap_row (opening invoices) and again as raw GL lines in the create_entry comprehension at opening_balance_import_service.py:117-126. Worth a closer look as a possible double-posting bug.


bank_statements

13. TransactionKind enum + transaction_kindplaceholder

  • Where: app/graphql/bank_statements/models/bank_statement_transaction.py:66-70, enum at models/enums.py:31-42
  • Intent: Classify each transaction (YAPPY / TRANSFER / ACH / CARD / WIRE / FEE / INTEREST / CASH) for downstream handling.
  • Why: Produced by regex and written/echoed, but never branched on or filtered by. No TransactionKind.<VALUE> comparison exists in business logic. Pure label.

14. TransactionStatus.SPLITplaceholder

15. BankStatementStatus.COMPLETEDplaceholder

16. confirm_match / ignore_transaction never settle the matched invoice/expense — partial

  • Where: app/graphql/bank_statements/services/bank_statement_service.py:157-211
  • Intent: Confirming a match should reconcile/settle the linked Invoice/Expense — that's the point of matching.
  • Why: confirm_match writes match_source_type/match_source_id/match_confidence/status and flips suggestion.accepted = True, but posts no ledger entry and touches no invoice/expense balance. The whole bank_statements/ package contains zero ledger-posting references (verified). Matching is recorded but has no accounting effect.

17. normalized_counterparty + reference_numberpartial

  • Where: app/graphql/bank_statements/models/bank_statement_transaction.py:51-60
  • Intent: Column comment: "Counterparty name extracted/normalized for matching and embeddings."
  • Why: The matcher keys solely on amount-within-tolerance + date-window ("No LLM, no fuzzy counterparty"). Neither field is read by the matcher — the "for matching" promise (Phase 2 / Qdrant) never landed.

18. balance_afterplaceholder

19. BankStatement.bank_account_id auto-detection — partial

  • Where: app/graphql/bank_statements/models/bank_statement.py:27-32
  • Intent: Column comment: "Detected/selected bank account; null until detection completes."
  • Why: Only ever set from the upload mutation argument; there is no detection logic that resolves the parsed bank_detected/account_number_masked to a BankAccount. The "detection completes" path doesn't exist.

bank_reconciliations

20. statement_id + statement relationship — placeholder

  • Where: app/graphql/bank_reconciliations/models/bank_reconciliation.py:54-60
  • Intent: Link a reconciliation to the imported PDF statement it closes against.
  • Why: Set from the open mutation arg and echoed, but never read. compute_summary/close/_cleared_buckets rely entirely on GL ledger lines and the manually-entered closing_balance_statement; the linked statement's transactions are never pulled in. The statement relationship is never traversed.

21. BankReconciliationStatus.IN_PROGRESS (vs DRAFT) — partial

  • Where: app/graphql/bank_reconciliations/models/enums.py:9-10
  • Intent: Distinct draft → in-progress lifecycle states.
  • Why: open() always creates with default DRAFT; nothing ever assigns IN_PROGRESS. It only appears inside the find_in_progress filter list. DRAFT and IN_PROGRESS are functionally identical and the latter is unreachable.

22. BankReconciliationRepository.last_closed()stub


bank_accounts

23. BankAccount.client / supplier / company forward relationships — partial

  • Where: app/graphql/bank_accounts/models/bank_account.py:39-58
  • Intent: Polymorphic source_type/source_id link from a bank account back to its owning Client/Supplier/Company.
  • Why: The three forward relationships are never traversed. Only the reverse company_bank_accounts has one consumer (PDF rendering); client/supplier directions are dead. No code resolves bank_account.client/.supplier/.company.

fixed_assets

24. DepreciationRunStatus.REVERSEDplaceholder

  • Where: app/graphql/fixed_assets/models/enums.py:35
  • Intent: Mark a depreciation run as reversed/un-posted.
  • Why: No code assigns it — the service only sets POSTED, and there is no reverse-run mutation. dispose() posts a fresh entry and sets the asset to DISPOSED but never touches a run's status.

25. DepreciationMethod.UNITS_OF_PRODUCTIONstub

26. DepreciationRunStatus.DRAFTpartial

  • Where: app/graphql/fixed_assets/models/depreciation_run.py:39
  • Intent: An unposted/draft run to be posted later.
  • Why: run_period() creates and immediately flips to POSTED, or leaves DRAFT only in the degenerate zero-depreciation case. There is no mutation to post a DRAFT run later, and _current_nbv sums POSTED runs only — so a DRAFT run is invisible to all calculations. Vestigial state.

27. DepreciationRunRepository.list_for_period()stub

Verified functional: salvage_value, useful_life_months, acquisition_cost, the three GL account FKs, FixedAssetStatus.FULLY_DEPRECIATED, and the disposal posting all drive real GL entries.


budgets

28. BudgetStatus.APPROVED / ARCHIVED — inert lifecycle — placeholder

  • Where: app/graphql/budgets/models/enums.py:10-11; written at budget_service.py:78 (approve), :86 (archive)
  • Intent: Gate budgets through draft → approved → archived so (e.g.) only approved budgets feed variance and archived ones are excluded.
  • Why: status is never read as a filter or guard. variance() sums BudgetLines with no status check — DRAFT and ARCHIVED budgets count exactly like APPROVED. approve_budget/archive_budget only mutate-and-echo the field.

29. Budget.approved_at / approved_by_idplaceholder

  • Where: app/graphql/budgets/models/budget.py:46-54
  • Intent: Record who approved a budget and when.
  • Why: Set in approve() and echoed, but never read by any guard/report. Tied to the inert approval workflow (#28).

30. Budget.project_id / office_id — ignored in variance — partial

  • Where: app/graphql/budgets/models/budget.py:56-67
  • Intent: Scope a budget to a project/office so variance is computed against that scope's ledger activity.
  • Why: Written and echoed, but variance() joins LedgerEntryLine purely by account_id + date with no project/office filter. A project-scoped budget is compared against company-wide actuals — the dimension is ignored.

ledger / ledger_audit

31. LedgerEntry.is_posted — never Falseplaceholder

  • Where: app/graphql/ledger/models/ledger_entry.py:81 (default=True)
  • Intent: Distinguish posted/finalized entries from drafts; ~a dozen report queries filter on is_posted.is_(True).
  • Why: No code anywhere sets is_posted = False (verified — zero hits across app/); no draft/post/unpost mutation exists. Every entry is born True, so all the is_posted.is_(True) filters are vacuous. The draft concept the column models doesn't exist.

32. LedgerEntry.reversal_of / reversed_by relationships — partial

  • Where: app/graphql/ledger/models/ledger_entry.py:94, :101
  • Intent: Navigate to the entry being reversed / the entry that reversed it.
  • Why: Both relationships are referenced only at their own definition. The reversal logic operates entirely on the scalar reversal_of_id / reversed_by_id columns (which are functional). The ORM relationships are never loaded/traversed.

33. LedgerAuditOperation.EDITED_MEMOplaceholder

  • Where: app/graphql/ledger_audit/models/enums.py:11
  • Intent: Record a "memo edited" event in the tamper-evident audit chain.
  • Why: Appears only in its own enum. The other four values have producers; there is no edit-memo mutation, so this operation is never recorded.

recurring_journals / journals

34. RecurringJournal.last_ledger_entry_id / last_run_atpartial

  • Where: app/graphql/recurring_journals/models/recurring_journal.py:62-65
  • Intent: Track the last materialized ledger entry and last run time.
  • Why: Assigned in _materialize_one and only echoed. No logic reads them back (no "already ran today" idempotency guard keys off last_run_at; last_ledger_entry_id is never dereferenced). Write-only pointers. (The rest of the recurring-journal scheduler is functional.)

35. Account.journal_details / User.journals back-references — partial


credits

36. SupplierCredit.supplier_credit_statusplaceholder

  • Where: app/graphql/credits/models/supplier_credit.py:87-93
  • Intent: Track the lifecycle (APPLIED/UNAPPLIED/CANCELED/VOID) of a supplier credit note.
  • Why: Defaults to UNAPPLIED and is never changed or filtered on. Contrast the sales Credit.credit_status, which is transitioned in credit_repository.py and read by recompute + client-statement report.

37. SupplierCreditRepository.cancel_supplier_credit — no-op — stub

  • Where: app/graphql/credits/repositories/supplier_credit_repository.py:90-94
  • Intent: Cancel a supplier credit note.
  • Why: Fetches the credit, calls flush() with no mutation, returns True. Sets no status, reverts no balance/ledger effect. The mutation is wired end-to-end but accomplishes nothing. (Compare the real sales cancel_credit / void_credit.)

38. SupplierCreditBalance.due_amount — hardcoded 0placeholder

  • Where: app/graphql/credits/models/supplier_credit_balance.py:26; writers supplier_credit_balance_repository.py:32,50
  • Intent: "Valor total pendiente de la nota de crédito" — outstanding/unapplied amount.
  • Why: Set to Decimal(0) in both add and recalculate_balances (the latter carries # TODO: Implement the logic...). No code applies a supplier credit against a supplier-invoice balance. Echo-only.

39. SupplierCreditDetail.supplier_invoice_detail_idplaceholder

  • Where: app/graphql/credits/models/supplier_credit_detail.py:44-48
  • Intent: Link a supplier-credit line to the original supplier-invoice line it credits.
  • Why: Written and echoed, never read by any allocation. The sales analog (CreditDetail.invoice_detail_id) is consumed to adjust canceled quantities; no such logic exists supplier-side.

40. CreditBalance.due_amount (sales) — hardcoded 0placeholder

  • Where: app/graphql/credits/models/credit_balance.py:26; writers credit_balance_repository.py:28,46 (# TODO at 45)
  • Intent: Outstanding/unapplied amount of the sales credit note.
  • Why: Hardcoded to 0; only read is the response echo. Sales credit application itself works (it cancels invoice-detail quantities), but the credit's own due_amount balance field is a placeholder.

Combined: findings #36–#39 mean the supplier-credit application/cancellation flow is a placeholder shell — a supplier credit can be created and its subtotal computed, but never applied, never status/balance-maintained, and not meaningfully cancelable.


recurring_invoices / payments

41. RecurringInvoiceRunStatus enum — entirely dead — placeholder

  • Where: app/graphql/recurring_invoices/models/enums.py:16-19
  • Intent: Represent the outcome (SUCCESS/FAILED) of a recurring-invoice materialization run.
  • Why: Zero references outside its own definition. Run outcome is actually tracked via last_run_at / last_run_error. The enum is never imported, stored, or returned.

42. Payment.qb_sync_enabledpartial

  • Where: app/graphql/payments/models/payment.py:102-107
  • Intent: Per-payment flag gating whether the payment syncs to QuickBooks.
  • Why: Written generically by the base repository, but no Payment path reads it — the QB bill-payment sync gates on payment.qb_bill_payment_id + the tenant-level context.qb_sync_enabled, never payment.qb_sync_enabled. Not even echoed in a payment response serializer.

Suggested follow-up priority

High impact (a workflow the UI suggests doesn't actually happen): - #16 / #20 — bank reconciliation confirms matches but posts nothing to the ledger or settles AR/AP. - #36–#39 — supplier-credit application/cancellation is a non-functional shell. - #11 / #31 — account landing-page balance always 0; ledger is_posted draft concept doesn't exist (so "posted-only" report filters are vacuous).

Correctness risks adjacent to placeholders (investigate as bugs, not cleanup): - #12 note — opening-balance commit may double-post AR/AP. - #27 — no duplicate-period guard on depreciation runs → double-posting if run twice.

Decide intent, then either build or remove: - #1–#3 — tax-rate → GL automation & PDF persistence (build the posting/persist path, or drop the columns). - #28–#30 — budget approval lifecycle and project/office scoping.

Dead code to delete: - #7 (pac_transactions stale .pyc), #8–#10 (SupplierDefault / client_defaults.py / orphan validator / AgeInvoice), #41 (RecurringInvoiceRunStatus), plus the unreachable enum states (#14, #15, #21, #24, #33) and dead methods/relationships (#5, #22, #23, #32, #34, #35).