Documentation
¶
Index ¶
- Constants
- func CheckoutHandler(store *Store, logger *slog.Logger) http.HandlerFunc
- func CheckoutHandlerWithConfig(store *Store, logger *slog.Logger, cfg Config) http.HandlerFunc
- func HasExplicitTier(toolName string) bool
- func Middleware(store *Store, adminEmailFn func(string) string) server.ToolHandlerMiddleware
- func PortalHandler(store *Store, logger *slog.Logger) http.HandlerFunc
- func PortalHandlerWithConfig(store *Store, logger *slog.Logger, cfg Config) http.HandlerFunc
- func TierMonthlyINR(t Tier) domain.Money
- func WebhookHandler(store *Store, signingSecret string, logger *slog.Logger, ...) http.HandlerFunc
- func WebhookHandlerWithConfig(store *Store, signingSecret string, logger *slog.Logger, ...) http.HandlerFunc
- type Config
- type Store
- func (s *Store) GetEmailByCustomerID(customerID string) string
- func (s *Store) GetSubscription(email string) *Subscription
- func (s *Store) GetTier(email string) Tier
- func (s *Store) GetTierForUser(email string, adminEmailFn func(string) string) Tier
- func (s *Store) InitEventLogTable() error
- func (s *Store) InitTable() error
- func (s *Store) IsEventProcessed(eventID string) bool
- func (s *Store) LoadFromDB() error
- func (s *Store) MarkEventProcessed(eventID, eventType string) error
- func (s *Store) SetChangeReason(reason string)
- func (s *Store) SetEventDispatcher(d *domain.EventDispatcher)
- func (s *Store) SetSubscription(sub *Subscription) error
- type Subscription
- type Tier
Constants ¶
const ( StatusActive = "active" StatusCanceled = "canceled" StatusPastDue = "past_due" StatusTrialing = "trialing" )
Subscription status constants.
Variables ¶
This section is empty.
Functions ¶
func CheckoutHandler ¶
func CheckoutHandler(store *Store, logger *slog.Logger) http.HandlerFunc
CheckoutHandler creates a Stripe Checkout Session and returns the URL. Protected by RequireAuthBrowser. Accepts plan query param (solo_pro/pro/premium).
Reads STRIPE_PRICE_* and EXTERNAL_URL from the environment. Prefer CheckoutHandlerWithConfig when you can pass a Config constructed at app wiring time — it makes tests t.Parallel-safe.
func CheckoutHandlerWithConfig ¶
CheckoutHandlerWithConfig is the injected-config variant of CheckoutHandler. Production wiring passes ConfigFromEnv(); tests pass a hand-built Config so they can run with t.Parallel() instead of t.Setenv.
func HasExplicitTier ¶
HasExplicitTier reports whether the given tool name has an explicit entry in the billing tier map. This is exported for cross-package tests that need to verify all tools are mapped without accessing the unexported map.
func Middleware ¶
func Middleware(store *Store, adminEmailFn func(string) string) server.ToolHandlerMiddleware
Middleware returns an MCP tool handler middleware that enforces billing tier requirements. Tools that require a higher tier than the user's current subscription are rejected with an upgrade prompt.
func PortalHandler ¶
func PortalHandler(store *Store, logger *slog.Logger) http.HandlerFunc
PortalHandler redirects the user to their Stripe Customer Portal where they can manage payment methods, view invoices, and cancel their subscription. If the user has no Stripe customer ID, they are redirected to /pricing.
Reads EXTERNAL_URL from the environment. Prefer PortalHandlerWithConfig when you can pass a Config constructed at app wiring time — it makes tests t.Parallel-safe.
func PortalHandlerWithConfig ¶
PortalHandlerWithConfig is the injected-config variant of PortalHandler. Only the Config.ExternalURL field is consulted here; the price IDs are only used by CheckoutHandler.
func TierMonthlyINR ¶
func TierMonthlyINR(t Tier) domain.Money
TierMonthlyINR returns the canonical monthly INR amount for the given tier as a domain.Money. TierFree is the zero Money — callers that want to detect "no paid plan" should use the IsZero() sentinel rather than comparing against a magic float, mirroring Slice 1's UserLimits zero-Money convention.
Unknown tiers fall through to zero Money rather than panicking — a future tier added without a price entry behaves like Free until the table is updated, which is the safer failure mode for audit-event annotation (downstream MRR consumers see "no contribution" rather than a corrupt figure).
func WebhookHandler ¶
func WebhookHandler(store *Store, signingSecret string, logger *slog.Logger, adminUpgrade func(email string)) http.HandlerFunc
WebhookHandler returns an http.HandlerFunc that verifies and processes Stripe webhook events. It handles:
- checkout.session.completed → create subscription (email + customer mapping)
- customer.subscription.updated → update tier/status/expiry
- customer.subscription.deleted → downgrade to Free, mark canceled
- invoice.payment_failed → mark subscription past_due
Events are checked for idempotency via the webhook_events table before processing. The handler returns 200 immediately; processing is synchronous but fast (no API calls).
Reads STRIPE_PRICE_* from the environment via ConfigFromEnv. Prefer WebhookHandlerWithConfig when you can pass a Config constructed at app wiring time — it makes tests t.Parallel-safe.
func WebhookHandlerWithConfig ¶
func WebhookHandlerWithConfig(store *Store, signingSecret string, logger *slog.Logger, adminUpgrade func(email string), cfg Config) http.HandlerFunc
WebhookHandlerWithConfig is the injected-config variant of WebhookHandler. Production wiring passes ConfigFromEnv(); tests pass a hand-built Config so they can run with t.Parallel() instead of relying on os.Setenv. Mirrors CheckoutHandlerWithConfig (kc/billing/checkout.go:36) — same pattern, same rationale.
Types ¶
type Config ¶
type Config struct {
// PriceSoloPro is the Stripe price ID for the Solo Pro plan.
PriceSoloPro string
// PricePro is the Stripe price ID for the Pro plan (5 users).
PricePro string
// PricePremium is the Stripe price ID for the Premium plan (20 users).
PricePremium string
// ExternalURL is the public base URL used when constructing Stripe
// success/cancel/return URLs. Falls back to http://localhost:8080
// when empty.
ExternalURL string
}
Config carries the Stripe price IDs and the external URL used by the checkout and portal HTTP handlers. It replaces the former pattern of reading STRIPE_PRICE_* / EXTERNAL_URL via os.Getenv at every request, which forced tests to use t.Setenv and therefore blocked t.Parallel.
Construct a Config once at app wiring time (via ConfigFromEnv) and pass it into CheckoutHandlerWithConfig / PortalHandlerWithConfig. Tests can construct a zero-valued Config or populate only the fields they care about and run with t.Parallel().
func ConfigFromEnv ¶
func ConfigFromEnv() Config
ConfigFromEnv builds a Config from the STRIPE_PRICE_* and EXTERNAL_URL environment variables. Intended for production wiring (called once at startup from app/). Tests should build Config directly.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is a thread-safe in-memory billing store backed by SQLite.
func NewStore ¶
NewStore creates a new billing store with SQLite persistence.
Public signature retains *slog.Logger for backward-compat with app/wire.go's call site; the value is wrapped via logport.NewSlog onto loggerPort. The duplicate *slog.Logger field was retired during the SOLID 99→100 deprecation-shim sweep (see commit body).
func (*Store) GetEmailByCustomerID ¶
GetEmailByCustomerID returns the email associated with a Stripe customer ID. Returns "" if no mapping exists. This is populated when checkout.session.completed fires.
func (*Store) GetSubscription ¶
func (s *Store) GetSubscription(email string) *Subscription
GetSubscription returns the subscription for an email. Returns nil if not found.
func (*Store) GetTier ¶
GetTier returns the current billing tier for an email. Returns TierFree if the user has no subscription or the subscription is not active.
func (*Store) GetTierForUser ¶
GetTierForUser returns the billing tier for a user, checking both direct subscription and admin parent linkage via adminEmailFn.
func (*Store) InitEventLogTable ¶
InitEventLogTable creates the webhook_events idempotency table.
func (*Store) IsEventProcessed ¶
IsEventProcessed returns true if the event has already been handled.
func (*Store) LoadFromDB ¶
LoadFromDB populates the in-memory store from the database.
monthly_amount is read as a REAL (float64) and reconstructed via domain.NewINR — this is the Slice 4 SQLite boundary: persistence stays primitive, the in-memory representation is Money. COALESCE guards pre-migration rows where the column might not have a value.
func (*Store) MarkEventProcessed ¶
MarkEventProcessed records that an event has been processed.
func (*Store) SetChangeReason ¶
SetChangeReason labels the next TierChangedEvent with the supplied reason string ("stripe_checkout", "stripe_subscription_updated", "stripe_subscription_deleted", "admin_set_billing_tier"). The reason is consumed by the next SetSubscription call and cleared afterwards, so each call site must opt in by pairing this with its mutation. Concurrency-safe (held under store lock) but not transactional — if two callers race, the second overrides the first; given that SetSubscription is per-email and tier changes are infrequent, this is acceptable for an audit-log-only signal.
func (*Store) SetEventDispatcher ¶
func (s *Store) SetEventDispatcher(d *domain.EventDispatcher)
SetEventDispatcher attaches a domain event dispatcher to the store. Once set, SetSubscription emits TierChangedEvent on every effective tier transition. Pattern mirrors usecases.CreateAlertUseCase / SessionService — the store stays usable without a dispatcher (older tests, in-memory wirings) and gains domain-event emission once wired.
func (*Store) SetSubscription ¶
func (s *Store) SetSubscription(sub *Subscription) error
SetSubscription creates or updates a subscription for the given email. On every successful write where the **effective** tier has changed (computed via effectiveTierOf below — applies the same status/expiry gating as GetTier), the store dispatches a domain.TierChangedEvent describing the transition. Identical-tier writes (idempotent webhook replays, status-only updates that don't shift the access tier) are silent so the audit log only records real state changes.
type Subscription ¶
type Subscription struct {
AdminEmail string `json:"admin_email"`
Tier Tier `json:"tier"`
StripeCustomerID string `json:"stripe_customer_id,omitempty"`
StripeSubID string `json:"stripe_sub_id,omitempty"`
Status string `json:"status"`
ExpiresAt time.Time `json:"expires_at,omitempty"`
UpdatedAt time.Time `json:"updated_at"`
MaxUsers int `json:"max_users"`
MonthlyAmount domain.Money `json:"monthly_amount"`
}
Subscription holds a user's billing subscription details.
MonthlyAmount carries the per-month rupee amount for the active plan as a domain.Money value (Slice 4 of the Money VO sweep). The zero Money is the "no paid plan / Free" sentinel — IsZero() returns true for free or unset subscriptions; paid tiers carry the canonical TierMonthlyINR(Tier) amount unless an explicit override is supplied (enterprise contract, custom price). Stripe remains the source of truth for what the user is actually charged; MonthlyAmount is the in-process mirror used to annotate TierChangedEvent and to render the dashboard plan card.
type Tier ¶
type Tier int
Tier represents a billing subscription level.
func RequiredTier ¶
RequiredTier returns the minimum billing tier needed to invoke the named tool. Unknown tools default to TierFree (fail open).
func (Tier) EffectiveTier ¶
EffectiveTier returns the tier used for tool-access comparisons. TierSoloPro grants the same tool access as TierPro (the difference is max_users, not feature gates), so it maps down to TierPro here.