Pular para conteúdo

Product Configuration Guide

This document explains how to configure products, commission items, and price versioning in Airo.


Core Concepts

┌─────────────────────────────────────────────────────────┐
│  Product                                                │
│  ├── name: "Tandem Completo"                            │
│  ├── type: jump                                         │
│  ├── price: R$1200                                      │
│  │                                                      │
│  ├── ProductItems (commission breakdown)                │
│  │   ├── Vaga Avião x2         R$400 → company          │
│  │   ├── Taxa Tandem           R$200 → company          │
│  │   ├── Comissão TM Pilot     R$300 → performer [TM]   │
│  │   └── Comissão Camera       R$300 → performer [CAM]  │
│  │                                                      │
│  └── ProductPrices (versioned history)                  │
│      ├── R$1000 [2026-01-01 → 2026-03-01)  Past        │
│      ├── R$1200 [2026-03-01 → NULL]        Current     │
│      └── R$1400 [2026-06-01 → NULL]        Scheduled   │
└─────────────────────────────────────────────────────────┘

Products define what a person is charged for (jump, rental, service, merchandise).

Product Items break down how the product price is distributed — who gets what. Items are optional. Products without items send all revenue to the company.

Product Prices track price changes over time. The billing engine uses the manifest date to resolve which price was active.


Recipient Types

Each product item has a recipient_type that controls where the money goes:

recipient_type jump_type_id recipient_id Who receives When to use
company Company (DZ) Aircraft fees, handling fees
performer Set Staff matched by jump type Package commissions (Tandem, AFF)
performer Not set Staff person in that slot Add-on commissions (Camera, Coach)
packer Person UUID Specific person always Fixed recipient (e.g., packer João)

Two Commission Models

Package Model

Staff commissions are baked into the customer's product. The customer pays one all-inclusive price. Staff slots have no product of their own.

Commission items use jump_type_id to route money to the correct staff member.

┌──────────────────────────────────────────────┐
│  Product: "Tandem Completo" R$1200           │
│                                              │
│  Items:                                      │
│  ├── Vaga Avião x2       R$400 → company     │
│  ├── Taxa Tandem         R$200 → company     │
│  ├── Comissão TM Pilot   R$300 → performer ──┼──→ matched via jump_type_id
│  └── Comissão Camera     R$300 → performer ──┼──→ matched via jump_type_id
│                                              │
│  Group on manifest:                          │
│  ├── Maria    (customer)  Product: this      │
│  ├── Paulo    (TM-PILOT)  Product: none      │
│  └── Cam Guy  (CAMERA)    Product: none      │
└──────────────────────────────────────────────┘

Billing:
  Maria  → debit  R$1200 (pays everything)
  Paulo  → credit R$300  (TM-PILOT commission)
  Cam    → credit R$300  (CAMERA commission)

Add-On Model

Staff has their own product with their own price. The staff cost is split equally among payers in the group.

Commission items do NOT need jump_type_id — the staff person is already in the slot.

┌──────────────────────────────────────────────┐
│  Product: "AFF-7" R$400                      │
│  Items:                                      │
│  ├── Vaga Avião x2       R$200 → company     │
│  ├── Taxa AFF            R$100 → company     │
│  └── Comissão JM         R$100 → performer ──┼──→ matched via jump_type_id
│                                              │
│  Product: "Camera Jump" R$200                │
│  Items:                                      │
│  ├── Vaga Avião          R$80  → company     │
│  └── Comissão Camera     R$120 → performer   │ ← no jump_type_id (add-on)
│                                              │
│  Group on manifest:                          │
│  ├── João     (customer)   Product: AFF-7    │
│  ├── Ricardo  (AFF-JM)     Product: none     │
│  └── Cam Guy  (CAMERA)     Product: Camera   │
└──────────────────────────────────────────────┘

Billing:
  João    → debit  R$400 (AFF-7)
  João    → debit  R$200 (Camera split 1/1)
  Ricardo → credit R$100 (JM commission from AFF-7)
  Cam     → credit R$120 (Camera add-on commission)

Sample Product Configurations

Solo Jump (No commissions)

Simple product with no items. All revenue goes to the company.

Product: "Solo" R$150
Items: (none)

Billing: Person → debit R$150

Tandem Completo (Package — single payer, all-inclusive)

Product: "Tandem Completo" R$1200
├── Vaga Avião x2           R$400  company
├── Taxa Tandem             R$200  company
├── Comissão Tandem Pilot   R$300  performer  [jump_type: TM-PILOT]
└── Comissão Camera         R$300  performer  [jump_type: CAMERA]

Group setup:

Slot Jump Type paid_by_group Product
Maria (customer) Tandem false Tandem Completo
Paulo (staff) TM-PILOT true
Cam Guy (staff) CAMERA true

Ledger on landing:

Person Type Description Amount
Maria debit Tandem Completo - Load #3 -R$1200
Paulo credit Comissão Tandem Pilot - Load #3, Group "Tandem - Maria" +R$300
Cam Guy credit Comissão Camera - Load #3, Group "Tandem - Maria" +R$300

AFF-7 + Camera (Package + Add-on hybrid)

AFF commission is in the customer's product (package). Camera is a separate product (add-on).

Product: "AFF-7" R$400
├── Vaga Avião x2           R$200  company
├── Taxa AFF                R$100  company
└── Comissão Jump Master    R$100  performer  [jump_type: AFF-JM]

Product: "Camera Jump" R$200
├── Vaga Avião              R$80   company
└── Comissão Camera         R$120  performer

Group setup:

Slot Jump Type paid_by_group Product
João (customer) AFF false AFF-7
Ricardo (staff) AFF-JM true
Cam Guy (staff) CAMERA true Camera Jump

Ledger on landing:

Person Type Description Amount
João debit AFF-7 - Load #5 -R$400
João debit Camera Jump - Load #5, Group "AFF - João" (1/1 share) -R$200
Ricardo credit Comissão Jump Master - Load #5, Group "AFF - João" +R$100
Cam Guy credit Comissão Camera - Load #5, Group "AFF - João" +R$120

Coach with Multiple Athletes (Cost split)

Coach cost is split equally among all payers in the group.

Product: "Solo" R$150
(no items)

Product: "Coach Jump" R$250
├── Vaga Avião              R$120  company
└── Comissão Coach          R$130  performer

Group setup:

Slot Jump Type paid_by_group Product
Athlete 1 (customer) Solo false Solo
Athlete 2 (customer) Solo false Solo
Coach (staff) COACH true Coach Jump

Ledger on landing:

Person Type Description Amount
Athlete 1 debit Solo - Load #7 -R$150
Athlete 2 debit Solo - Load #7 -R$150
Athlete 1 debit Coach Jump - Load #7, Group "Fun Jump" (1/2 share) -R$125
Athlete 2 debit Coach Jump - Load #7, Group "Fun Jump" (1/2 share) -R$125
Coach credit Comissão Coach - Load #7, Group "Fun Jump" +R$130

Tandem with Packer Fee (Static commission)

Packer commission always goes to a specific person.

Product: "Tandem Completo" R$1200
├── Vaga Avião x2           R$350  company
├── Taxa Tandem             R$200  company
├── Comissão Tandem Pilot   R$300  performer  [jump_type: TM-PILOT]
├── Comissão Camera         R$300  performer  [jump_type: CAMERA]
└── Taxa Dobrador           R$50   packer     [recipient: João-UUID]

Ledger on landing:

Person Type Description Amount
Maria debit Tandem Completo - Load #3 -R$1200
Paulo credit Comissão Tandem Pilot +R$300
Cam Guy credit Comissão Camera +R$300
João (packer) credit Taxa Dobrador +R$50

Price Versioning

Product prices are versioned with effective date ranges. The billing engine uses the manifest date (not the current date) to resolve the correct price.

Timeline for "Tandem Completo":

Jan 1          Mar 1          Jun 1
  │── R$1000 ──│── R$1200 ──│── R$1400 ──→
     Past         Current       Scheduled

  Manifest on Feb 15 → billed at R$1000
  Manifest on Apr 10 → billed at R$1200
  Manifest on Jul 01 → billed at R$1400

How it works

  1. Each price version has effective_from and effective_to (NULL = currently active)
  2. Database constraint prevents overlapping date ranges
  3. When scheduling a future price change, the current version's effective_to is set to the new version's effective_from
  4. The product.price field always reflects the current active price for display

Commission amounts (not versioned)

Product item amounts (product_items.amount) are updated in place — no separate versioning. Since billing runs at landing time (same day), items always reflect current values. The ledger transaction captures the actual amount paid, serving as the historical record.


Billing Engine Flow

Load lands → ProcessLoadBilling(load, manifest_date)
├── 1. Filter billable slots (exclude no_show, cancelled)
├── 2. Load products with items for all slots
├── 3. Resolve versioned prices by manifest_date
├── 4. Ungrouped slots → direct debit to person
└── 5. For each group:
    ├── a. Identify payers (paid_by_group=false)
    ├── b. Identify paid slots (paid_by_group=true)
    ├── c. Charge each payer their own product
    ├── d. Split each paid slot's cost equally among payers
    │      (last payer absorbs rounding remainder)
    ├── e. Add-on commissions:
    │      Staff's own product → credit staff person
    │      (performer items with no jump_type_id)
    ├── f. Package commissions:
    │      Payer's product → credit matched staff
    │      (performer items with jump_type_id → find staff slot)
    └── g. Static commissions:
           Any product → credit specific recipient
           (non-company/performer items with recipient_id)

Idempotency

The billing engine checks for existing transactions before creating new ones: - Slot charges: checked by reference_type=load_billing + slot ID - Split charges: checked by reference_type=load_billing_split + metadata source_slot_id - Commissions: checked by reference_type=load_commission + product_item_id

If a load is re-landed, no duplicate charges are created.


Aircraft Payback (Repasse pro avião)

When a load lands, slots whose product has bills_aircraft = true generate a payback to the aircraft owner. The amount is resolved per slot, per product using this priority:

1. AircraftProductPayback override (aircraft + product, active on manifest_date)
   → use override.payback_amount
2. Otherwise, fall back to AircraftSlotPrice (aircraft default for manifest_date)
   → use slotPrice.price
3. Neither configured → skip (no payback transaction for this slot)

Example

Load with mixed products on aircraft PT-XXX (default R$ 215/vaga, overrides: Treino=135, Staff=180):

Slot Product bills_aircraft Resolution
1 Vaga 14k true default → R$ 215
2 Vaga 14k true default → R$ 215
3 Vaga Treino true override → R$ 135
4 Vaga Staff true override → R$ 180
5 Vaga sem repasse false skip

Total payback = 215+215+135+180 = R$ 745 (credited to aircraft owner's wallet via aircraft_payback transaction).

Transaction metadata

The aircraft_payback transaction stores a per-product breakdown in metadata:

{
  "billing_type": "aircraft_payback",
  "load_id": "...",
  "aircraft_id": "...",
  "by_product": [
    { "product_id": "...", "product_name": "Vaga 14k",    "slots_used": 2, "unit_payback": "215.00", "subtotal": "430.00", "source": "default" },
    { "product_id": "...", "product_name": "Vaga Treino", "slots_used": 1, "unit_payback": "135.00", "subtotal": "135.00", "source": "override" },
    { "product_id": "...", "product_name": "Vaga Staff",  "slots_used": 1, "unit_payback": "180.00", "subtotal": "180.00", "source": "override" }
  ],
  "total_slots_used": 5,
  "paying_slots_used": 4
}

source is "override" when the value came from AircraftProductPayback, "default" when it came from AircraftSlotPrice. The UI (manifest details, reports) uses this to render an "override" chip.

📌 Backwards compatibility: transactions created before this feature have by_jump_type instead of by_product. UI consumers should detect which key is present and render accordingly.

Versioning

Both AircraftSlotPrice and AircraftProductPayback are versioned by effective_from / effective_to. Billing resolves the active version using manifest_date (not today), so historical loads keep their original values when reprocessed.

Creating a new version closes the previous active one (effective_to = new.effective_from - 1) in the same DB transaction. A partial unique index on (tenant_id, aircraft_id, product_id) WHERE effective_to IS NULL enforces "at most one active override per (aircraft, product)".


Quick Reference: When to Use What

Scenario Product Items Needed? Commission Model
Solo jump (no staff) No All revenue → company
Tandem (all-inclusive price) Yes Package (jump_type_id on items)
AFF (instructor included) Yes Package for JM
Camera added separately Yes Add-on (staff has own product)
Coach for group Yes Add-on (split among payers)
External coach (customer type, group-paid) No Cost split only, no commissions
Packer fee (fixed person) Yes Static (recipient_id)