@accounter/server
Version:
Accounter GraphQL server
384 lines (340 loc) • 13.6 kB
text/typescript
import type { Injector } from 'graphql-modules';
import { DECREASED_VAT_RATIO } from '../../../shared/constants.js';
import { dateToTimelessDateString, formatCurrency } from '../../../shared/helpers/index.js';
import type { LedgerProto, StrictLedgerProto } from '../../../shared/types/index.js';
import { AdminContextProvider } from '../../admin-context/providers/admin-context.provider.js';
import type { IGetChargesByIdsResult } from '../../charges/types.js';
import {
validateDocumentAllocation,
validateDocumentVat,
} from '../../documents/helpers/validate-document.helper.js';
import type { IGetDocumentsByChargeIdResult } from '../../documents/types.js';
import { ExchangeProvider } from '../../exchange-rates/providers/exchange.provider.js';
import { FinancialAccountsProvider } from '../../financial-accounts/providers/financial-accounts.provider.js';
import type { IGetTransactionsByChargeIdsResult } from '../../transactions/types.js';
import { VatProvider } from '../../vat/providers/vat.provider.js';
import { IGetBalanceCancellationByChargesIdsResult } from '../types.js';
import {
getFinancialAccountTaxCategoryId,
LedgerError,
validateTransactionBasicVariables,
} from './utils.helper.js';
export async function ledgerEntryFromDocument(
injector: Injector,
document: IGetDocumentsByChargeIdResult,
charge: IGetChargesByIdsResult,
ownerId: string,
taxCategoryId: string,
): Promise<StrictLedgerProto> {
if (!document.date) {
throw new LedgerError(`Document serial "${document.serial_number}" is missing the date`);
}
if (!document.debtor_id) {
throw new LedgerError(`Document serial "${document.serial_number}" is missing the debtor`);
}
if (!document.creditor_id) {
throw new LedgerError(`Document serial "${document.serial_number}" is missing the creditor`);
}
if (!document.total_amount) {
throw new LedgerError(`Document serial "${document.serial_number}" is missing amount`);
}
if (!document.currency_code) {
throw new LedgerError(`Document serial "${document.serial_number}" is missing currency code`);
}
const {
defaultLocalCurrency,
authorities: { inputVatTaxCategoryId, outputVatTaxCategoryId, propertyOutputVatTaxCategoryId },
} = await injector.get(AdminContextProvider).getVerifiedAdminContext();
let totalAmount = Math.abs(document.total_amount);
const isCreditorCounterparty = document.debtor_id === ownerId;
const counterpartyId = isCreditorCounterparty ? document.creditor_id : document.debtor_id;
const debitAccountID1 = isCreditorCounterparty ? taxCategoryId : counterpartyId;
const creditAccountID1 = isCreditorCounterparty ? counterpartyId : taxCategoryId;
const currency = formatCurrency(document.currency_code);
let foreignTotalAmount: number | null = null;
let amountWithoutVat = totalAmount;
let foreignAmountWithoutVat: number | null = null;
let vatAmount = document.vat_amount == null ? null : Math.abs(document.vat_amount);
let foreignVatAmount: number | null = null;
let vatTaxCategory: string | null = null;
if (vatAmount != null && charge.is_property) {
vatAmount = vatAmount * DECREASED_VAT_RATIO; // Adjust VAT for property charges
}
const isCreditInvoice = document.type === 'CREDIT_INVOICE';
if (vatAmount) {
amountWithoutVat = amountWithoutVat - vatAmount;
const adjustedOutputVatTaxCategoryId = charge.is_property
? propertyOutputVatTaxCategoryId
: outputVatTaxCategoryId;
vatTaxCategory =
isCreditorCounterparty === isCreditInvoice
? inputVatTaxCategoryId
: adjustedOutputVatTaxCategoryId;
const vatValue = await injector
.get(VatProvider)
.getVatValueByDateLoader.load(dateToTimelessDateString(document.date));
if (!vatValue) {
throw new LedgerError(`VAT value is missing for document ID=${document.id}`);
}
validateDocumentVat(document, vatValue, message => {
throw new LedgerError(message);
});
await validateDocumentAllocation(document, injector).then(valid => {
if (!valid) {
throw new LedgerError(`Allocation number missing for document [${document.serial_number}]`);
}
});
}
// handle non-local currencies
if (document.currency_code !== defaultLocalCurrency) {
let exchangeRate: number;
if (document.exchange_rate_override) {
exchangeRate = Number(document.exchange_rate_override);
} else {
exchangeRate = await injector
.get(ExchangeProvider)
.getExchangeRates(currency, defaultLocalCurrency, document.date);
}
// Set foreign amounts
foreignTotalAmount = totalAmount;
foreignAmountWithoutVat = amountWithoutVat;
// calculate amounts in ILS
totalAmount = exchangeRate * totalAmount;
amountWithoutVat = exchangeRate * amountWithoutVat;
if (vatAmount && vatAmount > 0) {
foreignVatAmount = vatAmount;
vatAmount = exchangeRate * vatAmount;
}
}
let creditAccountID2: string | null = null;
let debitAccountID2: string | null = null;
let creditAmount1: number | null;
let localCurrencyCreditAmount1: number;
let debitAmount1: number | null;
let localCurrencyDebitAmount1: number;
let creditAmount2: number | null = null;
let localCurrencyCreditAmount2: number | null = null;
let debitAmount2: number | null = null;
let localCurrencyDebitAmount2: number | null = null;
if (isCreditorCounterparty) {
localCurrencyCreditAmount1 = totalAmount;
creditAmount1 = foreignTotalAmount;
localCurrencyDebitAmount1 = amountWithoutVat;
debitAmount1 = foreignAmountWithoutVat;
if (vatAmount && vatAmount > 0) {
// add vat to debtor2
debitAmount2 = foreignVatAmount;
localCurrencyDebitAmount2 = vatAmount;
debitAccountID2 = vatTaxCategory;
}
} else {
localCurrencyDebitAmount1 = totalAmount;
debitAmount1 = foreignTotalAmount;
localCurrencyCreditAmount1 = amountWithoutVat;
creditAmount1 = foreignAmountWithoutVat;
if (vatAmount && vatAmount > 0) {
// add vat to creditor2
creditAmount2 = foreignVatAmount;
localCurrencyCreditAmount2 = vatAmount;
creditAccountID2 = vatTaxCategory;
}
}
const ledgerEntry: StrictLedgerProto = {
id: document.id,
invoiceDate: document.date,
valueDate: document.date,
currency,
creditAccountID1,
creditAmount1: creditAmount1 ?? undefined,
localCurrencyCreditAmount1,
debitAccountID1,
debitAmount1: debitAmount1 ?? undefined,
localCurrencyDebitAmount1,
creditAccountID2: creditAccountID2 ?? undefined,
creditAmount2: creditAmount2 ?? undefined,
localCurrencyCreditAmount2: localCurrencyCreditAmount2 ?? undefined,
debitAccountID2: debitAccountID2 ?? undefined,
debitAmount2: debitAmount2 ?? undefined,
localCurrencyDebitAmount2: localCurrencyDebitAmount2 ?? undefined,
description: undefined,
reference: document.serial_number ?? undefined,
isCreditorCounterparty,
isCreditInvoice,
ownerId,
chargeId: charge.id,
};
return ledgerEntry;
}
function getCreditCardAccountByTransactionReferenceAndDescription(
sourceReference: string,
sourceDescription: string | null,
): string | null {
if (sourceReference === '8547') {
return '6fed9073-f571-402e-9da0-ac0669f658d2'; // 9200
}
if (sourceReference === '13795') {
if (sourceDescription?.includes('2260')) {
return '0e86514c-9ddb-4c82-b731-6107f7293b1f'; //2260
}
if (sourceDescription?.includes('4333')) {
return '782070ce-ec7e-4be0-ab69-46c89528bf04'; //4333
}
}
if (sourceReference === '22200' && sourceDescription?.includes('מסטרקארד')) {
return '782070ce-ec7e-4be0-ab69-46c89528bf04'; //4333
}
if (sourceReference === '11200' && sourceDescription?.includes('כ.א.ל')) {
return '6fed9073-f571-402e-9da0-ac0669f658d2'; // 9200
}
return null;
}
export async function ledgerEntryFromMainTransaction(
transaction: IGetTransactionsByChargeIdsResult,
injector: Injector,
chargeId: string,
ownerId: string,
businessId?: string,
gotRelevantDocuments = false,
): Promise<StrictLedgerProto> {
const { currency, valueDate, transactionBusinessId } =
validateTransactionBasicVariables(transaction);
let mainAccountId: string = transactionBusinessId;
const {
defaultLocalCurrency,
financialAccounts: { internalWalletsIds },
} = await injector.get(AdminContextProvider).getVerifiedAdminContext();
if (
!gotRelevantDocuments &&
transaction.source_reference &&
businessId &&
internalWalletsIds.includes(businessId)
) {
const account = await injector
.get(FinancialAccountsProvider)
.getFinancialAccountByAccountNumberLoader.load(transaction.source_reference);
let accountId = account?.id ?? null;
accountId ??= getCreditCardAccountByTransactionReferenceAndDescription(
transaction.source_reference,
transaction.source_description,
);
if (!accountId) {
throw new LedgerError(
`Transaction reference "${transaction.source_reference}" is missing account`,
);
}
mainAccountId = await getFinancialAccountTaxCategoryId(injector, {
...transaction,
account_id: accountId,
});
}
let amount = Number(transaction.amount);
let foreignAmount: number | undefined = undefined;
if (currency !== defaultLocalCurrency) {
// get exchange rate for currency
const exchangeRate = await injector
.get(ExchangeProvider)
.getExchangeRates(currency, defaultLocalCurrency, valueDate);
foreignAmount = amount;
// calculate amounts in ILS
amount = exchangeRate * amount;
}
const accountTaxCategoryId = await getFinancialAccountTaxCategoryId(injector, transaction);
const isCreditorCounterparty = amount > 0;
const ledgerEntry: StrictLedgerProto = {
id: transaction.id,
invoiceDate: transaction.event_date,
valueDate,
currency,
creditAccountID1: isCreditorCounterparty ? mainAccountId : accountTaxCategoryId,
creditAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined,
localCurrencyCreditAmount1: Math.abs(amount),
debitAccountID1: isCreditorCounterparty ? accountTaxCategoryId : mainAccountId,
debitAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined,
localCurrencyDebitAmount1: Math.abs(amount),
description: transaction.source_description ?? undefined,
reference: transaction.origin_key,
isCreditorCounterparty,
ownerId,
currencyRate: transaction.currency_rate ? Number(transaction.currency_rate) : undefined,
chargeId,
};
return ledgerEntry;
}
export async function ledgerEntryFromBalanceCancellation(
balanceCancellation: IGetBalanceCancellationByChargesIdsResult,
ledgerBalance: Map<string, { amount: number; entityId: string }>,
financialAccountLedgerEntries: StrictLedgerProto[],
chargeId: string,
ownerId: string,
injector: Injector,
): Promise<LedgerProto> {
const {
defaultLocalCurrency,
general: {
taxCategories: { balanceCancellationTaxCategoryId },
},
} = await injector.get(AdminContextProvider).getVerifiedAdminContext();
const entityBalance = ledgerBalance.get(balanceCancellation.business_id);
if (!entityBalance) {
throw new LedgerError(
`Balance cancellation for business ${balanceCancellation.business_id} redundant - already balanced`,
);
}
const { amount, entityId } = entityBalance;
const financialAccountEntry = financialAccountLedgerEntries.find(entry =>
[
entry.creditAccountID1,
entry.creditAccountID2,
entry.debitAccountID1,
entry.debitAccountID2,
].includes(balanceCancellation.business_id),
);
if (!financialAccountEntry) {
throw new LedgerError(
`Balance cancellation for business ${balanceCancellation.business_id} failed - no financial account entry found`,
);
}
let foreignAmount: number | undefined = undefined;
if (
financialAccountEntry.currency !== defaultLocalCurrency &&
financialAccountEntry.currencyRate
) {
foreignAmount = financialAccountEntry.currencyRate * amount;
}
const isCreditorCounterparty = amount > 0;
const ledgerEntry: LedgerProto = {
id: balanceCancellation.charge_id,
invoiceDate: financialAccountEntry.invoiceDate,
valueDate: financialAccountEntry.valueDate,
currency: financialAccountEntry.currency,
creditAccountID1: isCreditorCounterparty ? balanceCancellationTaxCategoryId : entityId,
creditAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined,
localCurrencyCreditAmount1: Math.abs(amount),
debitAccountID1: isCreditorCounterparty ? entityId : balanceCancellationTaxCategoryId,
debitAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined,
localCurrencyDebitAmount1: Math.abs(amount),
description: balanceCancellation.description ?? undefined,
reference: financialAccountEntry.reference,
isCreditorCounterparty,
ownerId,
currencyRate: financialAccountEntry.currencyRate,
chargeId,
};
return ledgerEntry;
}
export function isRefundCharge(description?: string | null): boolean {
if (!description) {
return false;
}
const normalizedDescription = description.toLocaleLowerCase();
return (
normalizedDescription.includes('refund') || normalizedDescription.includes('reimbursement')
);
}
export function getExchangeDates(financialAccountLedgerEntries: LedgerProto[]): Date {
const timeValue = Math.max(
...financialAccountLedgerEntries.map(entry => entry.valueDate.getTime()),
);
const date = new Date(timeValue);
return date;
}