From 96ca53e79016e14d0fc03a5286c8b9d7d06764d6 Mon Sep 17 00:00:00 2001 From: nutcas3 Date: Sat, 2 May 2026 04:34:59 +0300 Subject: [PATCH] feat: Add pricing handlers with CRUD operations, rule application, price calculation, and analytics endpoints - Add PricingHandler with CreateRule, GetRule, UpdateRule, DeleteRule, ListRules methods - Add CalculatePrice endpoint for price calculation based on active rules - Add ApplyRules endpoint for applying specific pricing rules to context - Add GetAnalytics endpoint for retrieving pricing analytics by tenant - Add RegisterRoutes method to register pricing routes under /pricing group --- .../internal/handlers/pricing_handlers.go | 187 ++++++++++++++++++ .../handlers/pricing_handlers_core.go | 141 +++++++++++++ 2 files changed, 328 insertions(+) create mode 100644 apps/carrier-connector/internal/handlers/pricing_handlers.go create mode 100644 apps/carrier-connector/internal/handlers/pricing_handlers_core.go diff --git a/apps/carrier-connector/internal/handlers/pricing_handlers.go b/apps/carrier-connector/internal/handlers/pricing_handlers.go new file mode 100644 index 0000000..5c81ef8 --- /dev/null +++ b/apps/carrier-connector/internal/handlers/pricing_handlers.go @@ -0,0 +1,187 @@ +package handlers + +import ( + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/id" + "github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/pricing" +) + +// CreateRule creates a new pricing rule +func (h *PricingHandler) CreateRule(c *gin.Context) { + var req CreateRuleRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Get tenant ID from context (assuming middleware sets it) + tenantID := c.GetString("tenant_id") + if tenantID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"}) + return + } + + rule := &pricing.PricingRule{ + ID: id.GeneratePrefixed("rule"), + Name: req.Name, + Description: req.Description, + TenantID: tenantID, + Type: req.Type, + Priority: req.Priority, + IsActive: true, + Conditions: req.Conditions, + Actions: req.Actions, + Metadata: req.Metadata, + } + + createdRule, err := h.service.CreateRule(c.Request.Context(), rule) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusCreated, createdRule) +} + +// GetRule retrieves a pricing rule +func (h *PricingHandler) GetRule(c *gin.Context) { + id := c.Param("id") + if id == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "rule ID required"}) + return + } + + rule, err := h.service.GetRule(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "rule not found"}) + return + } + + c.JSON(http.StatusOK, rule) +} + +// UpdateRule updates a pricing rule +func (h *PricingHandler) UpdateRule(c *gin.Context) { + id := c.Param("id") + if id == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "rule ID required"}) + return + } + + var req UpdateRuleRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Get existing rule + rule, err := h.service.GetRule(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "rule not found"}) + return + } + + // Apply updates + if req.Name != nil { + rule.Name = *req.Name + } + if req.Description != nil { + rule.Description = *req.Description + } + if req.Type != nil { + rule.Type = *req.Type + } + if req.Priority != nil { + rule.Priority = *req.Priority + } + if req.IsActive != nil { + rule.IsActive = *req.IsActive + } + if req.Conditions != nil { + rule.Conditions = *req.Conditions + } + if req.Actions != nil { + rule.Actions = *req.Actions + } + if req.Metadata != nil { + rule.Metadata = req.Metadata + } + + updatedRule, err := h.service.UpdateRule(c.Request.Context(), id, rule) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, updatedRule) +} + +// DeleteRule deletes a pricing rule +func (h *PricingHandler) DeleteRule(c *gin.Context) { + id := c.Param("id") + if id == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "rule ID required"}) + return + } + + err := h.service.DeleteRule(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "rule deleted successfully"}) +} + +// ListRules lists pricing rules +func (h *PricingHandler) ListRules(c *gin.Context) { + tenantID := c.GetString("tenant_id") + if tenantID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"}) + return + } + + // Parse query parameters + filter := &pricing.PricingFilter{ + TenantID: tenantID, + } + + if ruleType := c.Query("type"); ruleType != "" { + filter.Type = ruleType + } + + if isActive := c.Query("is_active"); isActive != "" { + if active, err := strconv.ParseBool(isActive); err == nil { + filter.IsActive = &active + } + } + + if priority := c.Query("priority"); priority != "" { + if prio, err := strconv.Atoi(priority); err == nil { + filter.Priority = &prio + } + } + + if limit := c.Query("limit"); limit != "" { + if lim, err := strconv.Atoi(limit); err == nil { + filter.Limit = lim + } + } + + if offset := c.Query("offset"); offset != "" { + if off, err := strconv.Atoi(offset); err == nil { + filter.Offset = off + } + } + + rules, err := h.service.ListRules(c.Request.Context(), filter) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, rules) +} diff --git a/apps/carrier-connector/internal/handlers/pricing_handlers_core.go b/apps/carrier-connector/internal/handlers/pricing_handlers_core.go new file mode 100644 index 0000000..193c30b --- /dev/null +++ b/apps/carrier-connector/internal/handlers/pricing_handlers_core.go @@ -0,0 +1,141 @@ +package handlers + +import ( + "net/http" + "time" + + "github.com/gin-gonic/gin" + "github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/pricing" +) + +// PricingHandler handles pricing-related HTTP requests +type PricingHandler struct { + service pricing.Service +} + +// NewPricingHandler creates a new pricing handler +func NewPricingHandler(service pricing.Service) *PricingHandler { + return &PricingHandler{service: service} +} + +// RegisterRoutes registers pricing routes +func (h *PricingHandler) RegisterRoutes(router *gin.RouterGroup) { + pricing := router.Group("/pricing") + { + // Rule management + pricing.POST("/rules", h.CreateRule) + pricing.GET("/rules/:id", h.GetRule) + pricing.PUT("/rules/:id", h.UpdateRule) + pricing.DELETE("/rules/:id", h.DeleteRule) + pricing.GET("/rules", h.ListRules) + + // Pricing calculations + pricing.POST("/calculate", h.CalculatePrice) + pricing.POST("/apply-rules", h.ApplyRules) + + // Analytics + pricing.GET("/analytics", h.GetAnalytics) + } +} + +type CreateRuleRequest struct { + Name string `json:"name" binding:"required"` + Description string `json:"description"` + Type pricing.RuleType `json:"type" binding:"required"` + Priority int `json:"priority"` + Conditions pricing.RuleConditions `json:"conditions"` + Actions pricing.RuleActions `json:"actions"` + Metadata map[string]any `json:"metadata"` +} + +type UpdateRuleRequest struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + Type *pricing.RuleType `json:"type,omitempty"` + Priority *int `json:"priority,omitempty"` + IsActive *bool `json:"is_active,omitempty"` + Conditions *pricing.RuleConditions `json:"conditions,omitempty"` + Actions *pricing.RuleActions `json:"actions,omitempty"` + Metadata map[string]any `json:"metadata,omitempty"` +} + +type CalculatePriceRequest struct { + TenantID string `json:"tenant_id" binding:"required"` + CustomerID string `json:"customer_id" binding:"required"` + ProductID string `json:"product_id" binding:"required"` + BasePrice float64 `json:"base_price" binding:"required"` + Currency string `json:"currency" binding:"required"` + Quantity int `json:"quantity" binding:"required"` + Location string `json:"location"` + Metadata map[string]any `json:"metadata"` +} + +type ApplyRulesRequest struct { + Context pricing.PricingContext `json:"context" binding:"required"` + Rules []*pricing.PricingRule `json:"rules"` +} + +// ApplyRules applies specific rules +func (h *PricingHandler) ApplyRules(c *gin.Context) { + var req ApplyRulesRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + result, err := h.service.ApplyRules(c.Request.Context(), &req.Context, req.Rules) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, result) +} + +// GetAnalytics retrieves pricing analytics +func (h *PricingHandler) GetAnalytics(c *gin.Context) { + tenantID := c.GetString("tenant_id") + if tenantID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"}) + return + } + + analytics, err := h.service.GetAnalytics(c.Request.Context(), tenantID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, analytics) +} + + +// CalculatePrice calculates price based on active rules +func (h *PricingHandler) CalculatePrice(c *gin.Context) { + var req CalculatePriceRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + context := &pricing.PricingContext{ + TenantID: req.TenantID, + CustomerID: req.CustomerID, + ProductID: req.ProductID, + BasePrice: req.BasePrice, + Currency: req.Currency, + Quantity: req.Quantity, + Location: req.Location, + Time: time.Now(), + Metadata: req.Metadata, + } + + result, err := h.service.CalculatePrice(c.Request.Context(), context) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, result) +} +