2

When the HandlePayNow function is called, it executes all of its intended functionality but then crashes the system. It sends the order data to the API, the API logs it and returns a success message, the function gets this success message, it displays it's "Please Present Payment" alert with the correct numbers (matching the ones submitted when the button was clicked and those present in the logs), and then it crashes providing the error message:


Cannot read properties of undefined (reading 'toFixed')
TypeError: Cannot read properties of undefined (reading 'toFixed')
at http://localhost:3000/static/js/bundle.js:783:58
at Array.map (\<anonymous\>)
at App (http://localhost:3000/static.js.bundle.js:768:32)
at renderWithHooks (http://localhost:3000/static/js/bundle.js:22965:22)
at updateFunctionComponent (http://localhost:3000/static/js/buindle.js:27559:20)
at HTMLUnknownElement.callCallback (http://localhost:3000/static/js/bundle.js:12557:18)
at Object.invokeGuardedCallbackDev (http://localhost:3000/static/js/bundle.js:12601:20)
at invokeGuardedcallback (http://localhost:3000/static/js/bundle.js:12658:35)
at beginWork$1 (http://localhost:3000/static/js/bundle.js:32532:11)

This was not occuring before I modified the CalculateRunningTotal function. I'm about 99% sure (as always is the case when I come to get the assistance of the amazing StackOverflow community) that this is simply a case of stupidity where I'm blatantly overlooking something very obvious. My gut instinct tells me it's related to the way the subtotal state is being reset in the async, but I'm, frankly, terrible at JS. It's not my bag. If someone could explain my silliness, that would be greatly appreciated.

All related functions:


const logToConsole = (message) => {
    setConsoleLogs((prevLogs) => [...prevLogs, message]);
  };

  useEffect(() => {
    const customConsoleLog = console.log;

    console.log = (message) => logToConsole(message);

    return () => {
      console.log = customConsoleLog;
    };
  }, []);

  const calculateItemTotal = (item) => {
    let sizePrice = getSizePrice(item.size);
    let flavorPrice = item.flavors.length * 0.89;

    return sizePrice + flavorPrice + item.toppingsPrice;
  };

  const calculateRunningTotal = () => {
    let total = 0;
  
    for (const itemTotal of runningTotals) {
      total += itemTotal;
    }
  
    if (selectedDiscount) {
      const discount = discounts.find((discount) => discount.name === selectedDiscount);
      if (discount) {
        const discountMultiplier = 1 - discount.discount;
        total *= discountMultiplier;
      }
    }
  
    // Check if total is NaN before applying toFixed
    return isNaN(total) ? 0 : total.toFixed(2);
  };   

  const handleAddToOrder = () => {
    if (!selectedSize) {
      alert("Select a Size!");
      return;
    }
  
    const item = {
      size: selectedSize,
      flavors: selectedFlavors,
      toppingsPrice: toppingsPrice,
    };
  
    setOrderItems((prevItems) => [...prevItems, item]);
    setSubtotal((prevSubtotal) => prevSubtotal + calculateItemTotal(item));
    setRunningTotals((prevTotals) => [...prevTotals, calculateItemTotal(item)]);
  
    setSelectedSize(null);
    setSelectedFlavors([]);
    setToppingsPrice(0);
    setSyrupToppings((prevToppings) =>
      prevToppings.map((topping) => ({
        ...topping,
        isActive: false,
      }))
    );
  };  

  const getSizePrice = (size) => {
    switch (size) {
      case 'Small':
        return 2.49;
      case 'Medium':
        return 3.29;
      case 'Large':
        return 4.19;
      default:
        return 0;
    }
  };

  const handleAddTopping = (price) => {
    setToppingsPrice((prevPrice) => prevPrice + price);
  };

  const calculateCurrentItem = () => {
    let sizePrice = selectedSize ? getSizePrice(selectedSize) : 0;
    let flavorPrice = selectedFlavors.length * 0.89;
    let total = sizePrice + flavorPrice + toppingsPrice;
  
    if (selectedDiscount) {
      const discount = discounts.find((discount) => discount.name === selectedDiscount);
      if (discount) {
        const discountMultiplier = 1 - discount.discount;
        total *= discountMultiplier;
      }
    }
  
    return total.toFixed(2);
  };

  const handleRemoveOrder = (index) => {
  const removedItem = orderItems[index];
  const removedItemTotal = calculateItemTotal(removedItem);

  setOrderItems((prevItems) => prevItems.filter((_, i) => i !== index));
  setRunningTotals((prevTotals) => prevTotals.filter((_, i) => i !== index));

  setSubtotal((prevSubtotal) => {
    const newSubtotal = prevSubtotal - removedItemTotal;
    return newSubtotal < 0 ? 0 : newSubtotal;
  });
};

  const handlePayNow = async () => {
    if (orderItems.length === 0) {
      alert("No items in the order!"); // Make user select more than 0 items
      return;
    }
  
    let name = prompt("Enter customer name:"); // Prompt for customer name
  
    while (!name) {
      name = prompt("Enter customer name:"); // Re-prompt for customer name
    }
  
    setCustomerName(name); // Set the customer name
    
    // Reset the subtotal
    setSubtotal(0);
  
    let originalTotal = subtotal;
    let discountAmount = 0;
    let discountType = "None"; // Default value for discount type
  
    if (selectedDiscount) {
      const discount = discounts.find((discount) => discount.name === selectedDiscount);
      if (discount) {
        const discountMultiplier = 1 - discount.discount;
        discountAmount = originalTotal - (originalTotal * discountMultiplier);
        originalTotal = originalTotal * discountMultiplier;
        discountType = discount.name; // Store the discount type
      }
    }
  
    const logs = [];
    logs.push('');
    logs.push('');
    logs.push(`Size: ${selectedSize ? selectedSize.charAt(0).toUpperCase() + selectedSize.slice(1) : null}`);
  
    const flavorsText = selectedFlavors.length > 0 ? selectedFlavors.join(", ") : "None";
    logs.push(`Flavors: ${flavorsText}`);
  
    const nonCustomAddIns = syrupToppings.filter((topping) => topping.isActive && !topping.custom);
    const selectedAddIns = nonCustomAddIns.map((topping) => topping.name);
  
    if (selectedAddIns.length > 0) {
      logs.push(`Add-ins: ${selectedAddIns.join(", ")}`);
    } else {
      logs.push("Add-ins: None");
    }
  
    const tax = (originalTotal * 0.056).toFixed(2);
    const totalWithTax = (parseFloat(originalTotal) + parseFloat(tax)).toFixed(2);
  
    logs.push('');
    logs.push(`Subtotal: $${originalTotal.toFixed(2)}`);
    logs.push(`Tax: $${tax}`);
    logs.push(`Total: $${totalWithTax}`);
    logs.push('');
  
    setConsoleLogs(logs); // Update the console logs with the new order information
  
    const updatedToppings = syrupToppings.map((topping) => {
      const updatedTopping = { ...topping };
      if (updatedTopping.isActive) {
        updatedTopping.soldCount += 1;
      }
      updatedTopping.isActive = false;
      return updatedTopping;
    });
  
    setSyrupToppings(updatedToppings);
    setSelectedSize(null); // Reset selectedSize to null
    setSelectedFlavors([]); // Reset selectedFlavors to an empty array
    setToppingsPrice(0); // Reset toppingsPrice to 0
    setSelectedDiscount(null); // Reset selectedDiscount to null
    setRunningTotals([]); // Reset the running total
  
    const soldLogs = syrupToppings
      .filter((topping) => topping.isActive && !topping.custom)
      .map((topping) => {
        return {
          name: topping.name,
          price: topping.price,
          isActive: topping.isActive,
          soldCount: topping.soldCount + 1
        };
      });
  
    const requestOptions = {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(soldLogs)
    };
  
    try {
      const response = await fetch('http://localhost:5000/inventory', requestOptions);
      if (response.ok) {
        // Sold logs successfully updated in the inventory
        console.log('Sold logs updated in the inventory.');
      } else {
        console.error('Failed to update sold logs in the inventory.');
      }
    } catch (error) {
      console.error('Error occurred while updating sold logs in the inventory:', error);
    }
  
    // Send the relevant information to the simulated payment API
    const paymentData = {
      subtotal: originalTotal.toFixed(2),
      tax: tax,
      total: totalWithTax,
      discount: selectedDiscount ? true : false,
      discountAmount: discountAmount.toFixed(2),
      discountType: discountType,
      employee: loginNumberTracking,
    };
  
    try {
      const paymentResponse = await fetch('http://localhost:5000/pay', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(paymentData)
      });
  
      if (paymentResponse.ok) {
        const paymentResult = await paymentResponse.text();
        console.log('Payment API response:', paymentResult);
        if (!selectedDiscount) {
          alert(`Please Present Payment\nSubtotal: ${originalTotal.toFixed(2)}\nTax: ${tax}\nTotal: ${totalWithTax}`);
        } else {
          alert(`Please Present Payment\nSubtotal: $${originalTotal.toFixed(2)}\nDiscount Amount: $${discountAmount.toFixed(2)}\nDiscount Type: ${discountType}\nTax: $${tax}\nTotal (after discount and tax): $${(originalTotal + parseFloat(tax)).toFixed(2)}`);
        }
      } else {
        console.error('Failed to send payment data to the API.');
      }
    } catch (error) {
      console.error('Error occurred while sending payment data to the API:', error);
    }
  };     

  const handleSelectDiscount = (discount) => {
    if (selectedDiscount === discount) {
      setSelectedDiscount(null); // Deselect the currently selected discount
    } else {
      setSelectedDiscount(discount); // Select the clicked discount
    }
  };

StackOverflow is a last resort for me, generally speaking. If I'm here, I've been at it for several hours at least. I need someone who knows more than I do; my knowledge isn't cutting it here.

2 Answers2

1

As the error message says:

Cannot read properties of undefined (reading 'toFixed')

The error is happening because you're trying to call toFixed() method of an undefined variable.

From what you provided, it's hard to tell correctly which part caused the issue but it could have happened from subtotal not being initialized and used.

These lines:

    // Reset the subtotal
    setSubtotal(0);
  
    let originalTotal = subtotal;

React state setters are async so if your subtotal is undefined when you run setSubtotal(0), it's still undefined after running it, until after the next tick, which means originalTotal will also be undefined and thus causing the error by calling originalTotal.toFixed(2).

If this is not the case, there may be other statements that caused it.

technophyle
  • 7,972
  • 6
  • 29
  • 50
  • The subtotal variable is only ever used within these functions and calculations. The only thing related to the subtotal variable that was not shown was the original state declaration: const [subtotal, setSubtotal] = useState(0); And the related JSX. – Matthew Ford Jul 18 '23 at 18:57
  • @MatthewFord Ok, then that's not the cause. Are you able to find out which line caused the error from the dev tools? – technophyle Jul 18 '23 at 18:58
  • I'm working in React, so the error trace only leads to the bundles, not to any specific line. "The error occurred in app.js" is not particularly helpful since the entire page exists in app.js once it's bundled. That's as useful as "The error happened somewhere." I find the execution order of this error very strange. It executes with, displays, and writes the proper numbers everywhere, and they always match, input -> console -> display -> orderbook -> alert, they are always the correct numbers. After that alert pops up, toFixed reads the subtotal state as undefined and it crashes. – Matthew Ford Jul 18 '23 at 19:07
  • This leads me to believe that when it's trying to update back to 0 and "reset" the order, it's hitting this error. – Matthew Ford Jul 18 '23 at 19:08
  • Like I said, it's just strange. The entirety of the HandlePayNow function executes and THEN it crashes, but these 2 actions are linked. It will not crash until it executes the HandlePayNow function. This is almost certainly due to the updating of the subtotal state variable, either in HandlePayNow or in CalculateRunningTotal. I will note that this worked properly before modifications to CalculateRunningTotal. – Matthew Ford Jul 18 '23 at 19:18
  • My suggestion is to add `console.log`'s right before all `.toFixed()` calls and figure out which line caused the issue first, and then we can go from there. – technophyle Jul 18 '23 at 19:25
  • This is actually my worst nightmare. Console.logging before the toFixed calls is creating infinite re-renders and a permanent error. The subtotal state is storing, writing and alerting with the proper value right up until it hits the runtime error. It's as if it's being changed AGAIN right after or something. This is the most frustrating error I think i've come across yet. – Matthew Ford Jul 18 '23 at 19:53
  • Can you create a codesandbox or a minimum reproducible snippet? – technophyle Jul 18 '23 at 19:57
  • Maybe. This is a highly integrated project built from the ground up. Each new thing almost certainly requires the thing below it to be functional. Almost nothing here would work in a vacuum without the Flask server and the other functionality tied into other button components and the like. And especially this part, since it's one of the few functions that interact with almost every single other function in some way, since one is handling the payment and the other the item costs and running subtotals. – Matthew Ford Jul 18 '23 at 20:03
  • And like I said, it's baffling. It's either happening directly at the end of HandlePayNow (after the alert, which there is no more function there), or as soon as it drops out of this function. But it works in the normal loop before I submit an order via HandlePayNow. Only after this functionality is executed and is all the way complete is this runtime error created. – Matthew Ford Jul 18 '23 at 20:06
  • I don't understand how adding `console.log`'s can cause infinite re-renders. Can you just try simple console logs with the values used in `.toFixed()` methods? – technophyle Jul 18 '23 at 20:08
  • I also don't understand how console.log would cause infinite re-renders (I'm assuming because some on-screen content is based on console logs), but it does. I can tell you with certainty that directly before the runtime error, the numbers are correct. The alert that pops up is acting as a console log for these values, and they are correct within milliseconds of this error. This is a highly specific issue related likely to a single line of code out of place, its not a sweeping issue with the toFixed method. All of this worked appropriately fairly recently. – Matthew Ford Jul 18 '23 at 20:24
  • Like I said, as soon as this function completes, it hits the runtime error. This is probably related to the calculateRunningTotal function and the subtotal state, but as I said, beyond me. I need a JS expert. – Matthew Ford Jul 18 '23 at 20:25
  • I'll put you on the right path. It is 100% related to the CalculateRunningTotal function an the resetting of this array at line 269: "setRunningTotals([]); // Reset the running total". When this line is commented out, the error is no more, but obviously the running subtotal is not reset. Now I have completely isolated the issue down to one function. Why is this happening when it is reset to an empty array? – Matthew Ford Jul 18 '23 at 20:46
  • I can't find any usage of `calculateRunningTotal()` function's results. Can you add that too? – technophyle Jul 18 '23 at 20:48
  • Where this is being used or how is totally irrelevant. At the point where it is reset to an empty array by the HandlePayNow function, this error is created. This is absolutely the cause of the error because without the resetting line, the error is gone. I'm a decent architect, just not a great low-level code writer. This IS the issue, in some form. It lies nowhere else except in how or where this is being reset and the toFixed method acting on the empty array and then returning the error. To be fair, now that I've isolated the problem, I will probably solve it shortly. – Matthew Ford Jul 18 '23 at 20:55
  • Ok, if you think it's totally irrelevant maybe it is but from the code you shared, the only usage of `runningTotals` (which you indicated the source of the problem) is `calculateRunningTotal()` and since the function is not called anywhere, I asked where it is because if `runningTotals` is indeed only used in the function, that function would be directly related to the error. – technophyle Jul 18 '23 at 20:58
  • The only other place it is used is within the handleAddToOrder function -> "setRunningTotals((prevTotals) => [...prevTotals, calculateItemTotal(item)]);" to update the subtotal with the new subtotal when an item is added. Apologies if I'm curt. The only time I'm ever on SO is when it's been many hours of this. If nothing else, you've helped me isolate it down quite a lot through our discourse. – Matthew Ford Jul 18 '23 at 21:13
  • But the fact that the error no longer exists when the line "setRunningTotals([]);" in HandlePayNow is removed suggests that this IS the source of the error, this and the function defining it. – Matthew Ford Jul 18 '23 at 21:14
  • There we go. Just like I said :) Here's the one line that fixed it and was needed directly before the setRunningTotals([]); -> "setOrderItems([]);" With items still on the order in this manner, it was conflicting with trying to empty out the running total array since it is populated specifically by those items. Thank you for your help. The discourse kept me chugging along and head-down. – Matthew Ford Jul 18 '23 at 21:40
  • As it were, I don't use SO because I couldn't eventually debug this nonsense. I'm hoping someone excellent at JavaScript will catch that and save me the hours and headache. It works out sometimes, others not so much :P – Matthew Ford Jul 18 '23 at 21:41
  • If you ever need someone to help with Python, gimmie a call. I'm much better at Python low-level. JS is basically the Resident Evil of coding. A mess with some really great gems. – Matthew Ford Jul 18 '23 at 21:43
1

The running total is calculated based on the added prices of items in the OrderItems array. Since, despite the RunningTotals array being cleared the OrderItems persisted within the array, this obviously caused a conflict. The OrderItems array needed to be reset before the RunningTotals array.

setOrderItems([]);
setRunningTotals([]);