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
Table of Contents
Section titled “Table of Contents”- Overview
- Architecture
- How Addons Work Across Pages
- Addon Types & Value Types
- Trigger Modes
- Tax System
- Multi-Region Architecture
- External Resolver (Dynamic Pricing SPI)
- Waterfall Pricing Engine
- Conditions & Scoping
- Public Quotation Widget — Form Target Architecture
- Fuel Levy — Quotation Widgets & Forms
- GST Defaults & Tax Configuration
- Connote & Quote Model Integration
- Developer Reference
1. Overview
Section titled “1. Overview”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.
What Problem Does It Solve?
Section titled “What Problem Does It Solve?”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”).
Key Capabilities
Section titled “Key Capabilities”- One record, many forms — A single addon can target Bookings, Quotations, Rate Cards, Invoices, and more via the many-to-many
addon_form_targetstable. - 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.
2. Architecture
Section titled “2. Architecture”System Diagram
Section titled “System Diagram”+---------------------------------------------------------------------+| 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 | +---------------------+File Map
Section titled “File Map”| Layer | File | Purpose |
|---|---|---|
| Model | models/addon.py | SQLAlchemy ORM models for all addon tables |
| Model | models/tax_profile.py | TaxProfile model for multi-region support |
| Controller | controllers/addons_controller.py | All business logic, waterfall engine, resolver |
| API | api/addons_api.py | REST endpoints (/api/addons/*) |
| Config | utils/config.py | tenant_region, tenant_currency, tax_engine_type |
| Frontend | templates/portals/operations/additional_services.html | Addon management UI (CRUD) |
| Frontend | templates/portals/operations/create_booking.html | Booking page addon integration |
| Frontend | templates/portals/operations/simple_quote_form.html | Admin quote form with addon pricing cards |
| Frontend | templates/portals/operations/rate_card_details.html | Rate card surcharges tab |
| Frontend | templates/shared/tabbed_quote_form.html | Customer-facing 3-step quotation widget |
| Rate Engine | api/rate_entries_api.py | compute-rate endpoint (merges addon results from multiple form targets) |
| Model | models/connotes.py | Connote/booking model with fuel_levy_percentage, gst_rate, gst_amount fields |
| Model | models/unified_quote.py | Unified quote model with fuel_levy_amount field |
| Model | models/quote_template.py | Quote template model with add_fuel_levy boolean flag |
| Migration | migrations/create_addon_schema.sql | Core schema (tables, seed data) |
| Migration | migrations/add_addon_pricing_columns.sql | Trigger/pricing columns + system addons |
| Migration | migrations/add_addon_external_resolver.sql | Resolver columns + value type |
| Migration | migrations/add_region_profiles.sql | tax_profiles table + region migration |
How to Reach the Addons Page
Section titled “How to Reach the Addons Page”Browser URL: /operations/rate/addonsBlueprint: operations_portal (portals/operations/routes.py)Route: @operations_portal.route('/rate/addons')Template: portals/operations/additional_services.htmlSidebar: Operations Portal -> Rates -> AddonsAuth: @login_required (all routes)3. How Addons Work Across Pages
Section titled “3. How Addons Work Across Pages”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:
| Action | Function | API Call |
|---|---|---|
| Create | openCreateModal() -> saveAddon() | POST /api/addons/ |
| Edit | openEditModal(id) -> saveAddon() | PUT /api/addons/{id} |
| Delete | deleteAddon(id) | DELETE /api/addons/{id} |
| Toggle Active | toggleAddonStatus(btn) | PUT /api/addons/{id} |
| Manage Conditions | openConditionsModal(id) | GET/POST/DELETE /api/addons/{id}/conditions |
| Assign Customers | openCustomersModal(id) | POST /api/addons/{id}/customers |
| Assign Rate Cards | openRateCardsModal(id) | POST /api/addons/{id}/rate-cards |
3.2 Create Booking Page
Section titled “3.2 Create Booking Page”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 contextwindow._activeAddonItems = {}; // Keyed by addon_id -- currently applied line itemsIntegration 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 payloadHandling 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.
3.3 Rate Card Details Page
Section titled “3.3 Rate Card Details Page”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 | StatusRate card details also have an inline surcharges grid where addons can be imported directly into rate entry rows.
3.4 Admin Quote Form (Simple Quote Form)
Section titled “3.4 Admin Quote Form (Simple Quote Form)”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.25Pricing Card Rendering (lines 1022-1061):
// Separate fuel, GST (tax), and other surcharges from addon resultslet 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 linesif (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}3.5 Public Quotation Widget (Stencil.js)
Section titled “3.5 Public Quotation Widget (Stencil.js)”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 Section | form_target | Addon Trigger | What’s Assigned |
|---|---|---|---|
| Price totals (invisible, always computed) | widget_subtotal | mandatory | Fuel Levy, GST |
| Pickup row (checkboxes) | quotation_widget_pickup | automatic | Residential Pickup, Tail Lift (Pickup) |
| Delivery row (checkboxes) | quotation_widget_delivery | automatic | Residential Delivery, Tail Lift (Delivery) |
| “Additional Services” panel | quotation_widget_services | manual | DG, 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" panelPrice calculation — merge from all four targets:
// 1. Build ui_context from pickup/delivery checkboxesconst 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 targetsseen_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.
4. Addon Types & Value Types
Section titled “4. Addon Types & Value Types”Addon Types (What the addon IS)
Section titled “Addon Types (What the addon IS)”| Type | Description | Example |
|---|---|---|
surcharge | Additional charge on top of base rate | Fuel Levy, Tailgate Fee |
discount | Price reduction | Loyalty Discount, Volume Discount |
tax | Tax calculation (runs last in waterfall) | GST 10%, VAT 5% |
document_addition | Document/information requirement | Dangerous Goods Declaration |
Value Types (How the value is computed)
Section titled “Value Types (How the value is computed)”| Value Type | Calculation | Example |
|---|---|---|
fixed_amount | Static dollar amount | $20.00 flat fee |
percentage | Percentage of applies_on base | 20% of subtotal |
per_unit | Rate multiplied by quantity | $1.50 per km |
external_resolver | Fetched from external API | Dynamic toll cost |
text_input | Free text (no calculation) | Special instructions |
5. Trigger Modes
Section titled “5. Trigger Modes”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 |+------------------------------------------------------------------+UI Binding Values
Section titled “UI Binding Values”For automatic addons, the ui_binding field maps to a handling option button’s data-name attribute:
| ui_binding | Button Label | When Triggered |
|---|---|---|
pickup_tailgate | Tailgate (Pickup) | User clicks pickup tailgate button |
delivery_tailgate | Tailgate (Delivery) | User clicks delivery tailgate button |
pickup_residential | Residential (Pickup) | User clicks residential pickup button |
delivery_residential | Residential (Delivery) | User clicks residential delivery button |
pickup_manual_handling | Manual Handling (Pickup) | User clicks manual handling button |
delivery_manual_handling | Manual Handling (Delivery) | User clicks manual handling button |
6. Tax System
Section titled “6. Tax System”6.1 Tax Category
Section titled “6.1 Tax Category”Every addon has a tax_category that determines how it participates in the tax calculation:
| Tax Category | Meaning | Included in Tax Base? | Example |
|---|---|---|---|
standard | Standard taxable item | Yes | Fuel Levy, Tailgate Fee |
gst_free | GST-Free / Tax Exempt | No | International freight |
zero_rated | Zero-rated supply | No | Exports |
input_taxed | Input taxed (rare) | No | Financial services |
The is_taxable boolean is derived from tax_category:
is_taxable = (tax_category == 'standard')6.2 Tax Addons (addon_type = ‘tax’)
Section titled “6.2 Tax Addons (addon_type = ‘tax’)”When an addon’s type is tax, it behaves differently:
- Runs last in the waterfall (forced
calculation_order >= 900) - Calculates on the taxable running total, not the full running total
- 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)
6.3 Two-Pass Tax Engine
Section titled “6.3 Two-Pass Tax Engine”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 | +------------------------------------------------------------------+ | vPASS 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_total6.4 Price Breakdown Panel (Booking Page)
Section titled “6.4 Price Breakdown Panel (Booking Page)”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.007. Multi-Region Architecture
Section titled “7. Multi-Region Architecture”7.1 Tenant-Locked Model
Section titled “7.1 Tenant-Locked Model”Each iDrv5 deployment is locked to one tax jurisdiction via three environment variables:
TENANT_REGION=AU # AU, US, DXB, PH, or GLOBALTENANT_CURRENCY=AUD # AUD, USD, AED, PHPTAX_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.
7.2 Region Filtering
Section titled “7.2 Region Filtering”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.
7.3 Tax Profiles Table
Section titled “7.3 Tax Profiles Table”The tax_profiles table is the authoritative registry of supported regions:
| Code | Region | Tax | Rate | Currency | Engine |
|---|---|---|---|---|---|
AU | Australia | GST | 10% | AUD | INTERNAL |
US | United States | Sales Tax | Variable | USD | EXTERNAL |
DXB | Dubai (UAE) | VAT | 5% | AED | INTERNAL |
PH | Philippines | VAT | 12% | PHP | INTERNAL |
GLOBAL | Global (Custom) | Custom | 0% | USD | INTERNAL |
7.4 Adding a New Region
Section titled “7.4 Adding a New Region”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:
TENANT_REGION=NZTENANT_CURRENCY=NZDTAX_ENGINE_TYPE=INTERNALNo 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)”8.1 Concept
Section titled “8.1 Concept”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.
8.2 How It Works
Section titled “8.2 How It Works”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_value8.3 Safety Features
Section titled “8.3 Safety Features”| Feature | Implementation |
|---|---|
| 2-second timeout | requests.post(..., timeout=2.0) prevents slow APIs from blocking |
| 60-second cache | In-memory dict cache with TTL, keyed by route hash |
| Fallback value | fallback_value column used on any error/timeout |
| Secret masking | resolver_secret returned as *** in API responses, never pre-filled in edit form |
| Test endpoint | POST /api/addons/{id}/test-resolver with sample payload for development |
8.4 Building a Resolver API
Section titled “8.4 Building a Resolver API”Your external resolver must:
- Accept
POSTrequests withContent-Type: application/json - Validate the
X-Resolver-Secretheader - Return JSON with at minimum:
{ "status": "success", "cost": <number> } - Respond within 2 seconds
9. Waterfall Pricing Engine
Section titled “9. Waterfall Pricing Engine”9.1 Full Calculation Flow
Section titled “9.1 Full Calculation Flow”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.009.2 Value Override Priority
Section titled “9.2 Value Override Priority”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)9.3 Application Scope
Section titled “9.3 Application Scope”| Scope | Behaviour | Example |
|---|---|---|
per_booking | Flat amount per shipment | $20 tailgate fee |
per_unit | Rate x Quantity | $1.50/km x 250km = $375 |
Unit Types for per_unit scope:
| unit_type | Context Key | Example |
|---|---|---|
pallet | load_count | 5 pallets |
kg | chargeable_weight or actual_weight | 1200 kg |
km | distance | 250 km |
cubic_meter | cubic_meters | 8.5 m3 |
item | item_count or load_count | 12 items |
9.4 Min/Max Guardrails
Section titled “9.4 Min/Max Guardrails”For per-unit addons, guardrails prevent extreme values:
if amount < minimum_charge: amount = minimum_charge # Floorif amount > maximum_charge: amount = maximum_charge # CapException: Tax addons with tax_inclusive = true skip guardrails.
10. Conditions & Scoping
Section titled “10. Conditions & Scoping”10.1 Conditional Logic
Section titled “10.1 Conditional Logic”Addons can have conditions that control when they appear. All conditions use AND logic (all must pass).
| Condition Type | Operators | Example |
|---|---|---|
job_type | equals, not_equals, in | Show only for “FTL” jobs |
service_level_id | equals, in | Show only for Express service |
weight | greater_than, less_than, between | Show if weight > 500kg |
distance | greater_than, less_than, between | Show if distance > 100km |
customer_group | equals, in | Show for VIP customers |
Operators:
| Operator | Description | Value Format |
|---|---|---|
equals | Exact match | Single value |
not_equals | Not equal | Single value |
in | Value in list | JSON array |
greater_than | Numeric > | Single number |
less_than | Numeric < | Single number |
between | Range inclusive | Two values [min, max] |
10.2 Customer & Rate Card Assignment
Section titled “10.2 Customer & Rate Card Assignment”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_value10.3 Calculation Order
Section titled “10.3 Calculation Order”The calculation_order field controls the sequence in the waterfall:
| Range | Category | Examples |
|---|---|---|
| 1-49 | System (first) | Base adjustments |
| 50-99 | Handling | Tailgate, Residential |
| 100-199 | General | Fuel Levy, Distance surcharge |
| 200-499 | Service Level | Express premium |
| 500-899 | Custom | User-defined |
| 900+ | Tax (always last) | GST, VAT |
10.4 Applies-On Base
Section titled “10.4 Applies-On Base”The applies_on field determines what base amount a percentage addon uses:
| Value | Base Used | When to Use |
|---|---|---|
base_rate | Original base rate only | Fuel levy on base only |
subtotal | base_rate + flat_rate | Most surcharges |
running_total | Cumulative total so far | Compound calculations |
For tax addons, applies_on is interpreted as “Tax on Surcharges?”:
running_total= Tax on base + all taxable surchargesbase_rate= Tax on base rate only
11. Public Quotation Widget — Form Target Architecture
Section titled “11. Public Quotation Widget — Form Target Architecture”11.1 Form Targets for the Public Widget
Section titled “11.1 Form Targets for the Public Widget”The public quotation widget (Stencil.js, separate repo) uses four dedicated form targets to separate addon concerns by widget section:
| form_target | Widget Section | Trigger Mode | Purpose |
|---|---|---|---|
widget_subtotal | Price totals (computed, not a visible toggle section) | mandatory | Fuel Levy (% of subtotal), GST (% of grand total) |
quotation_widget_pickup | Pickup row checkboxes | automatic | Residential Pickup, Tail Lift Pickup |
quotation_widget_delivery | Delivery row checkboxes | automatic | Residential Delivery, Tail Lift Delivery |
quotation_widget_services | ”Additional Services” panel | manual | DG, Manual Handling, Time Slot, Hand Unload |
There are also related targets for other contexts:
| form_target | Context | Purpose |
|---|---|---|
quotation_widget_admin | Admin-only quotation widget addons | Internal-only surcharges |
customer_quotation | Customer portal quotation request form | Customer portal (not the public widget) |
booking | Booking form system addons | Fuel Levy, GST on booking page |
create_booking_pickup | Booking form pickup section | Booking-specific pickup handling |
create_booking_delivery | Booking form delivery section | Booking-specific delivery handling |
admin_quotation_pickup | Admin quotation pickup section | Admin quote pickup handling |
admin_quotation_delivery | Admin quotation delivery section | Admin 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 services10 | customer_quotation | Customer portal quotation request form11 | admin_quotation_pickup | Admin quotation — pickup section handling options12 | admin_quotation_delivery | Admin quotation — delivery section handling options21 | widget_subtotal | Public quotation widget — subtotal/total11.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):
| Addon | Type | Value | Applies On | Order |
|---|---|---|---|---|
| Fuel Surcharge | percentage | 22.5% | subtotal | 10 |
| GST | percentage | 10% | running_total (taxable) | 900 |
quotation_widget_pickup — Automatic pickup toggles:
| Addon | ui_binding | Type | Value |
|---|---|---|---|
| Pickup Residential Address | pickup_residential | fixed | $0+ |
| Pickup Tailgate Fee | pickup_tailgate | fixed | $0+ |
quotation_widget_delivery — Automatic delivery toggles:
| Addon | ui_binding | Type | Value |
|---|---|---|---|
| Delivery Residential Address | delivery_residential | fixed | $0+ |
| Delivery Tailgate Fee | delivery_tailgate | fixed | $0+ |
quotation_widget_services — Manual “Additional Services”:
| Addon | ui_binding | Type | Value |
|---|---|---|---|
| Dangerous Goods Handling | dangerous_goods | fixed | $0+ |
| Pickup Manual Handling | pickup_manual_handling | fixed | $0+ |
| Delivery Manual Handling | delivery_manual_handling | fixed | $0+ |
| Pickup Time Slot Required | pickup_time_slot | fixed | $0+ |
| Delivery Time Slot Required | delivery_time_slot | fixed | $0+ |
| Pickup Hand Unload | pickup_hand_unload | fixed | $0+ |
| Delivery Hand Unload | delivery_hand_unload | fixed | $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 + GSTINSERT INTO addon_form_targets (addon_id, form_target_id)SELECT a.id, ft.id FROM addons a CROSS JOIN form_targets ftWHERE 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 = trueON CONFLICT DO NOTHING;
-- quotation_widget_pickup: Pickup handlingINSERT INTO addon_form_targets (addon_id, form_target_id)SELECT a.id, ft.id FROM addons a CROSS JOIN form_targets ftWHERE ft.name = 'quotation_widget_pickup' AND a.ui_binding IN ('pickup_tailgate', 'pickup_residential')ON CONFLICT DO NOTHING;
-- quotation_widget_delivery: Delivery handlingINSERT INTO addon_form_targets (addon_id, form_target_id)SELECT a.id, ft.id FROM addons a CROSS JOIN form_targets ftWHERE ft.name = 'quotation_widget_delivery' AND a.ui_binding IN ('delivery_tailgate', 'delivery_residential')ON CONFLICT DO NOTHING;
-- quotation_widget_services: Manual additional servicesINSERT INTO addon_form_targets (addon_id, form_target_id)SELECT a.id, ft.id FROM addons a CROSS JOIN form_targets ftWHERE 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;11.5 Verification Query
Section titled “11.5 Verification Query”SELECT ft.name AS form_target, a.name, a.label, a.ui_binding, a.trigger_mode, a.default_value, at.name AS addon_typeFROM addon_form_targets aftJOIN addons a ON a.id = aft.addon_idJOIN form_targets ft ON ft.id = aft.form_target_idJOIN addon_types at ON at.id = a.addon_type_idWHERE 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 recalculate11.7 Auth Consideration
Section titled “11.7 Auth Consideration”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_requiredthat 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”12.1 How Fuel Levy Is Configured
Section titled “12.1 How Fuel Levy Is Configured”Fuel levy is the most critical surcharge in freight. It is configured as an addon with these settings:
name: Fuel Surchargealias: FUEL_LEVYaddon_type: surchargetrigger_mode: mandatory -- Always applied, user cannot removevalue_type: percentagedefault_value: 20 -- 20% (or 22.5% per latest rate update)calculation_order: 100 -- Runs after handling surchargescategory: generalapplies_on: subtotal -- Percentage of (base_rate + flat_rate)application_scope: per_bookingtax_category: standard -- GST applies ON TOP of fuel levyregion: australia12.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 │└─────────────────────────────────┘12.3 Fuel Levy in Stored Models
Section titled “12.3 Fuel Levy in Stored Models”| Model | Field | Default | Description |
|---|---|---|---|
connotes | fuel_levy_percentage | 20.00 | Stored percentage for the booking |
connotes | fuel_levy_amount | 0.00 | Calculated fuel dollar amount |
unified_quotes | fuel_levy_amount | 0.0 | Fuel amount on quote |
quote_templates | add_fuel_levy | True | Boolean flag to include/exclude fuel |
12.4 Fuel Levy in Quote Submission
Section titled “12.4 Fuel Levy in Quote Submission”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 itemconst 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 pricing12.5 Changing the Fuel Levy Rate
Section titled “12.5 Changing the Fuel Levy Rate”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%);13. GST Defaults & Tax Configuration
Section titled “13. GST Defaults & Tax Configuration”13.1 Default GST Rate
Section titled “13.1 Default GST Rate”GST defaults to 10% (Australian Goods and Services Tax). This is configured at multiple levels:
| Level | Location | Default | How to Change |
|---|---|---|---|
| Tenant Settings | tenant_settings.tax_rate | 0.1000 (10%) | Admin Portal > Tenant Settings |
| Tax Profile | tax_profiles.standard_rate WHERE region_code='AU' | 0.1000 | UPDATE tax_profiles SET standard_rate = 0.1000 WHERE region_code = 'AU' |
| GST Addon | addons.default_value WHERE alias='GST' | ’10’ | Portal > Rates > Addons > GST |
| Connote Default | connotes.gst_rate column default | 10.00 | Set per-booking, inherits from addon |
| Connote Default | connotes.gst_amount column default | 0.00 | Calculated from gst_rate |
13.2 GST Addon Configuration
Section titled “13.2 GST Addon Configuration”GST is implemented as a tax addon in the waterfall engine:
name: GSTaddon_type: tax -- Runs in Pass 2 (always after surcharges)trigger_mode: mandatory -- Always appliedvalue_type: percentagedefault_value: 10 -- 10%calculation_order: 900 -- Always lastcategory: taxapplies_on: running_total -- Tax on base + ALL taxable surchargestax_code: GSTtax_category: standardtax_inclusive: false -- GST is added on top (not included in price)region: australia13.3 GST Calculation Example
Section titled “13.3 GST Calculation Example”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.50Key: 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.
13.4 Tax-Inclusive vs Tax-Exclusive
Section titled “13.4 Tax-Inclusive vs Tax-Exclusive”| Setting | tax_inclusive | Behaviour |
|---|---|---|
| Tax-Exclusive (default) | false | GST calculated and added to grand total |
| Tax-Inclusive | true | GST calculated and shown but NOT added (already in price) |
13.5 Tenant Settings for Tax
Section titled “13.5 Tenant Settings for Tax”tax_rate = Column(db.Numeric(5, 4), default=0.1000) # 10%tax_name = Column(String(20), default='GST') # Display nameinclude_tax_in_quotes = Column(Boolean, default=True) # Show tax on quotesSetting 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}13.6 GST on the Quote Form
Section titled “13.6 GST on the Quote Form”In the admin quote form, GST is identified by is_tax_addon === true:
if (a.is_tax_addon) gstAmount += amount; // All tax addons → GST lineIt displays as the last line in the pricing breakdown: GST: $38.50
13.7 GST on Connote Edit
Section titled “13.7 GST on Connote Edit”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>13.8 GST on Quote PDF
Section titled “13.8 GST on Quote PDF”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>14. Connote & Quote Model Integration
Section titled “14. Connote & Quote Model Integration”14.1 Connote Pricing Fields
Section titled “14.1 Connote Pricing Fields”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)14.2 Connote Line Items
Section titled “14.2 Connote Line Items”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'14.3 Connote Addons
Section titled “14.3 Connote Addons”Applied addons are tracked via junction table:
class ConnoteAddon(db.Model): __tablename__ = 'connote_addons' connote_id # FK to connotes addon_id # FK to addons14.4 Connote Boolean Flags
Section titled “14.4 Connote Boolean Flags”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)14.5 Unified Quote Model
Section titled “14.5 Unified Quote Model”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 total14.6 Quote Template Model
Section titled “14.6 Quote Template Model”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.
15. Developer Reference
Section titled “15. Developer Reference”15.1 Database Schema
Section titled “15.1 Database Schema”Core Tables
Section titled “Core Tables”addons — Main addon definitions
| Column | Type | Default | Description |
|---|---|---|---|
id | SERIAL | PK | Auto-increment primary key |
name | VARCHAR(100) | — | Unique internal name |
label | VARCHAR(150) | — | Display name shown to users |
alias | VARCHAR(50) | — | Unique URL-friendly short code |
help_text | TEXT | — | Tooltip/help text |
description | TEXT | — | Full description |
is_active | BOOLEAN | true | Soft-delete flag |
addon_type_id | FK -> addon_types | — | surcharge, discount, tax, document_addition |
form_target_id | FK -> form_targets | — | DEPRECATED — use addon_form_targets |
value_type_id | FK -> value_types | — | fixed_amount, percentage, per_unit, external_resolver |
rate_calculation_method_id | FK -> rate_calculation_methods | NULL | For per_unit: weight, distance, time, load |
default_value | VARCHAR(255) | — | Static value or percentage |
display_order | INTEGER | 0 | UI display ordering |
trigger_mode | VARCHAR(20) | ‘manual’ | mandatory, automatic, manual |
client_visible | BOOLEAN | true | Show on customer docs |
ui_binding | VARCHAR(100) | — | Maps to form element (automatic mode) |
calculation_order | INTEGER | 100 | Waterfall execution order |
category | VARCHAR(50) | ‘general’ | system, handling, service_level, general, tax |
applies_on | VARCHAR(20) | ‘subtotal’ | base_rate, subtotal, running_total |
application_scope | VARCHAR(20) | ‘per_booking’ | per_booking, per_unit |
unit_type | VARCHAR(30) | — | pallet, kg, km, cubic_meter, item |
minimum_charge | NUMERIC(10,2) | — | Floor for per_unit |
maximum_charge | NUMERIC(10,2) | — | Cap for per_unit |
resolver_url | TEXT | — | External API endpoint |
resolver_secret | VARCHAR(255) | — | API authentication (masked in responses) |
fallback_value | NUMERIC(10,2) | 0 | Fallback if resolver fails |
required_fields | JSONB | ’[]‘ | Context keys needed by resolver |
is_taxable | BOOLEAN | true | Derived from tax_category |
tax_code | VARCHAR(30) | — | GST, VAT, etc. |
tax_inclusive | BOOLEAN | false | Price includes tax |
tax_category | VARCHAR(30) | ‘standard’ | standard, gst_free, zero_rated, input_taxed |
region | VARCHAR(30) | ‘GLOBAL’ | AU, US, DXB, PH, GLOBAL |
addon_types — Lookup table
| Seed Data | Description |
|---|---|
| surcharge | Additional charges |
| discount | Price reductions |
| tax | Tax calculations |
| document_addition | Document requirements |
value_types — Lookup table
| Seed Data | Description |
|---|---|
| fixed_amount | Fixed dollar amount |
| percentage | Percentage of base |
| per_unit | Amount per unit |
| text_input | Free text |
| external_resolver | Dynamic API fetch |
form_targets — Lookup table
| Name | Description |
|---|---|
booking | Applies to booking forms |
rate_card | Applies to rate card configurations |
invoice | Applies to invoice generation |
quotation_widget_services | Public quotation widget addon services |
quotation_widget_admin | Internal quotation forms |
create_booking_pickup | Create Booking form — pickup section handling options |
create_booking_delivery | Create Booking form — delivery section handling options |
quotation_widget_pickup | Public quotation widget — pickup section services |
quotation_widget_delivery | Public quotation widget — delivery section services |
customer_quotation | Customer portal quotation request form |
admin_quotation_pickup | Admin quotation — pickup section handling options |
admin_quotation_delivery | Admin quotation — delivery section handling options |
widget_subtotal | Public quotation widget — subtotal/total (Fuel Levy, GST) |
Junction Tables
Section titled “Junction Tables”addon_form_targets — Many-to-many: addons to form targets
| Column | Type | Constraint |
|---|---|---|
addon_id | FK -> addons | CASCADE |
form_target_id | FK -> form_targets | CASCADE |
| UNIQUE | (addon_id, form_target_id) |
addon_customers — Customer-specific assignments
| Column | Type | Description |
|---|---|---|
addon_id | FK -> addons | CASCADE |
customer_id | FK -> customers | CASCADE |
is_enabled | BOOLEAN | Active for this customer |
override_value | VARCHAR(255) | Custom value for this customer |
| UNIQUE | (addon_id, customer_id) |
addon_rate_cards — Rate card assignments
| Column | Type | Description |
|---|---|---|
addon_id | FK -> addons | CASCADE |
rate_card_id | FK -> rate_cards | CASCADE |
is_enabled | BOOLEAN | Active for this rate card |
override_value | VARCHAR(255) | Custom value for this rate card |
| UNIQUE | (addon_id, rate_card_id) |
addon_conditions — Conditional display logic
| Column | Type | Description |
|---|---|---|
addon_id | FK -> addons | CASCADE |
condition_type | VARCHAR(50) | job_type, weight, distance, etc. |
condition_operator | VARCHAR(20) | equals, greater_than, between, etc. |
condition_value | TEXT | JSON-encoded value(s) |
logic_operator | VARCHAR(10) | AND (default), OR (future) |
tax_profiles — Region tax registry
| Column | Type | Description |
|---|---|---|
region_code | VARCHAR(10) | UNIQUE short code (AU, US, etc.) |
region_name | VARCHAR(100) | Display name |
tax_name | VARCHAR(50) | GST, VAT, Sales Tax |
standard_rate | NUMERIC(5,4) | Decimal rate (0.1000 = 10%) |
currency_code | VARCHAR(10) | AUD, USD, etc. |
tax_engine_type | VARCHAR(20) | INTERNAL or EXTERNAL |
compliance_notes | TEXT | Region-specific invoicing rules |
15.2 API Reference
Section titled “15.2 API Reference”All endpoints require @login_required. Base URL: /api/addons
| Method | Path | Description |
|---|---|---|
| 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 |
Conditions
Section titled “Conditions”| Method | Path | Description |
|---|---|---|
| GET | /<id>/conditions | List conditions |
| POST | /<id>/conditions | Create condition |
| PUT | /<id>/conditions/<cid> | Update condition |
| DELETE | /<id>/conditions/<cid> | Delete condition |
Customer Assignments
Section titled “Customer Assignments”| Method | Path | Body |
|---|---|---|
| GET | /<id>/customers | — |
| POST | /<id>/customers | { "customer_ids": [1, 2, 3] } |
| DELETE | /<id>/customers/<cust_id> | — |
Rate Card Assignments
Section titled “Rate Card Assignments”| Method | Path | Body |
|---|---|---|
| GET | /<id>/rate-cards | — |
| POST | /<id>/rate-cards | { "rate_card_ids": [1, 2, 3] } |
| DELETE | /<id>/rate-cards/<rc_id> | — |
Context & Calculation
Section titled “Context & Calculation”| Method | Path | Description |
|---|---|---|
| GET/POST | /for-context | Get applicable addons. Params: form_target (required), customer_id, rate_card_id |
| POST | /calculate | Calculate single addon: { "addon_id": 1, "context": { "base_amount": 100 } } |
| POST | /calculate-batch | Full waterfall calculation (see 9.1) |
Testing & Config
Section titled “Testing & Config”| Method | Path | Description |
|---|---|---|
| POST | /<id>/test-resolver | Test external resolver with sample data |
| GET | /reference-data | All dropdown options + tenant config |
| GET | /tax-profiles | Tax profiles + current tenant context |
15.3 Controller Functions
Section titled “15.3 Controller Functions”File: controllers/addons_controller.py
| Function | Purpose |
|---|---|
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 |
15.4 Frontend JavaScript Functions
Section titled “15.4 Frontend JavaScript Functions”File: additional_services.html (Addon Management)
| Function | Purpose |
|---|---|
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)
| Function | Purpose |
|---|---|
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 |
15.5 Seeded System Addons
Section titled “15.5 Seeded System Addons”These are created by migrations/add_addon_pricing_columns.sql:
| Alias | Type | Trigger | Value | UI Binding | Order |
|---|---|---|---|---|---|
FUEL_LEVY | surcharge | mandatory | 20% | — | 100 |
TAILGATE_PICKUP | surcharge | automatic | $20 | pickup_tailgate | 50 |
TAILGATE_DELIVERY | surcharge | automatic | $20 | delivery_tailgate | 50 |
RES_PICKUP | surcharge | automatic | $15 | pickup_residential | 50 |
RES_DELIVERY | surcharge | automatic | $15 | delivery_residential | 50 |
15.6 Error Handling Patterns
Section titled “15.6 Error Handling Patterns”API responses always follow this structure:
// Success{ "success": true, "data": { ... } }
// Error{ "success": false, "error": "Human-readable message" }HTTP Status Codes:
| Code | Meaning |
|---|---|
| 200 | Success |
| 201 | Created |
| 400 | Validation error / Missing required field |
| 404 | Not found |
| 500 | Server error |
Controller error handling pattern:
try: # operations db.session.commit() return result_dictexcept Exception as e: db.session.rollback() logger.error(f"Error: {str(e)}") return {'error': str(e)}15.7 Common Gotchas
Section titled “15.7 Common Gotchas”-
resolver_secretis 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. -
form_target_idis deprecated — Theaddons.form_target_idcolumn still exists for backward compatibility, but the many-to-manyaddon_form_targetstable is the source of truth. Always useform_target_ids(array) in API calls. -
is_taxableis derived — Never setis_taxabledirectly. Settax_categoryinstead. The controller derivesis_taxable = (tax_category == 'standard'). -
Tax addons must have
calculation_order >= 900— The UI enforces this whenaddon_typeis set totax. This ensures the two-pass engine works correctly. -
Region codes are uppercase — The controller normalises to uppercase. Always use
AU,US,DXB,PH,GLOBAL(not lowercase). -
Reference data must load before edit modal — The edit modal relies on
referenceData.form_targetsfor badge rendering. If reference data fails to load, the modal will crash. A guard inopenEditModal()re-fetches reference data if missing. -
External resolver cache is in-memory — The 60-second cache lives in
_resolver_cachedict in the controller module. It does NOT survive server restarts. In multi-worker Gunicorn deployments, each worker has its own cache. -
The
create_booking.htmlfile is 260KB+ — Useoffset/limitorGrepwhen reading it. Never try to read the whole file at once. -
Fuel levy display depends on addon name — The quote form uses
/fuel/iregex 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. -
compute-rate merges three form targets — The rate computation endpoint runs the waterfall engine for
Booking,admin_quotation_pickup, andadmin_quotation_deliveryand merges results. Addons de-duplicated byaddon_id. If you add a handling addon, assign it to the correct quotation form target for it to appear. -
Connote model has its own fuel/GST defaults —
fuel_levy_percentagedefaults to 20.00 andgst_ratedefaults 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. -
GST is identified by
is_tax_addon, not by name — Unlike fuel levy (name regex), GST identification uses theis_tax_addonboolean flag derived fromaddon_type == 'tax'. Any addon of type “tax” will be grouped into the GST line on the quote form.
15.8 Migration Sequence
Section titled “15.8 Migration Sequence”Run these in order when setting up from scratch:
# 1. Core schema (tables + seed data)psql $DATABASE_URL -f migrations/create_addon_schema.sql
# 2. Pricing columns + system addonspsql $DATABASE_URL -f migrations/add_addon_pricing_columns.sql
# 3. Multi-select form targetspsql $DATABASE_URL -f migrations/convert_form_target_to_multi_select.sql
# 4. External resolver supportpsql $DATABASE_URL -f migrations/add_addon_external_resolver.sql
# 5. Tax profiles (multi-region)psql $DATABASE_URL -f migrations/add_region_profiles.sql15.9 Testing Checklist
Section titled “15.9 Testing Checklist”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_amountseparately - compute-rate merges addons from Booking + admin_quotation_pickup + admin_quotation_delivery
- Customer-facing quotation widget passes
ui_contextfor tailgate/DG checkboxes - Quote templates preserve
fuel_levy_amountand replay correctly - Connote creation populates
fuel_levy_percentage,fuel_levy_amount,gst_rate,gst_amount - Quote PDF renders fuel surcharge and GST amounts
15.10 Migration from Legacy Surcharges
Section titled “15.10 Migration from Legacy Surcharges”To migrate from the old surcharges table to the unified addons system:
python migrations/migrate_surcharges_to_addons.pyField Mappings:
| Legacy Field | Addon Field |
|---|---|
SurchargeValueType.PERCENTAGE | value_type = 'percentage' |
SurchargeValueType.FLAT_COST | value_type = 'fixed_amount' |
ApplySurchargePer.CONSIGNMENT | application_scope = 'per_booking' |
ApplySurchargePer.ITEM | application_scope = 'per_unit', unit_type = 'item' |
ApplySurchargePer.DISTANCE_KM | application_scope = 'per_unit', unit_type = 'km' |
ApplySurchargePer.DEAD_WEIGHT_KG | application_scope = 'per_unit', unit_type = 'kg' |
ApplySurchargePer.CHARGEABLE_WEIGHT_KG | application_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
15.11 Diagnostic Queries
Section titled “15.11 Diagnostic Queries”-- All active addons with calculation detailsSELECT 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.regionFROM addons aJOIN addon_types at ON at.id = a.addon_type_idJOIN value_types vt ON vt.id = a.value_type_idWHERE a.is_active = trueORDER BY a.calculation_order;
-- Customer overrides for a specific customerSELECT a.name, ac.override_value, ac.is_enabled, a.default_value as standard_valueFROM addon_customers acJOIN addons a ON a.id = ac.addon_idWHERE ac.customer_id = ?;
-- Form target assignmentsSELECT a.name, ft.name as form_targetFROM addon_form_targets aftJOIN addons a ON a.id = aft.addon_idJOIN form_targets ft ON ft.id = aft.form_target_idWHERE a.is_active = trueORDER BY a.name, ft.name;
-- Widget-specific addon assignmentsSELECT ft.name AS form_target, a.name, a.label, a.ui_binding, a.trigger_mode, a.default_value, at.name AS addon_typeFROM addon_form_targets aftJOIN addons a ON a.id = aft.addon_idJOIN form_targets ft ON ft.id = aft.form_target_idJOIN addon_types at ON at.id = a.addon_type_idWHERE ft.name IN ('widget_subtotal','quotation_widget_pickup', 'quotation_widget_delivery','quotation_widget_services')ORDER BY ft.name, a.calculation_order;
-- Addon conditionsSELECT a.name, ac.condition_type, ac.condition_operator, ac.condition_valueFROM addon_conditions acJOIN addons a ON a.id = ac.addon_id;Appendix A: Quick Reference Card
Section titled “Appendix A: Quick Reference Card”Default Rates (Australian Deployment)
Section titled “Default Rates (Australian Deployment)”| Addon | Type | Rate | Trigger | Order | Taxable |
|---|---|---|---|---|---|
| Fuel Surcharge | % | 22.5% of subtotal | Mandatory | 10 | Yes |
| Pickup Tailgate | Fixed | $25.00 | Automatic (pickup_tailgate) | 60 | Yes |
| Delivery Tailgate | Fixed | $25.00 | Automatic (delivery_tailgate) | 65 | Yes |
| Dangerous Goods | Fixed | $125.00 | Automatic (dangerous_goods) | 110 | Yes |
| Remote Area | % | 35% of base_rate | Automatic | 210 | Yes |
| After Hours | Fixed | $65.00 | Manual | 120 | Yes |
| Weekend Service | Fixed | $95.00 | Manual | 130 | Yes |
| GST | % | 10% of running_total | Mandatory | 900 | N/A (is tax) |
Environment Variables
Section titled “Environment Variables”TENANT_REGION=AU # Locks region to Australia (filters addon visibility)TENANT_CURRENCY=AUD # Currency codeTAX_ENGINE_TYPE=INTERNAL # INTERNAL (fixed-rate) or EXTERNAL (API-based)Key API Endpoints
Section titled “Key API Endpoints”POST /api/rate-entries/compute-rate -- Full rate + addon calculationPOST /api/addons/calculate-batch -- Standalone addon calculationGET /api/addons/for-context -- Get applicable addons for a contextGET /api/addons/reference-data -- All dropdown options + tenant configGET /api/addons/tax-profiles -- Tax configurationsPOST /api/addons/<id>/test-resolver -- Test external resolver endpointThis document is the single source of truth for the Unified Addons System. Update it when making changes to the addon architecture.