@accounter/server
Version:
Accounter GraphQL server
344 lines • 15 kB
JavaScript
import { addMonths, endOfMonth, format, startOfMonth, subMonths } from 'date-fns';
import { GraphQLError } from 'graphql';
import { DocumentType } from '../../../shared/enums.js';
import { dateToTimelessDateString } from '../../../shared/helpers/index.js';
import { GreenInvoiceClientProvider } from '../../app-providers/green-invoice-client.js';
import { ChargesProvider } from '../../charges/providers/charges.provider.js';
import { getProductName, getSubscriptionPlanName, normalizeProduct, normalizeSubscriptionPlan, } from '../../contracts/helpers/contracts.helper.js';
import { FinancialAccountsProvider } from '../../financial-accounts/providers/financial-accounts.provider.js';
import { FinancialBankAccountsProvider } from '../../financial-accounts/providers/financial-bank-accounts.provider.js';
import { validateClientIntegrations } from '../../financial-entities/helpers/clients.helper.js';
import { BusinessesProvider } from '../../financial-entities/providers/businesses.provider.js';
import { ClientsProvider } from '../../financial-entities/providers/clients.provider.js';
import { convertDocumentInputIntoGreenInvoiceInput, getTypeFromGreenInvoiceDocument, getVatTypeFromGreenInvoiceDocument, insertNewDocumentFromGreenInvoice, } from '../../green-invoice/helpers/green-invoice.helper.js';
import { IssuedDocumentsProvider } from '../providers/issued-documents.provider.js';
import { normalizeDocumentType } from '../resolvers/common.js';
export async function getPaymentsFromTransactions(injector, transactions) {
const payments = await Promise.all(transactions.map(async (transaction) => {
// get account
const account = await injector
.get(FinancialAccountsProvider)
.getFinancialAccountByAccountIDLoader.load(transaction.account_id);
if (!account) {
throw new Error(`Account with ID ${transaction.account_id} not found`);
}
// get account type
let type = 'OTHER';
switch (account.type) {
case 'BANK_ACCOUNT':
type = 'WIRE_TRANSFER';
break;
case 'CREDIT_CARD':
type = 'CREDIT_CARD';
break;
}
let bankAccount = undefined;
if (account.type === 'BANK_ACCOUNT') {
bankAccount = await injector
.get(FinancialBankAccountsProvider)
.getFinancialBankAccountByIdLoader.load(account.id);
if (!bankAccount) {
throw new GraphQLError(`Bank account details not found for financial account ID "${account.id}"`);
}
}
// get further fields
let paymentTypeSpecificAttributes = {};
switch (type) {
case 'CHEQUE':
paymentTypeSpecificAttributes = {
// chequeNum: _____,
};
break;
case 'CREDIT_CARD':
paymentTypeSpecificAttributes = {
cardType: 'MASTERCARD', // TODO: add logic to support other card types
cardNum: account.account_number,
numPayments: 1,
firstPayment: transaction.event_date.getTime() / 1000, // assuming first payment is the transaction date
};
break;
case 'WIRE_TRANSFER':
paymentTypeSpecificAttributes = {
bankName: bankAccount?.bank_number?.toString(),
bankBranch: bankAccount?.branch_number?.toString(),
bankAccount: account.account_number?.toString(),
};
break;
case 'PAYMENT_APP':
paymentTypeSpecificAttributes = {
// appType: _____,
// accountId: _____,
// transactionId: _____,
};
break;
case 'OTHER':
paymentTypeSpecificAttributes = {
// subType: _____,
// accountId: _____,
// transactionId: _____,
};
break;
case 'PAYPAL':
paymentTypeSpecificAttributes = {
// accountId: _____,
// transactionId: _____,
};
break;
case 'CASH':
case 'TAX_DEDUCTION':
case 'OTHER_DEDUCTION':
break;
}
const payment = {
currency: transaction.currency,
currencyRate: undefined,
date: dateToTimelessDateString(transaction.debit_date ?? transaction.event_date),
price: Number(transaction.amount),
type,
transactionId: transaction.id,
accountId: account.id,
...paymentTypeSpecificAttributes,
};
return payment;
}));
return payments;
}
export function getIncomeRecordsFromDocuments(greenInvoiceDocuments) {
const incomes = greenInvoiceDocuments
.map(greenInvoiceDocument => {
return greenInvoiceDocument.income.filter(Boolean).map(originIncome => {
if (!originIncome?.currency) {
throw new Error('Income currency is missing');
}
const income = {
description: originIncome.description,
quantity: originIncome.quantity,
price: originIncome.price,
currency: originIncome.currency,
currencyRate: undefined,
vatRate: originIncome.vatRate,
itemId: originIncome.itemId,
vatType: getVatTypeFromGreenInvoiceDocument(greenInvoiceDocument.vatType),
};
return income;
});
})
.flat();
return incomes;
}
export function getTypeFromDocumentsAndTransactions(documents, transactions) {
if (!documents.length) {
if (transactions.length) {
return DocumentType.InvoiceReceipt;
}
return DocumentType.Proforma;
}
const documentsTypes = new Set(documents.map(doc => doc.type));
if (documentsTypes.size === 1) {
switch (Array.from(documentsTypes)[0]) {
case DocumentType.Invoice:
return DocumentType.Receipt;
case DocumentType.Proforma:
return DocumentType.InvoiceReceipt;
}
}
return DocumentType.Proforma;
}
export function filterAndHandleSwiftTransactions(originTransactions, swiftBusinessId) {
const swiftTransactions = [];
const incomeTransactions = [];
for (const transaction of originTransactions) {
if (swiftBusinessId && transaction.business_id === swiftBusinessId) {
swiftTransactions.push(transaction);
}
else if (Number(transaction.amount) > 0) {
incomeTransactions.push(transaction);
}
}
if (swiftTransactions.length === 0) {
return incomeTransactions;
}
if (swiftTransactions.length === 1 && incomeTransactions.length === 1) {
const incomeTransaction = incomeTransactions[0];
return [
{
...incomeTransaction,
amount: (Number(incomeTransaction.amount) - Number(swiftTransactions[0].amount)).toFixed(2),
},
];
}
throw new GraphQLError("Unable to process transactions. Couldn't match swift fees to transactions");
}
export function getLinkedDocumentsAttributes(issuedDocuments, shouldCancel = false) {
const linkedDocumentIds = issuedDocuments.map(doc => doc.external_id);
let linkType = undefined;
if (linkedDocumentIds.length) {
if (shouldCancel) {
linkType = 'CANCEL';
}
else {
linkType = 'LINK';
}
}
return {
linkedDocumentIds,
linkType,
};
}
export function getDocumentDateOutOfTransactions(transactions) {
const debitDates = transactions.map(tx => tx.debit_date).filter(Boolean);
// if no debit dates, use current date
if (!debitDates.length)
return dateToTimelessDateString(new Date());
// Sort dates and take the first one
const sortedDates = [...debitDates].sort((a, b) => b.getTime() - a.getTime());
const firstDate = sortedDates[0];
// Return the date in the required format
return dateToTimelessDateString(firstDate);
}
export async function deduceVatTypeFromBusiness(injector, locality, businessId) {
if (!businessId)
return 'DEFAULT';
const business = await injector.get(BusinessesProvider).getBusinessByIdLoader.load(businessId);
if (!business)
return 'DEFAULT';
// Deduce VAT type based on business information
if (business.country === locality && !business.exempt_dealer) {
return 'MIXED';
}
return 'EXEMPT';
}
export function createRemarks(contract) {
const remarks = [];
if (contract.purchase_orders?.length) {
const lastPo = contract.purchase_orders[contract.purchase_orders.length - 1];
if (lastPo) {
remarks.push(`PO: ${lastPo}`);
}
}
if (contract.remarks) {
remarks.push(contract.remarks);
}
return remarks.join(', ');
}
export const convertContractToDraft = async (injector, contract, locality, issueMonth) => {
const businessPromise = injector
.get(BusinessesProvider)
.getBusinessByIdLoader.load(contract.client_id);
const clientPromise = injector.get(ClientsProvider).getClientByIdLoader.load(contract.client_id);
const [business, client] = await Promise.all([businessPromise, clientPromise]);
if (!business) {
throw new GraphQLError(`Business ID="${contract.client_id}" not found`);
}
if (!client) {
throw new GraphQLError(`Client not found for business ID="${contract.client_id}"`);
}
const greenInvoiceId = validateClientIntegrations(client.integrations)?.greenInvoiceId;
if (!greenInvoiceId) {
throw new GraphQLError(`Green invoice match not found for business ID="${contract.client_id}"`);
}
const today = issueMonth ? addMonths(new Date(issueMonth), 1) : new Date();
const monthStart = dateToTimelessDateString(startOfMonth(today));
const monthEnd = dateToTimelessDateString(endOfMonth(today));
const year = today.getFullYear() + (today.getMonth() === 0 ? -1 : 0);
const month = format(subMonths(today, 1), 'MMMM');
const vatType = await deduceVatTypeFromBusiness(injector, locality, contract.client_id);
const documentInput = {
remarks: createRemarks(contract),
description: `${getProductName(normalizeProduct(contract.product ?? ''))} ${getSubscriptionPlanName(normalizeSubscriptionPlan(contract.plan ?? ''))} - ${month} ${year}`,
type: normalizeDocumentType(contract.document_type),
date: monthStart,
dueDate: monthEnd,
language: 'ENGLISH',
currency: contract.currency,
vatType,
rounding: false,
signed: true,
client,
income: [
{
description: `${getProductName(normalizeProduct(contract.product ?? ''))} ${getSubscriptionPlanName(normalizeSubscriptionPlan(contract.plan ?? ''))} - ${month} ${year}`,
quantity: 1,
price: contract.amount,
currency: contract.currency,
vatType: 'EXEMPT',
},
],
};
return documentInput;
};
export async function executeDocumentIssue(injector, adminBusinessId, initialInput, emailContent, attachment = true, chargeId, sendEmail = false) {
try {
const coreInput = await convertDocumentInputIntoGreenInvoiceInput(initialInput, injector);
const input = {
...coreInput,
client: !coreInput.client || sendEmail
? coreInput.client
: {
...coreInput.client,
emails: [], // if client exists, and sendEmail is false, we don't want to send emails
},
emailContent,
attachment,
};
const document = await injector.get(GreenInvoiceClientProvider).addDocuments({ input });
if (!document) {
throw new GraphQLError('Failed to issue new document');
}
// validate all linked documents are closed
if (input.linkedDocumentIds?.length) {
await Promise.all(input.linkedDocumentIds.map(async (id) => {
if (id) {
await injector
.get(GreenInvoiceClientProvider)
.closeDocument(id)
.catch(e => {
const message = `ERROR: Failed to close Green Invoice linked document with ID ${id}`;
console.error(`${message}: ${e}`);
});
}
}));
}
if ('id' in document && document.id) {
const greenInvoiceDocument = await injector
.get(GreenInvoiceClientProvider)
.documentLoader.load(document.id);
if (!greenInvoiceDocument) {
console.error('Failed to fetch issued document from Green Invoice', document);
throw new GraphQLError('Failed to issue new document');
}
// Insert new issued document to DB
const newDocument = await insertNewDocumentFromGreenInvoice(injector, greenInvoiceDocument, adminBusinessId, chargeId);
if (!newDocument.charge_id) {
console.error('New document does not have a charge ID', newDocument);
throw new GraphQLError('Failed to issue new document');
}
// Close linked documents
if (coreInput.linkedDocumentIds?.length) {
await Promise.all(coreInput.linkedDocumentIds.map(async (id) => {
if (id) {
await injector
.get(IssuedDocumentsProvider)
.updateIssuedDocumentByExternalId({ externalId: id, status: 'CLOSED' })
.catch(e => {
const message = `Failed to close linked document with ID ${id}`;
console.error(`${message}: ${e}`);
throw new Error(message);
});
}
}));
}
if (getTypeFromGreenInvoiceDocument(coreInput.type) === DocumentType.CreditInvoice) {
// Close origin
}
return injector.get(ChargesProvider).getChargeByIdLoader.load(newDocument.charge_id);
}
console.error('Document issue failed', document);
throw new GraphQLError('Document issue failed');
}
catch (error) {
console.error('Error issuing document:', error);
throw new GraphQLError('Error issuing document');
}
}
//# sourceMappingURL=issue-document.helper.js.map