@accounter/server
Version:
Accounter GraphQL server
291 lines (264 loc) • 9.25 kB
text/typescript
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;
};