package usage import ( "context" "encoding/json" "errors" "fmt" "os" "time" "github.com/carlosframework/platform/internal/release" ) // Statement is usage/accounts//.json — one account's // month-to-date totals per meter, the durable input a future billing engine // consumes (statement × price sheet = invoice; spec: billing readiness). // Meters is keyed by the Meter* vocabulary. CoveredDays/DaysInMonth carry // the honesty label every rendered total must show. type Statement struct { V int `json:"v"` // 1 Account string `json:"account"` Month string `json:"month"` // "2006-01" Meters map[string]float64 `json:"meters"` CoveredDays int `json:"covered_days"` DaysInMonth int `json:"days_in_month"` UpdatedAt time.Time `json:"updated_at"` } // StatementKey is the bucket key for an account's month statement. func StatementKey(acct, month string) string { return "usage/accounts/" + acct + "/" + month + ".json" } // StatementsPrefix lists an account's statements. func StatementsPrefix(acct string) string { return "usage/accounts/" + acct + "/" } // AccountsPrefix discovers accounts that have statements. const AccountsPrefix = "usage/accounts/" // UpsertStatement CAS-replaces the account's month statement (3 attempts). func UpsertStatement(ctx context.Context, s release.Store, st Statement) error { key := StatementKey(st.Account, st.Month) body, err := json.Marshal(st) if err != nil { return err } for attempt := 0; attempt < 3; attempt++ { _, etag, err := s.Get(ctx, key) if err != nil { if !errors.Is(err, os.ErrNotExist) { return err } etag = "" } if _, err := s.Put(ctx, key, body, etag); err == nil { return nil } else if !errors.Is(err, release.ErrCASConflict) { return err } } return fmt.Errorf("usage: %s: lost CAS race three times", key) } // CreateDayObject writes key create-only. (false, nil) means another writer // won today — the daily-pass idempotence rule (spec: error handling). func CreateDayObject(ctx context.Context, s release.Store, key string, v any) (bool, error) { body, err := json.Marshal(v) if err != nil { return false, err } if _, err := s.Put(ctx, key, body, ""); err != nil { if errors.Is(err, release.ErrCASConflict) { return false, nil } return false, err } return true, nil }