@accounter/server
Version:
Accounter GraphQL server
127 lines (117 loc) • 4.68 kB
text/typescript
import type { Injector } from 'graphql-modules';
import type { ChargeFilter } from '../../../__generated__/types.js';
import { ChargeSortByField, ChargeTypeEnum } from '../../../shared/enums.js';
import { errorSimplifier } from '../../../shared/errors.js';
import { ChargesProvider } from '../providers/charges.provider.js';
import type { accountant_statusArray, IGetChargesByFiltersResult } from '../types.js';
import { getChargeType, normalizeDbType } from './charge-type.js';
type PaginatedChargesResult = {
__typename: 'PaginatedCharges';
nodes: IGetChargesByFiltersResult[];
pageInfo: {
totalPages: number;
totalRecords: number;
};
};
type FetchFilteredChargesParams = {
filters?: ChargeFilter | null;
page: number;
limit: number;
/**
* Narrows the result to these charge ids (on top of `filters`). An empty
* array means "no charge qualifies" and short-circuits — the provider reads
* an empty id list as "no id filter at all" and would return everything.
*/
restrictToIds?: string[];
};
/**
* Shared charge listing: maps a `ChargeFilter` onto the provider parameters,
* applies the post-fetch concrete-type narrowing and paginates. Used by both
* `allCharges` and `chargesWithMissingRequiredInfo` so the two screens offer
* the exact same filter set.
*
* `page` is zero-based: page 0 is the first page.
*/
export async function fetchFilteredCharges(
injector: Injector,
{ filters, page, limit, restrictToIds }: FetchFilteredChargesParams,
): Promise<PaginatedChargesResult> {
const emptyResult: PaginatedChargesResult = {
__typename: 'PaginatedCharges',
nodes: [],
pageInfo: { totalPages: 0, totalRecords: 0 },
};
if (restrictToIds?.length === 0) {
return emptyResult;
}
// handle sort column
let sortColumn: 'event_date' | 'event_amount' | 'abs_event_amount' = 'event_date';
switch (filters?.sortBy?.field) {
case ChargeSortByField.Amount:
sortColumn = 'event_amount';
break;
case ChargeSortByField.AbsAmount:
sortColumn = 'abs_event_amount';
break;
case ChargeSortByField.Date:
sortColumn = 'event_date';
break;
}
const charges = await injector
.get(ChargesProvider)
.getChargesByFilters({
IDs: restrictToIds,
ownerIds: filters?.byOwners,
// Both date families are forwarded: `fromDate`/`toDate` test the
// charge's main date (documents min/max, else transaction event
// dates) — containment — while `fromAnyDate`/`toAnyDate` test whether
// the charge's span across *all* date sources overlaps the range. The
// SQL has always supported both; only the `*AnyDate` pair was wired
// up here, so a caller passing `fromDate`/`toDate` (as the MCP charge
// tools now can) got an unfiltered result instead of a narrower one.
fromDate: filters?.fromDate,
toDate: filters?.toDate,
fromAnyDate: filters?.fromAnyDate,
toAnyDate: filters?.toAnyDate,
sortColumn,
asc: filters?.sortBy?.asc !== false,
chargeType: filters?.chargesType,
businessIds: filters?.byBusinesses,
businessTripIds: filters?.byBusinessTrips,
withMissingCounterparty: filters?.withMissingCounterparty,
withoutInvoice: filters?.withoutInvoice,
withoutReceipt: filters?.withoutReceipt,
withoutDocuments: filters?.withoutDocuments,
withOpenDocuments: filters?.withOpenDocuments,
withoutTransactions: filters?.withoutTransactions,
withoutLedger: filters?.withoutLedger,
freeText: filters?.freeText?.trim().toLowerCase(),
tags: filters?.byTags,
accountantStatuses: filters?.accountantStatus as accountant_statusArray | undefined,
})
.catch(error => {
throw errorSimplifier('Error fetching charges', error);
});
// charge __typename is resolved dynamically, so filter by concrete type post-fetch
let filteredCharges = charges;
if (filters?.byChargeTypes?.length) {
const wantedTypes = new Set<ChargeTypeEnum>(filters.byChargeTypes?.map(normalizeDbType));
const chargeTypes = await Promise.all(
charges.map(charge =>
getChargeType(charge, injector).catch(error => {
throw errorSimplifier('Failed to determine charge type', error);
}),
),
);
filteredCharges = charges.filter((_, index) => wantedTypes.has(chargeTypes[index]));
}
const pageCharges = filteredCharges.slice(page * limit, (page + 1) * limit);
return {
__typename: 'PaginatedCharges',
nodes: pageCharges,
pageInfo: {
totalPages: Math.ceil(filteredCharges.length / limit),
totalRecords: filteredCharges.length,
},
};
}