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¶
- Tax-related GL accounts must exist (
ITBMS payable,ITBMS receivable / input,ISR retention payable). TaxRaterows must be seeded with their account FKs (accountPayableId,accountReceivableId,withholdingAccountId) andvalidFromviacreateTaxRate. Rates are immutable oncode— to change a rate, setvalid_toon the current row andcreateTaxRatea successor with the samecode.
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 →fileTaxPeriodwith payment lines (typically DR ITBMS payable / CR cash). The mutation locks the period asFILEDand 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
amountSubjectandretentionAmountfrom 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¶
- Tax calendar / dashboard — list periods grouped by type, with status badges, due-date warnings.
- ITBMS month close wizard — three steps: review worksheet, confirm payment account, file. Show the resulting JE summary on success.
- 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
AMENDEDvia 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.