UNPKG

@accounter/server

Version:

The test suite is split into three Vitest projects for efficiency and isolation:

422 lines (392 loc) • 14.4 kB
import DataLoader from 'dataloader'; import { Injectable, Scope } from 'graphql-modules'; import { sql } from '@pgtyped/runtime'; import { getCacheInstance } from '../../../shared/helpers/index.js'; import type { Optional, TimelessDateString } from '../../../shared/types/index.js'; import { DBProvider } from '../../app-providers/db.provider.js'; import type { accountant_status, IBatchUpdateChargesParams, IBatchUpdateChargesQuery, IDeleteChargesByIdsParams, IDeleteChargesByIdsQuery, IGenerateChargeParams, IGenerateChargeQuery, IGetChargesByFiltersParams, IGetChargesByFiltersQuery, IGetChargesByFiltersResult, IGetChargesByIdsQuery, IGetChargesByMissingRequiredInfoQuery, IGetChargesByTransactionIdsQuery, IGetSimilarChargesParams, IGetSimilarChargesQuery, IUpdateAccountantApprovalParams, IUpdateAccountantApprovalQuery, IUpdateChargeParams, IUpdateChargeQuery, } from '../types.js'; export type ChargeRequiredWrapper< T extends { id: unknown; owner_id: unknown; is_property: unknown; accountant_status: unknown; updated_at: unknown; created_at: unknown; documents_optional_flag: unknown; optional_vat: unknown; }, > = Omit< T, | 'id' | 'owner_id' | 'is_property' | 'accountant_status' | 'updated_at' | 'created_at' | 'documents_optional_flag' | 'optional_vat' > & { id: NonNullable<T['id']>; owner_id: NonNullable<T['owner_id']>; is_property: NonNullable<T['is_property']>; accountant_status: NonNullable<T['accountant_status']>; updated_at: NonNullable<T['updated_at']>; created_at: NonNullable<T['created_at']>; documents_optional_flag: NonNullable<T['documents_optional_flag']>; optional_vat: NonNullable<T['optional_vat']>; }; const getChargesByIds = sql<IGetChargesByIdsQuery>` SELECT * FROM accounter_schema.charges WHERE id IN $$chargeIds;`; const getChargesByTransactionIds = sql<IGetChargesByTransactionIdsQuery>` SELECT t.id AS transaction_id, c.* FROM accounter_schema.transactions t LEFT JOIN accounter_schema.charges c ON t.charge_id = c.id WHERE t.id IN $$transactionIds;`; const getChargesByMissingRequiredInfo = sql<IGetChargesByMissingRequiredInfoQuery>` SELECT c.* FROM accounter_schema.charges c LEFT JOIN accounter_schema.charge_tags t ON t.charge_id = c.id WHERE c.user_description IS NULL OR t.tag_id IS NULL;`; const updateCharge = sql<IUpdateChargeQuery>` UPDATE accounter_schema.charges SET owner_id = COALESCE( $ownerId, owner_id ), user_description = COALESCE( $userDescription, user_description ), type = COALESCE( $type, type ), invoice_payment_currency_diff = COALESCE( $isInvoicePaymentDifferentCurrency, invoice_payment_currency_diff ), accountant_status = COALESCE( $accountantStatus, accountant_status ), tax_category_id = COALESCE( $taxCategoryId, tax_category_id ), optional_vat = COALESCE( $optionalVAT, optional_vat ), documents_optional_flag = COALESCE( $optionalDocuments, documents_optional_flag ), is_property = COALESCE( $isProperty, is_property ) WHERE id = $chargeId RETURNING *; `; const batchUpdateCharges = sql<IBatchUpdateChargesQuery>` UPDATE accounter_schema.charges SET owner_id = COALESCE( $ownerId, owner_id ), user_description = COALESCE( $userDescription, user_description ), type = COALESCE( $type, type ), invoice_payment_currency_diff = COALESCE( $isInvoicePaymentDifferentCurrency, invoice_payment_currency_diff ), accountant_status = COALESCE( $accountantStatus, accountant_status ), tax_category_id = COALESCE( $taxCategoryId, tax_category_id ), optional_vat = COALESCE( $optionalVAT, optional_vat ), documents_optional_flag = COALESCE( $optionalDocuments, documents_optional_flag ), is_property = COALESCE( $isProperty, is_property ) WHERE id in $$chargeIds RETURNING *; `; const updateAccountantApproval = sql<IUpdateAccountantApprovalQuery>` UPDATE accounter_schema.charges SET accountant_status = $accountantStatus WHERE id = $chargeId RETURNING *; `; const generateCharge = sql<IGenerateChargeQuery>` INSERT INTO accounter_schema.charges (owner_id, type, accountant_status, user_description, tax_category_id, optional_vat, documents_optional_flag, is_property) VALUES ($ownerId, $type, $accountantStatus, $userDescription, $taxCategoryId, $optionalVAT, $optionalDocuments, $isProperty) RETURNING *; `; const getChargesByFilters = sql<IGetChargesByFiltersQuery>` SELECT ec.*, ABS(ec.event_amount) as abs_event_amount FROM accounter_schema.charges c LEFT JOIN accounter_schema.extended_charges ec ON c.id = ec.id WHERE ($isIDs = 0 OR c.id IN $$IDs) AND ($isOwnerIds = 0 OR c.owner_id IN $$ownerIds) AND ($isBusinessIds = 0 OR ec.business_array && $businessIds) AND ($fromDate ::TEXT IS NULL OR COALESCE(ec.documents_min_date, ec.transactions_min_event_date)::TEXT::DATE >= date_trunc('day', $fromDate ::DATE)) AND ($fromAnyDate ::TEXT IS NULL OR GREATEST(ec.documents_max_date, ec.transactions_max_event_date, ec.transactions_max_debit_date, ec.ledger_max_invoice_date, ec.ledger_max_value_date)::TEXT::DATE >= date_trunc('day', $fromAnyDate ::DATE)) AND ($toDate ::TEXT IS NULL OR COALESCE(ec.documents_max_date, ec.transactions_max_event_date)::TEXT::DATE <= date_trunc('day', $toDate ::DATE)) AND ($toAnyDate ::TEXT IS NULL OR LEAST(ec.documents_min_date, ec.transactions_min_event_date, ec.transactions_min_debit_date, ec.ledger_min_invoice_date, ec.ledger_min_value_date)::TEXT::DATE <= date_trunc('day', $toAnyDate ::DATE)) AND ($chargeType = 'ALL' OR ($chargeType = 'INCOME' AND ec.transactions_event_amount > 0) OR ($chargeType = 'EXPENSE' AND ec.transactions_event_amount <= 0)) AND ($withoutInvoice = FALSE OR COALESCE(ec.invoices_count, 0) = 0) AND ($withoutReceipt = FALSE OR (COALESCE(ec.receipts_count, 0) = 0 AND (ec.no_invoices_required IS FALSE))) AND ($withoutDocuments = FALSE OR COALESCE(ec.documents_count, 0) = 0) AND ($withoutTransactions = FALSE OR COALESCE(ec.transactions_count, 0) = 0) AND ($withOpenDocuments = FALSE OR ec.open_docs_flag IS TRUE) AND ($withoutLedger = FALSE OR COALESCE(ec.ledger_count, 0) = 0) AND ($isAccountantStatuses = 0 OR ec.accountant_status = ANY ($accountantStatuses::accounter_schema.accountant_status[])) AND ($isTags = 0 OR ec.tags && $tags) ORDER BY CASE WHEN $asc = true AND $sortColumn = 'event_date' THEN (COALESCE(ec.transactions_min_debit_date, ec.transactions_min_event_date, ec.documents_min_date, ec.ledger_min_value_date, ec.ledger_min_invoice_date), COALESCE(ec.documents_min_date, ec.transactions_min_event_date), ec.id) END ASC, CASE WHEN $asc = false AND $sortColumn = 'event_date' THEN (COALESCE(ec.transactions_min_debit_date, ec.transactions_min_event_date, ec.documents_min_date, ec.ledger_min_value_date, ec.ledger_min_invoice_date), COALESCE(ec.documents_min_date, ec.transactions_min_event_date), ec.id) END DESC, CASE WHEN $asc = true AND $sortColumn = 'event_amount' THEN (ec.event_amount, ec.id) END ASC, CASE WHEN $asc = false AND $sortColumn = 'event_amount' THEN (ec.event_amount, ec.id) END DESC, CASE WHEN $asc = true AND $sortColumn = 'abs_event_amount' THEN ABS(cast(ec.event_amount as DECIMAL)) END ASC, CASE WHEN $asc = false AND $sortColumn = 'abs_event_amount' THEN ABS(cast(ec.event_amount as DECIMAL)) END DESC, ID; `; const getSimilarCharges = sql<IGetSimilarChargesQuery>` SELECT * FROM accounter_schema.extended_charges WHERE (CASE WHEN $withMissingTags IS TRUE THEN tags IS NULL ELSE TRUE END) AND (CASE WHEN $withMissingDescription IS TRUE THEN user_description IS NULL ELSE TRUE END) AND (CASE WHEN $tagsDifferentThan::UUID[] IS NOT NULL THEN NOT((tags @> $tagsDifferentThan) AND (tags <@ $tagsDifferentThan)) ELSE TRUE END) AND (CASE WHEN $descriptionDifferentThan::TEXT IS NOT NULL THEN NOT(user_description = $descriptionDifferentThan) ELSE TRUE END) AND ( (business_id IS NOT NULL AND business_id = $businessId) OR (business_array IS NOT NULL AND business_array @> $businessArray AND business_array <@ $businessArray) ) AND owner_id = $ownerId ORDER BY (COALESCE(documents_min_date, transactions_min_debit_date, transactions_min_event_date, ledger_min_value_date, ledger_min_invoice_date), COALESCE(transactions_min_event_date, documents_min_date), id) DESC;`; type IGetAdjustedChargesByFiltersParams = Optional< Omit< IGetChargesByFiltersParams, 'isOwnerIds' | 'isBusinessIds' | 'businessIds' | 'isIDs' | 'isTags' | 'tags' >, 'ownerIds' | 'IDs' | 'asc' | 'sortColumn' | 'toDate' | 'fromDate' > & { toDate?: TimelessDateString | null; fromDate?: TimelessDateString | null; tags?: readonly string[] | null; businessIds?: readonly string[] | null; }; const deleteChargesByIds = sql<IDeleteChargesByIdsQuery>` DELETE FROM accounter_schema.charges WHERE id IN $$chargeIds;`; @Injectable({ scope: Scope.Singleton, global: true, }) export class ChargesProvider { cache = getCacheInstance({ stdTTL: 60 * 60, // 1 hours }); constructor(private dbProvider: DBProvider) {} private async batchChargesByIds(ids: readonly string[]) { const charges = await getChargesByIds.run( { chargeIds: ids, }, this.dbProvider, ); return ids.map(id => charges.find(charge => charge.id === id)); } public getChargeByIdLoader = new DataLoader( (keys: readonly string[]) => this.batchChargesByIds(keys), { cacheKeyFn: id => `charge-${id}`, cacheMap: this.cache, }, ); private async batchChargesByTransactionIds(transactionIds: readonly string[]) { const charges = await getChargesByTransactionIds.run( { transactionIds, }, this.dbProvider, ); charges.map(c => this.getChargeByIdLoader.prime(c.id, c)); return transactionIds.map(id => charges.find(charge => charge.transaction_id === id)); } public getChargeByTransactionIdLoader = new DataLoader( (transactionIds: readonly string[]) => this.batchChargesByTransactionIds(transactionIds), { cache: false }, ); public async getChargesByMissingRequiredInfo() { return getChargesByMissingRequiredInfo.run(undefined, this.dbProvider).then(charges => charges.map(c => { this.getChargeByIdLoader.prime(c.id, c); return c; }), ); } public updateCharge(params: IUpdateChargeParams) { return updateCharge.run(params, this.dbProvider).then(([newCharge]) => { if (newCharge) { this.invalidateCharge(newCharge.id); this.getChargeByIdLoader.prime(newCharge.id, newCharge); } return newCharge; }); } public batchUpdateCharges(params: IBatchUpdateChargesParams) { return batchUpdateCharges.run(params, this.dbProvider).then(charges => { charges.map(charge => this.getChargeByIdLoader.prime(charge.id, charge)); return charges; }); } public updateAccountantApproval(params: IUpdateAccountantApprovalParams) { return updateAccountantApproval.run(params, this.dbProvider).then(([newCharge]) => { if (newCharge) { this.getChargeByIdLoader.prime(newCharge.id, newCharge); } return newCharge; }); } public generateCharge(params: IGenerateChargeParams) { const fullParams = { isProperty: false, userDescription: null, optionalVAT: false, optionalDocuments: false, accountantStatus: 'UNAPPROVED' as accountant_status, ...params, }; return generateCharge.run(fullParams, this.dbProvider).then(([newCharge]) => { if (newCharge) { this.getChargeByIdLoader.prime(newCharge.id, newCharge); } return newCharge; }); } public getChargesByFilters(params: IGetAdjustedChargesByFiltersParams) { const isOwnerIds = !!params?.ownerIds?.filter(Boolean).length; const isBusinessIds = !!params?.businessIds?.filter(Boolean).length; const isIDs = !!params?.IDs?.length; const isTags = !!params?.tags?.length; const isAccountantStatuses = !!params?.accountantStatuses?.length; const defaults = { asc: false, sortColumn: 'event_date', }; const fullParams: IGetChargesByFiltersParams = { ...defaults, isOwnerIds: isOwnerIds ? 1 : 0, isBusinessIds: isBusinessIds ? 1 : 0, isIDs: isIDs ? 1 : 0, isTags: isTags ? 1 : 0, isAccountantStatuses: isAccountantStatuses ? 1 : 0, ...params, fromDate: params.fromDate ?? null, toDate: params.toDate ?? null, ownerIds: isOwnerIds ? params.ownerIds! : [null], businessIds: isBusinessIds ? (params.businessIds! as string[]) : null, IDs: isIDs ? params.IDs! : [null], tags: isTags ? (params.tags! as string[]) : null, chargeType: params.chargeType ?? 'ALL', withoutInvoice: params.withoutInvoice ?? false, withoutReceipt: params.withoutReceipt ?? false, withoutDocuments: params.withoutDocuments ?? false, withOpenDocuments: params.withOpenDocuments ?? false, withoutTransactions: params.withoutTransactions ?? false, withoutLedger: params.withoutLedger ?? false, accountantStatuses: isAccountantStatuses ? params.accountantStatuses! : null, }; return getChargesByFilters.run(fullParams, this.dbProvider) as Promise< IGetChargesByFiltersResult[] >; } public async getSimilarCharges(params: IGetSimilarChargesParams) { try { return getSimilarCharges.run(params, this.dbProvider) as Promise< IGetChargesByFiltersResult[] >; } catch (error) { const message = 'Failed to fetch similar charges'; console.error(message, error); throw new Error(message); } } public deleteChargesByIds(params: IDeleteChargesByIdsParams) { return deleteChargesByIds.run(params, this.dbProvider); } public async invalidateCharge(chargeId: string) { this.getChargeByIdLoader.clear(chargeId); } public clearCache() { this.cache.clear(); } }