Skip to content

Tax filings (ITBMS, retentions)

Backend domain: app/graphql/tax_filings/ Migration: add_tax_filings RBAC Path: TAX_FILINGS

Overview

Three resources:

  • TaxRate — code (ITBMS_7, ITBMS_10, ITBMS_15, ITBMS_EXEMPT, ISR_RETENTION, ITBMS_RETENTION_50, ITBMS_RETENTION_100), decimal rate, GL accounts for payable / receivable / withholding, validity window.
  • TaxPeriod — month or year, type (ITBMS_MONTHLY, RETENTION_MONTHLY, ISR_ANNUAL), status (OPEN | FILED | AMENDED), optional uploaded declaration file, optional payment JE.
  • RetentionCertificate — auto-numbered (RC-00001), one per retained transaction (client or supplier), stores amount-subject + retention amount, optional PDF file.

The itbmsWorksheet(periodId) query rolls up invoice & supplier-invoice details for the period and returns gross sales / output tax / gross purchases / input tax / net payable.

Setup prerequisites

  1. Tax-related GL accounts must exist (ITBMS payable, ITBMS receivable / input, ISR retention payable).
  2. TaxRate rows must be seeded with their account FKs (accountPayableId, accountReceivableId, withholdingAccountId) and validFrom via createTaxRate. Rates are immutable on code — to change a rate, set valid_to on the current row and createTaxRate a successor with the same code.

GraphQL surface

Queries

taxRates: [TaxRate!]!
taxRate(rateId: UUID!): TaxRate!

taxPeriods: [TaxPeriod!]!
taxPeriod(periodId: UUID!): TaxPeriod!
itbmsWorksheet(periodId: UUID!): ITBMSWorksheet!
retentionSummary(periodId: UUID!): [RetentionSummaryRow!]!
retentionCertificates(periodId: UUID!): [RetentionCertificate!]!

retentionCertificatePdfUrl(certificateId: UUID!): String
taxDeclarationPdfUrl(periodId: UUID!): String

TaxPeriod: id, periodType, startDate, endDate, status, filedAt, filedById, declarationFileId, paymentLedgerEntryId.

ITBMSWorksheet: periodStart, periodEnd, totalOutput, totalInput, netPayable, rows: [ITBMSWorksheetRow!]! where each row is rateCode, grossSales, outputTax, grossPurchases, inputTax.

RetentionSummaryRow: entityKind (CLIENT | SUPPLIER), taxCode, totalSubject, totalRetained, count.

RetentionCertificate: id, certificateNumber, periodId, entityKind, entityId, sourceType, sourceId, taxCode, amountSubject, retentionAmount, pdfFileId.

Mutations

createTaxRate(taxRate: TaxRateCreateInput!): TaxRate!
updateTaxRate(taxRate: TaxRateUpdateInput!): TaxRate!
deleteTaxRate(rateId: UUID!): Boolean!

createTaxPeriod(
  periodType: TaxPeriodType!,
  startDate: Date!,
  endDate: Date!
): TaxPeriod!

fileTaxPeriod(
  periodId: UUID!,
  paymentLines: [TaxPaymentLineInput!]
): TaxPeriod!

generateRetentionCertificate(
  periodId: UUID!,
  entityKind: TaxEntityKind!,
  entityId: UUID!,
  sourceType: SourceType!,
  sourceId: UUID!,
  taxCode: TaxRateCode!,
  amountSubject: Decimal!,
  retentionAmount: Decimal!
): RetentionCertificate!

TaxPaymentLineInput: accountId, debitAmount, creditAmount, memo.

Behaviour to handle in the UI

  • ITBMS monthly close: pick a month → call itbmsWorksheet → display the worksheet → user reviews → fileTaxPeriod with payment lines (typically DR ITBMS payable / CR cash). The mutation locks the period as FILED and posts the JE.
  • Retention certificate generation is per-document. From a supplier-invoice or client-invoice screen, expose a "Generate retention certificate" action. Pre-fill amountSubject and retentionAmount from the source document.
  • Show certificates grouped by period; certificates are immutable once issued (no edit mutation by design).

Error states

Behaviour When Frontend message
ValueError ("is already filed") fileTaxPeriod on an already-filed period "This period was already filed on MM/DD. Use the Amend flow."

Suggested UX flow

  1. Tax calendar / dashboard — list periods grouped by type, with status badges, due-date warnings.
  2. ITBMS month close wizard — three steps: review worksheet, confirm payment account, file. Show the resulting JE summary on success.
  3. Certificate detail / list — surface PDF download once that integration lands.

PDFs

Both certificate and declaration PDFs are now generated server-side via the existing pdf_templates pipeline (a generic AccountingDocumentPdf generator drives both). Each query uploads the PDF to DigitalOcean Spaces under tmp/ and returns a presigned URL — call retentionCertificatePdfUrl(certificateId) or taxDeclarationPdfUrl(periodId) and hand the URL to the browser as a download.

The pdf_file_id column on retention_certificates and declaration_file_id on tax_periods remain available for cases where a custom-uploaded artifact should be attached to the record — they aren't auto-populated by these queries because the queries return URLs to disposable presigned objects.

Per-rate worksheet

itbmsWorksheet now groups by invoice_details.tax_rate_id / supplier_invoice_details.tax_rate_id (added via migration add_tax_rate_id_to_details), joins tax_rates to get the rate code, and emits one ITBMSWorksheetRow per code. Lines that haven't been re-saved since the migration land in the ITBMS_EXEMPT bucket — they show up correctly in totals but aren't broken out per rate until each line picks up a tax_rate_id.

To make the breakdown meaningful, the invoice / supplier-invoice line editors need to set tax_rate_id when the user picks a rate. Backwards-compat: leaving it null preserves current behavior.

Open follow-ups (not yet implemented)

  • Amend flow for filed periods isn't implemented as a mutation — currently you'd manually flip status to AMENDED via the DB or add a follow-up mutation.
  • ISR annual filing only has the period scaffold; the worksheet builder is ITBMS-monthly. Build out as needed.