UNPKG

@accounter/server

Version:
282 lines (256 loc) • 10.9 kB
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; `; @Injectable({ scope: Scope.Operation, global: true, }) 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(); } }