Bill voice minutes on the monthly invoice
Go · Go · beginner · modification
Voice usage now shows up on the monthly invoice as its own line, priced at the org's per-minute rate. Calls are converted to billable minutes one by one and summed, as the rate card asks, and the line is skipped for orgs that made no billable calls, so seat-only customers see the same invoice as before. The call query reuses the existing store method and its error handling matches the seat count.
Go 1.22 billing service, standard library only. `Store` (existing, already wired into `Builder` in `main.go`) has `ActiveSeats` and `CallsInPeriod`; the latter returns the org's completed calls whose `StartedAt` falls in `[from, to)`. The telephony provider guarantees `EndedAt >= StartedAt` and caps a call at 24 hours. Rates are `int64` cents per unit and at most 10 000, an org has at most 100 000 active seats and makes at most 1 million calls a month, so no amount can overflow `int64`. `Org`, `Period`, `Line`, `Invoice` and `Call` are existing types in `types.go` (same package).
Requirements
- Each call is billed in whole minutes: its duration (`EndedAt - StartedAt`) rounded to the nearest minute, with an exact half minute rounding up. So a 29-second call bills 0 minutes, a 30-second call 1, a 2:40 call 3. Rounding is per call, before summing (the rate card says so).
- The invoice gets one `Voice minutes` line after the `Seats` line: `Quantity` is the sum of the billable minutes of the org's calls in the period, `UnitCents` is `org.VoiceRateCents`, `AmountCents` is `Quantity * UnitCents`. No line when the quantity is 0.
- The calls come from `Store.CallsInPeriod(ctx, org.ID, p.Start, p.End)`; if it fails, `Build` returns the error wrapped with the org id, like the seat count does.
Files touched
- internal/billing/invoice.go
--- internal/billing/invoice.go
return Invoice{}, fmt.Errorf("count seats for org %d: %w", org.ID, err)
}
+ calls, err := b.Store.CallsInPeriod(ctx, org.ID, p.Start, p.End)
+ if err != nil {
+ return Invoice{}, fmt.Errorf("load calls for org %d: %w", org.ID, err)
+ }
inv := Invoice{OrgID: org.ID, Period: p}
AmountCents: seats * org.SeatRateCents,
})
+
+ var minutes int64
+ for _, c := range calls {
+ minutes += billableMinutes(c)
+ }
+ if minutes > 0 {
+ inv.Lines = append(inv.Lines, Line{
+ Description: "Voice minutes",
+ Quantity: minutes,
+ UnitCents: org.VoiceRateCents,
+ AmountCents: minutes * org.VoiceRateCents,
+ })
+ }
return inv, nil
}
+// billableMinutes is the call's duration in whole billable minutes.
+func billableMinutes(c Call) int64 {
+ d := c.EndedAt.Sub(c.StartedAt)