README docs INVOICE_REFACTOR.md

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_method is provided in object-like shape

Main Execution Flow

  1. Read invoice.items (fallback [])
  2. Split items into:
    • mainProducts: not add-on and not combo parent/child
    • comboItems: is_combo === 1 || is_combo === 2
  3. 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
  4. 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
  5. Recalculate aggregated item totals:
    • items_subtotal, items_vat_amount, items_total
  6. Rebuild invoice item structure:
    • invoice.items = [...mainProducts, ...comboItems, ...allAddOns]
    • products_count and items_count
  7. Compute invoice-level totals:
    • gross_total, payable, net_total, subtotal, vat
    • Applies invoice-level % discount from invoice.discountpct if present
  8. If payment split is present, compute:
    • paid, tip_amount, change
  9. 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, or 2)

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_price
    • qty
    • vat
    • modifier
    • extras_array
    • addOns

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 => "{}"
  • parseExtras safely parses extras object/string
  • getConnectedExtras exists but is not used in refactorInvoice

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
  • Returns extras total

6) Main item totals (handleItemTotal)

Given an item:

  • actualPrice = item.actual_price
  • itemTotalBeforeDiscount = actualPrice + modifierTotal + extrasTotal + addOnsTotal
  • itemDiscountAmount = itemTotalBeforeDiscount * discount_pct / 100
  • itemTotal = itemTotalBeforeDiscount - itemDiscountAmount
  • itemSubtotal = itemTotal * qty
  • lineVat = calculateLineVat(item)
  • lineTotalWithoutVAT = itemSubtotal - lineVat
  • lineTotal = 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 + extrasVat
  • lineVat = totalVat * item.qty

8) Combo totals (handleComboItemTotal)

Base price:

  • If is_combo === 1: use combo parent item.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 + totalExtrasTotal
  • totalDiscountAmount = basePriceWithExtras * item.discount_pct / 100
  • totalItemTotal = basePriceWithExtras - totalDiscountAmount
  • totalSubtotal = totalItemTotal * item.qty
  • totalLineVat = summedComboItemVat * item.qty
  • totalLineTotalWithoutVAT = totalSubtotal - totalLineVat
  • totalLineTotal = totalLineTotalWithoutVAT + totalLineVat

9) Invoice-level totals

After all item loops:

  • items_subtotal = sum of all item line_total_without_vat
  • items_vat_amount = sum of all item line_vat
  • items_total = sum of all item line_total
  • gross_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_amount
    • subtotal = items_subtotal

Invoice discount (discountpct) behavior:

  • Apply discount on orderPayable
  • Recompute net payable
  • Recompute VAT/subtotal again if vat_pct > 0

Final key outputs:

  • payable
  • net_total
  • subtotal
  • vat

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_method back 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_price
  • subtotal
  • line_total
  • line_total_without_vat
  • line_vat
  • item_total
  • item_subtotal
  • discount_pct
  • discount_amount
  • item_total_before_discount
  • extras (stringified map)

Invoice-level

  • products_count
  • items (rebuilt array)
  • items_count
  • gross_total
  • service_percentage
  • service_amount
  • delivery_fee
  • packaging_fee
  • items_subtotal
  • items_vat_amount
  • items_total
  • items_discount_amount
  • payable
  • net_total
  • subtotal
  • vat
  • optionally payment fields:
    • paid
    • tip_amount
    • change

Important Notes and Edge Cases

  • The function mutates the original invoice object and item objects in-place.
  • Missing numeric fields usually collapse to 0 via safeNumber, but this can hide bad input.
  • items_count comment 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.
  • getConnectedExtras and getConnectedAddOns exist but are currently unused inside refactorInvoice.

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.