UNPKG

@accounter/server

Version:

363 lines (322 loc) • 12.6 kB
import DataLoader from 'dataloader'; import { Injectable, Scope } from 'graphql-modules'; import { DBProvider } from '@modules/app-providers/db.provider.js'; import { sql } from '@pgtyped/runtime'; import { Currency } from '@shared/gql-types'; import { getCacheInstance } from '@shared/helpers'; import type { IDeleteBusinessTaxCategoryParams, IDeleteBusinessTaxCategoryQuery, IDeleteTaxCategoryQuery, IGetAllTaxCategoriesQuery, IGetAllTaxCategoriesResult, IGetTaxCategoryByBusinessAndOwnerIDsQuery, IGetTaxCategoryByChargeIDsQuery, IGetTaxCategoryByFinancialAccountIdsAndCurrenciesQuery, IGetTaxCategoryByFinancialAccountOwnerIdsQuery, IGetTaxCategoryByIDsQuery, IInsertBusinessTaxCategoryParams, IInsertBusinessTaxCategoryQuery, IInsertTaxCategoryParams, IInsertTaxCategoryQuery, IReplaceTaxCategoriesQuery, IUpdateBusinessTaxCategoryParams, IUpdateBusinessTaxCategoryQuery, IUpdateTaxCategoryParams, IUpdateTaxCategoryQuery, } from '../types.js'; const getTaxCategoryByBusinessAndOwnerIDs = sql<IGetTaxCategoryByBusinessAndOwnerIDsQuery>` SELECT fe.id, fe.name, fe.sort_code, fe.type, fe.created_at, fe.updated_at, tc.hashavshevet_name, tcm.business_id, tcm.owner_id FROM accounter_schema.tax_categories tc LEFT JOIN accounter_schema.financial_entities fe ON fe.id = tc.id LEFT JOIN accounter_schema.business_tax_category_match tcm ON tcm.tax_category_id = tc.id WHERE tcm.business_id IN $$BusinessIds AND tcm.owner_id IN $$OwnerIds;`; const getTaxCategoryByFinancialAccountIdsAndCurrencies = sql<IGetTaxCategoryByFinancialAccountIdsAndCurrenciesQuery>` SELECT fe.id, fe.name, fe.sort_code, fe.type, fe.created_at, fe.updated_at, fe.owner_id, tc.hashavshevet_name, fatc.financial_account_id, fatc.currency FROM accounter_schema.financial_accounts_tax_categories fatc LEFT JOIN accounter_schema.tax_categories tc ON fatc.tax_category_id = tc.id LEFT JOIN accounter_schema.financial_entities fe ON fatc.tax_category_id = fe.id WHERE fatc.currency IN $$Currencies AND fatc.financial_account_id IN $$FinancialAccountIds;`; const getTaxCategoryByFinancialAccountOwnerIds = sql<IGetTaxCategoryByFinancialAccountOwnerIdsQuery>` SELECT fe.id, fe.name, fe.sort_code, fe.type, fe.created_at, fe.updated_at, fe.owner_id, tc.hashavshevet_name, fatc.financial_account_id, fatc.currency, fa.owner as "financial_account_owner_id" FROM accounter_schema.financial_accounts_tax_categories fatc LEFT JOIN accounter_schema.tax_categories tc ON fatc.tax_category_id = tc.id LEFT JOIN accounter_schema.financial_entities fe ON fatc.tax_category_id = fe.id LEFT JOIN accounter_schema.financial_accounts fa ON fa.id = fatc.financial_account_id WHERE fa.owner IN $$ownerIds;`; const getTaxCategoryByChargeIDs = sql<IGetTaxCategoryByChargeIDsQuery>` SELECT fe.id, fe.name, fe.sort_code, fe.type, fe.created_at, fe.updated_at, tc.hashavshevet_name, c.business_id, c.owner_id, c.id as charge_id FROM accounter_schema.extended_charges c LEFT JOIN accounter_schema.tax_categories tc ON c.tax_category_id = tc.id LEFT JOIN accounter_schema.financial_entities fe ON fe.id = tc.id WHERE tc.id IS NOT NULL AND c.id IN $$chargeIds;`; const getTaxCategoryByIDs = sql<IGetTaxCategoryByIDsQuery>` SELECT fe.*, tc.hashavshevet_name FROM accounter_schema.tax_categories tc LEFT JOIN accounter_schema.financial_entities fe ON fe.id = tc.id WHERE tc.id IN $$Ids;`; const getAllTaxCategories = sql<IGetAllTaxCategoriesQuery>` SELECT fe.*, tc.hashavshevet_name FROM accounter_schema.tax_categories tc LEFT JOIN accounter_schema.financial_entities fe ON fe.id = tc.id;`; const updateTaxCategory = sql<IUpdateTaxCategoryQuery>` UPDATE accounter_schema.tax_categories SET hashavshevet_name = COALESCE( $hashavshevetName, hashavshevet_name ) WHERE id = $taxCategoryId RETURNING *; `; const updateBusinessTaxCategory = sql<IUpdateBusinessTaxCategoryQuery>` UPDATE accounter_schema.business_tax_category_match SET tax_category_id = COALESCE( $taxCategoryId, tax_category_id ) WHERE owner_id = $ownerId AND business_id = $businessId RETURNING *; `; const insertTaxCategory = sql<IInsertTaxCategoryQuery>` INSERT INTO accounter_schema.tax_categories (id, hashavshevet_name) VALUES ($id, $hashavshevetName) RETURNING *;`; const insertBusinessTaxCategory = sql<IInsertBusinessTaxCategoryQuery>` INSERT INTO accounter_schema.business_tax_category_match (business_id, owner_id, tax_category_id) VALUES ($businessId, $ownerId, $taxCategoryId) RETURNING *;`; const deleteBusinessTaxCategory = sql<IDeleteBusinessTaxCategoryQuery>` DELETE FROM accounter_schema.business_tax_category_match WHERE business_id = $businessId AND owner_id = $ownerId RETURNING *;`; const deleteTaxCategory = sql<IDeleteTaxCategoryQuery>` DELETE FROM accounter_schema.tax_categories WHERE id = $taxCategoryId RETURNING *;`; const replaceTaxCategories = sql<IReplaceTaxCategoriesQuery>` WITH accounts AS ( UPDATE accounter_schema.financial_accounts_tax_categories SET tax_category_id = $targetTaxCategoryId WHERE tax_category_id = $taxCategoryIdToReplace RETURNING financial_account_id ), business_match AS ( UPDATE accounter_schema.business_tax_category_match SET tax_category_id = $targetTaxCategoryId WHERE tax_category_id = $taxCategoryIdToReplace RETURNING business_id ) UPDATE accounter_schema.charges SET tax_category_id = $targetTaxCategoryId WHERE tax_category_id = $taxCategoryIdToReplace RETURNING id; `; @Injectable({ scope: Scope.Singleton, global: true, }) export class TaxCategoriesProvider { cache = getCacheInstance({ stdTTL: 60 * 5, }); constructor(private dbProvider: DBProvider) {} private async batchTaxCategoryByBusinessAndOwnerIDs( entries: readonly { businessId: string; ownerId: string }[], ): Promise<(IGetAllTaxCategoriesResult | undefined)[]> { const BusinessIdsSet = new Set<string | null>(entries.map(e => e.businessId)); const OwnerIdsSet = new Set<string | null>(entries.map(e => e.ownerId)); const taxCategories = await getTaxCategoryByBusinessAndOwnerIDs.run( { BusinessIds: BusinessIdsSet.size === 0 ? [null] : Array.from(BusinessIdsSet), OwnerIds: OwnerIdsSet.size === 0 ? [null] : Array.from(OwnerIdsSet), }, this.dbProvider, ); return entries.map(({ businessId, ownerId }) => taxCategories.find(tc => tc.business_id === businessId && tc.owner_id === ownerId), ); } public taxCategoryByBusinessAndOwnerIDsLoader = new DataLoader( (keys: readonly { businessId: string; ownerId: string }[]) => this.batchTaxCategoryByBusinessAndOwnerIDs(keys), { cache: false, }, ); private async batchTaxCategoryByIDs( ids: readonly string[], ): Promise<(IGetAllTaxCategoriesResult | undefined)[]> { const taxCategories = await getTaxCategoryByIDs.run( { Ids: ids, }, this.dbProvider, ); return ids.map(id => taxCategories.find(tc => tc.id === id)); } public taxCategoryByIdLoader = new DataLoader( (ids: readonly string[]) => this.batchTaxCategoryByIDs(ids), { cache: false, }, ); private async batchTaxCategoryByFinancialAccountIdsAndCurrencies( entries: readonly { financialAccountId: string; currency: Currency }[], ): Promise<(IGetAllTaxCategoriesResult | undefined)[]> { const FinancialAccountIdsSet = new Set<string | null>(entries.map(e => e.financialAccountId)); const CurrenciesSet = new Set<Currency | null>(entries.map(e => e.currency)); const taxCategories = await getTaxCategoryByFinancialAccountIdsAndCurrencies.run( { FinancialAccountIds: FinancialAccountIdsSet.size === 0 ? [null] : Array.from(FinancialAccountIdsSet), Currencies: CurrenciesSet.size === 0 ? [null] : Array.from(CurrenciesSet), }, this.dbProvider, ); return entries.map(({ financialAccountId, currency }) => taxCategories.find( tc => tc.financial_account_id === financialAccountId && tc.currency === currency, ), ); } public taxCategoryByFinancialAccountIdsAndCurrenciesLoader = new DataLoader( (keys: readonly { financialAccountId: string; currency: Currency }[]) => this.batchTaxCategoryByFinancialAccountIdsAndCurrencies(keys), { cache: false, }, ); private async batchTaxCategoryByFinancialAccountOwnerIds(ownerIds: readonly string[]) { const taxCategories = await getTaxCategoryByFinancialAccountOwnerIds.run( { ownerIds, }, this.dbProvider, ); return ownerIds.map(id => taxCategories.filter(tc => tc.financial_account_owner_id === id)); } public taxCategoryByFinancialAccountOwnerIdsLoader = new DataLoader( (keys: readonly string[]) => this.batchTaxCategoryByFinancialAccountOwnerIds(keys), { cache: false, }, ); private async batchTaxCategoryByChargeIDs(chargeIds: readonly string[]) { const taxCategories = await getTaxCategoryByChargeIDs.run( { chargeIds, }, this.dbProvider, ); return chargeIds.map(id => taxCategories.find(tc => tc.charge_id === id)); } public taxCategoryByChargeIDsLoader = new DataLoader( (chargeIDs: readonly string[]) => this.batchTaxCategoryByChargeIDs(chargeIDs), { cache: false, }, ); public getAllTaxCategories() { const data = this.cache.get<IGetAllTaxCategoriesResult[]>('all-tax-categories'); if (data) { return Promise.resolve(data); } return getAllTaxCategories.run(undefined, this.dbProvider).then(data => { this.cache.set('all-tax-categories', data); data.map(taxCategory => { this.cache.set(`tax-category-${taxCategory.id}`, taxCategory); }); return data; }); } public updateTaxCategory(params: IUpdateTaxCategoryParams) { if (params.taxCategoryId) this.invalidateTaxCategoryById(params.taxCategoryId); return updateTaxCategory.run(params, this.dbProvider); } public updateBusinessTaxCategory(params: IUpdateBusinessTaxCategoryParams) { if (params.taxCategoryId) this.invalidateTaxCategoryById(params.taxCategoryId); return updateBusinessTaxCategory.run(params, this.dbProvider); } public insertTaxCategory(params: IInsertTaxCategoryParams) { if (!params.id) { throw new Error('Missing required parameters'); } this.cache.delete('all-tax-categories'); return insertTaxCategory.run(params, this.dbProvider).catch(error => { console.error(`Failed to insert tax category: ${error.message}`); throw error; }); } public insertBusinessTaxCategory(params: IInsertBusinessTaxCategoryParams) { this.cache.delete('all-tax-categories'); return insertBusinessTaxCategory.run(params, this.dbProvider); } public deleteBusinessTaxCategory(params: IDeleteBusinessTaxCategoryParams) { this.cache.delete('all-tax-categories'); return deleteBusinessTaxCategory.run(params, this.dbProvider); } public async deleteTaxCategoryById(taxCategoryId: string) { const entity = await this.taxCategoryByIdLoader.load(taxCategoryId); if (!entity) { throw new Error(`Tax category with id ${taxCategoryId} not found`); } this.invalidateTaxCategoryById(taxCategoryId); // delete tax category deleteTaxCategory.run({ taxCategoryId }, this.dbProvider); } public async replaceTaxCategory( targetTaxCategoryId: string, taxCategoryIdToReplace: string, deleteTaxCategory: boolean = false, ) { const [taxCategoryToReplace, taxCategory] = await Promise.all([ this.taxCategoryByIdLoader.load(targetTaxCategoryId), this.taxCategoryByIdLoader.load(taxCategoryIdToReplace), ]); if (!taxCategoryToReplace) { throw new Error(`Tax category with id ${taxCategoryIdToReplace} not found`); } if (!taxCategory) { throw new Error(`Tax category with id ${targetTaxCategoryId} not found`); } this.invalidateTaxCategoryById(taxCategoryIdToReplace); this.invalidateTaxCategoryById(targetTaxCategoryId); await replaceTaxCategories.run( { targetTaxCategoryId, taxCategoryIdToReplace, }, this.dbProvider, ); if (deleteTaxCategory) { await this.deleteTaxCategoryById(taxCategoryIdToReplace); } } public invalidateTaxCategoryById(taxCategoryId: string) { this.cache.delete(`tax-category-${taxCategoryId}`); this.cache.delete('all-tax-categories'); } public clearCache() { this.cache.clear(); } }