Financial Calculations
All financial math uses shopspring/decimal — never float64. This ensures exact arithmetic for monetary values.
Calculation Pipeline
Rate Rules
Each application can have custom rate rules stored in contract_rate_rules. If no rule exists, defaults are derived from the application's hourly_rate and monthly_limit in ClickHouse.
| Field | Description | Default |
|---|---|---|
base_hourly_rate | Rate per hour | From application |
monthly_limit_hours | Hours included in SUP base amount | From application |
overtime_multiplier | Multiplier for overtime hours | 1.0 |
p1_p3_multiplier | Critical incident multiplier | 1.0 |
off_hours_multiplier | Off-hours work multiplier | 1.0 |
p1_p3_off_hours_multiplier | Critical + off-hours combined | 1.5 |
business_hours_start | Start of business hours | 09:00 |
business_hours_end | End of business hours | 18:00 |
business_hours_tz | Timezone | Asia/Tashkent |
weekend_days | Weekend days (1=Mon, 7=Sun) | [6, 7] |
Rate Tiers
Each worklog is assigned to the highest applicable rate tier:
| Priority | Tier | Condition | Default Multiplier |
|---|---|---|---|
| 1 | p1_p3_off_hours | Critical incident AND off-hours | 1.5 |
| 2 | p1_p3 | Critical incident during business hours | 1.0 |
| 3 | off_hours | Non-critical work during off-hours | 1.0 |
| 4 | standard / overtime | Standard business hours | 1.0 |
Critical Incident Detection
isCritical = (issue_type == 'Incident') AND (priority IN ['P1', 'P2', 'P3'])
Off-Hours Detection
isOffHours = (worklog.started < business_hours_start)
OR (worklog.started > business_hours_end)
OR (worklog.day_of_week IN weekend_days)
Formulas by Deal Type
SUP (Support)
hourly_rate = rate_rule.base_hourly_rate
monthly_limit = rate_rule.monthly_limit_hours
total_hours = sum(all billable_seconds) / 3600
if total_hours <= monthly_limit:
base_amount = application.deal_amount (or invoice_amount for international)
overtime_hours = 0
overtime_amount = 0
total_amount = base_amount
is_overtime = false
else:
base_amount = application.deal_amount
overtime_hours = total_hours - monthly_limit
overtime_amount = sum_by_tier(overtime_tiers, hourly_rate)
total_amount = base_amount + overtime_amount
is_overtime = true
The monthly_limit value is stored on the invoice as monthly_limit_hours at generation time (from contract_rate_rules) so it can be displayed in the overtime row without re-fetching rate rules.
Where sum_by_tier calculates:
for each tier in overtime_tiers:
tier_amount = tier.hours * hourly_rate * tier.multiplier
overtime_amount = sum(tier_amounts)
HR (Hourly)
hourly_rate = rate_rule.base_hourly_rate
for each rate_tier:
tier_amount = tier.hours * hourly_rate * tier.multiplier
total_amount = sum(all tier_amounts)
total_hours = sum(all tier_hours)
No base amount or overtime concept — every hour is billable.
FP (Fixed Price)
total_amount = application.deal_amount // always
Rate multipliers and hours do not affect the total. Hours are still tracked for reporting.
Base Amount Source
The base amount depends on whether the client is international:
if isInternational && application.InvoiceAmount > 0 {
baseAmount = application.InvoiceAmount
} else {
baseAmount = application.DealAmount
}
International clients may have a separate invoice_amount in a foreign currency, while deal_amount is the UZS equivalent.
Minimum Billable Time
Every individual worklog is rounded up to a minimum of 30 minutes (1800 seconds):
const MinBillableSeconds = 1800
func applyMinBillable(timeSeconds int) int {
if timeSeconds < MinBillableSeconds {
return MinBillableSeconds
}
return timeSeconds
}
This applies per-worklog, not per-issue. A 15-minute worklog becomes 30 minutes. A 45-minute worklog stays 45 minutes.
Financials Output
The calculation returns a Financials struct:
type Financials struct {
BaseAmount decimal.Decimal // Monthly/fixed amount
OvertimeHours decimal.Decimal // Hours beyond limit (SUP only)
OvertimeAmount decimal.Decimal // Overtime charge (SUP only)
TotalAmount decimal.Decimal // Final invoice total
TotalHours decimal.Decimal // Total billable hours
IsOvertime bool // Whether overtime occurred
RateTiers []RateTier // Breakdown by rate tier
}
type RateTier struct {
Label string
Multiplier decimal.Decimal
Hours decimal.Decimal
Amount decimal.Decimal
}