@accounter/server
Version:
Accounter GraphQL server
245 lines (227 loc) • 9.04 kB
text/typescript
import { endOfYear, startOfYear } from 'date-fns';
import { GraphQLError } from 'graphql';
import { Currency } from '../../../shared/enums.js';
import { dateToTimelessDateString, formatFinancialAmount } from '../../../shared/helpers/index.js';
import { TimelessDateString } from '../../../shared/types/index.js';
import { AdminContextProvider } from '../../admin-context/providers/admin-context.provider.js';
import { ScopeProvider } from '../../auth/providers/scope.provider.js';
import { CountriesProvider } from '../../countries/providers/countries.provider.js';
import { BusinessesProvider } from '../../financial-entities/providers/businesses.provider.js';
import { TaxCategoriesProvider } from '../../financial-entities/providers/tax-categories.provider.js';
import { AnnualRevenueReportProvider } from '../providers/annual-revenue-report.provider.js';
import type { ReportsModule } from '../types.js';
interface NormalizedRecord {
clientId: string;
countryCode: string;
amountIls: number;
amountUsd: number;
record: {
id: string;
chargeId: string;
businessId: string;
date: TimelessDateString;
description: string | null;
reference: string | null;
amountIls: number;
amountUsd: number;
amountOriginal: number;
currency: Currency;
};
}
export const annualRevenueResolvers: ReportsModule.Resolvers = {
Query: {
annualRevenueReport: async (_, { filters: { year, adminBusinessId } }, { injector }) => {
// Per-business report: validate an explicitly requested business against
// the read scope and use its admin context; default to the primary.
let targetAdminContext;
if (adminBusinessId) {
// Validates the id is within the read scope (throws otherwise), so the
// requested id can be used directly.
await injector.get(ScopeProvider).getReadScope([adminBusinessId]);
targetAdminContext = await injector
.get(AdminContextProvider)
.adminContextByOwnerIdLoader.load(adminBusinessId);
if (!targetAdminContext) {
throw new GraphQLError('Admin context not found for the requested business', {
extensions: { code: 'NOT_FOUND' },
});
}
} else {
targetAdminContext = await injector.get(AdminContextProvider).getVerifiedAdminContext();
}
const ownerId = targetAdminContext.ownerId;
const incomeTaxCategories = await injector
.get(TaxCategoriesProvider)
.taxCategoriesBySortCodeLoader.load(810);
const date = new Date(year, 6, 1);
const records = await injector.get(AnnualRevenueReportProvider).getNormalizedRevenueRecords({
incomeTaxCategoriesIDs: incomeTaxCategories.map(tc => tc.id),
incomeToCollectId: targetAdminContext.crossYear.incomeToCollectTaxCategoryId,
ownerId,
fromDate: dateToTimelessDateString(startOfYear(date)),
toDate: dateToTimelessDateString(endOfYear(date)),
});
// Normalize records (client, amount local currency, amount default foreign currency)
const businessesProvider = injector.get(BusinessesProvider);
const normalizedRecords: NormalizedRecord[] = [];
const businesses = await Promise.all(
records.map(r =>
r.business_id ? businessesProvider.getBusinessByIdLoader.load(r.business_id) : null,
),
);
records.map((record, index) => {
if (!record.business_id) return;
const business = businesses[index];
if (!business) return;
const amountIls = record.amount_local ? parseFloat(record.amount_local) : 0;
const amountUsd = record.amount_usd ? parseFloat(record.amount_usd) : 0;
normalizedRecords.push({
clientId: record.business_id,
countryCode: business.country,
amountIls,
amountUsd,
record: {
id: record.id ?? '',
chargeId: record.charge_id!,
businessId: record.business_id!,
date: dateToTimelessDateString(record.date!),
description: record.description ?? null,
reference: record.reference ?? null,
amountIls,
amountUsd,
amountOriginal: record.amount_foreign ? parseFloat(record.amount_foreign) : 0,
currency: record.currency as Currency,
},
});
});
// Group and sum by client
const clientSums = new Map<
string,
{
countryCode: string;
amountIls: number;
amountUsd: number;
records: {
id: string;
chargeId: string;
businessId: string;
date: TimelessDateString;
description: string | null;
reference: string | null;
amountIls: number;
amountUsd: number;
amountOriginal: number;
currency: Currency;
}[];
}
>();
for (const normalized of normalizedRecords) {
const existing = clientSums.get(normalized.clientId);
if (existing) {
existing.amountIls += normalized.amountIls;
existing.amountUsd += normalized.amountUsd;
existing.records.push(normalized.record);
} else {
clientSums.set(normalized.clientId, {
countryCode: normalized.countryCode,
amountIls: normalized.amountIls,
amountUsd: normalized.amountUsd,
records: [normalized.record],
});
}
}
// Group by country
const countrySums = new Map<
string,
{
amountIls: number;
amountUsd: number;
clients: Array<{
id: string;
amountIls: number;
amountUsd: number;
records: {
id: string;
chargeId: string;
businessId: string;
date: TimelessDateString;
description: string | null;
reference: string | null;
amountIls: number;
amountUsd: number;
amountOriginal: number;
currency: Currency;
}[];
}>;
}
>();
for (const [clientId, data] of clientSums.entries()) {
const existing = countrySums.get(data.countryCode);
if (existing) {
existing.amountIls += data.amountIls;
existing.amountUsd += data.amountUsd;
existing.clients.push({
id: clientId,
amountIls: data.amountIls,
amountUsd: data.amountUsd,
records: data.records,
});
} else {
countrySums.set(data.countryCode, {
amountIls: data.amountIls,
amountUsd: data.amountUsd,
clients: [
{
id: clientId,
amountIls: data.amountIls,
amountUsd: data.amountUsd,
records: data.records,
},
],
});
}
}
// Build the response
const countriesProvider = injector.get(CountriesProvider);
const countries = await Promise.all(
Array.from(countrySums.entries()).map(async ([countryCode, data]) => {
const country = await countriesProvider.getCountryByCodeLoader.load(countryCode);
const clients = await Promise.all(
data.clients.map(async client => {
const business = await businessesProvider.getBusinessByIdLoader.load(client.id);
return {
id: `annual-revenue-report-${year}-${countryCode}-${client.id}`,
name: business?.name ?? '',
revenueLocal: formatFinancialAmount(client.amountIls, Currency.Ils),
revenueDefaultForeign: formatFinancialAmount(client.amountUsd, Currency.Usd),
records: client.records.map(record => ({
id: `annual-revenue-report-${year}-${countryCode}-${client.id}-record-${record.id}`,
chargeId: record.chargeId,
date: record.date,
description: record.description,
reference: record.reference,
revenueLocal: formatFinancialAmount(record.amountIls, Currency.Ils),
revenueDefaultForeign: formatFinancialAmount(record.amountUsd, Currency.Usd),
revenueOriginal: formatFinancialAmount(record.amountOriginal, record.currency),
})),
};
}),
);
return {
id: `annual-revenue-report-${year}-${countryCode}`,
code: countryCode,
name: country?.name ?? countryCode,
revenueLocal: formatFinancialAmount(data.amountIls, Currency.Ils),
revenueDefaultForeign: formatFinancialAmount(data.amountUsd, Currency.Usd),
clients,
};
}),
);
return {
id: year.toString(),
year,
countries,
};
},
},
};