UNPKG

@accounter/server

Version:
291 lines (264 loc) • 9.25 kB
import { format, startOfMonth } from 'date-fns'; import type { Injector } from 'graphql-modules'; import { EntryType, pcnGenerator } from '@accounter/pcn874-generator'; import type { Pcn874RecordType } from '../../../__generated__/types.js'; import { dateToTimelessDateString, idValidator, yearMonthValidator, } from '../../../shared/helpers/index.js'; import { TimelessDateString } from '../../../shared/types/index.js'; import { BusinessesProvider } from '../../financial-entities/providers/businesses.provider.js'; import { getVatRecords } from '../resolvers/get-vat-records.resolver.js'; import type { RawVatReportRecord } from './vat-report.helper.js'; type GeneratorParameters = Parameters<typeof pcnGenerator>; type Header = GeneratorParameters[0]; type Transaction = GeneratorParameters[1][number]; export type ExtendedPCNTransaction = Omit<Transaction, 'totalVat'> & Required<Pick<Transaction, 'totalVat'>> & { isProperty: boolean }; export const getHeaderDataFromRecords = ( transactions: ExtendedPCNTransaction[], licensedDealerId: string, reportMonth = '', generationDate?: string, ) => { let derivedReportMonth: string = reportMonth; let taxableSalesAmount = 0; let taxableSalesVat = 0; let salesRecordCount = 0; let zeroValOrExemptSalesCount = 0; let otherInputsVat = 0; let equipmentInputsVat = 0; let inputsCount = 0; for (const t of transactions) { const invoiceSumFactor = t.invoiceSum >= 0 ? 1 : -1; switch (t.entryType) { case EntryType.SALE_REGULAR: { taxableSalesVat += t.totalVat * invoiceSumFactor; taxableSalesAmount += t.invoiceSum; salesRecordCount += 1; break; } case EntryType.SALE_UNIDENTIFIED_CUSTOMER: { if (t.totalVat === 0) { zeroValOrExemptSalesCount += t.invoiceSum; } else { taxableSalesVat += t.totalVat * invoiceSumFactor; taxableSalesAmount += t.invoiceSum; } salesRecordCount += 1; break; } case EntryType.SALE_UNIDENTIFIED_ZERO_OR_EXEMPT: { salesRecordCount += 1; zeroValOrExemptSalesCount += t.invoiceSum; break; } case EntryType.INPUT_REGULAR: { if (t.isProperty) { equipmentInputsVat += t.totalVat * invoiceSumFactor; } else { otherInputsVat += t.totalVat * invoiceSumFactor; } inputsCount += 1; break; } case EntryType.INPUT_PETTY_CASH: { if (t.isProperty) { equipmentInputsVat += t.totalVat * invoiceSumFactor; } else { otherInputsVat += t.totalVat * invoiceSumFactor; } inputsCount += 1; break; } default: { console.debug(`Transaction EntryType ${t.entryType} is not implemented yet`); } } if (t.invoiceDate.substring(0, 6) > derivedReportMonth) { derivedReportMonth = t.invoiceDate.substring(0, 6); } } const totalVat = taxableSalesVat - otherInputsVat - equipmentInputsVat; const header: Header = { licensedDealerId, reportMonth: reportMonth || derivedReportMonth, generationDate, taxableSalesAmount, taxableSalesVat, salesRecordCount, zeroValOrExemptSalesCount, otherInputsVat, equipmentInputsVat, inputsCount, totalVat, }; return header; }; export const getEntryTypeByRecord = (entry: { pcn874RecordType?: Pcn874RecordType; isExpense: boolean; vatNumber?: string | null; localVat?: number | null; foreignVatAfterDeduction?: number; }): EntryType => { if (entry.pcn874RecordType) { switch (entry.pcn874RecordType) { case EntryType.SALE_REGULAR: return EntryType.SALE_REGULAR; case EntryType.SALE_UNIDENTIFIED_ZERO_OR_EXEMPT: return EntryType.SALE_UNIDENTIFIED_ZERO_OR_EXEMPT; case EntryType.INPUT_REGULAR: return EntryType.INPUT_REGULAR; case EntryType.INPUT_PETTY_CASH: return EntryType.INPUT_PETTY_CASH; case EntryType.SALE_SELF_INVOICE: return EntryType.SALE_SELF_INVOICE; case EntryType.SALE_EXPORT: return EntryType.SALE_EXPORT; case EntryType.SALE_PALESTINIAN_CUSTOMER: return EntryType.SALE_PALESTINIAN_CUSTOMER; case EntryType.SALE_ZERO_OR_EXEMPT: return EntryType.SALE_ZERO_OR_EXEMPT; case EntryType.SALE_UNIDENTIFIED_CUSTOMER: return EntryType.SALE_UNIDENTIFIED_CUSTOMER; case EntryType.INPUT_IMPORT: return EntryType.INPUT_IMPORT; case EntryType.INPUT_PALESTINIAN_SUPPLIER: return EntryType.INPUT_PALESTINIAN_SUPPLIER; case EntryType.INPUT_SINGLE_DOC_BY_LAW: return EntryType.INPUT_SINGLE_DOC_BY_LAW; case EntryType.INPUT_SELF_INVOICE: return EntryType.INPUT_SELF_INVOICE; default: throw new Error(`Unhandled PCN874 Record Type: ${entry.pcn874RecordType}`); } } if (entry.isExpense) { return EntryType.INPUT_REGULAR; } if ((entry.vatNumber ?? 0) === 0) { if (Number(entry.foreignVatAfterDeduction ?? 0) === 0 && (entry.localVat ?? 0) === 0) { return EntryType.SALE_UNIDENTIFIED_ZERO_OR_EXEMPT; } return EntryType.SALE_UNIDENTIFIED_CUSTOMER; } if (Number(entry.foreignVatAfterDeduction ?? 0) === 0 && (entry.localVat ?? 0) === 0) { return EntryType.SALE_ZERO_OR_EXEMPT; } return EntryType.SALE_REGULAR; }; const NO_VAT_ID_ENTRIES: string[] = [ EntryType.SALE_UNIDENTIFIED_CUSTOMER, EntryType.SALE_UNIDENTIFIED_ZERO_OR_EXEMPT, EntryType.INPUT_PETTY_CASH, ] as const; // TODO: migrate helper functions to PCN874 generator export function getVatIdForTransaction(t: RawVatReportRecord): string { if (t.pcn874RecordType && NO_VAT_ID_ENTRIES.includes(t.pcn874RecordType)) { return '0'; } if (!t.vatNumber && t.pcn874RecordType === EntryType.INPUT_REGULAR) { return '999999999'; } return t.vatNumber ?? '0'; } export function getReferenceForTransaction(t: RawVatReportRecord): string { if (t.pcn874RecordType === EntryType.INPUT_IMPORT) { return '0'; } if (t.pcn874RecordType === EntryType.INPUT_PETTY_CASH) { return '1'; } if (t.documentSerial) { return t.documentSerial; } if ( t.pcn874RecordType === EntryType.SALE_UNIDENTIFIED_CUSTOMER || t.pcn874RecordType === EntryType.SALE_UNIDENTIFIED_ZERO_OR_EXEMPT ) { return '1'; } return '0'; } export function getTotalVAT(t: RawVatReportRecord): number { if ( t.pcn874RecordType === EntryType.SALE_ZERO_OR_EXEMPT || t.pcn874RecordType === EntryType.SALE_UNIDENTIFIED_ZERO_OR_EXEMPT || t.pcn874RecordType === EntryType.SALE_EXPORT ) { return 0; } return Math.round(Math.abs(Number(t.roundedVATToAdd ?? 0))); } const transactionsFromVatReportRecords = ( vatRecords: RawVatReportRecord[], ): ExtendedPCNTransaction[] => { const transactions: ExtendedPCNTransaction[] = []; for (const t of vatRecords) { if (!t.documentDate) { console.debug(`Document ${t.documentId} has no tax_invoice_date. Skipping it.`); continue; } const entryType = getEntryTypeByRecord(t); const transaction: ExtendedPCNTransaction = { entryType, vatId: getVatIdForTransaction(t), invoiceDate: format(new Date(t.documentDate!), 'yyyyMMdd'), refGroup: '0000', refNumber: getReferenceForTransaction(t), totalVat: getTotalVAT(t), invoiceSum: Math.round(Number(t.localAmountBeforeVAT)), isProperty: t.isProperty, allocationNumber: t.allocationNumber ?? undefined, }; transactions.push(transaction); } return transactions.sort((a, b) => a.invoiceDate.localeCompare(b.invoiceDate)); }; export async function getPcn874String( injector: Injector, businessId: string, rawMonthDate: TimelessDateString, ) { const monthDate = dateToTimelessDateString(startOfMonth(new Date(rawMonthDate))); const financialEntity = await injector .get(BusinessesProvider) .getBusinessByIdLoader.load(businessId); if (!financialEntity?.vat_number) { throw new Error(`Business entity ${businessId} has no VAT number`); } const vatRecords = await getVatRecords( { filters: { monthDate, financialEntityId: businessId } }, injector, ); const reportMonth = format(new Date(monthDate), 'yyyyMM'); const reportContent = generatePcnFromVatRecords( [ ...(vatRecords.income as RawVatReportRecord[]), ...(vatRecords.expenses as RawVatReportRecord[]), ], financialEntity.vat_number, reportMonth, ); return { reportContent, monthDate, reportMonth, financialEntity }; } const generatePcnFromVatRecords = ( vatRecords: RawVatReportRecord[], vatNumber: string, reportMonth: string, ) => { if (!yearMonthValidator(reportMonth)) { throw new Error( `Expected reportMonth to be legit date formed as YYYYMM, received "${reportMonth}"`, ); } if (!idValidator(vatNumber, 9)) { throw new Error(`Expected vatNumber to be 9 digits, received "${vatNumber}"`); } const transactions = transactionsFromVatReportRecords(vatRecords); const header = getHeaderDataFromRecords(transactions, vatNumber, reportMonth); const reportContent = pcnGenerator(header, transactions, { strict: false }); return reportContent; };