Invoice Refactor Documentation
This document explains exactly what refactorInvoice(invoice) does in src/utils/invoice-refactor.js, which fields it expects, how totals are calculated, and what fields are produced/updated.
Purpose
refactorInvoice normalizes and recalculates invoice pricing from raw invoice.items payload data. It:
- Separates main products, combo items, and add-ons
- Recomputes per-line totals (with discount and VAT)
- Rebuilds invoice-level totals (
payable,subtotal,vat, etc.) - Rewrites invoice item arrays and item counts
- Handles payment split summary if
payment_methodis provided in object-like shape
Main Execution Flow
- Read
invoice.items(fallback[]) - Split items into:
mainProducts: not add-on and not combo parent/childcomboItems:is_combo === 1 || is_combo === 2
- For each main product:
- Calculate modifier/extras/add-on totals
- Calculate item discount
- Calculate line subtotal, VAT, and line total
- Mutate item pricing fields
- Collect add-ons into a global add-on list
- For each combo item:
- Calculate combo base + nested extras/modifiers/add-ons
- Apply combo-level discount
- Calculate combo subtotal, VAT, line totals
- Mutate combo item pricing fields
- Recalculate aggregated item totals:
items_subtotal,items_vat_amount,items_total
- Rebuild invoice item structure:
invoice.items = [...mainProducts, ...comboItems, ...allAddOns]products_countanditems_count
- Compute invoice-level totals:
gross_total,payable,net_total,subtotal,vat- Applies invoice-level
%discount frominvoice.discountpctif present
- If payment split is present, compute:
paid,tip_amount,change
- Return mutated
invoice
Required vs Optional Input Fields
The function is tolerant and defaults missing numbers to 0 in many places. Still, for correct business results, these are the practical requirements.
Required Invoice Fields (Practical)
invoice.items(array): Required for meaningful totals; otherwise invoice totals stay near zero.invoice.vat_pct(number): Needed if invoice-level VAT should be recomputed from payable.
Optional Invoice Fields
invoice.discountpct(number): Invoice-level percentage discount.invoice.payment_method(object/string): Used for split payment/tip/change calculation.
Required Item Fields (Practical)
For each non-add-on product item:
actual_price(number)qty(number)vat(number; used in line VAT logic)discount_pct(number, optional but expected by formula)
Classification flags:
is_addon(truthy/falsy or 1/0)is_combo(0,1, or2)
Nested optional arrays:
modifier(array or JSON string)extras_array(array)addOns(array)
Combo-Specific Fields
For combo items (is_combo === 1 or 2):
combo_items(array)- each combo item may use:
actual_priceqtyvatmodifierextras_arrayaddOns
Calculation Details
1) Number safety
safeNumber(value):
- Converts to
Number - If
NaN=>0 - Rounds to 4 decimals (
toFixed(4))
Most downstream values are wrapped with this helper.
2) Extras normalization
extrasToString(extrasArr)converts extras array into JSON object string of{ [extraId]: quantity }- Empty extras =>
"{}" parseExtrassafely parses extras object/stringgetConnectedExtrasexists but is not used inrefactorInvoice
3) Modifier total
handleModifiersTotal(modifiers):
- Parses modifier input from array or JSON string
- For each modifier:
modifier.subtotal = actual_price * quantity
- For each nested modifier item:
modItem.subtotal = actual_price * quantity
- Returns sum of all modifier subtotals
4) Add-on total
handleAddOnsTotal(addOns):
- For each add-on:
addOn.subtotal = actual_price * qty
- Returns sum of add-on subtotals
5) Extras total
handleExtrasTotal(extras):
- For each extra:
- Uses
(actual_price || price || 0) * quantity
- Uses
- Returns extras total
6) Main item totals (handleItemTotal)
Given an item:
actualPrice = item.actual_priceitemTotalBeforeDiscount = actualPrice + modifierTotal + extrasTotal + addOnsTotalitemDiscountAmount = itemTotalBeforeDiscount * discount_pct / 100itemTotal = itemTotalBeforeDiscount - itemDiscountAmountitemSubtotal = itemTotal * qtylineVat = calculateLineVat(item)lineTotalWithoutVAT = itemSubtotal - lineVatlineTotal = lineTotalWithoutVAT + lineVat(equals itemSubtotal after rounding)
Also returns:
- stringified extras (
extras) - passthrough of add-ons/modifiers/extras arrays
7) Line VAT logic (calculateLineVat)
VAT components per single unit:
- Product VAT:
item.vat - Modifiers VAT: sum of each modifier item
vat * quantity - Add-ons VAT: sum of each add-on
vat * qty - Extras VAT: sum of each extra
vat * quantity
Then:
totalVat = productVat + modifiersVat + addOnsVat + extrasVatlineVat = totalVat * item.qty
8) Combo totals (handleComboItemTotal)
Base price:
- If
is_combo === 1: use combo parentitem.actual_price - If
is_combo === 2: sum combo child(actual_price * qty)
Then for all combo_items:
- Add modifiers total
- Add extras total
- Add add-ons total
- Add VAT from each combo item (
comboItem.vat)
Discount and line math:
basePriceWithExtras = basePrice + totalModifierTotal + totalAddOnsTotal + totalExtrasTotaltotalDiscountAmount = basePriceWithExtras * item.discount_pct / 100totalItemTotal = basePriceWithExtras - totalDiscountAmounttotalSubtotal = totalItemTotal * item.qtytotalLineVat = summedComboItemVat * item.qtytotalLineTotalWithoutVAT = totalSubtotal - totalLineVattotalLineTotal = totalLineTotalWithoutVAT + totalLineVat
9) Invoice-level totals
After all item loops:
items_subtotal= sum of all itemline_total_without_vatitems_vat_amount= sum of all itemline_vatitems_total= sum of all itemline_totalgross_total= running sum of item subtotals (orderPayable)- default service/delivery/packaging values forced to zero
VAT/subtotal derivation:
- If
vat_pct > 0:vat = payable - payable / (1 + vat_pct/100)subtotal = payable - vat
- Else fallback:
vat = items_vat_amountsubtotal = items_subtotal
Invoice discount (discountpct) behavior:
- Apply discount on
orderPayable - Recompute net payable
- Recompute VAT/subtotal again if
vat_pct > 0
Final key outputs:
payablenet_totalsubtotalvat
10) Payment split handling
If typeof invoice.payment_method === "object":
- Parses payments object
- Sums all
amount=>invoice.paid - Sums all
tip_amount=>invoice.tip_amount invoice.change = paid - (payable + totalTips)- Converts
invoice.payment_methodback to JSON string
Expected payment object shape:
{
"cash": { "amount": 100, "tip_amount": 10 },
"card": { "amount": 3, "tip_amount": 0 },
"bank": { "amount": 3, "tip_amount": 0 },
"other": { "amount": 100, "tip_amount": 10 }
}
Fields Mutated/Produced by Refactor
Per item (main/combo where relevant)
actual_pricesubtotalline_totalline_total_without_vatline_vatitem_totalitem_subtotaldiscount_pctdiscount_amountitem_total_before_discountextras(stringified map)
Invoice-level
products_countitems(rebuilt array)items_countgross_totalservice_percentageservice_amountdelivery_feepackaging_feeitems_subtotalitems_vat_amountitems_totalitems_discount_amountpayablenet_totalsubtotalvat- optionally payment fields:
paidtip_amountchange
Important Notes and Edge Cases
- The function mutates the original
invoiceobject and item objects in-place. - Missing numeric fields usually collapse to
0viasafeNumber, but this can hide bad input. items_countcomment says extras/modifiers are included, but implementation counts only:- main products + combo items + collected add-ons
- Payment handling checks
typeof payment_method === "object"but then parses JSON; input type expectations should be standardized to avoid runtime issues. getConnectedExtrasandgetConnectedAddOnsexist but are currently unused insiderefactorInvoice.
Recommended Minimal Payload
Use at least:
{
"vat_pct": 15,
"discountpct": 0,
"items": [
{
"is_addon": 0,
"is_combo": 0,
"actual_price": 100,
"qty": 1,
"vat": 15,
"discount_pct": 0,
"modifier": [],
"extras_array": [],
"addOns": []
}
]
}
This ensures the refactor can compute line totals and invoice totals consistently.