Prorating Fixed Monthly Service Fees

A fixed monthly service fee — the flat availability or customer charge a utility levies regardless of how much water, gas, or power a household actually uses — is the one line item practitioners assume needs no math. It is a constant, after all. The complication appears the moment a customer is on the system for only part of a month: a new connection on the 12th, a final bill closed on the 20th, a rate schedule that changes the fixed charge mid-cycle. Now the flat fee must be split by day, and the two hard parts are choosing the denominator (how many days is “a month”?) and rounding the result to a clean cent without leaking a fraction. This page sits under surcharge and fee application logic, part of the broader automated rate calculation and rule engines subsystem, and covers the day-weighted proration of a flat fixed fee specifically — the mechanics and the rounding — not the wider account-lifecycle question of when service starts and stops.

A note on scope: computing the service window itself — deriving billable days from a move-in date, a move-out date, or a transfer — is a distinct problem handled in prorating charges for move-in and move-out dates. This page assumes those dates are already resolved and focuses purely on turning a billable-day count into a correctly rounded fixed-fee amount.

Minimal Prerequisites

  • Python 3.11+ — for modern typing and the standard-library datetime/calendar modules used below; no third-party runtime dependency is needed for the core calculation.
  • decimal.Decimal for the fee and the day fraction — the fee, the day counts, and the prorated result all stay in Decimal. A fraction like 19/31 of a fee is a non-terminating decimal, so the division must be controlled, not left to float.
  • datetime.date for the period boundaries — the billable window is expressed as inclusive date endpoints; day counting is done on dates, never on wall-clock datetimes, to sidestep DST entirely for a whole-day fee.
  • calendar.monthrange — the authoritative source for how many days a given month has, which is what makes leap-February handling automatic.

Data assumptions: the fee itself is a validated flat Decimal (for example Decimal("18.50")) drawn from an authoritative, version-controlled rule record with a governing PUC reference; the billable start and end dates for the period are already resolved and both fall within a single calendar month; and the proration basis — actual days in the month versus a fixed 30-day convention — is a stated policy of the utility, not an ad-hoc choice made in code. The single decision to fix before implementing is the denominator: most municipal fixed-fee tariffs prorate on the actual number of days in the billing month, so that is the default here, with the 30-day banker’s convention noted as an explicit alternative.

Annotated Implementation

The calculation is one pure function: given the fee, the inclusive service window, and the month it falls in, return the prorated amount rounded to the cent. Keeping it free of I/O and clocks makes it deterministically replayable when an auditor reruns a historical bill.

from __future__ import annotations

import calendar
from datetime import date
from decimal import Decimal, ROUND_HALF_UP

CENTS = Decimal("0.01")


def prorate_fixed_fee(
    monthly_fee: Decimal,     # the flat fixed service fee for a full month
    service_start: date,      # first day of service, inclusive
    service_end: date,        # last day of service, inclusive
    use_actual_days: bool = True,  # False -> 30-day banker's convention
) -> Decimal:
    """Prorate a flat fixed monthly fee by billable days within one month."""
    if service_end < service_start:
        raise ValueError("service_end must be on or after service_start")
    if service_start.year != service_end.year or service_start.month != service_end.month:
        raise ValueError("window must fall within a single calendar month")
    if monthly_fee < 0:
        raise ValueError("monthly_fee must be non-negative")

    # Inclusive day count: a window of the 12th through the 20th is 9 days,
    # so add one to the difference. Both endpoints are counted as served.
    billable_days = Decimal((service_end - service_start).days + 1)

    # Denominator: actual days in THIS month (28/29/30/31) or a flat 30.
    if use_actual_days:
        days_in_month = Decimal(calendar.monthrange(service_start.year, service_start.month)[1])
    else:
        days_in_month = Decimal("30")

    # A full month bills the full fee — never prorate a complete period,
    # which would introduce a rounding cent for no reason.
    if billable_days >= days_in_month:
        return monthly_fee.quantize(CENTS, rounding=ROUND_HALF_UP)

    # Multiply before dividing to keep precision; quantize ONCE at the end.
    prorated = monthly_fee * billable_days / days_in_month
    return prorated.quantize(CENTS, rounding=ROUND_HALF_UP)

Three decisions carry the correctness. First, the day count is inclusive — service from the 12th through the 20th is nine served days, so the code adds one to the date difference; billing eight days there is the most common off-by-one in fixed-fee proration and it silently short-changes the utility on every partial bill. Second, calendar.monthrange supplies the denominator, so February prorates over 28 or 29 days automatically and a 31-day month is never treated as 30 unless the tariff explicitly says so. Third, the multiplication happens before the division and the result is quantized exactly once with ROUND_HALF_UP; computing a per-day rate first and rounding it, then multiplying, compounds a rounding error across every day and drifts the total. This single-quantization discipline is the same one applied throughout surcharge and fee application logic for every money boundary.

How a flat fixed monthly fee is prorated by billable days A month timeline runs from day one to the last day of the month with a mark where service begins part way through. Days before service begins are not billed; days from the service start through the end of the period are billable. Below, the full monthly fee is multiplied by the billable-day fraction — billable days divided by the actual number of days in that month — to produce a prorated amount, which is then quantized to the cent with round-half-up. only these days are billed days 1 – 11 not in service days 12 – 30 billable (inclusive) day 1 service start month end Full monthly fee flat, one month Day fraction billable / days_in_month Prorated fee fee × fraction Quantize to cent ROUND_HALF_UP

Edge Cases and Billing Gotchas

Flat-fee proration breaks at the boundaries of the calendar, not the meter. These are the cases a naive implementation charges wrong.

  • Single-day service. A connection and disconnection on the same date is one billable day, not zero — the inclusive count ((end - start).days + 1) returns 1, and the fee is one day’s slice of the month. Billing zero for a same-day service or, worse, raising on it, is a frequent regression; the inclusive-count guard covers it and the verification snippet pins it down.
  • Month-length variance. Nine days of a Decimal("18.50") fee is not a fixed amount — it is 18.50 × 9 / 30 in a 30-day month but 18.50 × 9 / 31 in a 31-day month, a difference of real cents. Deriving the denominator from calendar.monthrange rather than assuming 30 keeps each month’s proration faithful to that month, unless the tariff specifically mandates the 30-day convention via use_actual_days=False.
  • Leap February. A partial February must prorate over 29 days in a leap year and 28 otherwise. Because calendar.monthrange(year, 2) returns the correct length for the specific year, February 29 is handled with no hardcoded calendar and no special case — the same value flows straight into the denominator.
  • Rounding to the cent. The fraction is almost always non-terminating, so quantize exactly once, at the end, with ROUND_HALF_UP; never round a per-day rate and then multiply. If a utility requires the summed proration across a mid-month rate change to reconcile to the full monthly fee to the cent, allocate the final cent to the longer segment rather than re-rounding both — the same largest-remainder discipline used when splitting any charge across proration and billing period calculation segments.
  • A full month sneaking through the proration path. When the billable window covers the whole month, bill the full fee directly instead of computing fee × days / days, which can round to a figure a cent off the intended flat charge. The early return guards this so a complete period always posts the exact tariff amount.

Verification Snippet

Test the calendar boundaries that produce real cent differences: a single day, a leap February, a 31-day month, and a full month that must bypass proration entirely.

from datetime import date
from decimal import Decimal


def test_prorate_fixed_fee_calendar_boundaries() -> None:
    fee = Decimal("18.50")

    # Partial month: 12th through 30th inclusive = 19 days in a 30-day month.
    part = prorate_fixed_fee(fee, date(2026, 4, 12), date(2026, 4, 30))
    assert part == Decimal("11.72")            # 18.50 * 19 / 30 = 11.7166..., half-up

    # Single-day service is one billable day, not zero.
    one = prorate_fixed_fee(fee, date(2026, 4, 15), date(2026, 4, 15))
    assert one == Decimal("0.62")              # 18.50 * 1 / 30

    # Leap February prorates over 29 days; non-leap over 28.
    leap = prorate_fixed_fee(fee, date(2028, 2, 1), date(2028, 2, 14))
    assert leap == Decimal("8.93")             # 18.50 * 14 / 29 = 8.931..., half-up
    common = prorate_fixed_fee(fee, date(2027, 2, 1), date(2027, 2, 14))
    assert common == Decimal("9.25")           # 18.50 * 14 / 28

    # A 31-day month uses 31 as the denominator, not 30.
    july = prorate_fixed_fee(fee, date(2026, 7, 1), date(2026, 7, 15))
    assert july == Decimal("8.95")             # 18.50 * 15 / 31 = 8.951..., half-up

    # A full month bills the exact flat fee, never a prorated approximation.
    full = prorate_fixed_fee(fee, date(2026, 6, 1), date(2026, 6, 30))
    assert full == Decimal("18.50")

Running these against fixtures pulled from reconciled historical bills — especially February and same-day connections — is what catches the off-by-one day and the wrong-denominator cases before a cycle posts. A stored proration that no longer matches a fresh recomputation is the signal that a fee or the proration basis changed without being versioned.

FAQ

Should I prorate over the actual days in the month or a fixed 30 days?
Most municipal fixed-fee tariffs prorate on the actual number of days in the billing month, which is why the function derives its denominator from calendar.monthrange and treats February, 30-day, and 31-day months faithfully. Some utilities adopt a 30-day banker's convention for uniformity across months; the function supports that with use_actual_days=False. The important thing is that the basis is a stated policy, applied consistently, not a value chosen ad hoc in code, because the two conventions produce genuinely different cents.
Is a single day of service billed as one day or zero?
One day. The day count is inclusive of both endpoints, computed as the date difference plus one, so a connection and disconnection on the same date counts as one billable day and bills one day's slice of the fee. Treating it as zero, or letting it raise, is a common regression; the inclusive count handles it directly and the verification snippet pins the single-day case explicitly.
How is leap February handled?
Automatically. The denominator comes from calendar.monthrange(year, 2), which returns 29 in a leap year and 28 otherwise for the specific year being billed. That value flows straight into the proration, so a partial February prorates over the correct number of days with no hardcoded calendar and no special-case branch. This is why deriving the denominator from the calendar module rather than assuming a constant matters.
Why quantize only once at the end?
The day fraction is almost always a non-terminating decimal, so any intermediate rounding compounds. Computing a per-day rate, rounding it, then multiplying by the day count drifts the total by cents across the period. Multiplying the full fee by the billable days before dividing, keeping full precision, then quantizing exactly once with ROUND_HALF_UP produces a single, reproducible, auditable figure that matches what the tariff predicts.
How is this different from move-in and move-out proration?
This page is about the mechanics of turning a known billable-day count into a correctly rounded flat-fee amount, including the denominator choice and the rounding. Deriving that billable window in the first place — from a move-in date, a move-out date, or a mid-cycle transfer, and coordinating it with usage-based charges — is a separate account-lifecycle problem covered in prorating charges for move-in and move-out dates. Use that page to establish the dates, then this one to prorate the fixed fee over them.

Up: Surcharge & Fee Application Logic · Automated Rate Calculation & Rule Engines