Cost Handler Development
This guide explains how to create handlers that modify cost-related data in Gyrinx. Handlers encapsulate business logic for operations like purchasing equipment, removing items, and selling from stash.
Prerequisites: Familiarity with Fighter Cost System Reference and Django transactions.
Overview
Handlers are functions that:
Perform business logic atomically (within a transaction)
Calculate cost deltas before and after changes
Propagate cost changes through the cache hierarchy
Create
ListActionrecords for audit trails
Key Components
Delta
gyrinx/core/cost/propagation.py
Represents a cost change to propagate
propagate_from_assignment()
gyrinx/core/cost/propagation.py
Updates assignment and fighter cache
propagate_from_fighter()
gyrinx/core/cost/propagation.py
Updates fighter cache
is_stash_linked()
gyrinx/core/cost/routing.py
Determines rating vs stash routing
create_action()
gyrinx/core/models/list.py
Creates ListAction and updates list cache
Handler Structure
Every cost handler follows this pattern:
Required Decorators
@traced("handler_name")- Enables tracing for performance monitoring@transaction.atomic- Ensures all operations succeed or none do
Result Dataclasses
Always return a typed result dataclass rather than a tuple or dict:
The Delta Pattern
The Delta dataclass represents a cost change that needs to propagate up the cache hierarchy:
Calculating Deltas
For additions (purchases):
For removals:
For changes (upgrades):
Propagation Functions
When to Use Which
Equipment added/removed/changed
propagate_from_assignment()
Purchase, accessory addition
Fighter-level change (no assignment)
propagate_from_fighter()
Advancement, base cost override
Deleting assignment
propagate_from_fighter()
Equipment removal (assignment deleted)
propagate_from_assignment()
Updates both the assignment and its parent fighter:
propagate_from_fighter()
Updates only the fighter (use when assignment doesn't exist or will be deleted):
The Guard Condition
Propagation only runs when:
This prevents double-counting between the facts system (pull-based) and propagation system (push-based). You don't need to check this manually - the propagation functions handle it.
Rating vs Stash Routing
Costs go to different fields depending on the fighter type:
Fighter Type
Cost Field
is_stash
is_stash_linked()
Active fighter
rating_current
False
False
Stash fighter
stash_current
True
True
Vehicle/beast on stash
stash_current
False
True
Vehicle/beast on active
rating_current
False
False
Using is_stash_linked()
For complex scenarios involving child fighters:
Creating ListActions
Every cost change must create a ListAction to:
Track before/after values for audit
Apply deltas to the list's cached fields
Enable undo/history features
Building ListAction Args
Capture before values and calculate deltas before any mutations:
Calling create_action()
Key Parameters
rating_delta
Change to lst.rating_current
stash_delta
Change to lst.stash_current
credits_delta
Change to lst.credits_current
update_credits
Set True to apply credits_delta to list
*_before
Before values for audit trail
Complete Example: Equipment Sale
This example shows a handler that sells equipment from stash, demonstrating negative deltas and credit addition:
Testing Handlers
Handlers are designed for easy testing without HTTP machinery:
Existing Handlers Reference
handle_equipment_purchase
handlers/equipment/purchase.py
Buy equipment for fighter
handle_accessory_purchase
handlers/equipment/purchase.py
Add accessory to equipment
handle_weapon_profile_purchase
handlers/equipment/purchase.py
Add weapon profile
handle_equipment_upgrade
handlers/equipment/purchase.py
Change equipment upgrades
handle_equipment_removal
handlers/equipment/removal.py
Remove equipment from fighter
handle_equipment_component_removal
handlers/equipment/removal.py
Remove profile/accessory/upgrade
handle_equipment_sale
handlers/equipment/sale.py
Sell equipment from stash
handle_equipment_reassignment
handlers/equipment/reassignment.py
Move equipment between fighters
handle_equipment_cost_override
handlers/equipment/cost_override.py
Override equipment cost
handle_fighter_advancement
handlers/fighter/advancement.py
Apply advancement to fighter
handle_fighter_kill
handlers/fighter/kill.py
Kill fighter in campaign
Common Pitfalls
1. Forgetting to propagate before deletion
2. Wrong propagation function for deletion
3. Missing update_credits=True
4. Calculating deltas after mutation
See Also
Fighter Cost System Reference - Cost calculation hierarchy
Fighter Cost System Design - Design decisions and philosophy
Cost Propagation Architecture - Technical architecture
Last updated