Search code examples
javascriptjsonjavascript-objectsupdating

Javascript: I am updating the value of an object key. BUT after the update another key pair also changes value


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);
   }

}

  • We have 3 line objects to th "lines" array.
  • I iterate over the array to fill the info for every line.
  • Each line has it's own incomeClassifications
  • There's a summary object tha needs to collect each classification combination. (in the example I am using only 1 combination)
  • After the iteration I am filling the summary fields that needs to be filled (totalNetValue,totalVatAmount,totalGrossValue).
  • Then I using a findIndex() to find if the classification combination (type & category) exist inside summary.
  • If findIndex is <0 (there isn't a combination that maches the line) I am adding the combination drom the line.
  • Else, I am just Updating the amount of the given index with the addition of the current 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():

  • the 1st line classification amount is giving me the expected price of '10.00' but
  • the summary classification amount is getting stucked at 10.00 because it doesn't get updated with each new line.

Maybe there is another way of doing this, but why is this happening in the first place?

Any help would be ENORMOUSLY appreciated...


Solution

  • 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.