@accounter/server
Version:
Accounter GraphQL server
97 lines (84 loc) • 3.4 kB
text/typescript
import type { charge_type } from '../../charges/types.js';
import type { ChargesMatcherModule } from '../types.js';
type ChargeMatchQueueMode = ChargesMatcherModule.ChargeMatchQueueMode;
/**
* DB charge types that are expected to hold both documents and transactions,
* and therefore require a document ↔ transaction match. Every other type (e.g.
* BANK_DEPOSIT, CREDITCARD_BANK, DIVIDEND, FOREIGN_SECURITIES, INTERNAL, VAT,
* PAYROLL, CONVERSION, FINANCIAL) never needs an accounting document and is
* excluded from the awaiting-match queue. A whitelist keeps new,
* non-matchable types out of the queue by default.
*/
const MATCHABLE_CHARGE_TYPES = new Set<charge_type>(['COMMON', 'BUSINESS_TRIP']);
/**
* Only COMMON and BUSINESS_TRIP charges (or charges with a not-yet-resolved
* `null` type, which resolve to either COMMON or BUSINESS_TRIP) are expected to
* hold both documents and transactions, so only they belong in the
* awaiting-match queue.
*/
export function chargeRequiresMatch(charge: { type?: charge_type | null }): boolean {
return !charge.type || MATCHABLE_CHARGE_TYPES.has(charge.type);
}
/**
* Max number of charges evaluated when sorting the queue BY_SCORE. Scoring is
* calculated on the fly and each pass loads candidates, transactions and
* documents, so the evaluation window is deliberately capped to bound request
* latency and DB load.
*/
export const BY_SCORE_EVALUATION_CAP = 100;
/**
* The subset of enriched charge fields the queue filtering relies on.
* Counts are int8 aggregates, surfaced by pgtyped as strings.
*/
export interface QueueChargeCounts {
transactions_count: string | null;
invoices_count: string | null;
receipts_count: string | null;
}
function hasTransactions(charge: QueueChargeCounts): boolean {
return Number(charge.transactions_count ?? 0) > 0;
}
function hasReceiptDocuments(charge: QueueChargeCounts): boolean {
return Number(charge.receipts_count ?? 0) > 0;
}
function hasAccountingDocuments(charge: QueueChargeCounts): boolean {
return Number(charge.invoices_count ?? 0) > 0 || hasReceiptDocuments(charge);
}
/**
* A charge is transaction-based when it has transactions but no receipt
* documents (mirrors the auto-match unmatched-charge semantics)
*/
export function isTransactionBaseCharge(charge: QueueChargeCounts): boolean {
return hasTransactions(charge) && !hasReceiptDocuments(charge);
}
/**
* A charge is document-based when it has accounting documents but no
* transactions
*/
export function isDocumentBaseCharge(charge: QueueChargeCounts): boolean {
return !hasTransactions(charge) && hasAccountingDocuments(charge);
}
/**
* A charge belongs in the awaiting-match queue when it is unmatched:
* transaction-based XOR document-based. Charges with both sides (matched) or
* neither (empty) are excluded.
*/
export function isUnmatchedBaseCharge(charge: QueueChargeCounts): boolean {
return isTransactionBaseCharge(charge) || isDocumentBaseCharge(charge);
}
/**
* Apply the optional queue mode filter on top of the unmatched check
*/
export function matchesQueueMode(
charge: QueueChargeCounts,
mode?: ChargeMatchQueueMode | null,
): boolean {
switch (mode) {
case 'DOC_BASE':
return isDocumentBaseCharge(charge);
case 'TRANSACTION_BASE':
return isTransactionBaseCharge(charge);
default:
return isUnmatchedBaseCharge(charge);
}
}