UNPKG

synt_backend

Version:

Synt light-weight node backend service

469 lines (431 loc) 13 kB
import db from "../mysql/models"; const { Op } = require("sequelize"); import { getBankTransactionsDB } from "./banking"; export async function getSettlementDB({ t, FinancialYearId, User, VME }) { const Settlement = await getSettlementByYearId(FinancialYearId); if (Settlement) { // Settlement exists return { success: true, Settlement, }; } // Settlement does not exists yet const FinancialYear = await getFinancialYearById(FinancialYearId); if (!FinancialYear || FinancialYear.VMEId !== VME.id) { return { success: false, error: t( "api.accounting.settlement.errors.notYourVme", "This financial year does not belong to your VME." ), }; } const Purchases = await getPurchases({ start_date: FinancialYear.start_date, end_date: FinancialYear.end_date, VMEId: VME.id, }); const Lots = await getLots({ VME }); /* let Commissioners = []; Lots.forEach((Lot) => { if (Lot.Commissioner && Lot.Commissioner.id) { let Commissioner = Lot.Commissioner; // merge when exists let exists = false; Commissioners.map((C) => { if (C.id === Commissioner.id) { exists = true; C.setDataValue( "representing_shares", C.getDataValue("representing_shares") + Lot.share ); } return C; }); if (!exists) { Commissioner.setDataValue("representing_shares", Lot.share); Commissioners.push(Commissioner); } } }); */ const Allocations = calculateAllocations({ start_date: FinancialYear.start_date, end_date: FinancialYear.end_date, Lots, Purchases, }); const Provisions = await getProvisionsByYearId(FinancialYear.id); // Provisions: // working_capital: 410100 // guarantee_fund: 429200 // purchase_result: 440000 // Retrieve bank transactions and process for balance const BankAccounts = await getBankAccounts({ FinancialYear, t, User, VME }); const Balance = getBalance({ BankAccounts, Provisions, Purchases }); // TE BETALEN LEVERANCIERS Balance.total_suppliers_paid = Balance.PurchaseAccounts.reduce( (sum, PA) => sum + PA.total_paid, 0 ); Balance.total_suppliers_unpaid = Balance.PurchaseAccounts.reduce( (sum, PA) => sum + PA.total_unpaid, 0 ); return { success: true, Settlement: { FinancialYear, Purchases, Allocations, Provisions, Balance, }, }; } function getBalance({ BankAccounts, Provisions, Purchases }) { const Balance = { BankAccounts, ProvisionAccounts: Object.entries( groupBy("GeneralLedgerAccountId", Provisions) ).map(([GeneralLedgerAccountId, Ps]) => { console.log(GeneralLedgerAccountId); let total_paid = formatCurrency( Ps.reduce((sum, P) => sum + (P.paid_at ? P.amount : 0), 0) ); let total_unpaid = formatCurrency( Ps.reduce((sum, P) => sum + (P.paid_at ? 0 : P.amount), 0) ); return { GeneralLedgerAccount: Ps[0].GeneralLedgerAccount, total_paid, total_unpaid, }; }), PurchaseAccounts: Object.entries( groupBy( "GeneralLedgerAccountId", Purchases.reduce( (entries, P) => [ ...entries, ...P.PurchaseEntries.map((PE) => ({ ...PE.toJSON(), paid_at: P.paid_at, })), ], [] ) ) ).map(([GeneralLedgerAccountId, PEs]) => { console.log(GeneralLedgerAccountId); let total_paid = formatCurrency( PEs.reduce( (sum, PE) => sum + (PE.paid_at ? PE.amount * (1 + PE.vat_percentage) : 0), 0 ) ); let total_unpaid = formatCurrency( PEs.reduce( (sum, PE) => sum + (PE.paid_at ? 0 : PE.amount * (1 + PE.vat_percentage)), 0 ) ); return { GeneralLedgerAccount: PEs[0].GeneralLedgerAccount, total_paid, total_unpaid, }; }), }; return Balance; } async function getBankAccounts({ VME, t, User, FinancialYear }) { let BankAccounts = []; if (VME.has_banking) { let bankTransactions = await getBankTransactionsDB({ t, User, VME, }); if (bankTransactions.success) { let start = new Date(FinancialYear.start_date); let end = new Date(FinancialYear.end_date); let accounts = bankTransactions.accounts; accounts.forEach((account) => { let incoming = 0; let outgoing = 0; account.transactions.forEach((transaction) => { let date = new Date(transaction.executionDate); if (date >= start && date < end) { if (transaction.amount < 0) { outgoing += -transaction.amount; } else { incoming += transaction.amount; } } }); BankAccounts.push({ incoming, outgoing, reference: account.reference, referenceType: account.referenceType, product: account.product, currency: account.currency, }); }); } } return BankAccounts; } async function getProvisionsByYearId(id) { let Provisions = await db.Provision.findAll({ where: { FinancialYearId: id }, include: [ { model: db.Lot, include: [ { model: db.LotPhase, include: [ { model: db.User, attributes: { exclude: [ "phone", "email", "email_verified_at", "phone_verified_at", ], }, include: db.Company, }, ], }, ], }, { model: db.GeneralLedgerAccount, }, ], }); Provisions = Provisions.map((P) => { // not last period, but based on invoice_date // FIXME: important: who paid... that's it let period = P.Lot.LotPhases.find( (LP) => new Date(P.invoice_date) >= new Date(LP.starts_at) && (!LP.ends_at || new Date(P.invoice_date) <= new Date(LP.ends_at)) ); //let period = P.Lot.LotPhases.find((LP) => !LP.ends_at); let Commissioner = period?.Users.find( (U) => U.LotPhaseUser.is_commissioner ); const pjson = P.toJSON(); return { ...pjson, Lot: { ...pjson.Lot, Commissioner }, }; }); return Provisions; } async function getSettlementByYearId(id) { const Settlement = await db.Settlement.findOne({ where: { FinancialYearId: id }, include: [db.SettlementFile, db.FinancialYear, db.PaymentCondition], }); if (!!Settlement) { // if it exists set promise to add files await Promise.all( (Settlement?.SettlementFiles || []).map(async (SF) => { SF.setDataValue("presignedUrl", await SF.getPresignedUrl()); }) ); } return Settlement; } export async function getLots({ VME }) { return await VME.getLots({ include: { model: db.LotPhase, include: { model: db.User, attributes: { exclude: ["phone", "email", "email_verified_at", "phone_verified_at"], }, }, }, }); } export function calculateAllocations({ Purchases, start_date, end_date, Lots, }) { let Allocations = []; for (const P of Purchases) { for (const PE of P.PurchaseEntries) { for (const PA of PE.PurchaseAllocations) { let total_shares = PE.GeneralLedgerAccount.DistributionKey.Lots.reduce( (sum, L) => sum + parseInt(L.share), 0 ); let Lot = Lots.find((L) => L.id === PA.LotId); let total = Purchases.reduce( (sum, item) => sum + item.PurchaseEntries.reduce( (s, i) => s + i.GeneralLedgerAccountId === PE.GeneralLedgerAccountId ? formatCurrency(i.amount * (1 + i.vat_percentage)) : 0, 0 ), 0 ); // find commissioners for (const LP of Lot.LotPhases) { let { days, start, end } = dateRangeOverlap( new Date(start_date), new Date(end_date), new Date(LP.starts_at), LP.ends_at ? new Date(LP.ends_at) : new Date() ); if (days > 0) { const Commissioner = LP.Users.find( (U) => U.LotPhaseUser.is_commissioner ); if (Commissioner) { let financial_year_days = Math.ceil( (new Date(end_date) - new Date(start_date)) / (1000 * 60 * 60 * 24) + 1 ); let weight = days / financial_year_days; Allocations.push({ UserId: Commissioner.id, LotId: PA.LotId, amount: weight * PA.amount, code: PE.GeneralLedgerAccount.code, name: PE.GeneralLedgerAccount.name, vat_amount: formatCurrency( weight * PA.amount * PE.vat_percentage ), distribution_key: Lot.share + "/" + total_shares, total, ownership_days: days, ownership_start: start, ownership_end: end, financial_year_days, }); } } } } } } Allocations = Object.entries(groupBy("UserId", Allocations)).map( ([UserId, A]) => { let AllocatedLots = Object.entries(groupBy("LotId", A)).map( ([LotId, B]) => { return { ...Lots.find((L) => parseInt(L.id) === parseInt(LotId)).toJSON(), UserId, LotId, amount: B.reduce((sum, item) => sum + item.amount, 0), vat_amount: B.reduce((sum, item) => sum + item.vat_amount, 0), ownership_days: B[0].ownership_days, ownership_start: B[0].ownership_start, ownership_end: B[0].ownership_end, financial_year_days: B[0].financial_year_days, GeneralLedgerAccounts: Object.entries(groupBy("code", B)).map( ([code, C]) => { let amount = 0; let vat_amount = 0; C.forEach((D) => { amount = amount + D.amount; vat_amount = vat_amount + D.vat_amount; }); return { code, name: C[0]?.name, amount, vat_amount, distribution_key: C[0]?.distribution_key, total_amount_incl: C[0]?.total, total_vat_amount: Allocations.reduce( (sum, item) => item.code === parseInt(code) ? sum + item.vat_amount : sum, 0 ), }; } ), }; } ); return { UserId, Lots: AllocatedLots }; } ); return Allocations; } export async function getPurchases({ VMEId, start_date, end_date }) { return await db.Purchase.findAll({ where: { VMEId: VMEId, invoice_date: { [Op.between]: [start_date, end_date], }, }, include: [ { model: db.PurchaseEntry, include: [ { model: db.PurchaseAllocation }, { model: db.GeneralLedgerAccount, include: [{ model: db.DistributionKey, include: db.Lot }], }, ], }, ], }); } async function getFinancialYearById(id) { return await db.FinancialYear.findOne({ where: { id }, }); } function dateRangeOverlap(a_start, a_end, b_start, b_end) { let days = Math.ceil((a_end - a_start) / (1000 * 60 * 60 * 24)) + 1; if (b_start < a_start && a_end < b_end) return { days, start: a_start, end: a_end }; // a in b if (b_start > a_end) return { days: 0 }; // a before b if (b_end < a_start) return { days: 0 }; // a after b let start = a_start, end = a_end; if (a_start <= b_start && b_start <= a_end) { days -= Math.ceil((b_start - a_start) / (1000 * 60 * 60 * 24)); // b starts in a start = b_start; } if (a_start <= b_end && b_end <= a_end) { days -= Math.ceil((a_end - b_end) / (1000 * 60 * 60 * 24)); // b ends in a end = b_end; } return { days, start, end }; } export function formatCurrency(value) { return Math.round((value + Number.EPSILON) * 100) / 100; } export const groupBy = (key, array) => array.reduce((objectsByKeyValue, obj) => { const value = obj[key]; objectsByKeyValue[value] = (objectsByKeyValue[value] || []).concat(obj); return objectsByKeyValue; }, {});