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

Files touched

--- 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)

Review this PR

Go practice