Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
d26292a
Slots, Manage Slots
royendo Mar 12, 2026
0ddc000
prodSlots
royendo Mar 12, 2026
a0fa631
prettier
royendo Mar 12, 2026
7f7d7a1
CHC cluster size detection and Auto prompt for API key
royendo Mar 13, 2026
0442be6
remove minimum pill
royendo Mar 13, 2026
722efff
Update Messaging in Modals for better understanding
royendo Mar 14, 2026
06580cf
polling CHC cluster info, save info to PostGres, Add CHC info to Orb …
royendo Mar 14, 2026
6f681ae
prettier and tests
royendo Mar 14, 2026
9fe6fc2
go fixes
royendo Mar 14, 2026
803ee80
auto-detect CHC hibernation and update status modal XX:55
royendo Mar 16, 2026
f66fd9c
Rill-Managed also show more slots, just wihtout CHC info
royendo Mar 16, 2026
c2f83fe
mis calculate 1/2 to 1/4
royendo Mar 16, 2026
9d4fccd
Update slots-utils.ts
royendo Mar 16, 2026
c9550d9
project prod slots on deploy = 1 slot; when upgrading to teams; keep …
royendo Mar 16, 2026
d109aec
prettier
royendo Mar 16, 2026
5081298
mohterduck
royendo Mar 16, 2026
78b01e8
code checks
royendo Mar 16, 2026
f7441a3
go code
royendo Mar 16, 2026
f8924f9
Growth
royendo Mar 16, 2026
a79bdc5
handled in 9070
royendo Mar 16, 2026
218f09b
removing MD detection, covered in 9071
royendo Mar 16, 2026
16a118b
removing "Growth" syntax will follow up PR when pricin gis ready
royendo Mar 16, 2026
ef07c84
hide Slots always, no flashing
royendo Mar 17, 2026
b7ee8ec
Proto (api.proto): …
royendo Mar 17, 2026
94451fe
royendo Mar 17, 2026
15ca6be
free-plan and growth-plan implementation
royendo Mar 17, 2026
c2e9ce7
adding Growth Flows parallel to Teams
royendo Mar 18, 2026
316a15a
cli command to add credits
royendo Mar 18, 2026
7ccf0a0
return base 4 slots always
royendo Mar 18, 2026
26d48b3
credits check/ upgrade
royendo Mar 18, 2026
bfc1dec
CheckBlockingBillingErrors — now also blocks on BillingIssueTypeCre…
royendo Mar 18, 2026
888f098
Credit detection fix (orb.go GetCreditBalance): - Added Currency:…
royendo Mar 18, 2026
3b39c20
Merge branch 'main' into status-page-slots-modal
royendo Mar 18, 2026
9518b0d
Merge main and resolve conflicts
royendo Mar 18, 2026
299b475
fix afte rmerge
royendo Mar 18, 2026
e5c1fa8
remove CHC API modals, query OLAP for info
royendo Mar 18, 2026
65abf2c
add credits
royendo Mar 18, 2026
ef44469
stripping CHC API code
royendo Mar 18, 2026
63aff3e
lost in the madn3ss
royendo Mar 18, 2026
6805435
cli
royendo Mar 18, 2026
c4824c8
rmeove teams and tiral for growth and free
royendo Mar 18, 2026
4b86060
4 slots default
royendo Mar 18, 2026
b29a2fe
simplify UI; provisioned slots
royendo Mar 18, 2026
ddf6aa7
nit fixes
royendo Mar 18, 2026
7642d0f
Deployment Page
royendo Mar 18, 2026
1b8f3c4
move olap ping to backend river billing reporter
royendo Mar 18, 2026
afac888
Update DeploymentsPage.svelte
royendo Mar 18, 2026
11e1cb4
dont use Orb Credits; just use Rill based credits... not sure why it …
royendo Mar 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 62 additions & 27 deletions admin/billing.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,16 +201,18 @@ func (s *Service) RepairOrganizationBilling(ctx context.Context, org *database.O
return nil, nil, fmt.Errorf("failed to start trial: %w", err)
}

// send trial started email
err = s.Email.SendTrialStarted(&email.TrialStarted{
ToEmail: org.BillingEmail,
ToName: org.Name,
OrgName: org.Name,
FrontendURL: s.URLs.Frontend(),
TrialEndDate: sub.TrialEndDate,
})
if err != nil {
s.Logger.Named("billing").Error("failed to send trial started email", zap.String("org_name", org.Name), zap.String("org_id", org.ID), zap.String("billing_email", org.BillingEmail), zap.Error(err))
// Only send the trial-started email for trial-based plans; free plan has no trial period
if sub.Plan == nil || sub.Plan.PlanType != billing.FreePlanType {
err = s.Email.SendTrialStarted(&email.TrialStarted{
ToEmail: org.BillingEmail,
ToName: org.Name,
OrgName: org.Name,
FrontendURL: s.URLs.Frontend(),
TrialEndDate: sub.TrialEndDate,
})
if err != nil {
s.Logger.Named("billing").Error("failed to send trial started email", zap.String("org_name", org.Name), zap.String("org_id", org.ID), zap.String("billing_email", org.BillingEmail), zap.Error(err))
}
}
} else {
s.Logger.Named("billing").Warn("subscription already exists for org", zap.String("org_id", org.ID), zap.String("org_name", org.Name))
Expand Down Expand Up @@ -273,7 +275,8 @@ func (s *Service) StartTrial(ctx context.Context, org *database.Organization) (*
return nil, nil, fmt.Errorf("failed to create subscription: %w", err)
}

if org.CreatedByUserID != nil {
// Only count trial orgs for trial-based plans (not Free)
if plan.PlanType != billing.FreePlanType && org.CreatedByUserID != nil {
err = s.DB.IncrementCurrentTrialOrgCount(ctx, *org.CreatedByUserID)
if err != nil {
return nil, nil, fmt.Errorf("failed to increment current trial org count: %w", err)
Expand All @@ -286,22 +289,6 @@ func (s *Service) StartTrial(ctx context.Context, org *database.Organization) (*
return org, sub, nil
}

var userEmail string
if org.CreatedByUserID != nil {
user, err := s.DB.FindUser(ctx, *org.CreatedByUserID)
if err != nil {
return nil, nil, fmt.Errorf("failed to get user info: %w", err)
}
userEmail = user.Email
}

s.Logger.Named("billing").Info("started trial for organization",
zap.String("org_name", org.Name),
zap.String("org_id", org.ID),
zap.String("trial_end_date", sub.TrialEndDate.String()),
zap.String("user_email", userEmail),
)

org, err = s.DB.UpdateOrganization(ctx, org.ID, &database.UpdateOrganizationOptions{
Name: org.Name,
DisplayName: org.DisplayName,
Expand Down Expand Up @@ -339,6 +326,28 @@ func (s *Service) StartTrial(ctx context.Context, org *database.Organization) (*
return nil, nil, err
}

// Free plan: grant initial credits in DB; no trial issue, billing tracked via credit worker
if plan.PlanType == billing.FreePlanType {
creditAmount := 250.0
creditExpiry := time.Now().AddDate(1, 0, 0) // 1 year from now
err := s.DB.SetOrganizationCredits(ctx, org.ID, creditAmount, creditExpiry)
if err != nil {
return nil, nil, fmt.Errorf("failed to grant free plan credits: %w", err)
}
s.Logger.Named("billing").Info("started free plan for organization",
zap.String("org_name", org.Name),
zap.String("org_id", org.ID),
zap.Float64("credits_granted", creditAmount),
)
return org, sub, nil
}

s.Logger.Named("billing").Info("started trial for organization",
zap.String("org_name", org.Name),
zap.String("org_id", org.ID),
zap.String("trial_end_date", sub.TrialEndDate.String()),
)

// raise on-trial billing warning
_, err = s.DB.UpsertBillingIssue(ctx, &database.UpsertBillingIssueOptions{
OrgID: org.ID,
Expand Down Expand Up @@ -418,6 +427,21 @@ func (s *Service) CleanupTrialBillingIssues(ctx context.Context, orgID string) e
return nil
}

// CleanupCreditBillingIssues removes free-plan credit billing issues (called on upgrade to Growth)
func (s *Service) CleanupCreditBillingIssues(ctx context.Context, orgID string) error {
for _, t := range []database.BillingIssueType{
database.BillingIssueTypeCreditLow,
database.BillingIssueTypeCreditCritical,
database.BillingIssueTypeCreditExhausted,
} {
err := s.DB.DeleteBillingIssueByTypeForOrg(ctx, orgID, t)
if err != nil && !errors.Is(err, database.ErrNotFound) {
return fmt.Errorf("failed to delete credit billing issue: %w", err)
}
}
return nil
}

// CleanupSubscriptionBillingIssues removes subscription related billing issues
func (s *Service) CleanupSubscriptionBillingIssues(ctx context.Context, orgID string) error {
err := s.DB.DeleteBillingIssueByTypeForOrg(ctx, orgID, database.BillingIssueTypeNeverSubscribed)
Expand Down Expand Up @@ -481,6 +505,17 @@ func (s *Service) CheckBlockingBillingErrors(ctx context.Context, orgID string)
return fmt.Errorf("subscription cancelled")
}

be, err = s.DB.FindBillingIssueByTypeForOrg(ctx, orgID, database.BillingIssueTypeCreditExhausted)
if err != nil {
if !errors.Is(err, database.ErrNotFound) {
return err
}
}

if be != nil {
return fmt.Errorf("free credit exhausted")
}

return nil
}

Expand Down
30 changes: 24 additions & 6 deletions admin/billing/biller.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@

ReportUsage(ctx context.Context, usage []*Usage) error

// GetCreditBalance returns the credit balance for the given customer.
// Returns nil if the customer has no credit grants (e.g. not on a free plan).
GetCreditBalance(ctx context.Context, customerID string) (*CreditBalance, error)
// AddCredits adds a credit grant to the given customer. Returns the new total balance.
AddCredits(ctx context.Context, customerID string, amount float64, expiryDate time.Time, description string) (*CreditBalance, error)
// VoidCredits voids all active credit grants for the given customer (called on upgrade from Free to Growth).
VoidCredits(ctx context.Context, customerID string) error

GetReportingGranularity() UsageReportingGranularity
GetReportingWorkerCron() string

Expand All @@ -81,6 +89,8 @@
TeamPlanType
ManagedPlanType
EnterprisePlanType
FreePlanType
GrowthPlanType
)

type Plan struct {
Expand Down Expand Up @@ -108,12 +118,12 @@
type planMetadata struct {
Default bool `mapstructure:"default"`
Public bool `mapstructure:"public"`
StorageLimitBytesPerDeployment *int64 `mapstructure:"storage_limit_bytes_per_deployment"`
NumProjects *int `mapstructure:"num_projects"`
NumDeployments *int `mapstructure:"num_deployments"`
NumSlotsTotal *int `mapstructure:"num_slots_total"`
NumSlotsPerDeployment *int `mapstructure:"num_slots_per_deployment"`
NumOutstandingInvites *int `mapstructure:"num_outstanding_invites"`
StorageLimitBytesPerDeployment *int64 `mapstructure:"storage_limit_bytes_per_deployment"`

Check failure on line 121 in admin/billing/biller.go

View workflow job for this annotation

GitHub Actions / lint

File is not properly formatted (gci)
NumProjects *int `mapstructure:"num_projects"`
NumDeployments *int `mapstructure:"num_deployments"`
NumSlotsTotal *int `mapstructure:"num_slots_total"`
NumSlotsPerDeployment *int `mapstructure:"num_slots_per_deployment"`
NumOutstandingInvites *int `mapstructure:"num_outstanding_invites"`
}

type Subscription struct {
Expand All @@ -136,6 +146,14 @@
PortalURL string
}

type CreditBalance struct {
TotalCredit float64
UsedCredit float64
RemainingCredit float64
ExpiryDate time.Time
BurnRatePerDay float64 // Estimated daily burn rate based on recent usage
}

type Usage struct {
CustomerID string
MetricName string
Expand Down
12 changes: 12 additions & 0 deletions admin/billing/noop.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,18 @@ func (n noop) UnmarkCustomerTaxExempt(ctx context.Context, customerID string) er
return nil
}

func (n noop) GetCreditBalance(ctx context.Context, customerID string) (*CreditBalance, error) {
return nil, nil
}

func (n noop) AddCredits(ctx context.Context, customerID string, amount float64, expiryDate time.Time, description string) (*CreditBalance, error) {
return nil, nil
}

func (n noop) VoidCredits(ctx context.Context, customerID string) error {
return nil
}

func (n noop) ReportUsage(ctx context.Context, usage []*Usage) error {
return nil
}
Expand Down
Loading
Loading