Skip to content

Commit 1b27bc2

Browse files
authored
feat/billing gate (#2021)
* billing functinoalities
1 parent ef73e19 commit 1b27bc2

File tree

7 files changed

+110
-13
lines changed

7 files changed

+110
-13
lines changed

backend/bootstrap/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,9 @@ func Bootstrap(templates embed.FS, diggerController controllers.DiggerController
229229
orgsApiGroup.GET("/settings/", controllers.GetOrgSettingsApi)
230230
orgsApiGroup.PUT("/settings/", controllers.UpdateOrgSettingsApi)
231231

232+
billingApiGroup := apiGroup.Group("/billing")
233+
billingApiGroup.GET("/", controllers.BillingStatusApi)
234+
232235
reposApiGroup := apiGroup.Group("/repos")
233236
reposApiGroup.GET("/", controllers.ListReposApi)
234237
reposApiGroup.GET("/:repo_id/jobs", controllers.GetJobsForRepoApi)

backend/controllers/billing.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package controllers
2+
3+
import (
4+
"errors"
5+
"github.com/diggerhq/digger/backend/middleware"
6+
"github.com/diggerhq/digger/backend/models"
7+
"github.com/gin-gonic/gin"
8+
"gorm.io/gorm"
9+
"log/slog"
10+
"net/http"
11+
)
12+
13+
func BillingStatusApi(c *gin.Context) {
14+
organisationId := c.GetString(middleware.ORGANISATION_ID_KEY)
15+
organisationSource := c.GetString(middleware.ORGANISATION_SOURCE_KEY)
16+
17+
var org models.Organisation
18+
err := models.DB.GormDB.Where("external_id = ? AND external_source = ?", organisationId, organisationSource).First(&org).Error
19+
if err != nil {
20+
if errors.Is(err, gorm.ErrRecordNotFound) {
21+
slog.Info("Organisation not found", "organisationId", organisationId, "source", organisationSource)
22+
c.String(http.StatusNotFound, "Could not find organisation: "+organisationId)
23+
} else {
24+
slog.Error("Error fetching organisation", "organisationId", organisationId, "source", organisationSource, "error", err)
25+
c.String(http.StatusInternalServerError, "Error fetching organisation")
26+
}
27+
return
28+
}
29+
30+
monitoredProjectsCount, remainingFreeProjects, billableProjectsCount, err := models.DB.GetProjectsRemainingInFreePLan(org.ID)
31+
if err != nil {
32+
slog.Error("Error fetching remaining free projects", "error", err)
33+
c.String(http.StatusInternalServerError, "Error fetching remaining free projects")
34+
return
35+
}
36+
37+
c.JSON(http.StatusOK, gin.H{
38+
"billing_plan": org.BillingPlan,
39+
"billing_stripe_subscription_id": org.BillingStripeSubscriptionId,
40+
"remaining_free_projects": remainingFreeProjects,
41+
"monitored_projects_count": monitoredProjectsCount,
42+
"billable_projects_count": billableProjectsCount,
43+
"monitored_projects_limit": models.MaxFreePlanProjectsPerOrg,
44+
})
45+
}

backend/controllers/orgs.go

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ func GetOrgSettingsApi(c *gin.Context) {
4242
"drift_enabled": org.DriftEnabled,
4343
"drift_cron_tab": org.DriftCronTab,
4444
"drift_webhook_url": org.DriftWebhookUrl,
45+
"billing_plan": org.BillingPlan,
4546
})
4647
}
4748

@@ -62,9 +63,11 @@ func UpdateOrgSettingsApi(c *gin.Context) {
6263
return
6364
}
6465
var reqBody struct {
65-
DriftEnabled bool `json:"drift_enabled"`
66-
DriftCronTab string `json:"drift_cron_tab"`
67-
DriftWebhookUrl string `json:"drift_webhook_url"`
66+
DriftEnabled *bool `json:"drift_enabled,omitempty"`
67+
DriftCronTab *string `json:"drift_cron_tab,omitempty"`
68+
DriftWebhookUrl *string `json:"drift_webhook_url,omitempty"`
69+
BillingPlan *string `json:"billing_plan,omitempty"`
70+
BillingStripeSubscriptionId *string `json:"billing_stripe_subscription_id,omitempty"`
6871
}
6972
err = json.NewDecoder(c.Request.Body).Decode(&reqBody)
7073
if err != nil {
@@ -73,9 +76,26 @@ func UpdateOrgSettingsApi(c *gin.Context) {
7376
return
7477
}
7578

76-
org.DriftEnabled = reqBody.DriftEnabled
77-
org.DriftCronTab = reqBody.DriftCronTab
78-
org.DriftWebhookUrl = reqBody.DriftWebhookUrl
79+
if reqBody.DriftEnabled != nil {
80+
org.DriftEnabled = *reqBody.DriftEnabled
81+
}
82+
83+
if reqBody.DriftCronTab != nil {
84+
org.DriftCronTab = *reqBody.DriftCronTab
85+
}
86+
87+
if reqBody.DriftWebhookUrl != nil {
88+
org.DriftWebhookUrl = *reqBody.DriftWebhookUrl
89+
}
90+
91+
if reqBody.BillingPlan != nil {
92+
org.BillingPlan = models.BillingPlan(*reqBody.BillingPlan)
93+
}
94+
95+
if reqBody.BillingStripeSubscriptionId != nil {
96+
org.BillingStripeSubscriptionId = *reqBody.BillingStripeSubscriptionId
97+
}
98+
7999
err = models.DB.GormDB.Save(&org).Error
80100
if err != nil {
81101
slog.Error("Error saving organisation", "organisationId", organisationId, "source", organisationSource, "error", err)

backend/migrations/20250712014920.sql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- Modify "organisations" table
2+
ALTER TABLE "public"."organisations" ADD COLUMN "billing_plan" text NULL DEFAULT 'free', ADD COLUMN "billing_stripe_subscription_id" text NULL;

backend/migrations/atlas.sum

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
h1:94F268ZEPf2Y9xBgyPcV2kVk0fb4+6flqUIRqoaO4cs=
1+
h1:lTHG1XO1wASaCOl6gEp635MxFxAzxBuzxTe8pOPqKQc=
22
20231227132525.sql h1:43xn7XC0GoJsCnXIMczGXWis9d504FAWi4F1gViTIcw=
33
20240115170600.sql h1:IW8fF/8vc40+eWqP/xDK+R4K9jHJ9QBSGO6rN9LtfSA=
44
20240116123649.sql h1:R1JlUIgxxF6Cyob9HdtMqiKmx/BfnsctTl5rvOqssQw=
@@ -56,3 +56,4 @@ h1:94F268ZEPf2Y9xBgyPcV2kVk0fb4+6flqUIRqoaO4cs=
5656
20250711030148.sql h1:hN972A3rU9TN4bCUo1muJWwEqNQQyIPlmYgsePNH00w=
5757
20250711030248.sql h1:oKzrMdfE75UyMSx++OMrXOAi1AOLN6kuv8iBLq8+srs=
5858
20250711030323.sql h1:vN9g0H99CItCw9aG6tRo73py5qQ4iD6qew2Y66WcoBk=
59+
20250712014920.sql h1:tu6MmFyXwCEvXjhjMYze79Vs3+OPqv6te5IiJsjnd5k=

backend/models/orgs.go

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,26 @@ import (
66
"gorm.io/gorm"
77
)
88

9+
type BillingPlan string
10+
11+
const (
12+
BillingPlanFree BillingPlan = "free"
13+
BillingPlanPro BillingPlan = "pro"
14+
BillingPlanPremium BillingPlan = "unlimited" // custom plan with unlimited resources
15+
)
16+
17+
const MaxFreePlanProjectsPerOrg uint = 3
18+
919
type Organisation struct {
1020
gorm.Model
11-
Name string `gorm:"Index:idx_organisation"`
12-
ExternalSource string `gorm:"uniqueIndex:idx_external_source"`
13-
ExternalId string `gorm:"uniqueIndex:idx_external_source"`
14-
DriftEnabled bool `gorm:"default:false"`
15-
DriftWebhookUrl string
16-
DriftCronTab string `gorm:"default:'0 0 * * *'"`
21+
Name string `gorm:"Index:idx_organisation"`
22+
ExternalSource string `gorm:"uniqueIndex:idx_external_source"`
23+
ExternalId string `gorm:"uniqueIndex:idx_external_source"`
24+
DriftEnabled bool `gorm:"default:false"`
25+
DriftWebhookUrl string
26+
DriftCronTab string `gorm:"default:'0 0 * * *'"`
27+
BillingPlan BillingPlan `gorm:"default:'free'"`
28+
BillingStripeSubscriptionId string
1729
}
1830

1931
type Repo struct {

backend/models/storage.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"fmt"
77
"log/slog"
8+
"math"
89
"net/http"
910
"time"
1011

@@ -44,6 +45,19 @@ func (db *Database) GetProjectsFromContext(c *gin.Context, orgIdKey string) ([]P
4445
return projects, true
4546
}
4647

48+
func (db *Database) GetProjectsRemainingInFreePLan(orgId uint) (uint, uint, uint, error) {
49+
50+
var countOfMonitoredProjects int64
51+
err := db.GormDB.Model(&Project{}).Where("organisation_id = ? AND drift_enabled = ?", orgId, true).Count(&countOfMonitoredProjects).Error
52+
if err != nil {
53+
slog.Error("Error fetching project count", "error", err)
54+
return 0, 0, 0, err
55+
}
56+
remainingFreeProjects := uint(math.Max(0, float64(MaxFreePlanProjectsPerOrg)-float64(countOfMonitoredProjects)))
57+
billableProjectsCount := uint(math.Max(0, float64(countOfMonitoredProjects)-float64(MaxFreePlanProjectsPerOrg)))
58+
return uint(countOfMonitoredProjects), remainingFreeProjects, billableProjectsCount, nil
59+
}
60+
4761
func (db *Database) GetReposFromContext(c *gin.Context, orgIdKey string) ([]Repo, bool) {
4862
loggedInOrganisationId, exists := c.Get(orgIdKey)
4963

0 commit comments

Comments
 (0)