mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-10-28 15:51:15 +00:00
feat: scheduled report digests + per-session capacity (M33)
POST /v1/internal/jobs/reports/dispatch emails each tenant owner a trailing-window digest (bookings, revenue, per-service and per-staff breakdown) localized to the tenant locale; zero-activity tenants are skipped. Metrics cores extracted to tenant-scoped variants so the job needs no principal. PATCH /v1/catalog/class-sessions/:id sets a session's seat count with a confirmed-bookings floor — shrinking below it needs allowOverCapacity, matching the waitlist-promotion convention. Session rows gain an inline capacity editor with an overbook confirm state. 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
e6f1ec38a4
commit
e680e0c2b8
14 files changed
+808
-10
No files matched your search
@@ -25,6 +25,7 @@ import (
|
||||
"bookra/apps/backend/internal/export"
|
||||
"bookra/apps/backend/internal/httpx"
|
||||
"bookra/apps/backend/internal/notifications"
|
||||
"bookra/apps/backend/internal/reports"
|
||||
"bookra/apps/backend/internal/sms"
|
||||
"bookra/apps/backend/internal/tenancy"
|
||||
|
||||
@@ -65,6 +66,7 @@ func NewServer(cfg config.Config, pools *db.Pools) (*Server, error) {
|
||||
authService := auth.NewService(repository, cfg.AuthJWTSecret)
|
||||
adminService := admin.NewService(repository, cfg.AdminEmail, cfg.AdminKey)
|
||||
smsService := sms.NewService(cfg, repository)
|
||||
reportService := reports.NewService(repository, bookingService, cfg.FrontendURL, cfg.EmailFrom)
|
||||
notificationService.SetSMSSender(func(ctx context.Context, tenantID, phone, body string) error {
|
||||
_, err := smsService.SendMessage(ctx, tenantID, domain.SendSMSRequest{To: phone, Body: body})
|
||||
return err
|
||||
@@ -1103,6 +1105,28 @@ func NewServer(cfg config.Config, pools *db.Pools) (*Server, error) {
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
})
|
||||
protected.PATCH("/catalog/class-sessions/:sessionID", func(c *gin.Context) {
|
||||
var request domain.UpdateClassSessionRequest
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request"})
|
||||
return
|
||||
}
|
||||
response, err := catalogService.UpdateClassSession(c.Request.Context(), auth.PrincipalFromContext(c), c.Param("sessionID"), request)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
switch {
|
||||
case errors.Is(err, catalog.ErrTenantMembership), errors.Is(err, catalog.ErrClassSessionNotFound):
|
||||
status = http.StatusNotFound
|
||||
case errors.Is(err, catalog.ErrInvalidInput):
|
||||
status = http.StatusBadRequest
|
||||
case errors.Is(err, catalog.ErrCapacityBelowBooked):
|
||||
status = http.StatusConflict
|
||||
}
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
})
|
||||
protected.DELETE("/catalog/class-sessions/:sessionID", func(c *gin.Context) {
|
||||
err := catalogService.DeleteClassSession(c.Request.Context(), auth.PrincipalFromContext(c), c.Param("sessionID"))
|
||||
if err != nil {
|
||||
@@ -1697,6 +1721,23 @@ func NewServer(cfg config.Config, pools *db.Pools) (*Server, error) {
|
||||
c.JSON(http.StatusOK, response)
|
||||
})
|
||||
|
||||
server.router.POST("/v1/internal/jobs/reports/dispatch", func(c *gin.Context) {
|
||||
if !authorizeJobRunner(c, cfg) {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||
return
|
||||
}
|
||||
days, _ := strconv.Atoi(c.DefaultQuery("days", "7"))
|
||||
response, err := reportService.DispatchDigests(c.Request.Context(), days, c.Query("tenantId"), func(ctx context.Context, msg notifications.EmailMessage) error {
|
||||
_, sendErr := notificationService.SendRawEmail(ctx, msg)
|
||||
return sendErr
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
})
|
||||
|
||||
// Widget embeddable script endpoint - serves JavaScript for external sites
|
||||
server.router.GET("/v1/public/widget.js", func(c *gin.Context) {
|
||||
c.Header("Content-Type", "application/javascript; charset=utf-8")
|
||||
|
||||
Reference in new issue
Block a user