synt_backend
Version:
Synt light-weight node backend service
469 lines (431 loc) • 13 kB
JavaScript
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;
}, {});