Go · 2773 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 package billing
4
5 import (
6 "errors"
7 "fmt"
8 "strconv"
9 )
10
11 // Principal is the routing key for the polymorphic billing surface.
12 // PRO02 Q3 ratified the abstraction: every billing service function
13 // that takes a subject takes a Principal, and the service-layer
14 // internals branch to the org or user sqlc query based on Kind.
15 //
16 // PRO04 lands the type and uses it internally — the existing
17 // org-shaped service wrappers (`ApplySubscriptionSnapshot(...snap)`
18 // where snap embeds OrgID) build a Principal{SubjectKindOrg,
19 // snap.OrgID} and call the Principal-taking sibling. PRO05 sweeps
20 // the handler-side gating call sites to use Principal directly.
21 //
22 // The zero value (`Principal{}`) is invalid by design: a routing
23 // key that doesn't say where to route silently is a bug, not a
24 // default.
25 type Principal struct {
26 Kind SubjectKind
27 ID int64
28 }
29
30 // ErrInvalidPrincipal surfaces from `Validate` and is returned by
31 // any billing service function called with a zero-value or otherwise
32 // malformed Principal. Routes that reach the database with a bad
33 // Principal would surface a less-clear "invalid input value for
34 // enum" error from postgres — checking up front is friendlier.
35 var ErrInvalidPrincipal = errors.New("billing: invalid principal")
36
37 // PrincipalForOrg is the canonical constructor for the org branch.
38 // Equivalent to `Principal{Kind: SubjectKindOrg, ID: orgID}` but
39 // the named constructor reads better at call sites and prevents
40 // the kind/id field-order mix-up that a struct literal allows.
41 func PrincipalForOrg(orgID int64) Principal {
42 return Principal{Kind: SubjectKindOrg, ID: orgID}
43 }
44
45 // PrincipalForUser is the user-side counterpart.
46 func PrincipalForUser(userID int64) Principal {
47 return Principal{Kind: SubjectKindUser, ID: userID}
48 }
49
50 // Validate reports whether `p` is well-formed. Used at service-layer
51 // entry points to surface bad routing keys with a clear error rather
52 // than letting them reach the SQL layer.
53 func (p Principal) Validate() error {
54 if !p.Kind.Valid() {
55 return fmt.Errorf("%w: kind %q", ErrInvalidPrincipal, p.Kind)
56 }
57 if p.ID <= 0 {
58 return fmt.Errorf("%w: id %d", ErrInvalidPrincipal, p.ID)
59 }
60 return nil
61 }
62
63 // IsOrg / IsUser are kind-narrowing predicates for call sites that
64 // want a typed branch without comparing constants directly.
65 func (p Principal) IsOrg() bool { return p.Kind == SubjectKindOrg }
66 func (p Principal) IsUser() bool { return p.Kind == SubjectKindUser }
67
68 // String formats `<kind>:<id>` for log fields and error messages.
69 // Matches the metadata-key convention used in Stripe events
70 // (`shithub_subject_kind` + `shithub_subject_id`).
71 func (p Principal) String() string {
72 return string(p.Kind) + ":" + strconv.FormatInt(p.ID, 10)
73 }
74