Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate partial ledger #622

Merged
merged 4 commits into from
Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading