@accounter/server
Version:
Accounter GraphQL server
282 lines (256 loc) • 10.9 kB
text/typescript
import DataLoader from 'dataloader';
import { GraphQLError } from 'graphql';
import { Injectable, Scope } from 'graphql-modules';
import { sql } from '@pgtyped/runtime';
import { reassureOwnerIdExists } from '../../../shared/helpers/index.js';
import { AdminContextProvider } from '../../admin-context/providers/admin-context.provider.js';
import { TenantAwareDBClient } from '../../app-providers/tenant-db-client.js';
import { TransactionsProvider } from '../../transactions/providers/transactions.provider.js';
import type { IGetTransactionsByChargeIdsResult } from '../../transactions/types.js';
import type {
IDeleteBusinessTripExpenseParams,
IDeleteBusinessTripExpenseQuery,
IGetBusinessTripsExpensesByBusinessTripIdsQuery,
IGetBusinessTripsExpensesByIdsQuery,
IInsertBusinessTripExpenseParams,
IInsertBusinessTripExpenseQuery,
IUpdateBusinessTripExpenseParams,
IUpdateBusinessTripExpenseQuery,
} from '../types.js';
import { BusinessTripAccommodationsExpensesProvider } from './business-trips-expenses-accommodations.provider.js';
import { BusinessTripCarRentalExpensesProvider } from './business-trips-expenses-car-rental.provider.js';
import { BusinessTripFlightsExpensesProvider } from './business-trips-expenses-flights.provider.js';
import { BusinessTripOtherExpensesProvider } from './business-trips-expenses-other.provider.js';
import { BusinessTripTravelAndSubsistenceExpensesProvider } from './business-trips-expenses-travel-and-subsistence.provider.js';
import { BusinessTripsProvider } from './business-trips.provider.js';
const getBusinessTripsExpensesByBusinessTripIds = sql<IGetBusinessTripsExpensesByBusinessTripIdsQuery>`
SELECT *
FROM accounter_schema.extended_business_trip_transactions
WHERE business_trip_id IN $$businessTripIds;`;
const getBusinessTripsExpensesByIds = sql<IGetBusinessTripsExpensesByIdsQuery>`
SELECT *
FROM accounter_schema.extended_business_trip_transactions
WHERE id IN $$expenseIds;`;
const updateBusinessTripExpense = sql<IUpdateBusinessTripExpenseQuery>`
UPDATE accounter_schema.business_trips_transactions
SET
business_trip_id = COALESCE(
$businessTripId,
business_trip_id
),
category = COALESCE(
$category,
category
)
WHERE
id = $businessTripExpenseId
RETURNING *;
`;
const insertBusinessTripExpense = sql<IInsertBusinessTripExpenseQuery>`
INSERT INTO accounter_schema.business_trips_transactions (business_trip_id, category, owner_id)
VALUES($businessTripId, $category, $ownerId)
RETURNING *;`;
const deleteBusinessTripExpense = sql<IDeleteBusinessTripExpenseQuery>`
DELETE FROM accounter_schema.business_trips_transactions
WHERE id = $businessTripExpenseId
RETURNING id;
`;
export class BusinessTripExpensesProvider {
constructor(
private db: TenantAwareDBClient,
private adminContextProvider: AdminContextProvider,
private businessTripsProvider: BusinessTripsProvider,
private flightExpensesProvider: BusinessTripFlightsExpensesProvider,
private accommodationsExpensesProvider: BusinessTripAccommodationsExpensesProvider,
private travelAndSubsistenceExpensesProvider: BusinessTripTravelAndSubsistenceExpensesProvider,
private otherExpensesProvider: BusinessTripOtherExpensesProvider,
private carRentalExpensesProvider: BusinessTripCarRentalExpensesProvider,
private transactionsProvider: TransactionsProvider,
) {}
private async batchBusinessTripsExpensesByBusinessTripIds(businessTripIds: readonly string[]) {
const businessTrips = await getBusinessTripsExpensesByBusinessTripIds.run(
{
businessTripIds,
},
this.db,
);
return businessTripIds.map(id =>
businessTrips.filter(record => record.business_trip_id === id),
);
}
public getBusinessTripsExpensesByBusinessTripIdLoader = new DataLoader((ids: readonly string[]) =>
this.batchBusinessTripsExpensesByBusinessTripIds(ids),
);
private async batchBusinessTripsExpenseMatchesByIds(expenseIds: readonly string[]) {
const businessTripsExpenses = await getBusinessTripsExpensesByIds.run(
{
expenseIds,
},
this.db,
);
return expenseIds.map(id => businessTripsExpenses.find(record => record.id === id));
}
public getBusinessTripsExpensesByIdLoader = new DataLoader((ids: readonly string[]) =>
this.batchBusinessTripsExpenseMatchesByIds(ids),
);
public updateBusinessTripExpense(params: IUpdateBusinessTripExpenseParams) {
return updateBusinessTripExpense.run(params, this.db);
}
public async insertBusinessTripExpense(params: IInsertBusinessTripExpenseParams) {
if (params.businessTripId) {
this.invalidateByBusinessTripId(params.businessTripId);
}
const { ownerId } = await this.adminContextProvider.getVerifiedAdminContext();
return insertBusinessTripExpense.run(reassureOwnerIdExists(params, ownerId), this.db);
}
public async getBusinessTripExtendedExpensesByChargeId(chargeId: string) {
const businessTrip =
await this.businessTripsProvider.getBusinessTripsByChargeIdLoader.load(chargeId);
if (!businessTrip) {
throw new GraphQLError(`Business trip not found for charge id ${chargeId}`);
}
const [
allExpenses,
flightExpenses,
accommodationsExpenses,
travelAndSubsistenceExpenses,
otherExpenses,
carRentalExpenses,
] = await Promise.all([
this.getBusinessTripsExpensesByBusinessTripIdLoader.load(businessTrip.id),
this.flightExpensesProvider.getBusinessTripsFlightsExpensesByBusinessTripIdLoader.load(
businessTrip.id,
),
this.accommodationsExpensesProvider.getBusinessTripsAccommodationsExpensesByBusinessTripIdLoader.load(
businessTrip.id,
),
this.travelAndSubsistenceExpensesProvider.getBusinessTripsTravelAndSubsistenceExpensesByBusinessTripIdLoader.load(
businessTrip.id,
),
this.otherExpensesProvider.getBusinessTripsOtherExpensesByBusinessTripIdLoader.load(
businessTrip.id,
),
this.carRentalExpensesProvider.getBusinessTripsCarRentalExpensesByBusinessTripIdLoader.load(
businessTrip.id,
),
]);
const extendedIds = new Set<string>(
[
...flightExpenses,
...accommodationsExpenses,
...travelAndSubsistenceExpenses,
...otherExpenses,
...carRentalExpenses,
].map(t => t.id),
);
return {
nonExtendedExpenses: allExpenses?.filter(t => !extendedIds.has(t.id!)),
flightExpenses,
accommodationsExpenses,
travelAndSubsistenceExpenses,
otherExpenses,
carRentalExpenses,
};
}
public async getBusinessTripExtendedExpensesByBusinessTripId(businessTripId: string) {
const [
allExpenses,
flightExpenses,
accommodationsExpenses,
travelAndSubsistenceExpenses,
otherExpenses,
carRentalExpenses,
] = await Promise.all([
this.getBusinessTripsExpensesByBusinessTripIdLoader.load(businessTripId),
this.flightExpensesProvider.getBusinessTripsFlightsExpensesByBusinessTripIdLoader.load(
businessTripId,
),
this.accommodationsExpensesProvider.getBusinessTripsAccommodationsExpensesByBusinessTripIdLoader.load(
businessTripId,
),
this.travelAndSubsistenceExpensesProvider.getBusinessTripsTravelAndSubsistenceExpensesByBusinessTripIdLoader.load(
businessTripId,
),
this.otherExpensesProvider.getBusinessTripsOtherExpensesByBusinessTripIdLoader.load(
businessTripId,
),
this.carRentalExpensesProvider.getBusinessTripsCarRentalExpensesByBusinessTripIdLoader.load(
businessTripId,
),
]);
const extendedIds = new Set<string>(
[
...flightExpenses,
...accommodationsExpenses,
...travelAndSubsistenceExpenses,
...otherExpenses,
...carRentalExpenses,
].map(t => t.id),
);
return {
nonExtendedExpenses: allExpenses?.filter(t => !extendedIds.has(t.id!)),
flightExpenses,
accommodationsExpenses,
travelAndSubsistenceExpenses,
otherExpenses,
carRentalExpenses,
};
}
public async getTransactionsByBusinessTripId(businessTripId: string) {
const chargeIds =
await this.businessTripsProvider.getChargeIdsByBusinessTripIdLoader.load(businessTripId);
return this.transactionsProvider.transactionsByChargeIDLoader
.loadMany(chargeIds)
.then(
res => res.filter(r => !(r instanceof Error)).flat() as IGetTransactionsByChargeIdsResult[],
);
}
public deleteBusinessTripExpense(params: IDeleteBusinessTripExpenseParams) {
if (params.businessTripExpenseId) {
this.invalidateById(params.businessTripExpenseId);
}
return deleteBusinessTripExpense.run(params, this.db);
}
private invalidateExpenseExtensionsById(expenseId: string) {
this.flightExpensesProvider.invalidateById(expenseId);
this.accommodationsExpensesProvider.invalidateById(expenseId);
this.travelAndSubsistenceExpensesProvider.invalidateById(expenseId);
this.otherExpensesProvider.invalidateById(expenseId);
this.carRentalExpensesProvider.invalidateById(expenseId);
}
private invalidateExpenseExtensionsByBusinessTripId(businessTripId: string) {
this.flightExpensesProvider.invalidateByBusinessTripId(businessTripId);
this.accommodationsExpensesProvider.invalidateByBusinessTripId(businessTripId);
this.travelAndSubsistenceExpensesProvider.invalidateByBusinessTripId(businessTripId);
this.otherExpensesProvider.invalidateByBusinessTripId(businessTripId);
this.carRentalExpensesProvider.invalidateByBusinessTripId(businessTripId);
}
public async invalidateById(expenseId: string) {
const expense = await this.getBusinessTripsExpensesByIdLoader.load(expenseId);
if (expense?.business_trip_id) {
this.getBusinessTripsExpensesByBusinessTripIdLoader.clear(expense.business_trip_id);
this.invalidateExpenseExtensionsByBusinessTripId(expense.business_trip_id);
}
this.getBusinessTripsExpensesByIdLoader.clear(expenseId);
this.invalidateExpenseExtensionsById(expenseId);
}
public async invalidateByBusinessTripId(businessTripId: string) {
const expenses = await this.getBusinessTripsExpensesByBusinessTripIdLoader.load(businessTripId);
for (const expense of expenses ?? []) {
if (expense.id) {
this.getBusinessTripsExpensesByIdLoader.clear(expense.id);
this.invalidateExpenseExtensionsById(expense.id);
}
}
this.getBusinessTripsExpensesByBusinessTripIdLoader.clear(businessTripId);
this.invalidateExpenseExtensionsByBusinessTripId(businessTripId);
}
public clearCache() {
this.getBusinessTripsExpensesByBusinessTripIdLoader.clearAll();
this.getBusinessTripsExpensesByIdLoader.clearAll();
}
}