Skip to content

Commit

Permalink
Generate partial ledger (#622)
Browse files Browse the repository at this point in the history
* return errors list instead of throw on ledger generation (common charge)

* full ledger partial generation

* UI for ledger errors + some fixes

* minor  fixes + better error messages
  • Loading branch information
gilgardosh committed Apr 24, 2024
1 parent 6d71c78 commit 72451c5
Show file tree
Hide file tree
Showing 23 changed files with 1,709 additions and 1,388 deletions.
43 changes: 43 additions & 0 deletions packages/client/src/components/all-charges/charge-errors.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { ReactElement } from 'react';
import { List, Paper, Text } from '@mantine/core';
import { AllChargesErrorsFieldsFragmentDoc } from '../../gql/graphql.js';
import { FragmentType, getFragmentData } from '../../gql/index.js';

// eslint-disable-next-line @typescript-eslint/no-unused-expressions -- used by codegen
/* GraphQL */ `
fragment AllChargesErrorsFields on Charge {
id
... on Charge @defer {
ledger {
... on Ledger @defer {
validate {
... on LedgerValidation @defer {
errors
}
}
}
}
}
}
`;

interface Props {
data?: FragmentType<typeof AllChargesErrorsFieldsFragmentDoc>;
}

export const ChargeErrors = ({ data }: Props): ReactElement | null => {
const charge = getFragmentData(AllChargesErrorsFieldsFragmentDoc, data);

return charge?.ledger?.validate?.errors?.length ? (
<Paper shadow="xs" p="md">
<Text c="red">Errors:</Text>
<List size="sm" withPadding>
{charge.ledger.validate.errors.map((error, i) => (
<List.Item key={i}>
<Text c="red">{error}</Text>
</List.Item>
))}
</List>
</Paper>
) : null;
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useQuery } from 'urql';
import { Accordion, ActionIcon, Box, Burger, Collapse, Loader, Menu, Tooltip } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import {
AllChargesErrorsFieldsFragmentDoc,
ConversionChargeInfoFragmentDoc,
DocumentsGalleryFieldsFragmentDoc,
FetchChargeDocument,
Expand All @@ -19,6 +20,7 @@ import {
ConfirmationModal,
RegenerateLedgerRecordsButton,
} from '../common/index.js';
import { ChargeErrors } from './charge-errors.js';
import { DocumentsGallery } from './documents/documents-gallery.js';
import { DocumentsTable } from './documents/documents-table.js';
import { ConversionInfo } from './extended-info/conversion-info.js';
Expand Down Expand Up @@ -54,6 +56,7 @@ import { TransactionsTable } from './transactions/transactions-table.js';
}
}
}
...AllChargesErrorsFields @defer
}
}
`;
Expand Down Expand Up @@ -153,6 +156,9 @@ export function ChargeExtendedInfo({
{fetching && (
<Loader className="flex self-center my-5" color="dark" size="xl" variant="dots" />
)}
{isFragmentReady(FetchChargeDocument, AllChargesErrorsFieldsFragmentDoc, charge) && (
<ChargeErrors data={charge} />
)}
{!fetching && charge && (
<div className="flex flex-row">
<Accordion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,12 @@ export const chargesResolvers: ChargesModule.Resolvers &
generatedledgerPromise,
]);

if (!generated || 'message' in generated || generated.balance?.isBalanced === false) {
if (
!generated ||
'message' in generated ||
generated.balance?.isBalanced === false ||
generated.errors?.length > 0
) {
return 'INVALID';
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
import { Injector } from 'graphql-modules';
import type { IGetDocumentsByChargeIdResult } from '@modules/documents/types';
import { getRateForCurrency } from '@modules/exchange-rates/helpers/exchange.helper.js';
import { ExchangeProvider } from '@modules/exchange-rates/providers/exchange.provider.js';
import { FiatExchangeProvider } from '@modules/exchange-rates/providers/fiat-exchange.provider.js';
import type { IGetTransactionsByChargeIdsResult } from '@modules/transactions/types.js';
import {
BALANCE_CANCELLATION_TAX_CATEGORY_ID,
DEFAULT_LOCAL_CURRENCY,
INPUT_VAT_TAX_CATEGORY_ID,
INTERNAL_WALLETS_IDS,
OUTPUT_VAT_TAX_CATEGORY_ID,
} from '@shared/constants';
import { Currency } from '@shared/enums';
import { formatCurrency } from '@shared/helpers';
import type { LedgerProto, StrictLedgerProto } from '@shared/types';
import { IGetBalanceCancellationByChargesIdsResult } from '../types.js';
import {
getFinancialAccountTaxCategoryId,
LedgerError,
validateTransactionBasicVariables,
} from './utils.helper.js';

export async function ledgerEntryFromDocument(
document: IGetDocumentsByChargeIdResult,
injector: Injector,
chargeId: string,
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`);
}

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) {
amountWithoutVat = amountWithoutVat - vatAmount;
vatTaxCategory = isCreditorCounterparty
? OUTPUT_VAT_TAX_CATEGORY_ID
: INPUT_VAT_TAX_CATEGORY_ID;
}

// handle non-local currencies
if (document.currency_code !== DEFAULT_LOCAL_CURRENCY) {
const exchangeRate = await getExchangeRateForDate(injector, document.date, currency);

// 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 = null;
let localCurrencyCreditAmount1 = 0;
let debitAmount1: number | null = null;
let localCurrencyDebitAmount1 = 0;
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: document.description ?? undefined,
reference1: document.serial_number ?? undefined,
isCreditorCounterparty,
ownerId,
chargeId,
};

return ledgerEntry;
}

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;

if (
!gotRelevantDocuments &&
transaction.source_reference &&
businessId &&
INTERNAL_WALLETS_IDS.includes(businessId)
) {
mainAccountId = await getFinancialAccountTaxCategoryId(injector, transaction, currency, true);
}

let amount = Number(transaction.amount);
let foreignAmount: number | undefined = undefined;

if (currency !== DEFAULT_LOCAL_CURRENCY) {
// get exchange rate for currency
const exchangeRate = await injector
.get(ExchangeProvider)
.getExchangeRates(currency, DEFAULT_LOCAL_CURRENCY, valueDate);

foreignAmount = amount;
// calculate amounts in ILS
amount = exchangeRate * amount;
}

const accountTaxCategoryId = await getFinancialAccountTaxCategoryId(
injector,
transaction,
currency,
);

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,
reference1: transaction.source_id,
isCreditorCounterparty,
ownerId,
currencyRate: transaction.currency_rate ? Number(transaction.currency_rate) : undefined,
chargeId,
};

return ledgerEntry;
}

export function ledgerEntryFromBalanceCancellation(
balanceCancellation: IGetBalanceCancellationByChargesIdsResult,
ledgerBalance: Map<string, { amount: number; entityId: string }>,
financialAccountLedgerEntries: StrictLedgerProto[],
chargeId: string,
ownerId: string,
): LedgerProto {
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 !== DEFAULT_LOCAL_CURRENCY &&
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 ? BALANCE_CANCELLATION_TAX_CATEGORY_ID : entityId,
creditAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined,
localCurrencyCreditAmount1: Math.abs(amount),
debitAccountID1: isCreditorCounterparty ? entityId : BALANCE_CANCELLATION_TAX_CATEGORY_ID,
debitAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined,
localCurrencyDebitAmount1: Math.abs(amount),
description: balanceCancellation.description ?? undefined,
reference1: financialAccountEntry.reference1,
isCreditorCounterparty,
ownerId,
currencyRate: financialAccountEntry.currencyRate,
chargeId,
};

return ledgerEntry;
}

async function getExchangeRateForDate(injector: Injector, date: Date, currency: Currency) {
const exchangeRates = await injector
.get(FiatExchangeProvider)
.getExchangeRatesByDatesLoader.load(date);
return getRateForCurrency(currency, exchangeRates);
}
Loading

0 comments on commit 72451c5

Please sign in to comment.