@accounter/server
Version:
The test suite is split into three Vitest projects for efficiency and isolation:
422 lines (392 loc) • 14.4 kB
text/typescript
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();
}
}