@accounter/server
Version:
Accounter GraphQL server
378 lines (335 loc) • 14.1 kB
text/typescript
import DataLoader from 'dataloader';
import { Injectable, Scope } from 'graphql-modules';
import { sql } from '@pgtyped/runtime';
import type { Currency } from '../../../shared/enums.js';
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 type {
IDeleteBusinessTaxCategoryParams,
IDeleteBusinessTaxCategoryQuery,
IDeleteTaxCategoryQuery,
IGetAllTaxCategoriesQuery,
IGetAllTaxCategoriesResult,
IGetTaxCategoryByBusinessIDsQuery,
IGetTaxCategoryByFinancialAccountIdsAndCurrenciesQuery,
IGetTaxCategoryByFinancialAccountIdsQuery,
IGetTaxCategoryByFinancialAccountOwnerIdsQuery,
IGetTaxCategoryByIDsQuery,
IGetTaxCategoryBySortCodesQuery,
IInsertBusinessTaxCategoryParams,
IInsertBusinessTaxCategoryQuery,
IInsertTaxCategoryParams,
IInsertTaxCategoryQuery,
IReplaceTaxCategoriesQuery,
IUpdateBusinessTaxCategoryParams,
IUpdateBusinessTaxCategoryQuery,
IUpdateTaxCategoryParams,
IUpdateTaxCategoryQuery,
} from '../types.js';
const getTaxCategoryByBusinessIDs = sql<IGetTaxCategoryByBusinessIDsQuery>`
SELECT fe.id, fe.name, fe.sort_code, fe.type, fe.created_at, fe.updated_at, fe.irs_code, fe.is_active, tc.hashavshevet_name, tc.tax_excluded, 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;`;
const getTaxCategoryBySortCodes = sql<IGetTaxCategoryBySortCodesQuery>`
SELECT fe.id, fe.name, fe.sort_code, fe.type, fe.created_at, fe.updated_at, fe.irs_code, fe.is_active, tc.hashavshevet_name, tc.tax_excluded, fe.owner_id
FROM accounter_schema.tax_categories tc
LEFT JOIN accounter_schema.financial_entities fe
ON fe.id = tc.id
WHERE fe.sort_code IN $$sortCodes;`;
const getTaxCategoryByFinancialAccountIdsAndCurrencies = sql<IGetTaxCategoryByFinancialAccountIdsAndCurrenciesQuery>`
SELECT fe.id, fe.name, fe.sort_code, fe.type, fe.created_at, fe.updated_at, fe.owner_id, fe.irs_code, fe.is_active, tc.hashavshevet_name, tc.tax_excluded, 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, fe.irs_code, fe.is_active, tc.hashavshevet_name, tc.tax_excluded, 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 getTaxCategoryByFinancialAccountIds = sql<IGetTaxCategoryByFinancialAccountIdsQuery>`
SELECT fe.id, fe.name, fe.sort_code, fe.type, fe.created_at, fe.updated_at, fe.owner_id, fe.irs_code, fe.is_active, tc.hashavshevet_name, tc.tax_excluded, 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.financial_account_id IN $$financialAccountIds;`;
const getTaxCategoryByIDs = sql<IGetTaxCategoryByIDsQuery>`
SELECT fe.*, tc.hashavshevet_name, tc.tax_excluded
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, tc.tax_excluded
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
),
tax_excluded = COALESCE(
$taxExcluded,
tax_excluded
)
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, tax_excluded, owner_id)
VALUES ($id, $hashavshevetName, $taxExcluded, $ownerId)
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;
`;
({
scope: Scope.Operation,
global: true,
})
export class TaxCategoriesProvider {
constructor(
private db: TenantAwareDBClient,
private adminContextProvider: AdminContextProvider,
) {}
private async batchTaxCategoryByBusinessIDs(
businessIds: readonly string[],
): Promise<(IGetAllTaxCategoriesResult | undefined)[]> {
const BusinessIdsSet = new Set<string | null>(businessIds);
const taxCategories = await getTaxCategoryByBusinessIDs.run(
{
BusinessIds: BusinessIdsSet.size === 0 ? [null] : Array.from(BusinessIdsSet),
},
this.db,
);
return businessIds.map(
businessId =>
taxCategories.find(tc => tc.business_id === businessId) as unknown as
| IGetAllTaxCategoriesResult
| undefined, // TODO: temporary type casting, should be fixed later
);
}
public taxCategoryByBusinessIDsLoader = new DataLoader((businessIds: readonly string[]) =>
this.batchTaxCategoryByBusinessIDs(businessIds),
);
private async batchTaxCategoryByIDs(
ids: readonly string[],
): Promise<(IGetAllTaxCategoriesResult | undefined)[]> {
const taxCategories = await getTaxCategoryByIDs.run(
{
Ids: ids,
},
this.db,
);
return ids.map(id => taxCategories.find(tc => tc.id === id));
}
public taxCategoryByIdLoader = new DataLoader((ids: readonly string[]) =>
this.batchTaxCategoryByIDs(ids),
);
private async batchTaxCategoriesBySortCodes(
sortCodes: readonly number[],
): Promise<IGetAllTaxCategoriesResult[][]> {
const taxCategories = await getTaxCategoryBySortCodes.run(
{
sortCodes: [...sortCodes],
},
this.db,
);
return sortCodes.map(sortCode => taxCategories.filter(tc => tc.sort_code === sortCode));
}
public taxCategoriesBySortCodeLoader = new DataLoader((sortCodes: readonly number[]) =>
this.batchTaxCategoriesBySortCodes(sortCodes),
);
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.db,
);
return entries.map(
({ financialAccountId, currency }) =>
taxCategories.find(
tc => tc.financial_account_id === financialAccountId && tc.currency === currency,
) as unknown as IGetAllTaxCategoriesResult | undefined, // TODO: temporary type casting, should be fixed later
);
}
public taxCategoryByFinancialAccountIdsAndCurrenciesLoader = new DataLoader(
(keys: readonly { financialAccountId: string; currency: Currency }[]) =>
this.batchTaxCategoryByFinancialAccountIdsAndCurrencies(keys),
{
cacheKeyFn: key => `${key.financialAccountId}-${key.currency}`,
},
);
private async batchTaxCategoryByFinancialAccountOwnerIds(ownerIds: readonly string[]) {
const taxCategories = await getTaxCategoryByFinancialAccountOwnerIds.run(
{
ownerIds,
},
this.db,
);
return ownerIds.map(id => taxCategories.filter(tc => tc.financial_account_owner_id === id));
}
public taxCategoryByFinancialAccountOwnerIdsLoader = new DataLoader((keys: readonly string[]) =>
this.batchTaxCategoryByFinancialAccountOwnerIds(keys),
);
private async batchTaxCategoryByFinancialAccountIds(financialAccountIds: readonly string[]) {
const taxCategories = await getTaxCategoryByFinancialAccountIds.run(
{
financialAccountIds,
},
this.db,
);
return financialAccountIds.map(id =>
taxCategories.filter(tc => tc.financial_account_id === id),
);
}
public taxCategoryByFinancialAccountIdsLoader = new DataLoader((keys: readonly string[]) =>
this.batchTaxCategoryByFinancialAccountIds(keys),
);
public getAllTaxCategories() {
return getAllTaxCategories.run(undefined, this.db);
}
public updateTaxCategory(params: IUpdateTaxCategoryParams) {
if (params.taxCategoryId) this.invalidateTaxCategoryById(params.taxCategoryId);
return updateTaxCategory.run(params, this.db);
}
public updateBusinessTaxCategory(params: IUpdateBusinessTaxCategoryParams) {
if (params.taxCategoryId) this.invalidateTaxCategoryById(params.taxCategoryId);
return updateBusinessTaxCategory.run(params, this.db);
}
public async insertTaxCategory(params: IInsertTaxCategoryParams) {
if (!params.id) {
throw new Error('Missing required parameters');
}
const { ownerId } = await this.adminContextProvider.getVerifiedAdminContext();
return insertTaxCategory.run(reassureOwnerIdExists(params, ownerId), this.db).catch(error => {
console.error(`Failed to insert tax category: ${error.message}`);
throw error;
});
}
public insertBusinessTaxCategory(params: IInsertBusinessTaxCategoryParams) {
return insertBusinessTaxCategory.run(params, this.db);
}
public deleteBusinessTaxCategory(params: IDeleteBusinessTaxCategoryParams) {
return deleteBusinessTaxCategory.run(params, this.db);
}
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.db);
}
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.db,
);
if (deleteTaxCategory) {
await this.deleteTaxCategoryById(taxCategoryIdToReplace);
}
}
public invalidateTaxCategoryById(taxCategoryId: string) {
this.taxCategoryByBusinessIDsLoader.clearAll();
this.taxCategoryByFinancialAccountIdsAndCurrenciesLoader.clearAll();
this.taxCategoryByFinancialAccountIdsLoader.clearAll();
this.taxCategoryByFinancialAccountOwnerIdsLoader.clearAll();
this.taxCategoriesBySortCodeLoader.clearAll();
this.taxCategoryByIdLoader.clear(taxCategoryId);
}
public clearCache() {
this.taxCategoryByIdLoader.clearAll();
this.taxCategoriesBySortCodeLoader.clearAll();
this.taxCategoryByBusinessIDsLoader.clearAll();
this.taxCategoryByFinancialAccountIdsAndCurrenciesLoader.clearAll();
this.taxCategoryByFinancialAccountIdsLoader.clearAll();
this.taxCategoryByFinancialAccountOwnerIdsLoader.clearAll();
}
}