Skip to content

Unified Addons System

> **Version:** 2.1 | **Last Updated:** 2026-04-09 | **Module Owner:** Operations Portal

Version: 2.1 | Last Updated: 2026-04-09 | Module Owner: Operations Portal


  1. Overview
  2. Architecture
  3. How Addons Work Across Pages
  4. Addon Types & Value Types
  5. Trigger Modes
  6. Tax System
  7. Multi-Region Architecture
  8. External Resolver (Dynamic Pricing SPI)
  9. Waterfall Pricing Engine
  10. Conditions & Scoping
  11. Public Quotation Widget — Form Target Architecture
  12. Fuel Levy — Quotation Widgets & Forms
  13. GST Defaults & Tax Configuration
  14. Connote & Quote Model Integration
  15. Developer Reference

The Unified Addons System is a flexible, rule-driven pricing engine that replaces the legacy hardcoded surcharges model. It provides a single abstraction for surcharges, discounts, taxes, and document additions across all forms in the iDrv5 platform.

Before this system, surcharges like fuel levies, tailgate fees, and GST were scattered across different tables and calculated with bespoke logic in each page. The Unified Addons System consolidates all of these into one table (addons) with a standardised calculation pipeline (the “waterfall engine”).

  • One record, many forms — A single addon can target Bookings, Quotations, Rate Cards, Invoices, and more via the many-to-many addon_form_targets table.
  • Three trigger modes — Mandatory (always applied), Automatic (fired by a UI toggle), Manual (user picks from a list).
  • Tax-aware two-pass engine — Non-tax addons run first; tax addons run last on the taxable subtotal.
  • Multi-region tenant lock — Each deployment is locked to one tax jurisdiction (AU, US, DXB, PH) via TENANT_REGION.
  • Dynamic pricing via External Resolvers — Plug any third-party API (tolls, insurance, carbon offsets) without code changes.
  • Customer & Rate Card overrides — Per-customer or per-rate-card value overrides without duplicating addon records.

+---------------------------------------------------------------------+
| iDrv5 Application |
| |
| +------------------+ +------------------+ +--------------+ |
| | Operations Portal| | Customer Portal | | Admin Portal | |
| | | | | | | |
| | additional_ | | book_shipment | | (future) | |
| | services.html | | .html | | | |
| | create_booking | | | | | |
| | .html | | | | | |
| | rate_card_ | | | | | |
| | details.html | | | | | |
| +--------+---------+ +--------+---------+ +------+-------+ |
| | | | |
| +------------+------------+----------------------+ |
| | |
| v |
| +-------------------------------------------------------------+ |
| | REST API Layer | |
| | /api/addons/* endpoints | |
| | (addons_api.py) | |
| +----------------------------+--------------------------------+ |
| | |
| v |
| +-------------------------------------------------------------+ |
| | Controller / Business Logic | |
| | (addons_controller.py) | |
| | | |
| | get_addons_for_context() calculate_addons_batch() | |
| | create_addon() _call_external_resolver() | |
| | update_addon() _get_quantity_for_unit_type() | |
| +----------------------------+--------------------------------+ |
| | |
| v |
| +-------------------------------------------------------------+ |
| | ORM Models | |
| | (models/addon.py) | |
| | | |
| | Addon AddonType ValueType FormTarget AddonCondition | |
| | AddonCustomer AddonRateCard AddonFormTarget | |
| +-------------------------------------------------------------+ |
| | |
+-------------------------------|--------------------------------------+
v
+---------------------+
| PostgreSQL (Supabase) |
| Database: postgres |
| |
| addons |
| addon_types |
| value_types |
| form_targets |
| addon_conditions |
| addon_customers |
| addon_rate_cards |
| addon_form_targets |
| tax_profiles |
+---------------------+
LayerFilePurpose
Modelmodels/addon.pySQLAlchemy ORM models for all addon tables
Modelmodels/tax_profile.pyTaxProfile model for multi-region support
Controllercontrollers/addons_controller.pyAll business logic, waterfall engine, resolver
APIapi/addons_api.pyREST endpoints (/api/addons/*)
Configutils/config.pytenant_region, tenant_currency, tax_engine_type
Frontendtemplates/portals/operations/additional_services.htmlAddon management UI (CRUD)
Frontendtemplates/portals/operations/create_booking.htmlBooking page addon integration
Frontendtemplates/portals/operations/simple_quote_form.htmlAdmin quote form with addon pricing cards
Frontendtemplates/portals/operations/rate_card_details.htmlRate card surcharges tab
Frontendtemplates/shared/tabbed_quote_form.htmlCustomer-facing 3-step quotation widget
Rate Engineapi/rate_entries_api.pycompute-rate endpoint (merges addon results from multiple form targets)
Modelmodels/connotes.pyConnote/booking model with fuel_levy_percentage, gst_rate, gst_amount fields
Modelmodels/unified_quote.pyUnified quote model with fuel_levy_amount field
Modelmodels/quote_template.pyQuote template model with add_fuel_levy boolean flag
Migrationmigrations/create_addon_schema.sqlCore schema (tables, seed data)
Migrationmigrations/add_addon_pricing_columns.sqlTrigger/pricing columns + system addons
Migrationmigrations/add_addon_external_resolver.sqlResolver columns + value type
Migrationmigrations/add_region_profiles.sqltax_profiles table + region migration
Browser URL: /operations/rate/addons
Blueprint: operations_portal (portals/operations/routes.py)
Route: @operations_portal.route('/rate/addons')
Template: portals/operations/additional_services.html
Sidebar: Operations Portal -> Rates -> Addons
Auth: @login_required (all routes)

3.1 Additional Services Page (Addon Management)

Section titled “3.1 Additional Services Page (Addon Management)”

URL: /operations/rate/addons

This is the admin CRUD page where operators create, edit, and manage addon definitions. It does not calculate prices — it defines the rules that other pages consume.

Page Load
|
+-> loadReferenceData() GET /api/addons/reference-data
| |
| +-> populateReferenceDropdowns() (fills all selects/filters)
|
+-> loadAddons() GET /api/addons/?filters
|
+-> renderAddons() (renders addon cards with badges)

User Actions:

ActionFunctionAPI Call
CreateopenCreateModal() -> saveAddon()POST /api/addons/
EditopenEditModal(id) -> saveAddon()PUT /api/addons/{id}
DeletedeleteAddon(id)DELETE /api/addons/{id}
Toggle ActivetoggleAddonStatus(btn)PUT /api/addons/{id}
Manage ConditionsopenConditionsModal(id)GET/POST/DELETE /api/addons/{id}/conditions
Assign CustomersopenCustomersModal(id)POST /api/addons/{id}/customers
Assign Rate CardsopenRateCardsModal(id)POST /api/addons/{id}/rate-cards

URL: /operations/booking/create

The booking page is where addons are applied and calculated against a real shipment. Three global variables drive the addon state:

window._addonCache = {}; // Keyed by ui_binding (e.g. 'pickup_tailgate')
window._addonCacheAll = []; // All applicable addons for this booking context
window._activeAddonItems = {}; // Keyed by addon_id -- currently applied line items

Integration Flow:

Booking Page Load
|
+-> loadBookingAddons(customerId, rateCardId)
| |
| +-> GET /api/addons/for-context?form_target=Booking&customer_id=X&rate_card_id=Y
| |
| +-> Populate _addonCache (keyed by ui_binding for automatic addons)
| +-> Populate _addonCacheAll (all addons for this context)
| +-> Auto-add FUEL_LEVY as mandatory line item
|
+-> [User clicks handling option button, e.g. "Tailgate"]
| |
| +-> toggleHandlingOption(button)
| |
| +-> addAddonByBinding('pickup_tailgate')
| | |
| | +-> Look up addon in _addonCache['pickup_tailgate']
| | +-> Create line item in _activeAddonItems[addonId]
| | +-> renderAddonLineItem(item) -> append to #surcharges-list
| |
| +-> updateTotalCost() (waterfall recalculation)
|
+-> [User clicks "Add Surcharge / Addon" button]
| |
| +-> populateSurchargesModal() (shows manual addons as checkboxes)
| +-> [User checks desired addons]
| +-> applySelectedAddons()
| |
| +-> Add/remove line items in _activeAddonItems
| +-> renderAddonLineItem() for each new addon
| +-> updateTotalCost() (waterfall recalculation)
|
+-> [Booking Submission]
|
+-> Collects addon_line_items from _activeAddonItems
+-> Includes in booking payload

Handling Option Buttons:

When a user clicks a handling toggle (e.g. “Tailgate Pickup”), the button’s data-name attribute maps to a ui_binding value on an addon. If an addon with trigger_mode=automatic and ui_binding=pickup_tailgate exists, it is automatically added/removed as the button toggles.

URL: /operations/rate/rate-cards/{id}

The Surcharges tab shows which addons are associated with the current rate card.

Surcharges Tab Selected
|
+-> loadSurchargesForRateCard()
|
+-> GET /api/addons/for-context?form_target=Rate Card&rate_card_id={id}
+-> Render table: Name | Type | Trigger | Default Value | Scope | Order | Status

Rate card details also have an inline surcharges grid where addons can be imported directly into rate entry rows.

URL: /operations/quotes/create Template: templates/portals/operations/simple_quote_form.html

The admin quote form is a two-panel layout that integrates addons directly into the pricing comparison cards. Unlike the booking page where addons are managed as separate line items, here they are calculated server-side as part of the compute-rate response.

Integration Flow:

Admin Quote Form Load
|
+-> User fills: customer, origin, destination, cargo items, job type
|
+-> [User clicks "Get Rates" or form auto-computes]
| |
| +-> For each service level (Express, Standard, Economy):
| POST /api/rate-entries/compute-rate
| {
| customer_id, pickup_suburb, delivery_suburb,
| items: [...], service_level_id,
| ui_context: { pickup_tailgate: true/false, ... },
| selected_addon_ids: []
| }
|
+-> renderPricing(results)
| |
| +-> For each service level response:
| Parse comp.addons.addons[] array
| Separate into three buckets:
| - fuelLevy (regex /fuel/i on addon name)
| - gstAmount (is_tax_addon === true)
| - addonSurcharges (everything else)
| Grand total = initialCost + finalTotal + fuelLevy + addonSurcharges + gstAmount
|
+-> Display pricing cards with breakdown:
Base: $300.00 | Items: $50.00 | Fuel: $67.50 | Surcharges: $25.00 | GST: $44.25

Pricing Card Rendering (lines 1022-1061):

// Separate fuel, GST (tax), and other surcharges from addon results
let fuelLevy = 0, gstAmount = 0, addonSurcharges = 0;
if (comp.addons && comp.addons.addons) {
comp.addons.addons.forEach(a => {
const amount = parseFloat(a.amount || 0);
if (a.is_tax_addon) gstAmount += amount; // Tax addons -> GST line
else if (/fuel/i.test(a.name)) fuelLevy += amount; // Fuel -> separate line
else addonSurcharges += amount; // Others -> surcharges line
});
}
const grandTotal = initialCost + finalTotal + fuelLevy + addonSurcharges + gstAmount;
// Breakdown display lines
if (fuelLevy > 0) breakdownParts.push(`Fuel: $${fuelLevy.toFixed(2)}`);
if (addonSurcharges > 0) breakdownParts.push(`Surcharges: $${addonSurcharges.toFixed(2)}`);
if (gstAmount > 0) breakdownParts.push(`GST: $${gstAmount.toFixed(2)}`);

Quote Submission Payload (lines 1132-1164):

When saving a quote (draft or send), the fuel, surcharges, and GST are stored separately:

const payload = {
base_rate: primaryRate.base,
fuel_surcharge: primaryRate.fuel, // Fuel levy amount (separated)
additional_charges: primaryRate.surcharges, // Other surcharges total
gst_amount: primaryRate.gst, // GST amount
total_amount: primaryRate.total, // Grand total (all inclusive)
// Multi-service quotes stored as JSON in special_requirements
special_requirements: JSON.stringify(serviceLevelQuotes)
};
// POST to /api/quotes/

Template Save Payload (lines 1227-1247):

When saving as a reusable quote template:

{
base_price: primaryRate.base,
fuel_levy_amount: primaryRate.fuel, // Fuel levy preserved in template
total_price: primaryRate.total
}

External repo: Stencil.js web component embedded on customer-facing websites (e.g. Jatt Logistics).

The widget has four distinct addon sections, each backed by its own form_target:

Widget Sectionform_targetAddon TriggerWhat’s Assigned
Price totals (invisible, always computed)widget_subtotalmandatoryFuel Levy, GST
Pickup row (checkboxes)quotation_widget_pickupautomaticResidential Pickup, Tail Lift (Pickup)
Delivery row (checkboxes)quotation_widget_deliveryautomaticResidential Delivery, Tail Lift (Delivery)
“Additional Services” panelquotation_widget_servicesmanualDG, Manual Handling, Time Slot, Hand Unload

Widget UI Layout:

┌──────────────────────────────────────────────────┐
│ Freight Quotation Tool │
│ │
│ Weight: [____] kg │
│ │
│ ┌─ quotation_widget_pickup ───────────────────┐ │
│ │ ☐ Residential Pickup ☐ Tail Lift Required│ │
│ └─────────────────────────────────────────────┘ │
│ ┌─ quotation_widget_delivery ─────────────────┐ │
│ │ ☐ Residential Delivery ☐ Tail Lift Required│ │
│ └─────────────────────────────────────────────┘ │
│ │
│ FREIGHT ITEMS │
│ [Packaging Type] [L] [W] [H] [Wt] │
│ │
│ ┌─ quotation_widget_services ─────────────────┐ │
│ │ ADDITIONAL SERVICES [+ ADD MORE] │ │
│ │ ☐ Dangerous Goods ☐ Manual Handling ... │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ ┌─ widget_subtotal (computed, not visible) ───┐ │
│ │ Subtotal: $300.00 │ │
│ │ Fuel Levy: $67.50 (mandatory, 22.5%) │ │
│ │ GST: $36.75 (mandatory, 10%) │ │
│ │ Total: $404.25 │ │
│ └─────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────┘

Fetching addons — four parallel calls:

const [totals, pickup, delivery, services] = await Promise.all([
fetchAddonsForContext('widget_subtotal'),
fetchAddonsForContext('quotation_widget_pickup'),
fetchAddonsForContext('quotation_widget_delivery'),
fetchAddonsForContext('quotation_widget_services'),
]);
// totals.data → mandatory (Fuel Levy, GST) -- no UI toggles
// pickup.data → automatic -- render checkboxes in pickup row
// delivery.data → automatic -- render checkboxes in delivery row
// services.data → manual -- render in "Additional Services" panel

Price calculation — merge from all four targets:

// 1. Build ui_context from pickup/delivery checkboxes
const uiContext = {
pickup_tailgate: pickupTailLiftChecked,
pickup_residential: pickupResidentialChecked,
delivery_tailgate: deliveryTailLiftChecked,
delivery_residential: deliveryResidentialChecked,
};
const selectedAddonIds = [/* manually checked addon IDs from services panel */];
// 2. Primary call: widget_subtotal (gets Fuel Levy + GST)
let result = await calculateBatch({
form_target: 'widget_subtotal',
base_rate: computedBaseRate,
ui_context: uiContext,
selected_addon_ids: selectedAddonIds,
quantity_context: { chargeable_weight: totalWeight }
});
// 3. Merge pickup, delivery, services (de-duplicate by addon_id)
for (const target of ['quotation_widget_pickup','quotation_widget_delivery','quotation_widget_services']) {
const extra = await calculateBatch({ form_target: target, base_rate: computedBaseRate, ui_context, selected_addon_ids, quantity_context });
extra.data.addons.forEach(a => {
if (!result.data.addons.find(r => r.addon_id === a.addon_id)) {
result.data.addons.push(a);
result.data.grand_total += a.amount;
}
});
}

Note: The /api/addons/for-context and /api/addons/calculate-batch endpoints currently require @login_required. For the public widget, either create public wrapper routes or use token-based auth.

3.6 compute-rate Multi-Form-Target Merging

Section titled “3.6 compute-rate Multi-Form-Target Merging”

IMPORTANT: The compute-rate endpoint in api/rate_entries_api.py (lines 2170-2211) runs the waterfall engine across three form targets and merges the results:

# 1. Primary: Booking form target (fuel, GST, DG, etc.)
addons_result = calculate_addons_batch(form_target='Booking', **addon_kwargs)
# 2. Merge: admin_quotation_pickup and admin_quotation_delivery targets
seen_ids = {a['addon_id'] for a in addons_result.get('addons', [])}
for qt in ['admin_quotation_pickup', 'admin_quotation_delivery']:
qt_result = calculate_addons_batch(form_target=qt, **addon_kwargs)
for a in qt_result.get('addons', []):
if a.get('addon_id') not in seen_ids:
addons_result['addons'].append(a)
seen_ids.add(a.get('addon_id'))

This means:

  • Booking form target: Captures system-level addons (Fuel Levy, GST)
  • admin_quotation_pickup: Captures pickup-specific handling addons (Pickup Tailgate, Residential Pickup)
  • admin_quotation_delivery: Captures delivery-specific handling addons (Delivery Tailgate, Residential Delivery)

Addons are de-duplicated by addon_id so an addon assigned to both Booking and admin_quotation_pickup only appears once.


TypeDescriptionExample
surchargeAdditional charge on top of base rateFuel Levy, Tailgate Fee
discountPrice reductionLoyalty Discount, Volume Discount
taxTax calculation (runs last in waterfall)GST 10%, VAT 5%
document_additionDocument/information requirementDangerous Goods Declaration
Value TypeCalculationExample
fixed_amountStatic dollar amount$20.00 flat fee
percentagePercentage of applies_on base20% of subtotal
per_unitRate multiplied by quantity$1.50 per km
external_resolverFetched from external APIDynamic toll cost
text_inputFree text (no calculation)Special instructions

Trigger modes control when an addon is applied to a booking/quote.

+------------------------------------------------------------------+
| TRIGGER MODE DECISION |
| |
| +------------------+ |
| | MANDATORY | Always applied. User cannot remove. |
| | | Example: Fuel Levy |
| +------------------+ |
| | |
| | (no user action needed) |
| v |
| +------------------+ |
| | AUTOMATIC | Applied when a UI element is toggled. |
| | | Tied to ui_binding field. |
| | | Example: Tailgate Fee when "Tailgate" |
| | | button is clicked |
| +------------------+ |
| | |
| | (user clicks handling option button) |
| v |
| +------------------+ |
| | MANUAL | User selects from addon modal. |
| | | Example: Insurance, Premium Packaging |
| +------------------+ |
| | |
| | (user opens modal, checks addon, clicks Apply) |
| v |
| Addon added to _activeAddonItems |
+------------------------------------------------------------------+

For automatic addons, the ui_binding field maps to a handling option button’s data-name attribute:

ui_bindingButton LabelWhen Triggered
pickup_tailgateTailgate (Pickup)User clicks pickup tailgate button
delivery_tailgateTailgate (Delivery)User clicks delivery tailgate button
pickup_residentialResidential (Pickup)User clicks residential pickup button
delivery_residentialResidential (Delivery)User clicks residential delivery button
pickup_manual_handlingManual Handling (Pickup)User clicks manual handling button
delivery_manual_handlingManual Handling (Delivery)User clicks manual handling button

Every addon has a tax_category that determines how it participates in the tax calculation:

Tax CategoryMeaningIncluded in Tax Base?Example
standardStandard taxable itemYesFuel Levy, Tailgate Fee
gst_freeGST-Free / Tax ExemptNoInternational freight
zero_ratedZero-rated supplyNoExports
input_taxedInput taxed (rare)NoFinancial services

The is_taxable boolean is derived from tax_category:

is_taxable = (tax_category == 'standard')

When an addon’s type is tax, it behaves differently:

  1. Runs last in the waterfall (forced calculation_order >= 900)
  2. Calculates on the taxable running total, not the full running total
  3. Has special fields:
    • tax_code — Display label (e.g. “GST”, “VAT 12%”)
    • tax_inclusive — If true, the amount is already included in the price (no addition to total)
    • applies_on — For tax addons, this becomes “Tax on Surcharges?” (running_total = tax on everything, base_rate = tax on base only)
PASS 1: Non-Tax Addons (sorted by calculation_order ascending)
+------------------------------------------------------------------+
| For each non-tax addon: |
| |
| 1. Calculate amount (fixed, %, per-unit, or resolver) |
| 2. Apply min/max guardrails |
| 3. running_total += amount |
| 4. If is_taxable: taxable_running_total += amount |
| 5. If NOT is_taxable: non_taxable_total += amount |
+------------------------------------------------------------------+
|
v
PASS 2: Tax Addons (sorted by calculation_order ascending)
+------------------------------------------------------------------+
| For each tax addon: |
| |
| 1. applied_on_amount = taxable_running_total |
| 2. Calculate: taxable_running_total * (rate / 100) |
| 3. If tax_inclusive: do NOT add to running_total |
| 4. If tax_exclusive: running_total += tax_amount |
+------------------------------------------------------------------+
|
v
grand_total = running_total

When tax addons are present, the booking page shows a grouped breakdown:

Taxable Subtotal $1,250.00
----------------------------------------
GST (10%) $125.00
----------------------------------------
Estimated Total (inc. tax) $1,375.00

Each iDrv5 deployment is locked to one tax jurisdiction via three environment variables:

.env
TENANT_REGION=AU # AU, US, DXB, PH, or GLOBAL
TENANT_CURRENCY=AUD # AUD, USD, AED, PHP
TAX_ENGINE_TYPE=INTERNAL # INTERNAL (fixed-rate) or EXTERNAL (API-based)

These values are read via utils/config.py and exposed as properties on the global config singleton.

When addons are fetched for a context (booking, quote, etc.), the controller filters by region:

get_addons_for_context()
|
+-> Load TENANT_REGION from config (e.g. 'AU')
|
+-> For each addon:
|
+-> addon.region == 'GLOBAL'? --> INCLUDE (global addons apply everywhere)
+-> addon.region == 'AU'? --> INCLUDE (matches tenant)
+-> addon.region == 'US'? --> SKIP (wrong region for AU tenant)
+-> addon.region == 'DXB'? --> SKIP (wrong region for AU tenant)

This guarantees a Dubai tenant never sees Australian GST addons, and vice versa.

The tax_profiles table is the authoritative registry of supported regions:

CodeRegionTaxRateCurrencyEngine
AUAustraliaGST10%AUDINTERNAL
USUnited StatesSales TaxVariableUSDEXTERNAL
DXBDubai (UAE)VAT5%AEDINTERNAL
PHPhilippinesVAT12%PHPINTERNAL
GLOBALGlobal (Custom)Custom0%USDINTERNAL

To add a new region (e.g. New Zealand):

INSERT INTO tax_profiles (region_code, region_name, tax_name, standard_rate, currency_code, tax_engine_type, compliance_notes)
VALUES ('NZ', 'New Zealand', 'GST', 0.1500, 'NZD', 'INTERNAL', 'Requires GST number on tax invoices');

Then set in .env:

Terminal window
TENANT_REGION=NZ
TENANT_CURRENCY=NZD
TAX_ENGINE_TYPE=INTERNAL

No code changes required. The region dropdown in the UI auto-populates from tax_profiles.


8. External Resolver (Dynamic Pricing SPI)

Section titled “8. External Resolver (Dynamic Pricing SPI)”

Some addon costs cannot be determined by static math. Toll fees depend on the route, insurance depends on cargo value, carbon offsets depend on distance. The External Resolver pattern lets you plug any API into the pricing engine by setting value_type = 'external_resolver' on an addon.

Waterfall Engine encounters addon with value_type = 'external_resolver'
|
+-> _call_external_resolver(addon_dict, context, timeout=2.0)
|
+-> Build cache key: hash(addon_id + origin + destination + vehicle_type)
|
+-> Cache hit and fresh (<60s)?
| |
| +-> YES: Return cached amount
| +-> NO: Continue to API call
|
+-> Build standardised JSON payload:
| {
| "context": "pricing_calculation",
| "addon_id": "toll_fee",
| "shipment_data": {
| "origin": {"lat": -33.86, "lng": 151.20},
| "destination": {"lat": -33.89, "lng": 151.27},
| "weight_kg": 500,
| "vehicle_type": "rigid_truck",
| "route_distance_km": 12.5
| },
| "customer_id": 42
| }
|
+-> POST to resolver_url with:
| Headers: Content-Type: application/json
| X-Resolver-Secret: <resolver_secret>
| Timeout: 2.0 seconds
|
+-> Parse response:
| {
| "status": "success",
| "cost": 18.50,
| "currency": "AUD",
| "taxable": true,
| "meta_data": {"toll_gates": 3}
| }
|
+-> On success: Cache result, return cost (18.50)
+-> On failure/timeout: Return fallback_value
FeatureImplementation
2-second timeoutrequests.post(..., timeout=2.0) prevents slow APIs from blocking
60-second cacheIn-memory dict cache with TTL, keyed by route hash
Fallback valuefallback_value column used on any error/timeout
Secret maskingresolver_secret returned as *** in API responses, never pre-filled in edit form
Test endpointPOST /api/addons/{id}/test-resolver with sample payload for development

Your external resolver must:

  1. Accept POST requests with Content-Type: application/json
  2. Validate the X-Resolver-Secret header
  3. Return JSON with at minimum: { "status": "success", "cost": <number> }
  4. Respond within 2 seconds

INPUT:
base_rate = $800 (from rate entry)
flat_rate = $50 (flat pickup fee)
INITIALISE:
subtotal = base_rate + flat_rate = $850
running_total = $850
taxable_running_total = $850
non_taxable_total = $0
PASS 1: NON-TAX ADDONS (by calculation_order ASC)
+-----------+----------+---------+--------+--------+----------+---------+
| Addon | Order | Type | Value | Scope | Taxable? | Amount |
+-----------+----------+---------+--------+--------+----------+---------+
| Tailgate | 50 | fixed | $20 | booking| yes | $20.00 |
| Fuel Levy | 100 | percent | 20% | subtotl| yes | $170.00 |
| Insurance | 200 | resolver| API | booking| no | $45.00 |
+-----------+----------+---------+--------+--------+----------+---------+
After Pass 1:
running_total = $850 + $20 + $170 + $45 = $1,085
taxable_running_total = $850 + $20 + $170 = $1,040
non_taxable_total = $45
PASS 2: TAX ADDONS (by calculation_order ASC)
+-----------+----------+---------+--------+----------+---------+
| Addon | Order | Type | Rate | Base | Amount |
+-----------+----------+---------+--------+----------+---------+
| GST | 900 | percent | 10% | $1,040 | $104.00 |
+-----------+----------+---------+--------+----------+---------+
After Pass 2:
running_total = $1,085 + $104 = $1,189
OUTPUT:
subtotal = $850.00
addon_total = $339.00
taxable_subtotal = $1,040.00
non_taxable_total = $45.00
grand_total = $1,189.00

When calculating the raw value for an addon:

1. customer_override_value (from addon_customers.override_value)
|
+-> NULL? Fall through
|
2. rate_card_override_value (from addon_rate_cards.override_value)
|
+-> NULL? Fall through
|
3. default_value (from addons.default_value)
ScopeBehaviourExample
per_bookingFlat amount per shipment$20 tailgate fee
per_unitRate x Quantity$1.50/km x 250km = $375

Unit Types for per_unit scope:

unit_typeContext KeyExample
palletload_count5 pallets
kgchargeable_weight or actual_weight1200 kg
kmdistance250 km
cubic_metercubic_meters8.5 m3
itemitem_count or load_count12 items

For per-unit addons, guardrails prevent extreme values:

if amount < minimum_charge:
amount = minimum_charge # Floor
if amount > maximum_charge:
amount = maximum_charge # Cap

Exception: Tax addons with tax_inclusive = true skip guardrails.


Addons can have conditions that control when they appear. All conditions use AND logic (all must pass).

Condition TypeOperatorsExample
job_typeequals, not_equals, inShow only for “FTL” jobs
service_level_idequals, inShow only for Express service
weightgreater_than, less_than, betweenShow if weight > 500kg
distancegreater_than, less_than, betweenShow if distance > 100km
customer_groupequals, inShow for VIP customers

Operators:

OperatorDescriptionValue Format
equalsExact matchSingle value
not_equalsNot equalSingle value
inValue in listJSON array
greater_thanNumeric >Single number
less_thanNumeric <Single number
betweenRange inclusiveTwo values [min, max]

Addons can be scoped to specific customers or rate cards:

Addon has apply_to_all_customers = false
|
+-> Only customers in addon_customers table get this addon
+-> Each assignment can have an override_value
Addon has apply_to_all_rate_cards = false
|
+-> Only rate cards in addon_rate_cards table get this addon
+-> Each assignment can have an override_value

The calculation_order field controls the sequence in the waterfall:

RangeCategoryExamples
1-49System (first)Base adjustments
50-99HandlingTailgate, Residential
100-199GeneralFuel Levy, Distance surcharge
200-499Service LevelExpress premium
500-899CustomUser-defined
900+Tax (always last)GST, VAT

The applies_on field determines what base amount a percentage addon uses:

ValueBase UsedWhen to Use
base_rateOriginal base rate onlyFuel levy on base only
subtotalbase_rate + flat_rateMost surcharges
running_totalCumulative total so farCompound calculations

For tax addons, applies_on is interpreted as “Tax on Surcharges?”:

  • running_total = Tax on base + all taxable surcharges
  • base_rate = Tax on base rate only

11. Public Quotation Widget — Form Target Architecture

Section titled “11. Public Quotation Widget — Form Target Architecture”

The public quotation widget (Stencil.js, separate repo) uses four dedicated form targets to separate addon concerns by widget section:

form_targetWidget SectionTrigger ModePurpose
widget_subtotalPrice totals (computed, not a visible toggle section)mandatoryFuel Levy (% of subtotal), GST (% of grand total)
quotation_widget_pickupPickup row checkboxesautomaticResidential Pickup, Tail Lift Pickup
quotation_widget_deliveryDelivery row checkboxesautomaticResidential Delivery, Tail Lift Delivery
quotation_widget_services”Additional Services” panelmanualDG, Manual Handling, Time Slot, Hand Unload

There are also related targets for other contexts:

form_targetContextPurpose
quotation_widget_adminAdmin-only quotation widget addonsInternal-only surcharges
customer_quotationCustomer portal quotation request formCustomer portal (not the public widget)
bookingBooking form system addonsFuel Levy, GST on booking page
create_booking_pickupBooking form pickup sectionBooking-specific pickup handling
create_booking_deliveryBooking form delivery sectionBooking-specific delivery handling
admin_quotation_pickupAdmin quotation pickup sectionAdmin quote pickup handling
admin_quotation_deliveryAdmin quotation delivery sectionAdmin quote delivery handling

11.2 Complete form_targets Table (Current State)

Section titled “11.2 Complete form_targets Table (Current State)”
id | name | description
---+-----------------------------+------------------------------------------------------------
1 | booking | Applies to booking forms
2 | rate_card | Applies to rate card configurations
3 | invoice | Applies to invoice generation
4 | quotation_widget_services | Public quotation widget addon services
5 | quotation_widget_admin | Applies to internal quotation forms
6 | create_booking_pickup | Create Booking form — pickup section handling options
7 | create_booking_delivery | Create Booking form — delivery section handling options
8 | quotation_widget_pickup | Public quotation widget — pickup section services
9 | quotation_widget_delivery | Public quotation widget — delivery section services
10 | customer_quotation | Customer portal quotation request form
11 | admin_quotation_pickup | Admin quotation — pickup section handling options
12 | admin_quotation_delivery | Admin quotation — delivery section handling options
21 | widget_subtotal | Public quotation widget — subtotal/total

11.3 Addon-to-Form-Target Assignments for the Widget

Section titled “11.3 Addon-to-Form-Target Assignments for the Widget”

widget_subtotal — Mandatory pricing addons (no UI toggles):

AddonTypeValueApplies OnOrder
Fuel Surchargepercentage22.5%subtotal10
GSTpercentage10%running_total (taxable)900

quotation_widget_pickup — Automatic pickup toggles:

Addonui_bindingTypeValue
Pickup Residential Addresspickup_residentialfixed$0+
Pickup Tailgate Feepickup_tailgatefixed$0+

quotation_widget_delivery — Automatic delivery toggles:

Addonui_bindingTypeValue
Delivery Residential Addressdelivery_residentialfixed$0+
Delivery Tailgate Feedelivery_tailgatefixed$0+

quotation_widget_services — Manual “Additional Services”:

Addonui_bindingTypeValue
Dangerous Goods Handlingdangerous_goodsfixed$0+
Pickup Manual Handlingpickup_manual_handlingfixed$0+
Delivery Manual Handlingdelivery_manual_handlingfixed$0+
Pickup Time Slot Requiredpickup_time_slotfixed$0+
Delivery Time Slot Requireddelivery_time_slotfixed$0+
Pickup Hand Unloadpickup_hand_unloadfixed$0+
Delivery Hand Unloaddelivery_hand_unloadfixed$0+

11.4 SQL to Assign Addons to Widget Form Targets

Section titled “11.4 SQL to Assign Addons to Widget Form Targets”
-- widget_subtotal: Fuel Levy + GST
INSERT INTO addon_form_targets (addon_id, form_target_id)
SELECT a.id, ft.id FROM addons a CROSS JOIN form_targets ft
WHERE ft.name = 'widget_subtotal'
AND (a.name ILIKE '%fuel%surcharge%' OR a.name ILIKE '%fuel%levy%'
OR a.name ILIKE '%gst%')
AND a.is_active = true
ON CONFLICT DO NOTHING;
-- quotation_widget_pickup: Pickup handling
INSERT INTO addon_form_targets (addon_id, form_target_id)
SELECT a.id, ft.id FROM addons a CROSS JOIN form_targets ft
WHERE ft.name = 'quotation_widget_pickup'
AND a.ui_binding IN ('pickup_tailgate', 'pickup_residential')
ON CONFLICT DO NOTHING;
-- quotation_widget_delivery: Delivery handling
INSERT INTO addon_form_targets (addon_id, form_target_id)
SELECT a.id, ft.id FROM addons a CROSS JOIN form_targets ft
WHERE ft.name = 'quotation_widget_delivery'
AND a.ui_binding IN ('delivery_tailgate', 'delivery_residential')
ON CONFLICT DO NOTHING;
-- quotation_widget_services: Manual additional services
INSERT INTO addon_form_targets (addon_id, form_target_id)
SELECT a.id, ft.id FROM addons a CROSS JOIN form_targets ft
WHERE ft.name = 'quotation_widget_services'
AND a.ui_binding IN ('dangerous_goods','pickup_manual_handling','delivery_manual_handling',
'pickup_time_slot','delivery_time_slot',
'pickup_hand_unload','delivery_hand_unload')
ON CONFLICT DO NOTHING;
SELECT ft.name AS form_target, a.name, a.label, a.ui_binding,
a.trigger_mode, a.default_value, at.name AS addon_type
FROM addon_form_targets aft
JOIN addons a ON a.id = aft.addon_id
JOIN form_targets ft ON ft.id = aft.form_target_id
JOIN addon_types at ON at.id = a.addon_type_id
WHERE ft.name IN ('widget_subtotal','quotation_widget_pickup',
'quotation_widget_delivery','quotation_widget_services')
ORDER BY ft.name, a.calculation_order;

11.6 Widget Rendering Rules by form_target

Section titled “11.6 Widget Rendering Rules by form_target”
widget_subtotal:
→ DO NOT render any UI toggles or checkboxes
→ These addons (Fuel Levy, GST) are mandatory and always calculated
→ Show the amounts in the price breakdown section only
quotation_widget_pickup:
→ Render as checkboxes/toggles in the PICKUP row of the form
→ Map each addon's ui_binding to a checkbox:
pickup_residential → "Residential Pickup" checkbox
pickup_tailgate → "Tail Lift Required" checkbox
→ When toggled: set uiContext[ui_binding] = true/false and recalculate
quotation_widget_delivery:
→ Render as checkboxes/toggles in the DELIVERY row of the form
→ Map each addon's ui_binding to a checkbox:
delivery_residential → "Residential Delivery" checkbox
delivery_tailgate → "Tail Lift Required" checkbox
→ When toggled: set uiContext[ui_binding] = true/false and recalculate
quotation_widget_services:
→ Render in the "Additional Services" panel
→ Show as a selectable list with "+ ADD MORE" button
→ When checked: add addon.id to selectedAddonIds[] and recalculate
→ When unchecked: remove addon.id from selectedAddonIds[] and recalculate

The /api/addons/for-context and /api/addons/calculate-batch endpoints currently require @login_required. For the public widget, create either:

  • A new public route without @login_required that wraps the same controller logic, or
  • A token/API-key-based auth middleware for widget API calls

12. Fuel Levy — Quotation Widgets & Forms

Section titled “12. Fuel Levy — Quotation Widgets & Forms”

Fuel levy is the most critical surcharge in freight. It is configured as an addon with these settings:

name: Fuel Surcharge
alias: FUEL_LEVY
addon_type: surcharge
trigger_mode: mandatory -- Always applied, user cannot remove
value_type: percentage
default_value: 20 -- 20% (or 22.5% per latest rate update)
calculation_order: 100 -- Runs after handling surcharges
category: general
applies_on: subtotal -- Percentage of (base_rate + flat_rate)
application_scope: per_booking
tax_category: standard -- GST applies ON TOP of fuel levy
region: australia

12.2 Fuel Levy Display on Quotation Widgets

Section titled “12.2 Fuel Levy Display on Quotation Widgets”

The admin quote form (simple_quote_form.html) separates fuel levy from other surcharges using a regex match on the addon name:

// Fuel levy detection: any addon whose name contains "fuel" (case-insensitive)
else if (/fuel/i.test(a.name)) fuelLevy += amount;

This means the addon must have “fuel” in its name to appear as a separate “Fuel” line item. If the addon is named “Diesel Surcharge” without “fuel”, it will appear under “Surcharges” instead.

Pricing card display:

┌─────────────────────────────────┐
│ STANDARD │
│ Base: $300.00 │
│ Items: $50.00 │
│ Fuel: $70.00 ◄─────── Fuel levy (separated by regex /fuel/i)
│ Surcharges: $25.00 ◄─────── All other non-tax, non-fuel addons
│ GST: $44.50 ◄─────── Tax addons (is_tax_addon === true)
│ │
│ Total: $489.50 │
└─────────────────────────────────┘
ModelFieldDefaultDescription
connotesfuel_levy_percentage20.00Stored percentage for the booking
connotesfuel_levy_amount0.00Calculated fuel dollar amount
unified_quotesfuel_levy_amount0.0Fuel amount on quote
quote_templatesadd_fuel_levyTrueBoolean flag to include/exclude fuel

When saving a quote from the admin form:

// Saved to unified_quotes via POST /api/quotes/
{
fuel_surcharge: primaryRate.fuel // e.g. 70.00
}

When saving as a template:

// Saved to quote_templates via POST /api/quote-templates/
{
fuel_levy_amount: primaryRate.fuel // Preserved in template for re-use
}

When pre-filling from a saved template:

// Fixed rate templates show fuel as a line item
const fuel = parseFloat(data.fuel_levy_amount || 0);
computedRates['fixed'] = { base, fuel, surcharges: 0, total };
// Display: "Base: $300.00 | Fuel: $70.00" with a lock icon indicating fixed pricing

From the portal: Navigate to Rates > Addons, edit the Fuel Surcharge addon, change default_value.

From the database:

UPDATE addons SET default_value = '22.5' WHERE alias = 'FUEL_LEVY';

Per-customer override:

INSERT INTO addon_customers (addon_id, customer_id, is_enabled, override_value)
VALUES (
(SELECT id FROM addons WHERE alias = 'FUEL_LEVY'),
123, true, '18.0' -- This customer pays 18% instead of 22.5%
);

GST defaults to 10% (Australian Goods and Services Tax). This is configured at multiple levels:

LevelLocationDefaultHow to Change
Tenant Settingstenant_settings.tax_rate0.1000 (10%)Admin Portal > Tenant Settings
Tax Profiletax_profiles.standard_rate WHERE region_code='AU'0.1000UPDATE tax_profiles SET standard_rate = 0.1000 WHERE region_code = 'AU'
GST Addonaddons.default_value WHERE alias='GST'’10’Portal > Rates > Addons > GST
Connote Defaultconnotes.gst_rate column default10.00Set per-booking, inherits from addon
Connote Defaultconnotes.gst_amount column default0.00Calculated from gst_rate

GST is implemented as a tax addon in the waterfall engine:

name: GST
addon_type: tax -- Runs in Pass 2 (always after surcharges)
trigger_mode: mandatory -- Always applied
value_type: percentage
default_value: 10 -- 10%
calculation_order: 900 -- Always last
category: tax
applies_on: running_total -- Tax on base + ALL taxable surcharges
tax_code: GST
tax_category: standard
tax_inclusive: false -- GST is added on top (not included in price)
region: australia
Base Rate: $300.00
+ Fuel Levy (20%): $60.00 (tax_category = standard → taxable)
+ Tailgate ($25): $25.00 (tax_category = standard → taxable)
────────────────────────────
taxable_running_total = $385.00
GST (10% of $385.00) = $38.50
Grand Total = $385.00 + $38.50 = $423.50

Key: GST applies to fuel levy and surcharges too, because they have tax_category = 'standard'. An addon with tax_category = 'gst_free' would NOT be included in the GST base.

Settingtax_inclusiveBehaviour
Tax-Exclusive (default)falseGST calculated and added to grand total
Tax-InclusivetrueGST calculated and shown but NOT added (already in price)
models/tenant_settings.py
tax_rate = Column(db.Numeric(5, 4), default=0.1000) # 10%
tax_name = Column(String(20), default='GST') # Display name
include_tax_in_quotes = Column(Boolean, default=True) # Show tax on quotes

Setting via Admin Portal: Admin > Tenant Settings > Tax Configuration

Setting via API:

PUT /admin/settings
{
"tax_rate": 0.10,
"tax_name": "GST",
"include_tax_in_quotes": true
}

In the admin quote form, GST is identified by is_tax_addon === true:

if (a.is_tax_addon) gstAmount += amount; // All tax addons → GST line

It displays as the last line in the pricing breakdown: GST: $38.50

The connote edit form (connote_edit.html) shows editable GST fields:

<label>GST Rate (%)</label>
<input type="number" id="gst_rate" value="{{ connote.gst_rate or 10 }}">
<label>GST Amount ($)</label>
<input type="number" id="gst_amount" value="{{ connote.gst_amount or 0 }}" readonly>

The quote PDF template (quote_pdf.html) renders:

<div class="field"><label>Fuel Surcharge</label><span>${{ '%.2f' | format(quote.fuel_surcharge | float) }}</span></div>
<div class="field"><label>GST</label><span>${{ '%.2f' | format(quote.gst_amount | float) }}</span></div>

The connotes table (models/connotes.py) stores final pricing with explicit fuel and GST columns:

class Connote(db.Model):
base_rate = Column(Numeric(10, 2), default=0.00)
fuel_levy_percentage = Column(Numeric(5, 2), default=20.00) # Default 20%
fuel_levy_amount = Column(Numeric(10, 2), default=0.00)
subtotal = Column(Numeric(10, 2), default=0.00)
gst_rate = Column(Numeric(5, 2), default=10.00) # Default 10%
gst_amount = Column(Numeric(10, 2), default=0.00)
total_amount = Column(Numeric(10, 2), default=0.00)

Individual charges are stored as line items with typed categories:

class ConnoteLineItem(db.Model):
__tablename__ = 'connote_line_items'
line_item_type = Column(String(30))
# Valid types: 'base_rate', 'fuel_levy', 'addon', 'surcharge', 'discount', 'tax', 'other'

Applied addons are tracked via junction table:

class ConnoteAddon(db.Model):
__tablename__ = 'connote_addons'
connote_id # FK to connotes
addon_id # FK to addons

Common surcharge triggers are also stored as boolean flags for quick filtering:

is_dangerous_goods = Column(Boolean, default=False)
is_tailgate_pickup = Column(Boolean, default=False)
is_tailgate_delivery = Column(Boolean, default=False)
is_express = Column(Boolean, default=False)
is_insurance_required = Column(Boolean, default=False)

The unified_quotes table (models/unified_quote.py) stores quote-specific addon amounts:

class UnifiedQuote(db.Model):
base_rate = Column(Float)
fuel_levy_amount = Column(Float, default=0.0) # Fuel levy dollar amount
dg_amount = Column(Float) # DG surcharge amount
tailgate_pickup_amount = Column(Float) # Pickup tailgate amount
tailgate_delivery_amount = Column(Float) # Delivery tailgate amount
total_cost = Column(Float) # Grand total

The quote_templates table (models/quote_template.py) includes addon-related flags:

class QuoteTemplate(db.Model):
add_fuel_levy = Column(Boolean, default=True) # Include fuel levy?
origin_tailgate = Column(Boolean, default=False) # Pickup tailgate?
dest_tailgate = Column(Boolean, default=False) # Delivery tailgate?

When a template with add_fuel_levy = True is loaded, the fuel levy is automatically included in the pricing calculation. Templates saved with fixed pricing also store the fuel_levy_amount for replay.


addons — Main addon definitions

ColumnTypeDefaultDescription
idSERIALPKAuto-increment primary key
nameVARCHAR(100)Unique internal name
labelVARCHAR(150)Display name shown to users
aliasVARCHAR(50)Unique URL-friendly short code
help_textTEXTTooltip/help text
descriptionTEXTFull description
is_activeBOOLEANtrueSoft-delete flag
addon_type_idFK -> addon_typessurcharge, discount, tax, document_addition
form_target_idFK -> form_targetsDEPRECATED — use addon_form_targets
value_type_idFK -> value_typesfixed_amount, percentage, per_unit, external_resolver
rate_calculation_method_idFK -> rate_calculation_methodsNULLFor per_unit: weight, distance, time, load
default_valueVARCHAR(255)Static value or percentage
display_orderINTEGER0UI display ordering
trigger_modeVARCHAR(20)‘manual’mandatory, automatic, manual
client_visibleBOOLEANtrueShow on customer docs
ui_bindingVARCHAR(100)Maps to form element (automatic mode)
calculation_orderINTEGER100Waterfall execution order
categoryVARCHAR(50)‘general’system, handling, service_level, general, tax
applies_onVARCHAR(20)‘subtotal’base_rate, subtotal, running_total
application_scopeVARCHAR(20)‘per_booking’per_booking, per_unit
unit_typeVARCHAR(30)pallet, kg, km, cubic_meter, item
minimum_chargeNUMERIC(10,2)Floor for per_unit
maximum_chargeNUMERIC(10,2)Cap for per_unit
resolver_urlTEXTExternal API endpoint
resolver_secretVARCHAR(255)API authentication (masked in responses)
fallback_valueNUMERIC(10,2)0Fallback if resolver fails
required_fieldsJSONB’[]‘Context keys needed by resolver
is_taxableBOOLEANtrueDerived from tax_category
tax_codeVARCHAR(30)GST, VAT, etc.
tax_inclusiveBOOLEANfalsePrice includes tax
tax_categoryVARCHAR(30)‘standard’standard, gst_free, zero_rated, input_taxed
regionVARCHAR(30)‘GLOBAL’AU, US, DXB, PH, GLOBAL

addon_types — Lookup table

Seed DataDescription
surchargeAdditional charges
discountPrice reductions
taxTax calculations
document_additionDocument requirements

value_types — Lookup table

Seed DataDescription
fixed_amountFixed dollar amount
percentagePercentage of base
per_unitAmount per unit
text_inputFree text
external_resolverDynamic API fetch

form_targets — Lookup table

NameDescription
bookingApplies to booking forms
rate_cardApplies to rate card configurations
invoiceApplies to invoice generation
quotation_widget_servicesPublic quotation widget addon services
quotation_widget_adminInternal quotation forms
create_booking_pickupCreate Booking form — pickup section handling options
create_booking_deliveryCreate Booking form — delivery section handling options
quotation_widget_pickupPublic quotation widget — pickup section services
quotation_widget_deliveryPublic quotation widget — delivery section services
customer_quotationCustomer portal quotation request form
admin_quotation_pickupAdmin quotation — pickup section handling options
admin_quotation_deliveryAdmin quotation — delivery section handling options
widget_subtotalPublic quotation widget — subtotal/total (Fuel Levy, GST)

addon_form_targets — Many-to-many: addons to form targets

ColumnTypeConstraint
addon_idFK -> addonsCASCADE
form_target_idFK -> form_targetsCASCADE
UNIQUE(addon_id, form_target_id)

addon_customers — Customer-specific assignments

ColumnTypeDescription
addon_idFK -> addonsCASCADE
customer_idFK -> customersCASCADE
is_enabledBOOLEANActive for this customer
override_valueVARCHAR(255)Custom value for this customer
UNIQUE(addon_id, customer_id)

addon_rate_cards — Rate card assignments

ColumnTypeDescription
addon_idFK -> addonsCASCADE
rate_card_idFK -> rate_cardsCASCADE
is_enabledBOOLEANActive for this rate card
override_valueVARCHAR(255)Custom value for this rate card
UNIQUE(addon_id, rate_card_id)

addon_conditions — Conditional display logic

ColumnTypeDescription
addon_idFK -> addonsCASCADE
condition_typeVARCHAR(50)job_type, weight, distance, etc.
condition_operatorVARCHAR(20)equals, greater_than, between, etc.
condition_valueTEXTJSON-encoded value(s)
logic_operatorVARCHAR(10)AND (default), OR (future)

tax_profiles — Region tax registry

ColumnTypeDescription
region_codeVARCHAR(10)UNIQUE short code (AU, US, etc.)
region_nameVARCHAR(100)Display name
tax_nameVARCHAR(50)GST, VAT, Sales Tax
standard_rateNUMERIC(5,4)Decimal rate (0.1000 = 10%)
currency_codeVARCHAR(10)AUD, USD, etc.
tax_engine_typeVARCHAR(20)INTERNAL or EXTERNAL
compliance_notesTEXTRegion-specific invoicing rules

All endpoints require @login_required. Base URL: /api/addons

MethodPathDescription
GET/List addons (query: addon_type, form_target, is_active, search, page, per_page)
POST/Create addon
GET/<id>Get addon with conditions and assignments
PUT/<id>Update addon
DELETE/<id>Hard delete with cascade
MethodPathDescription
GET/<id>/conditionsList conditions
POST/<id>/conditionsCreate condition
PUT/<id>/conditions/<cid>Update condition
DELETE/<id>/conditions/<cid>Delete condition
MethodPathBody
GET/<id>/customers
POST/<id>/customers{ "customer_ids": [1, 2, 3] }
DELETE/<id>/customers/<cust_id>
MethodPathBody
GET/<id>/rate-cards
POST/<id>/rate-cards{ "rate_card_ids": [1, 2, 3] }
DELETE/<id>/rate-cards/<rc_id>
MethodPathDescription
GET/POST/for-contextGet applicable addons. Params: form_target (required), customer_id, rate_card_id
POST/calculateCalculate single addon: { "addon_id": 1, "context": { "base_amount": 100 } }
POST/calculate-batchFull waterfall calculation (see 9.1)
MethodPathDescription
POST/<id>/test-resolverTest external resolver with sample data
GET/reference-dataAll dropdown options + tenant config
GET/tax-profilesTax profiles + current tenant context

File: controllers/addons_controller.py

FunctionPurpose
get_all_addons(filters, pagination)Query addons with optional filters and pagination
get_addon_by_id(addon_id)Get single addon with conditions/assignments
create_addon(data)Create addon + form target assignments
update_addon(addon_id, data)Update addon fields + sync form targets
delete_addon(addon_id)Hard delete with cascade
get_addon_conditions(addon_id)Get conditions for addon
create_condition(addon_id, data)Add condition
update_condition(condition_id, data)Update condition
delete_condition(condition_id)Delete condition
get_addon_customers(addon_id)Get customer assignments
assign_customers(addon_id, ids)Bulk assign customers
unassign_customer(addon_id, cust_id)Remove customer assignment
get_addon_rate_cards(addon_id)Get rate card assignments
assign_rate_cards(addon_id, ids)Bulk assign rate cards
unassign_rate_card(addon_id, rc_id)Remove rate card assignment
get_addons_for_context(form_target, ...)Get applicable addons with region/assignment/condition filtering
calculate_addons_batch(form_target, ...)Full waterfall calculation engine
calculate_addon_value(addon, base, ctx)Calculate single addon value
_call_external_resolver(addon, ctx)Call external pricing API with cache + fallback
_get_quantity_for_unit_type(ctx, unit)Extract quantity from context by unit type
get_quantity_from_context(ctx, method_id)Extract quantity by rate calculation method
get_reference_data()Get all lookup tables for UI
generate_alias(name)Generate URL-friendly alias from name

File: additional_services.html (Addon Management)

FunctionPurpose
loadReferenceData()Fetch and cache reference data from API
populateReferenceDropdowns()Fill all select dropdowns from reference data
loadAddons()Fetch addon list with active filters
renderAddons(addons)Render addon cards with badges
openCreateModal()Reset form and show modal for new addon
openEditModal(addonId)Fetch addon data and populate edit form
saveAddon()POST/PUT addon to API
deleteAddon(addonId)DELETE addon after confirmation
toggleAddonStatus(btn)Toggle is_active via PUT
setupFormTargetsMultiSelect()Initialise clickable form target picker
toggleFormTarget(id, name)Add/remove form target from selection
updateFormTargetBadges()Render selected form target badges
handleAddonTypeChange()Show/hide tax fields when type changes
handleValueTypeChange()Show/hide resolver/rate-calc fields
handleTriggerModeChange()Show/hide ui_binding field
handleApplicationScopeChange()Show/hide unit type / min/max fields
testResolver()Test external resolver endpoint
openConditionsModal(id)Show conditions management modal
openCustomersModal(id)Show customer assignment modal
openRateCardsModal(id)Show rate card assignment modal

File: create_booking.html (Booking Integration)

FunctionPurpose
loadBookingAddons(custId, rcId)Fetch addons for booking context
addAddonByBinding(binding)Add automatic addon by ui_binding
removeAddonByBinding(binding)Remove automatic addon by ui_binding
renderAddonLineItem(item)Render addon line in surcharges list
removeManualAddon(addonId)Remove manually-selected addon
populateSurchargesModal()Fill modal with manual addon checkboxes
applySelectedAddons()Apply checked addons from modal
toggleHandlingOption(button)Toggle handling button + trigger addon
updateTotalCost()Recalculate total with waterfall engine

These are created by migrations/add_addon_pricing_columns.sql:

AliasTypeTriggerValueUI BindingOrder
FUEL_LEVYsurchargemandatory20%100
TAILGATE_PICKUPsurchargeautomatic$20pickup_tailgate50
TAILGATE_DELIVERYsurchargeautomatic$20delivery_tailgate50
RES_PICKUPsurchargeautomatic$15pickup_residential50
RES_DELIVERYsurchargeautomatic$15delivery_residential50

API responses always follow this structure:

// Success
{ "success": true, "data": { ... } }
// Error
{ "success": false, "error": "Human-readable message" }

HTTP Status Codes:

CodeMeaning
200Success
201Created
400Validation error / Missing required field
404Not found
500Server error

Controller error handling pattern:

try:
# operations
db.session.commit()
return result_dict
except Exception as e:
db.session.rollback()
logger.error(f"Error: {str(e)}")
return {'error': str(e)}
  1. resolver_secret is write-only — It’s stored in the database but returned as *** in API responses. The edit form never pre-fills it. Only updates if a new non-empty value is provided.

  2. form_target_id is deprecated — The addons.form_target_id column still exists for backward compatibility, but the many-to-many addon_form_targets table is the source of truth. Always use form_target_ids (array) in API calls.

  3. is_taxable is derived — Never set is_taxable directly. Set tax_category instead. The controller derives is_taxable = (tax_category == 'standard').

  4. Tax addons must have calculation_order >= 900 — The UI enforces this when addon_type is set to tax. This ensures the two-pass engine works correctly.

  5. Region codes are uppercase — The controller normalises to uppercase. Always use AU, US, DXB, PH, GLOBAL (not lowercase).

  6. Reference data must load before edit modal — The edit modal relies on referenceData.form_targets for badge rendering. If reference data fails to load, the modal will crash. A guard in openEditModal() re-fetches reference data if missing.

  7. External resolver cache is in-memory — The 60-second cache lives in _resolver_cache dict in the controller module. It does NOT survive server restarts. In multi-worker Gunicorn deployments, each worker has its own cache.

  8. The create_booking.html file is 260KB+ — Use offset/limit or Grep when reading it. Never try to read the whole file at once.

  9. Fuel levy display depends on addon name — The quote form uses /fuel/i regex to separate fuel from other surcharges. If the addon name does not contain “fuel”, it will group under “Surcharges” instead of getting its own “Fuel” line.

  10. compute-rate merges three form targets — The rate computation endpoint runs the waterfall engine for Booking, admin_quotation_pickup, and admin_quotation_delivery and merges results. Addons de-duplicated by addon_id. If you add a handling addon, assign it to the correct quotation form target for it to appear.

  11. Connote model has its own fuel/GST defaultsfuel_levy_percentage defaults to 20.00 and gst_rate defaults to 10.00 at the column level. These are separate from the addon system defaults and must be kept in sync manually when creating connotes.

  12. GST is identified by is_tax_addon, not by name — Unlike fuel levy (name regex), GST identification uses the is_tax_addon boolean flag derived from addon_type == 'tax'. Any addon of type “tax” will be grouped into the GST line on the quote form.

Run these in order when setting up from scratch:

Terminal window
# 1. Core schema (tables + seed data)
psql $DATABASE_URL -f migrations/create_addon_schema.sql
# 2. Pricing columns + system addons
psql $DATABASE_URL -f migrations/add_addon_pricing_columns.sql
# 3. Multi-select form targets
psql $DATABASE_URL -f migrations/convert_form_target_to_multi_select.sql
# 4. External resolver support
psql $DATABASE_URL -f migrations/add_addon_external_resolver.sql
# 5. Tax profiles (multi-region)
psql $DATABASE_URL -f migrations/add_region_profiles.sql

When making changes to the addons system, verify:

  • Addon CRUD works on Additional Services page (create, edit, delete, toggle)
  • Form target multi-select saves and loads correctly
  • Addons appear on Create Booking page for correct form_target
  • Mandatory addons (e.g. Fuel Levy) auto-apply
  • Automatic addons trigger when handling buttons are clicked
  • Manual addons appear in the surcharges modal
  • Waterfall calculation produces correct totals in updateTotalCost()
  • Tax addons calculate on taxable subtotal only (not full running total)
  • Region filtering hides addons from other regions
  • Customer/rate card assignments filter correctly
  • Conditions evaluate correctly (test with different contexts)
  • External resolver calls succeed and fall back on timeout
  • Price breakdown panel shows when tax addons are present
  • Admin quote form separates Fuel, Surcharges, and GST on pricing cards
  • Fuel levy shows as “Fuel:” line (verify addon name contains “fuel”)
  • GST shows as “GST:” line (verify addon type is “tax”)
  • Quote submission stores fuel_surcharge, additional_charges, gst_amount separately
  • compute-rate merges addons from Booking + admin_quotation_pickup + admin_quotation_delivery
  • Customer-facing quotation widget passes ui_context for tailgate/DG checkboxes
  • Quote templates preserve fuel_levy_amount and replay correctly
  • Connote creation populates fuel_levy_percentage, fuel_levy_amount, gst_rate, gst_amount
  • Quote PDF renders fuel surcharge and GST amounts

To migrate from the old surcharges table to the unified addons system:

Terminal window
python migrations/migrate_surcharges_to_addons.py

Field Mappings:

Legacy FieldAddon Field
SurchargeValueType.PERCENTAGEvalue_type = 'percentage'
SurchargeValueType.FLAT_COSTvalue_type = 'fixed_amount'
ApplySurchargePer.CONSIGNMENTapplication_scope = 'per_booking'
ApplySurchargePer.ITEMapplication_scope = 'per_unit', unit_type = 'item'
ApplySurchargePer.DISTANCE_KMapplication_scope = 'per_unit', unit_type = 'km'
ApplySurchargePer.DEAD_WEIGHT_KGapplication_scope = 'per_unit', unit_type = 'kg'
ApplySurchargePer.CHARGEABLE_WEIGHT_KGapplication_scope = 'per_unit', unit_type = 'kg'

Post-Migration Checklist:

  • Verify addon count matches surcharge count
  • Test fuel levy calculation with POST /api/addons/calculate-batch
  • Test GST calculation
  • Verify customer overrides migrated (check addon_customers)
  • Test quotation widget end-to-end
  • Update any hardcoded surcharge IDs in frontend code
  • Test rate computation via POST /api/rate-entries/compute-rate
-- All active addons with calculation details
SELECT a.id, a.name, a.alias, a.trigger_mode, a.calculation_order,
a.default_value, a.applies_on, a.application_scope,
at.name as type, vt.name as value_type,
a.tax_category, a.region
FROM addons a
JOIN addon_types at ON at.id = a.addon_type_id
JOIN value_types vt ON vt.id = a.value_type_id
WHERE a.is_active = true
ORDER BY a.calculation_order;
-- Customer overrides for a specific customer
SELECT a.name, ac.override_value, ac.is_enabled,
a.default_value as standard_value
FROM addon_customers ac
JOIN addons a ON a.id = ac.addon_id
WHERE ac.customer_id = ?;
-- Form target assignments
SELECT a.name, ft.name as form_target
FROM addon_form_targets aft
JOIN addons a ON a.id = aft.addon_id
JOIN form_targets ft ON ft.id = aft.form_target_id
WHERE a.is_active = true
ORDER BY a.name, ft.name;
-- Widget-specific addon assignments
SELECT ft.name AS form_target, a.name, a.label, a.ui_binding,
a.trigger_mode, a.default_value, at.name AS addon_type
FROM addon_form_targets aft
JOIN addons a ON a.id = aft.addon_id
JOIN form_targets ft ON ft.id = aft.form_target_id
JOIN addon_types at ON at.id = a.addon_type_id
WHERE ft.name IN ('widget_subtotal','quotation_widget_pickup',
'quotation_widget_delivery','quotation_widget_services')
ORDER BY ft.name, a.calculation_order;
-- Addon conditions
SELECT a.name, ac.condition_type, ac.condition_operator, ac.condition_value
FROM addon_conditions ac
JOIN addons a ON a.id = ac.addon_id;

AddonTypeRateTriggerOrderTaxable
Fuel Surcharge%22.5% of subtotalMandatory10Yes
Pickup TailgateFixed$25.00Automatic (pickup_tailgate)60Yes
Delivery TailgateFixed$25.00Automatic (delivery_tailgate)65Yes
Dangerous GoodsFixed$125.00Automatic (dangerous_goods)110Yes
Remote Area%35% of base_rateAutomatic210Yes
After HoursFixed$65.00Manual120Yes
Weekend ServiceFixed$95.00Manual130Yes
GST%10% of running_totalMandatory900N/A (is tax)
Terminal window
TENANT_REGION=AU # Locks region to Australia (filters addon visibility)
TENANT_CURRENCY=AUD # Currency code
TAX_ENGINE_TYPE=INTERNAL # INTERNAL (fixed-rate) or EXTERNAL (API-based)
POST /api/rate-entries/compute-rate -- Full rate + addon calculation
POST /api/addons/calculate-batch -- Standalone addon calculation
GET /api/addons/for-context -- Get applicable addons for a context
GET /api/addons/reference-data -- All dropdown options + tenant config
GET /api/addons/tax-profiles -- Tax configurations
POST /api/addons/<id>/test-resolver -- Test external resolver endpoint

This document is the single source of truth for the Unified Addons System. Update it when making changes to the addon architecture.