mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-10-28 15:51:15 +00:00
refactor(db): replace sqlc with handwritten pgx repos
- Finish Paddle-to-Stripe billing migration; drop paddle/stripe libs - Renumber migrations; add 00012 memberships+manage_token, 00013 location_type_capacity - Add tests: admin, auth, bookings customer service - Remove graphify-out artifacts and stale env files Authored By: TDvorak <[email protected]> Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
2039669e2c
commit
f2f7439050
93 files changed
+6987
-69060
No files matched your search
@@ -49,8 +49,8 @@ func NewServer(cfg config.Config, pools *db.Pools) (*Server, error) {
|
||||
|
||||
repository := db.NewRepository(pools, cfg.DemoMode)
|
||||
notificationService := notifications.NewService(cfg, repository)
|
||||
bookingService := bookings.NewService(repository, notificationService)
|
||||
customerBookingService := bookings.NewCustomerService(repository, notificationService)
|
||||
bookingService := bookings.NewService(repository, notificationService, cfg.FrontendURL)
|
||||
customerBookingService := bookings.NewCustomerService(repository, notificationService, cfg.FrontendURL)
|
||||
tenantService := tenancy.NewService(repository)
|
||||
billingService := billing.NewService(cfg, repository)
|
||||
catalogService := catalog.NewService(repository, billingService, notificationService)
|
||||
@@ -73,9 +73,10 @@ func NewServer(cfg config.Config, pools *db.Pools) (*Server, error) {
|
||||
|
||||
server.router.Use(gin.Logger(), gin.Recovery(), cors.New(cors.Config{
|
||||
AllowOrigins: allowedOrigins(cfg),
|
||||
AllowHeaders: []string{"Authorization", "Content-Type"},
|
||||
AllowMethods: []string{"GET", "POST", "OPTIONS"},
|
||||
AllowHeaders: []string{"Authorization", "Content-Type", "X-Admin-Email", "X-Admin-Key"},
|
||||
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
|
||||
AllowCredentials: true,
|
||||
MaxAge: 12 * time.Hour,
|
||||
}), httpx.SecurityHeaders())
|
||||
|
||||
server.router.GET("/healthz", func(c *gin.Context) {
|
||||
@@ -86,24 +87,24 @@ func NewServer(cfg config.Config, pools *db.Pools) (*Server, error) {
|
||||
})
|
||||
})
|
||||
|
||||
// Test endpoint for Sentry
|
||||
server.router.GET("/debug/sentry", func(c *gin.Context) {
|
||||
sentry.CaptureMessage("Test message from Bookra API")
|
||||
c.JSON(http.StatusOK, gin.H{"status": "sent", "message": "Test error sent to Sentry"})
|
||||
})
|
||||
|
||||
// Test endpoint for billing
|
||||
server.router.GET("/debug/billing-test", func(c *gin.Context) {
|
||||
if billingService != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "ok",
|
||||
"billingService": "initialized",
|
||||
"message": "Billing service is working",
|
||||
})
|
||||
} else {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok", "billingService": "not initialized"})
|
||||
}
|
||||
})
|
||||
if cfg.Environment == "development" || cfg.Environment == "test" {
|
||||
// Test endpoints, only exposed outside production
|
||||
server.router.GET("/debug/sentry", func(c *gin.Context) {
|
||||
sentry.CaptureMessage("Test message from Bookra API")
|
||||
c.JSON(http.StatusOK, gin.H{"status": "sent", "message": "Test error sent to Sentry"})
|
||||
})
|
||||
server.router.GET("/debug/billing-test", func(c *gin.Context) {
|
||||
if billingService != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "ok",
|
||||
"billingService": "initialized",
|
||||
"message": "Billing service is working",
|
||||
})
|
||||
} else {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok", "billingService": "not initialized"})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
server.router.GET("/v1/meta/config", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
@@ -119,6 +120,7 @@ func NewServer(cfg config.Config, pools *db.Pools) (*Server, error) {
|
||||
// AUTH API
|
||||
// ============================================
|
||||
authGroup := server.router.Group("/v1/auth")
|
||||
authGroup.Use(publicRateLimiter.Middleware())
|
||||
{
|
||||
authGroup.POST("/register", func(c *gin.Context) {
|
||||
var request struct {
|
||||
@@ -255,7 +257,7 @@ func NewServer(cfg config.Config, pools *db.Pools) (*Server, error) {
|
||||
// ADMIN API
|
||||
// ============================================
|
||||
adminGroup := server.router.Group("/v1/admin")
|
||||
adminGroup.Use(admin.RequireAdmin(adminService, authService))
|
||||
adminGroup.Use(admin.RequireAdmin(adminService, verifier, repository, cfg.DemoMode))
|
||||
{
|
||||
adminGroup.GET("/stats", func(c *gin.Context) {
|
||||
stats, err := adminService.GetDashboardStats(c.Request.Context())
|
||||
@@ -294,10 +296,9 @@ func NewServer(cfg config.Config, pools *db.Pools) (*Server, error) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request"})
|
||||
return
|
||||
}
|
||||
adminUserID, _ := c.Get("userID")
|
||||
err := adminService.UpdateUserRole(
|
||||
c.Request.Context(),
|
||||
adminUserID.(string),
|
||||
admin.ActorFromContext(c),
|
||||
c.Param("userID"),
|
||||
request.Role,
|
||||
c.ClientIP(),
|
||||
@@ -433,6 +434,45 @@ func NewServer(cfg config.Config, pools *db.Pools) (*Server, error) {
|
||||
c.JSON(http.StatusCreated, response)
|
||||
})
|
||||
|
||||
protected.PUT("/tenants/brand", func(c *gin.Context) {
|
||||
var request domain.UpdateBrandRequest
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request"})
|
||||
return
|
||||
}
|
||||
response, err := tenantService.UpdateBrand(c.Request.Context(), auth.PrincipalFromContext(c), request)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
})
|
||||
protected.GET("/tenants/email-templates", func(c *gin.Context) {
|
||||
response, err := tenantService.ListEmailTemplates(c.Request.Context(), auth.PrincipalFromContext(c))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"templates": response})
|
||||
})
|
||||
protected.PUT("/tenants/email-template", func(c *gin.Context) {
|
||||
var request domain.UpdateEmailTemplateRequest
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request"})
|
||||
return
|
||||
}
|
||||
response, err := tenantService.UpdateEmailTemplate(c.Request.Context(), auth.PrincipalFromContext(c), request)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
if errors.Is(err, tenancy.ErrInvalidOnboarding) {
|
||||
status = http.StatusBadRequest
|
||||
}
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
})
|
||||
|
||||
// ============================================
|
||||
// CATALOG API - Locations / Zones
|
||||
// ============================================
|
||||
@@ -501,6 +541,29 @@ func NewServer(cfg config.Config, pools *db.Pools) (*Server, error) {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "deleted"})
|
||||
})
|
||||
|
||||
protected.POST("/catalog/bookings", func(c *gin.Context) {
|
||||
var request domain.DashboardBookingRequest
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request"})
|
||||
return
|
||||
}
|
||||
response, err := catalogService.CreateBooking(c.Request.Context(), auth.PrincipalFromContext(c), request)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
switch {
|
||||
case errors.Is(err, catalog.ErrTenantMembership):
|
||||
status = http.StatusNotFound
|
||||
case errors.Is(err, catalog.ErrInvalidBooking):
|
||||
status = http.StatusBadRequest
|
||||
case errors.Is(err, catalog.ErrBookingConflict):
|
||||
status = http.StatusConflict
|
||||
}
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, response)
|
||||
})
|
||||
|
||||
// ============================================
|
||||
// CATALOG API - Blocked Days
|
||||
// ============================================
|
||||
@@ -691,8 +754,11 @@ func NewServer(cfg config.Config, pools *db.Pools) (*Server, error) {
|
||||
response, err := customerBookingService.GetBookingByReference(c.Request.Context(), c.Param("reference"), token)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
if errors.Is(err, bookings.ErrBookingNotFound) {
|
||||
switch {
|
||||
case errors.Is(err, bookings.ErrBookingNotFound):
|
||||
status = http.StatusNotFound
|
||||
case errors.Is(err, bookings.ErrUnauthorized):
|
||||
status = http.StatusForbidden
|
||||
}
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
@@ -712,10 +778,14 @@ func NewServer(cfg config.Config, pools *db.Pools) (*Server, error) {
|
||||
switch {
|
||||
case errors.Is(err, bookings.ErrBookingNotFound):
|
||||
status = http.StatusNotFound
|
||||
case errors.Is(err, bookings.ErrUnauthorized):
|
||||
status = http.StatusForbidden
|
||||
case errors.Is(err, bookings.ErrBookingCancelled):
|
||||
status = http.StatusConflict
|
||||
case errors.Is(err, bookings.ErrInvalidReschedule):
|
||||
status = http.StatusBadRequest
|
||||
case errors.Is(err, bookings.ErrBookingConflict):
|
||||
status = http.StatusConflict
|
||||
}
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
@@ -730,6 +800,8 @@ func NewServer(cfg config.Config, pools *db.Pools) (*Server, error) {
|
||||
switch {
|
||||
case errors.Is(err, bookings.ErrBookingNotFound):
|
||||
status = http.StatusNotFound
|
||||
case errors.Is(err, bookings.ErrUnauthorized):
|
||||
status = http.StatusForbidden
|
||||
case errors.Is(err, bookings.ErrBookingCancelled):
|
||||
status = http.StatusConflict
|
||||
}
|
||||
@@ -766,7 +838,7 @@ func NewServer(cfg config.Config, pools *db.Pools) (*Server, error) {
|
||||
if errors.Is(err, billing.ErrBillingPlanUnsupported) {
|
||||
status = http.StatusBadRequest
|
||||
}
|
||||
if errors.Is(err, billing.ErrPaddleNotConfigured) {
|
||||
if errors.Is(err, billing.ErrStripeNotConfigured) {
|
||||
status = http.StatusServiceUnavailable
|
||||
}
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
@@ -781,7 +853,7 @@ func NewServer(cfg config.Config, pools *db.Pools) (*Server, error) {
|
||||
if errors.Is(err, billing.ErrBillingMembership) {
|
||||
status = http.StatusNotFound
|
||||
}
|
||||
if errors.Is(err, billing.ErrPaddleNotConfigured) {
|
||||
if errors.Is(err, billing.ErrStripeNotConfigured) {
|
||||
status = http.StatusServiceUnavailable
|
||||
}
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
@@ -798,7 +870,7 @@ func NewServer(cfg config.Config, pools *db.Pools) (*Server, error) {
|
||||
status = http.StatusNotFound
|
||||
case errors.Is(err, billing.ErrBillingCustomerMissing):
|
||||
status = http.StatusBadRequest
|
||||
case errors.Is(err, billing.ErrPaddleNotConfigured):
|
||||
case errors.Is(err, billing.ErrStripeNotConfigured):
|
||||
status = http.StatusServiceUnavailable
|
||||
}
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
@@ -942,13 +1014,6 @@ func NewServer(cfg config.Config, pools *db.Pools) (*Server, error) {
|
||||
c.JSON(http.StatusOK, response)
|
||||
})
|
||||
|
||||
server.router.POST("/v1/webhooks/paddle", func(c *gin.Context) {
|
||||
if err := billingService.HandleWebhook(c.Request.Context(), c.Request); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"status": "accepted"})
|
||||
})
|
||||
server.router.POST("/v1/webhooks/stripe", func(c *gin.Context) {
|
||||
if err := billingService.HandleStripeWebhook(c.Request.Context(), c.Request); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
@@ -956,13 +1021,6 @@ func NewServer(cfg config.Config, pools *db.Pools) (*Server, error) {
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"status": "accepted"})
|
||||
})
|
||||
server.router.POST("/api/paddle_webhook", func(c *gin.Context) {
|
||||
if err := billingService.HandleWebhook(c.Request.Context(), c.Request); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"status": "accepted"})
|
||||
})
|
||||
|
||||
server.router.POST("/v1/internal/jobs/reminders/dispatch", func(c *gin.Context) {
|
||||
if !authorizeJobRunner(c, cfg) {
|
||||
|
||||
Reference in new issue
Block a user