githubEdit

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:

  1. Perform business logic atomically (within a transaction)

  2. Calculate cost deltas before and after changes

  3. Propagate cost changes through the cache hierarchy

  4. Create ListAction records for audit trails

Key Components

Component
Location
Purpose

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

Scenario
Function
Example

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:

  1. Track before/after values for audit

  2. Apply deltas to the list's cached fields

  3. Enable undo/history features

Building ListAction Args

Capture before values and calculate deltas before any mutations:

Calling create_action()

Key Parameters

Parameter
Purpose

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

Handler
Location
Purpose

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

Last updated