carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package usage
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "os"
9 "time"
10
11 "github.com/carlosframework/platform/internal/release"
12)
13
14// Statement is usage/accounts/<acct>/<month>.json — one account's
15// month-to-date totals per meter, the durable input a future billing engine
16// consumes (statement × price sheet = invoice; spec: billing readiness).
17// Meters is keyed by the Meter* vocabulary. CoveredDays/DaysInMonth carry
18// the honesty label every rendered total must show.
19type Statement struct {
20 V int `json:"v"` // 1
21 Account string `json:"account"`
22 Month string `json:"month"` // "2006-01"
23 Meters map[string]float64 `json:"meters"`
24 CoveredDays int `json:"covered_days"`
25 DaysInMonth int `json:"days_in_month"`
26 UpdatedAt time.Time `json:"updated_at"`
27}
28
29// StatementKey is the bucket key for an account's month statement.
30func StatementKey(acct, month string) string {
31 return "usage/accounts/" + acct + "/" + month + ".json"
32}
33
34// StatementsPrefix lists an account's statements.
35func StatementsPrefix(acct string) string { return "usage/accounts/" + acct + "/" }
36
37// AccountsPrefix discovers accounts that have statements.
38const AccountsPrefix = "usage/accounts/"
39
40// UpsertStatement CAS-replaces the account's month statement (3 attempts).
41func UpsertStatement(ctx context.Context, s release.Store, st Statement) error {
42 key := StatementKey(st.Account, st.Month)
43 body, err := json.Marshal(st)
44 if err != nil {
45 return err
46 }
47 for attempt := 0; attempt < 3; attempt++ {
48 _, etag, err := s.Get(ctx, key)
49 if err != nil {
50 if !errors.Is(err, os.ErrNotExist) {
51 return err
52 }
53 etag = ""
54 }
55 if _, err := s.Put(ctx, key, body, etag); err == nil {
56 return nil
57 } else if !errors.Is(err, release.ErrCASConflict) {
58 return err
59 }
60 }
61 return fmt.Errorf("usage: %s: lost CAS race three times", key)
62}
63
64// CreateDayObject writes key create-only. (false, nil) means another writer
65// won today — the daily-pass idempotence rule (spec: error handling).
66func CreateDayObject(ctx context.Context, s release.Store, key string, v any) (bool, error) {
67 body, err := json.Marshal(v)
68 if err != nil {
69 return false, err
70 }
71 if _, err := s.Put(ctx, key, body, ""); err != nil {
72 if errors.Is(err, release.ErrCASConflict) {
73 return false, nil
74 }
75 return false, err
76 }
77 return true, nil
78}
79