Skip to main content

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.

FieldDescriptionDefault
base_hourly_rateRate per hourFrom application
monthly_limit_hoursHours included in SUP base amountFrom application
overtime_multiplierMultiplier for overtime hours1.0
p1_p3_multiplierCritical incident multiplier1.0
off_hours_multiplierOff-hours work multiplier1.0
p1_p3_off_hours_multiplierCritical + off-hours combined1.5
business_hours_startStart of business hours09:00
business_hours_endEnd of business hours18:00
business_hours_tzTimezoneAsia/Tashkent
weekend_daysWeekend days (1=Mon, 7=Sun)[6, 7]

Rate Tiers

Each worklog is assigned to the highest applicable rate tier:

PriorityTierConditionDefault Multiplier
1p1_p3_off_hoursCritical incident AND off-hours1.5
2p1_p3Critical incident during business hours1.0
3off_hoursNon-critical work during off-hours1.0
4standard / overtimeStandard business hours1.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
}