@accounter/server
Version:
Accounter GraphQL server
167 lines (150 loc) • 5.73 kB
text/typescript
import { Injector } from 'graphql-modules';
import type { ResolversTypes } from '../../../__generated__/types.js';
import { ChargeTypeEnum, MissingChargeInfo } from '../../../shared/enums.js';
import { AdminContextProvider } from '../../admin-context/providers/admin-context.provider.js';
import { isInvoice } from '../../documents/helpers/common.helper.js';
import { validateDocumentAllocation } from '../../documents/helpers/validate-document.helper.js';
import { DocumentsProvider } from '../../documents/providers/documents.provider.js';
import { BusinessesProvider } from '../../financial-entities/providers/businesses.provider.js';
import { ChargeTagsProvider } from '../../tags/providers/charge-tags.provider.js';
import type { IGetChargesByIdsResult } from '../types.js';
import { getChargeType } from './charge-type.js';
import {
getChargeBusinesses,
getChargeDocumentsMeta,
getChargeTaxCategoryId,
getChargeTransactionsMeta,
} from './common.helper.js';
export const validateCharge = async (
charge: IGetChargesByIdsResult,
injector: Injector,
): Promise<ResolversTypes['ValidationData']> => {
const missingInfo: Array<MissingChargeInfo> = [];
const [chargeType, { mainBusinessId }, taxCategoryId] = await Promise.all([
getChargeType(charge, injector),
getChargeBusinesses(charge.id, injector),
getChargeTaxCategoryId(charge.id, injector),
]);
const adminContext = await injector.get(AdminContextProvider).getVerifiedAdminContext();
const isGeneralFees =
taxCategoryId === adminContext.general.taxCategories.generalFeeTaxCategoryId;
// check for consistent counterparty business
const businessNotRequired =
[ChargeTypeEnum.InternalTransfer, ChargeTypeEnum.Salary, ChargeTypeEnum.Financial].includes(
chargeType,
) || isGeneralFees;
const businessPromise =
mainBusinessId && !businessNotRequired
? injector.get(BusinessesProvider).getBusinessByIdLoader.load(mainBusinessId)
: Promise.resolve(undefined);
const [
business,
{ invalidTransactions, transactionsCount },
{ documentsVatAmount, invoiceCount, receiptCount, invalidDocuments },
] = await Promise.all([
businessPromise,
getChargeTransactionsMeta(charge.id, injector),
getChargeDocumentsMeta(charge.id, injector),
]);
const businessIsFine = businessNotRequired || !!business;
if (!businessIsFine) {
missingInfo.push(MissingChargeInfo.Counterparty);
}
// validate documents
let shouldHaveDocuments = true;
switch (chargeType) {
case ChargeTypeEnum.Salary:
case ChargeTypeEnum.InternalTransfer:
case ChargeTypeEnum.Dividend:
case ChargeTypeEnum.Conversion:
case ChargeTypeEnum.MonthlyVat:
case ChargeTypeEnum.CreditcardBankCharge:
case ChargeTypeEnum.Financial:
case ChargeTypeEnum.BusinessTrip:
case ChargeTypeEnum.BankDeposit:
shouldHaveDocuments = false;
}
if (isGeneralFees || business?.no_invoices_required === true || charge.documents_optional_flag) {
shouldHaveDocuments = false;
}
let documentsAreFine: boolean;
if (shouldHaveDocuments) {
const documents = await injector
.get(DocumentsProvider)
.getDocumentsByChargeIdLoader.load(charge.id);
let missingAllocationNumber = false;
await Promise.all(
documents.map(async doc => {
if (isInvoice(doc.type)) {
const validAllocation = await validateDocumentAllocation(doc, injector);
if (!validAllocation) {
missingAllocationNumber = true;
}
}
}),
);
const isReceiptEnough = !!(business?.can_settle_with_receipt && receiptCount > 0);
const dbDocumentsAreValid = !invalidDocuments;
documentsAreFine =
dbDocumentsAreValid && (invoiceCount > 0 || isReceiptEnough) && !missingAllocationNumber;
} else {
documentsAreFine = true;
}
if (!documentsAreFine) {
missingInfo.push(MissingChargeInfo.Documents);
}
// validate transactions
const hasTransaction = transactionsCount >= 1;
const transactionsNotRequired = [ChargeTypeEnum.Financial].includes(chargeType);
const transactionsAreFine = transactionsNotRequired || (hasTransaction && !invalidTransactions);
if (!transactionsAreFine) {
missingInfo.push(MissingChargeInfo.Transactions);
}
// validate description
const descriptionIsFine = (charge.user_description?.trim().length ?? 0) > 0;
if (!descriptionIsFine) {
missingInfo.push(MissingChargeInfo.Description);
}
// validate tags
const tags = await injector.get(ChargeTagsProvider).getTagsByChargeIDLoader.load(charge.id);
const tagsAreFine = tags.length > 0;
if (!tagsAreFine) {
missingInfo.push(MissingChargeInfo.Tags);
}
// validate vat
const isVATlessBusiness =
charge.optional_vat ||
(business &&
(business.country !== adminContext.locality ||
business.exempt_dealer ||
business.optional_vat));
const vatIsFine =
!shouldHaveDocuments ||
isGeneralFees ||
(documentsVatAmount != null && (isVATlessBusiness || documentsVatAmount !== 0));
if (!vatIsFine) {
missingInfo.push(MissingChargeInfo.Vat);
}
// validate tax category
const shouldHaveTaxCategory = ![ChargeTypeEnum.Salary, ChargeTypeEnum.InternalTransfer].includes(
chargeType,
);
const taxCategoryIsFine = !shouldHaveTaxCategory || !!taxCategoryId;
if (!taxCategoryIsFine) {
missingInfo.push(MissingChargeInfo.TaxCategory);
}
//TODO(Gil): validate balance
//TODO(Gil): validate ledger
const allFine =
documentsAreFine &&
businessIsFine &&
descriptionIsFine &&
tagsAreFine &&
vatIsFine &&
transactionsAreFine &&
taxCategoryIsFine;
return {
isValid: allFine,
missingInfo,
};
};