-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
1 parent
6d71c78
commit 72451c5
Showing
23 changed files
with
1,709 additions
and
1,388 deletions.
There are no files selected for viewing
43 changes: 43 additions & 0 deletions
43
packages/client/src/components/all-charges/charge-errors.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
285 changes: 285 additions & 0 deletions
285
packages/server/src/modules/ledger/helpers/common-charge-ledger.helper.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
Oops, something went wrong.