Documentation
¶
Overview ¶
Package internal is every implementation of the billing module. Nothing outside modules/billing can import it, which is the compiler enforcing idea 3: a consumer takes contracts.Service, and taking anything else does not build.
Index ¶
- func RefuseWhileSubscribed(token tenancy.SystemToken) func(context.Context, db.Tx[db.Tenant], *contracts.Plan) error
- func RegisterRoutes(api *httpx.API, svc contracts.Service)
- func Renew(tenants jobs.TenantLister, svc contracts.Service, ...) jobs.Job
- type Manual
- type Service
- func (s *Service) Cancel(ctx context.Context, tx db.Tx[db.Tenant], atPeriodEnd bool) (*contracts.Subscription, error)
- func (s *Service) Current(_ context.Context, tx db.Tx[db.Tenant]) (*contracts.Subscription, error)
- func (s *Service) Renew(ctx context.Context, tx db.Tx[db.Tenant]) (*contracts.Subscription, *contracts.Charge, error)
- func (s *Service) Settle(ctx context.Context, tx db.Tx[db.Tenant], c contracts.Charge, ...) (*contracts.Subscription, error)
- func (s *Service) Subscribe(ctx context.Context, tx db.Tx[db.Tenant], planID uuid.UUID) (*contracts.Subscription, error)
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func RefuseWhileSubscribed ¶
func RefuseWhileSubscribed(token tenancy.SystemToken) func(context.Context, db.Tx[db.Tenant], *contracts.Plan) error
RefuseWhileSubscribed is the plan delete route's hook: a plan somebody is still on is not one the operator may remove. It is why the subscriptions table needs no foreign key — a key would refuse the delete of any plan a row had ever named, cancelled ones included, and would say so as a constraint name.
It counts under system access, and that is the consequence of the catalogue being shared: the plan being deleted is one every tenant can subscribe to, and the operator's own transaction sees only the operator's subscriptions. A tenant-scoped count here would have let the operator delete a plan half its customers were being billed for and reported nothing.
The count is in a transaction of its own, on a detached context, because a system transaction cannot nest inside a tenant one. The refusal still rolls the delete back: it is an error returned from a hook that runs inside the request's transaction.
func RegisterRoutes ¶
RegisterRoutes mounts the two commands beside the singleton's read.
The read is rest.Singleton now — one row per tenant, mounted by ../module.go — and it carries no Write: a subscription is moved by subscribe and cancel, which are rules about the state it is in, and a PUT would be a customer writing its own period and its own price. The two commands are here because kit/rest's Command puts an id in the path and a singleton has none, which is the only thing a singleton changes about a command.
func Renew ¶
func Renew(tenants jobs.TenantLister, svc contracts.Service, payments contracts.PaymentProvider, every time.Duration) jobs.Job
Renew is the module's periodic work: the one thing an outbox cannot express, because a period running out is not something that happened to anybody (docs/adr/0004). One instance in the cluster runs it per tick, kit/jobs taking an advisory lock named after the job.
Types ¶
type Manual ¶
type Manual struct{}
Manual is the PaymentProvider for an installation that takes money somewhere else: it records what is owed, in the log, and settles nothing.
It is not a stub. An unsettled receipt is a real answer — Settle marks the subscription past due and publishes billing.past_due — so a deployment with no payment processor still runs the whole lifecycle and still says, once per customer, that somebody owes money. A provider that lied and said Settled would be a deployment quietly giving everything away.
func NewManual ¶
func NewManual() *Manual
NewManual returns the provider that moves no money. main wires it, so the choice is visible in the file that composes the application.
func (Manual) Charge ¶
Charge records what is due and takes nothing. The reference is the charge's own idempotency key, so asking twice for one period is one reference: a renewal that runs again after a crash must not look like a second debt. That is what the interface asks of every implementation, and the one in this repository is where it is demonstrated.
type Service ¶
type Service struct{}
Service is the subscription lifecycle. It has no fields: everything a command needs arrives with the transaction it is given, and the one thing it does not do is take money — that is the caller's, between two transactions.
func NewService ¶
func NewService() *Service
NewService returns the lifecycle commands. It takes nothing, on purpose: see the type. module.go constructs it.
func (*Service) Cancel ¶
func (s *Service) Cancel(ctx context.Context, tx db.Tx[db.Tenant], atPeriodEnd bool) (*contracts.Subscription, error)
Cancel ends the subscription, now or at the end of the period. See contracts.Service.
func (*Service) Current ¶
Current is the tenant's one subscription: a query rather than a crud.Get, because the caller has no id to give and row-level security has already narrowed the table to the one row there is.
func (*Service) Renew ¶
func (s *Service) Renew(ctx context.Context, tx db.Tx[db.Tenant]) (*contracts.Subscription, *contracts.Charge, error)
Renew is the free half of a renewal. See contracts.Service.