diff --git a/.changeset/vat-report-exclude-other-documents.md b/.changeset/vat-report-exclude-other-documents.md new file mode 100644 index 000000000..ca2cea66c --- /dev/null +++ b/.changeset/vat-report-exclude-other-documents.md @@ -0,0 +1,17 @@ +--- +'@accounter/server': patch +--- + +Exclude non-financial documents from the VAT report's by-date logic (issue #3375). + +Documents of type `OTHER` (and other non-financial documents) carry a `date`, so they were returned +by the VAT report's date-filtered document query and registered their linked `charge_id` before the +financial-document type check ran. That pulled unrelated charges into the report's `missingInfo` / +`differentMonthDoc` / `businessTrips` buckets whenever a charge's only in-month tie was a hidden +`OTHER`-document date. + +The report now registers a document's charge only after confirming it is a financial (invoice) +document linked to a charge carrying both counterparties, via the new `isVatReportRelevantDocument` +type guard in the vat-report helper (built on the canonical `isInvoice` definition). All callers of +`getVatRecords` — the VAT report screen, PCN874 generation, monthly-VAT ledger validation and +description suggestions — benefit from the corrected filtering. diff --git a/packages/server/src/modules/reports/helpers/__tests__/vat-report.helper.test.ts b/packages/server/src/modules/reports/helpers/__tests__/vat-report.helper.test.ts index ce444793e..f4b3f6080 100644 --- a/packages/server/src/modules/reports/helpers/__tests__/vat-report.helper.test.ts +++ b/packages/server/src/modules/reports/helpers/__tests__/vat-report.helper.test.ts @@ -1,7 +1,9 @@ import { describe, expect, it } from 'vitest'; -import { Currency } from '../../../../shared/enums.js'; +import { Currency, DocumentType } from '../../../../shared/enums.js'; +import type { IGetDocumentsByFiltersResult } from '../../../documents/types.js'; import { calculateMonthlyVatTotalAmount, + isVatReportRelevantDocument, isWithinMonthlyVatAmountTolerance, type RawVatReportRecord, } from '../vat-report.helper.js'; @@ -52,3 +54,45 @@ describe('vat-report helper monthly VAT utilities', () => { expect(isWithinMonthlyVatAmountTolerance(100, 100)).toBe(false); }); }); + +function createDocument( + overrides: Partial = {}, +): IGetDocumentsByFiltersResult { + return { + charge_id: 'charge-1', + creditor_id: 'creditor-1', + debtor_id: 'debtor-1', + type: DocumentType.Invoice, + ...overrides, + } as IGetDocumentsByFiltersResult; +} + +describe('isVatReportRelevantDocument', () => { + it('accepts financial (invoice) documents linked to a charge with both counterparties', () => { + expect(isVatReportRelevantDocument(createDocument({ type: DocumentType.Invoice }))).toBe(true); + expect( + isVatReportRelevantDocument(createDocument({ type: DocumentType.InvoiceReceipt })), + ).toBe(true); + expect( + isVatReportRelevantDocument(createDocument({ type: DocumentType.CreditInvoice })), + ).toBe(true); + }); + + it('rejects "OTHER" documents even when they carry a date and counterparties (issue #3375)', () => { + expect(isVatReportRelevantDocument(createDocument({ type: DocumentType.Other }))).toBe(false); + }); + + it('rejects other non-financial document types', () => { + expect(isVatReportRelevantDocument(createDocument({ type: DocumentType.Receipt }))).toBe(false); + expect(isVatReportRelevantDocument(createDocument({ type: DocumentType.Proforma }))).toBe(false); + expect( + isVatReportRelevantDocument(createDocument({ type: DocumentType.Unprocessed })), + ).toBe(false); + }); + + it('rejects documents missing a charge or a counterparty', () => { + expect(isVatReportRelevantDocument(createDocument({ charge_id: null }))).toBe(false); + expect(isVatReportRelevantDocument(createDocument({ creditor_id: null }))).toBe(false); + expect(isVatReportRelevantDocument(createDocument({ debtor_id: null }))).toBe(false); + }); +}); diff --git a/packages/server/src/modules/reports/helpers/vat-report.helper.ts b/packages/server/src/modules/reports/helpers/vat-report.helper.ts index f7bd5d2d1..06bf316ea 100644 --- a/packages/server/src/modules/reports/helpers/vat-report.helper.ts +++ b/packages/server/src/modules/reports/helpers/vat-report.helper.ts @@ -10,6 +10,7 @@ import { } from '../../charges/helpers/common.helper.js'; import type { IGetChargesByIdsResult } from '../../charges/types.js'; import { DepreciationProvider } from '../../depreciation/providers/depreciation.provider.js'; +import { isInvoice } from '../../documents/helpers/common.helper.js'; import type { IGetDocumentsByFiltersResult } from '../../documents/types.js'; import { ExchangeProvider } from '../../exchange-rates/providers/exchange.provider.js'; import type { IGetBusinessesByIdsResult } from '../../financial-entities/types.js'; @@ -47,6 +48,30 @@ export type RawVatReportRecord = { pcn874RecordType?: Pcn874RecordType; }; +/** + * Determines whether a document should be considered by the VAT report. + * + * Only financial (invoice) documents linked to a charge and carrying both counterparties are + * relevant. Non-financial documents — most notably `OTHER`, but also `RECEIPT`, `PROFORMA` and + * `UNPROCESSED` — may still carry a date, counterparty or amount, yet they must never be included + * in the report nor pull their charge into it via that date (issue #3375). + * + * Acts as a type guard so callers can safely treat `charge_id`, `creditor_id` and `debtor_id` + * as non-null strings — all three are asserted present here. + */ +export function isVatReportRelevantDocument( + doc: IGetDocumentsByFiltersResult, +): doc is IGetDocumentsByFiltersResult & { + charge_id: string; + creditor_id: string; + debtor_id: string; +} { + if (!doc.charge_id || !doc.creditor_id || !doc.debtor_id) { + return false; + } + return isInvoice(doc.type); +} + export const MONTHLY_VAT_AMOUNT_TOLERANCE = 0.01; export function calculateMonthlyVatTotalAmount( diff --git a/packages/server/src/modules/reports/resolvers/get-vat-records.resolver.ts b/packages/server/src/modules/reports/resolvers/get-vat-records.resolver.ts index 38e015f25..f6c7bb59d 100644 --- a/packages/server/src/modules/reports/resolvers/get-vat-records.resolver.ts +++ b/packages/server/src/modules/reports/resolvers/get-vat-records.resolver.ts @@ -15,6 +15,7 @@ import { BusinessesProvider } from '../../financial-entities/providers/businesse import { isRefundCharge } from '../../ledger/helpers/common-charge-ledger.helper.js'; import { adjustTaxRecord, + isVatReportRelevantDocument, type RawVatReportRecord, type VatReportRecordSources, } from '../helpers/vat-report.helper.js'; @@ -68,10 +69,6 @@ export const getVatRecords = async ( }) .then(documents => documents.filter(doc => { - if (doc.charge_id) { - docsChargesIDs.add(doc.charge_id); - } - // filter documents with vat_report_date_override outside of the date range if (doc.vat_report_date_override) { const isBeforeFromDate = @@ -82,12 +79,15 @@ export const getVatRecords = async ( } } - if (!doc.charge_id || !doc.creditor_id || !doc.debtor_id) { - // filter invoice documents with linked charge + // Only financial (invoice) documents linked to a charge are considered by the VAT + // report. Non-financial documents (e.g. "OTHER") may carry a date but must not be + // included nor register their charge via `docsChargesIDs` (issue #3375). + if (!isVatReportRelevantDocument(doc)) { return false; } - const isRelevantDoc = ['INVOICE', 'INVOICE_RECEIPT', 'CREDIT_INVOICE'].includes(doc.type); - return isRelevantDoc; + + docsChargesIDs.add(doc.charge_id); + return true; }), );