You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
boundary/internal/billing/repository.go

108 lines
3.8 KiB

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package billing
import (
"context"
"database/sql"
"time"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/db/timestamp"
"github.com/hashicorp/boundary/internal/errors"
)
// A Repository retrieves the persistent type in the billing
// package. It is not safe to use a repository concurrently.
// It provides a method for requesting pre-aggregated user counts
// per month. Depending on whether a start time and/or end time are given,
// an ActiveUsers object will be returned:
// - for every month from the provided start date until the present date,
// with the present date being a cumulative count up to the present date.
// - for every month from the provided start date until the provided end date.
// - for the previous month and current month, with the current month being a
// cumulative count up to the present date.
type Repository struct {
reader db.Reader
}
// NewRepository creates a new Repository. The returned repository is not safe for concurrent go
// routines to access it.
func NewRepository(ctx context.Context, r db.Reader) (*Repository, error) {
const op = "billing.NewRepository"
switch {
case r == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "nil db reader")
}
return &Repository{
reader: r,
}, nil
}
// MonthlyActiveUsers returns the active users for a range of months, from most recent to least.
// If no start or end time is provided, it will return the active users for the last two months.
// If a start time is provided, it will return the active users for that month until the current month.
// If both a start and end time are provided, it will return the active users for that time range,
// starting time inclusive and ending time exclusive.
// The times provided must be the start of the month at midnight UTC.
func (r *Repository) MonthlyActiveUsers(ctx context.Context, opt ...Option) ([]ActiveUsers, error) {
const op = "billing.Repository.MonthlyActiveUsers"
opts := getOpts(opt...)
switch {
case opts.withEndTime != nil && opts.withStartTime == nil:
return nil, errors.New(ctx, errors.InvalidParameter, op, "end time set without start time")
case opts.withEndTime != nil && !opts.withEndTime.After(*opts.withStartTime):
return nil, errors.New(ctx, errors.InvalidParameter, op, "start time is not before end time")
}
query := activeUsersLastTwoMonthsQuery
var args []any
if opts.withStartTime != nil {
if *opts.withStartTime != time.Date(opts.withStartTime.Year(), opts.withStartTime.Month(), 1, 0, 0, 0, 0, time.UTC) {
return nil, errors.New(ctx, errors.InvalidParameter, op, "start time must be the first day of the month at midnight UTC")
}
query = activeUsersWithStartTimeQuery
args = append(args,
sql.Named("start_time", timestamp.New(*opts.withStartTime)))
}
if opts.withEndTime != nil {
if *opts.withEndTime != time.Date(opts.withEndTime.Year(), opts.withEndTime.Month(), 1, 0, 0, 0, 0, time.UTC) {
return nil, errors.New(ctx, errors.InvalidParameter, op, "end time must be the first day of the month at midnight UTC")
}
query = activeUsersWithStartTimeAndEndTimeQuery
args = append(args,
sql.Named("end_time", timestamp.New(*opts.withEndTime)))
}
var activeUsers []ActiveUsers
rows, err := r.reader.Query(ctx, query, args)
if err != nil {
return nil, err
}
defer rows.Close()
if err := rows.Err(); err != nil {
return nil, err
}
for rows.Next() {
var startTime time.Time
var endTime time.Time
var count uint32
if err := rows.Scan(&startTime, &endTime, &count); err != nil {
return nil, err
}
// set start and end times to be in UTC
auUTC := ActiveUsers{
ActiveUsersCount: count,
StartTime: startTime.UTC(),
EndTime: endTime.UTC(),
}
activeUsers = append(activeUsers, auUTC)
}
return activeUsers, nil
}