I 'm working on an API for an invoicing programm. My problem is that when I am trying to constuct a json and update some keys, a key, changes without me updating it.
Here is a MRE without all the api calls. I am just using prepared objects, for data.
const business = {
vatNumber: '050112718',
country: 'GR',
branch: 0
}
const customer = {
vatNumber: '050112915',
country: 'GR',
branch: 0,
postalCode: 28100,
city: 'Argostoli'
}
const invoiceProperties = {
series: 0,
aa: 1,
issueDate: '2021-12-15',
type: '1_1',
currency: 'EUR'
}
const payment = {
type: 3
}
const product = {
price: 10,
taxCode: 1,
classificationType: 'type a',
classificationCategory: 'category 1',
}
const lines = [
{
lineNumber: 1,
netValue: 0.00,
vatCategory: 0,
vatAmount: 0,
incomeClassification: {
'icls:classificationType': '',
'icls:classificationCategory': '',
'icls:amount': 0.00
}
},
{
lineNumber: 2,
netValue: 0.00,
vatCategory: 0,
vatAmount: 0,
incomeClassification: {
'icls:classificationType': '',
'icls:classificationCategory': '',
'icls:amount': 0.00
}
},
{
lineNumber: 3,
netValue: 0.00,
vatCategory: 0,
vatAmount: 0,
incomeClassification: {
'icls:classificationType': '',
'icls:classificationCategory': '',
'icls:amount': 0.00
}
}
]
// json invoice creation
const invoiceObj = {
invoice: {
issuer: {
vatNumber: business.vatNumber,
country: business.country,
branch: business.branch
},
counterpart: {
vatNumber: customer.vatNumber,
country: customer.country,
branch: customer.branch,
address: {
postalCode: customer.postalCode,
city: customer.city
}
},
invoiceHeader: {
series: invoiceProperties.series,
aa: invoiceProperties.aa,
issueDate: invoiceProperties.issueDate,
invoiceType: invoiceProperties.type,
currency: invoiceProperties.currency
},
paymentMethods: {
paymentMethodDetails: {
type: payment.type,
amount: ""
}
},
invoiceDetails: [],
invoiceSummary: {
totalNetValue: 0,
totalVatAmount: 0,
totalWithheldAmount: '0.00',
totalFeμesAmount: '0.00',
totalStampDutyAmount: '0.00',
totalOtherTaxesAmount: '0.00',
totalDeductionsAmount: '0.00',
totalGrossValue: 0,
incomeClassification: []
}
}
}
//making paths into object more simle
const invoiceDetails = invoiceObj.invoice.invoiceDetails;
const summaryClassification = invoiceObj.invoice.invoiceSummary.incomeClassification;
const makeLines = ()=> {
for (const line of lines) {
line.netValue = product.price.toFixed(2);
line.vatCategory = product.taxCode;
line.incomeClassification["icls:classificationType"] = product.classificationType;
line.incomeClassification["icls:classificationCategory"] = product.classificationCategory;
line.incomeClassification["icls:amount"] = product.price.toFixed(2);
switch (line.vatCategory) {
case 1:
line.vatAmount = (line.netValue * 0.24).toFixed(2);
break;
case 2:
line.vatAmount = (line.netValue * 0.17).toFixed(2);
break;
case 3:
line.vatAmount = (line.netValue * 0.13).toFixed(2);
break;
case 4:
line.vatAmount = (line.netValue * 0.09).toFixed(2);
break;
case 5:
line.vatAmount = (line.netValue * 0.06).toFixed(2);
break;
case 6:
line.vatAmount = (line.netValue * 0.04).toFixed(2);
break;
case 7:
line.vatAmount = '0.00';
break;
case 8:
line.vatAmount = '0.00';
break;
};
console.log(line); //just to see something in dev mode
invoiceObj.invoice.invoiceSummary.totalNetValue = (invoiceObj.invoice.invoiceSummary.totalNetValue/1 + line.netValue/1).toFixed(2);
invoiceObj.invoice.invoiceSummary.totalVatAmount = (invoiceObj.invoice.invoiceSummary.totalVatAmount/1 + line.vatAmount/1).toFixed(2);
invoiceObj.invoice.invoiceSummary.totalGrossValue = (invoiceObj.invoice.invoiceSummary.totalGrossValue/1 + line.netValue/1 + line.vatAmount/1).toFixed(2);
const lineClassification = line.incomeClassification;
const lineType = lineClassification["icls:classificationType"];
const lineCategory = lineClassification["icls:classificationCategory"];
const classificationIndex = summaryClassification.findIndex(o => o["icls:classificationType"] === lineType && o["icls:classificationCategory"] === lineCategory);
// if there isn't a array item with the same combination of categories, we add the classifications object
if (classificationIndex < 0) {
summaryClassification.push(lineClassification);
} else { // else if there is a an item in the array with the same categories, we add the amount of the item to the total
summaryClassification[classificationIndex]["icls:amount"] = (summaryClassification[classificationIndex]["icls:amount"]/1 + line.netValue/1 ).toFixed(2);
}
invoiceDetails.push(line);
}
}
My Problem
The else statement somehow changes the value os the incomeClassification of the 1st line. +
Given that from the first iteration, that key should be "10.00" following the code line.incomeClassification["icls:amount"] = product.price.toFixed(2);
with the product.price beeing = 10.
Also, clearly the else statement code updates the summary properties and not the line ones summaryClassification[classificationIndex]["icls:amount"] = (summaryClassification[classificationIndex]["icls:amount"]/1 + line.netValue/1 ).toFixed(2);
.
If I comment out the else statment at the and of makeLines():
Maybe there is another way of doing this, but why is this happening in the first place?
Any help would be ENORMOUSLY appreciated...
Addressing the immediate problem: each line
has a lineClassification
prop, which is itself an object. As lines
are being iterated, some of those lineClassification's are being placed in another array (summaryClassification
). Subsequent turns of the outer loop are looking up elements of the array and mutating them, but these lineClassifications are the same objects pointed to by the lines.
Here's a much more minimal MRE to illustrate...
const lines = [
{ name: 'line0', classificationObject: { name: 'class0', number: 10 } },
{ name: 'line1', classificationObject: { name: 'class1', number: 10 } },
{ name: 'line2', classificationObject: { name: 'class0', number: 10 } },
]
let summary = []
for (line of lines) {
let classificationObject = line.classificationObject;
let classificationName = classificationObject.name
let index = summary.findIndex(s => s.name === classificationName);
if (index == -1) {
summary.push(classificationObject); // the problem is here.
// the SAME classification object now appears in TWO places:
// in the line object, and in the summary array
// future mutations will be seen by both
} else {
// as a result, this line seems to modify two objects
// it's really modifying one object, pointed to from multiple places
summary[index].number += 12
}
}
console.log("logging the summary, it's what we expect. The saved class0 classification has been mutated:")
console.log(summary)
console.log("But logging the lines, we're surprised to find line 0's classification object has been mutated. it happens because that same object was also placed in the summary array, and was mutated in a subequent turn of the loop:")
console.log(lines)
Fixing the MRE:
It's apparent that the summaryClassification
array must be given a copy of the lineClassification
object, one that can be mutated independently of the line
it once belonged to.
const lines = [
{ name: 'line0', classificationObject: { name: 'class0', number: 10 } },
{ name: 'line1', classificationObject: { name: 'class1', number: 10 } },
{ name: 'line2', classificationObject: { name: 'class0', number: 10 } },
]
let summary = []
for (line of lines) {
// do some stuff that doesn't matter
let classificationObject = line.classificationObject;
let classificationName = classificationObject.name
let index = summary.findIndex(s => s.name === classificationName);
if (index == -1) {
let classificationCopy = Object.assign({}, classificationObject); // see here: make a copy
summary.push(classificationCopy); // subsequent turns will
// mutate the copy, leaving the original line alone
} else {
summary[index].number += 12
}
}
console.log(summary)
console.log("logging the lines, see what we expect in line 0, an unchanged classification object")
console.log(lines)
In OP terms:
if (classificationIndex < 0) {
let lineClassificationCopy = Object.assign({}, lineClassification); // see here: make a copy
summaryClassification.push(lineClassificationCopy);
} else { // else if there is a an item in the array with the same categories, we add the amount of the item to the total
summaryClassification[classificationIndex]["icls:amount"] = (summaryClassification[classificationIndex]["icls:amount"]/1 + line.netValue/1 ).toFixed(2);
}
As an aside, the code represents numerical values as strings. A better practice is to represent numbers as numbers, do math on numbers, store intermediate results as numbers, etc, and convert the numbers to strings only when they must be presented to the user. For currency, this should be done using locale sensitive formatting.
A further aside is that the code appears to aim to do real-life-complexity business computing with insufficiently powerful technique. If this is a production system, I worry that the eventual owners will find it opaque and unmaintainable. I suggest reviewing and employing object oriented concepts in the next revision.