@accounter/server
Version:
Accounter GraphQL server
74 lines (67 loc) • 2.97 kB
text/typescript
import { GraphQLError } from 'graphql';
import { AdminContextProvider } from '../../admin-context/providers/admin-context.provider.js';
import { ChargesProvider } from '../../charges/providers/charges.provider.js';
import {
BY_SCORE_EVALUATION_CAP,
chargeRequiresMatch,
matchesQueueMode,
} from '../helpers/awaiting-match-queue.helper.js';
import { QueueMatchEvaluatorProvider } from '../providers/queue-match-evaluator.provider.js';
import type { ChargesMatcherModule } from '../types.js';
export const chargesAwaitingMatchQueueResolver: ChargesMatcherModule.Resolvers = {
Query: {
chargesAwaitingMatchQueue: async (
_,
{ limit, offset, businessId, fromDate, toDate, mode, sortBy },
{ injector },
) => {
try {
// Tenant scoping: only ever fetch charges of the verified admin owner,
// so a requested businessId can't leak other tenants' data
const { ownerId } = await injector.get(AdminContextProvider).getVerifiedAdminContext();
// Most recent first (default sort of getChargesByFilters)
const charges = await injector.get(ChargesProvider).getChargesByFilters({
ownerIds: [ownerId],
businessIds: businessId ? [businessId] : undefined,
fromDate,
toDate,
});
const queue = charges.filter(
charge => chargeRequiresMatch(charge) && matchesQueueMode(charge, mode),
);
const safeLimit = Math.max(0, limit ?? 20);
const safeOffset = Math.max(0, offset ?? 0);
const evaluator = injector.get(QueueMatchEvaluatorProvider);
if (sortBy === 'BY_SCORE') {
// Evaluate the capped window of most recent charges, sort the whole
// derived list by top confidence score, and paginate that list
const window = queue.slice(0, BY_SCORE_EVALUATION_CAP);
const evaluated = await evaluator.evaluateMatchesForCharges(window);
const topScore = (suggestions: { confidenceScore: number }[]): number =>
suggestions[0]?.confidenceScore ?? 0;
const sorted = evaluated.toSorted(
(a, b) => topScore(b.suggestions) - topScore(a.suggestions),
);
return {
baseCharges: sorted.slice(safeOffset, safeOffset + safeLimit),
totalCount: window.length,
};
}
// BY_DATE (default): paginate first, then lazily evaluate scores for
// the requested page only
const page = queue.slice(safeOffset, safeOffset + safeLimit);
const baseCharges = await evaluator.evaluateMatchesForCharges(page);
return {
baseCharges,
totalCount: queue.length,
};
} catch (e) {
if (e instanceof GraphQLError) {
throw e;
}
const message = (e as Error)?.message ?? 'Unknown error';
throw new GraphQLError(`Failed to fetch charges awaiting match: ${message}`);
}
},
},
};