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
This commit is contained in:
nutcas3
2026-05-02 04:34:59 +03:00
parent 9414a8f871
commit 96ca53e790
2 changed files with 328 additions and 0 deletions
@@ -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)
}
@@ -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)
}