@accounter/server
Version:
Accounter GraphQL server
171 lines • 8.11 kB
JavaScript
import { DocumentType } from '../../../shared/enums.js';
import { IssuedDocumentsProvider } from '../../documents/providers/issued-documents.provider.js';
import { ClientsProvider } from '../../financial-entities/providers/clients.provider.js';
import { calculateAmountConfidence } from '../helpers/amount-confidence.helper.js';
import { calculateBusinessConfidence } from '../helpers/business-confidence.helper.js';
import { calculateCurrencyConfidence } from '../helpers/currency-confidence.helper.js';
import { calculateDateConfidence } from '../helpers/date-confidence.helper.js';
import { calculateOverallConfidence } from '../helpers/overall-confidence.helper.js';
import { aggregateDocuments } from './document-aggregator.js';
import { aggregateTransactions } from './transaction-aggregator.js';
/**
* Select appropriate transaction date based on document type
* Per specification:
* - INVOICE/CREDIT_INVOICE: Use event_date
* - RECEIPT/INVOICE_RECEIPT: Use debit_date (debit_timestamp or debit_date)
* - OTHER/PROFORMA/UNPROCESSED: Calculate both and use better score
*
* @param transaction - Aggregated transaction data
* @param documentType - Document type being matched
* @returns The date to use for matching
*/
export function selectTransactionDate(transaction, documentType) {
switch (documentType) {
case DocumentType.Invoice:
case DocumentType.CreditInvoice:
case DocumentType.Receipt:
case DocumentType.InvoiceReceipt:
// For invoices, use event_date
return transaction.date;
case DocumentType.Other:
case DocumentType.Proforma:
case DocumentType.Unprocessed:
// For flexible types, use event_date as default
// (caller should calculate both and use better score)
return transaction.date;
default:
// Fallback to event_date for unknown types
return transaction.date;
}
}
/**
* Score a potential match between transaction charge and document charge
* Aggregates data from both charges and calculates confidence score
*
* @param txCharge - Transaction charge to match
* @param docCharge - Document charge candidate
* @param userId - Current user UUID for business extraction
* @param injector - Optional GraphQL modules injector for provider access (for client matching)
* @returns Match score with confidence and component breakdown
* @throws Error if aggregation fails (mixed currencies, multiple businesses, etc.)
*/
export async function scoreMatch(txCharge, docCharge, userId, injector) {
// Aggregate transaction data
const aggregatedTransaction = aggregateTransactions(txCharge.transactions);
// Aggregate document data (includes business extraction and amount normalization)
const aggregatedDocument = aggregateDocuments(docCharge.documents, userId);
// // For flexible document types, try both dates and use the better score
// if (
// aggregatedDocument.type === 'OTHER' ||
// aggregatedDocument.type === 'PROFORMA' ||
// aggregatedDocument.type === 'UNPROCESSED'
// ) {
// // Calculate score with event_date
// const scoreWithEventDate = await calculateScoreWithDate(
// aggregatedTransaction,
// aggregatedDocument,
// aggregatedTransaction.date,
// docCharge.chargeId,
// injector,
// );
// // Calculate score with debit_date (if available)
// if (aggregatedTransaction.debitDate) {
// const scoreWithDebitDate = await calculateScoreWithDate(
// aggregatedTransaction,
// aggregatedDocument,
// aggregatedTransaction.debitDate,
// docCharge.chargeId,
// injector,
// );
// // Return the better score
// return scoreWithEventDate.confidenceScore > scoreWithDebitDate.confidenceScore
// ? scoreWithEventDate
// : scoreWithDebitDate;
// }
// return scoreWithEventDate;
// }
// For specific document types, use the appropriate date
const transactionDate = selectTransactionDate(aggregatedTransaction, aggregatedDocument.type);
return calculateScoreWithDate(aggregatedTransaction, aggregatedDocument, transactionDate, docCharge.chargeId, injector);
}
/**
* Helper function to calculate match score with a specific transaction date
* @param transaction - Aggregated transaction
* @param document - Aggregated document
* @param transactionDate - Date to use from transaction
* @param chargeId - Document charge ID
* @param injector - Optional GraphQL modules injector for provider access
* @returns Match score
*/
async function calculateScoreWithDate(transaction, document, transactionDate, chargeId, injector) {
// Check if transaction and document share the same business entity
const businessesMatch = transaction.businessId != null && transaction.businessId === document.businessId;
// If businesses match, verify if it's a registered CLIENT
let isClientMatch = false;
if (businessesMatch && transaction.businessId) {
try {
const client = await injector
.get(ClientsProvider)
.getClientByIdLoader.load(transaction.businessId);
// isClientMatch is true only if business is a registered client
isClientMatch = client != null;
}
catch (error) {
console.error(`Error looking up client for business ID ${transaction.businessId}:`, error);
isClientMatch = false;
}
}
// Determine gentle eligibility for client matches
// Conditions:
// - Same business and registered client
// - Document type is INVOICE or PROFORMA
// - Document status is OPEN (via issued documents status by charge id)
// - Document date <= transaction date (date-only compare)
let isGentleEligible = false;
if (isClientMatch) {
// Type gating
const typeIsEligible = document.type === DocumentType.Invoice || document.type === DocumentType.Proforma;
// Date-only comparison: doc.date <= transactionDate
const docDate = new Date(document.date.getFullYear(), document.date.getMonth(), document.date.getDate());
const txDate = new Date(transactionDate.getFullYear(), transactionDate.getMonth(), transactionDate.getDate());
const dateIsEligible = docDate.getTime() <= txDate.getTime();
// Status gating via DataLoader
let statusIsEligible;
try {
const status = (await injector
.get(IssuedDocumentsProvider)
.getIssuedDocumentsStatusByChargeIdLoader.load(chargeId));
// Expect shape with open_docs_flag boolean
statusIsEligible = !!(status && status.open_docs_flag === true);
}
catch {
// If status lookup fails, treat as ineligible (do not throw)
statusIsEligible = false;
}
isGentleEligible = typeIsEligible && dateIsEligible && statusIsEligible;
}
// Calculate individual confidence scores
const amountScore = calculateAmountConfidence(transaction.amount, document.amount);
const currencyScore = calculateCurrencyConfidence(transaction.currency, document.currency);
const businessScore = calculateBusinessConfidence(transaction.businessId, document.businessId);
// Use gentle eligibility to compute date confidence
const dateScore = calculateDateConfidence(transactionDate, document.date, isGentleEligible);
// Create components object
const components = {
amount: amountScore,
currency: currencyScore,
business: businessScore,
date: dateScore,
};
// Calculate overall confidence
const confidenceScore = calculateOverallConfidence(components);
return {
chargeId,
confidenceScore,
components,
// Expose gentle flag for tie-breaker handling upstream
// (optional property tolerated by consumer types)
gentleMode: isGentleEligible,
};
}
//# sourceMappingURL=match-scorer.provider.js.map