mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-10-18 02:52:53 +00:00
refactor: Add rate plan currency integration service and update tenant repository with user count method
- Add RatePlanCurrencyIntegrator with SubscribeToPlanWithCurrency, CalculatePlanCostInCurrency, calculateOverageCost methods - Add NewRatePlanCurrencyIntegrator constructor with billingService, exchangeService, ratePlanService, logger, baseCurrency fields - Add currency conversion support for rate plan subscriptions with exchange rate metadata tracking - Add billing integration for initial subscription
This commit is contained in:
@@ -4,11 +4,14 @@ go 1.26
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.12.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/prometheus/client_golang v1.20.5
|
||||
github.com/rabbitmq/amqp091-go v1.11.0
|
||||
github.com/rs/zerolog v1.35.0
|
||||
github.com/sirupsen/logrus v1.9.4
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/utrack/gin-csrf v0.0.0-20190424104817-40fb8d2c8fca
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
)
|
||||
@@ -57,7 +60,6 @@ require (
|
||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
github.com/utrack/gin-csrf v0.0.0-20190424104817-40fb8d2c8fca // indirect
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||
golang.org/x/arch v0.22.0 // indirect
|
||||
golang.org/x/crypto v0.50.0 // indirect
|
||||
|
||||
@@ -47,6 +47,8 @@ github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
@@ -124,6 +126,8 @@ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjR
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/rs/zerolog v1.35.0 h1:VD0ykx7HMiMJytqINBsKcbLS+BJ4WYjz+05us+LRTdI=
|
||||
github.com/rs/zerolog v1.35.0/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
|
||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/currency"
|
||||
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/rateplan"
|
||||
)
|
||||
|
||||
// RatePlanCurrencyIntegrator integrates currency system with rate plans
|
||||
type RatePlanCurrencyIntegrator struct {
|
||||
billingService currency.BillingService
|
||||
exchangeService currency.ExchangeRateService
|
||||
ratePlanService rateplan.Service
|
||||
logger *logrus.Logger
|
||||
baseCurrency string
|
||||
}
|
||||
|
||||
// NewRatePlanCurrencyIntegrator creates a new rate plan currency integrator
|
||||
func NewRatePlanCurrencyIntegrator(
|
||||
billingService currency.BillingService,
|
||||
exchangeService currency.ExchangeRateService,
|
||||
ratePlanService rateplan.Service,
|
||||
logger *logrus.Logger,
|
||||
baseCurrency string,
|
||||
) *RatePlanCurrencyIntegrator {
|
||||
return &RatePlanCurrencyIntegrator{
|
||||
billingService: billingService,
|
||||
exchangeService: exchangeService,
|
||||
ratePlanService: ratePlanService,
|
||||
logger: logger,
|
||||
baseCurrency: baseCurrency,
|
||||
}
|
||||
}
|
||||
|
||||
// SubscribeToPlanWithCurrency subscribes to a rate plan with currency conversion
|
||||
func (rpci *RatePlanCurrencyIntegrator) SubscribeToPlanWithCurrency(ctx context.Context, profileID string, planID string, targetCurrency string) (*rateplan.RatePlanSubscription, error) {
|
||||
// Get the rate plan
|
||||
plan, err := rpci.ratePlanService.GetRatePlan(ctx, planID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get rate plan: %w", err)
|
||||
}
|
||||
|
||||
// Convert price to requested currency if needed
|
||||
subscriptionPrice := plan.BasePrice
|
||||
exchangeRate := 1.0
|
||||
|
||||
if targetCurrency != plan.Currency {
|
||||
conversion, err := rpci.billingService.ConvertAmount(ctx, ¤cy.CurrencyConversionRequest{
|
||||
Amount: plan.BasePrice,
|
||||
FromCurrency: plan.Currency,
|
||||
ToCurrency: targetCurrency,
|
||||
})
|
||||
if err != nil {
|
||||
rpci.logger.WithError(err).Error("Failed to convert rate plan price")
|
||||
return nil, fmt.Errorf("currency conversion failed: %w", err)
|
||||
}
|
||||
subscriptionPrice = conversion.ConvertedAmount
|
||||
exchangeRate = conversion.ExchangeRate
|
||||
}
|
||||
|
||||
// Create subscription with currency information
|
||||
subscription := &rateplan.RatePlanSubscription{
|
||||
ProfileID: profileID,
|
||||
RatePlanID: planID,
|
||||
Status: rateplan.SubscriptionStatusActive,
|
||||
StartedAt: time.Now(),
|
||||
Metadata: map[string]interface{}{
|
||||
"original_currency": plan.Currency,
|
||||
"subscription_currency": targetCurrency,
|
||||
"original_price": plan.BasePrice,
|
||||
"subscription_price": subscriptionPrice,
|
||||
"exchange_rate": exchangeRate,
|
||||
},
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Create subscription request
|
||||
subscribeReq := &rateplan.SubscribeRequest{
|
||||
ProfileID: profileID,
|
||||
RatePlanID: planID,
|
||||
AutoRenew: true,
|
||||
Metadata: subscription.Metadata,
|
||||
}
|
||||
|
||||
createdSubscription, err := rpci.ratePlanService.SubscribeToPlan(ctx, subscribeReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create subscription: %w", err)
|
||||
}
|
||||
|
||||
// Process initial billing
|
||||
billingReq := ¤cy.BillingRequest{
|
||||
ProfileID: profileID,
|
||||
SubscriptionID: createdSubscription.ID,
|
||||
Amount: subscriptionPrice,
|
||||
Currency: targetCurrency,
|
||||
Description: fmt.Sprintf("Initial subscription to %s", plan.Name),
|
||||
BillingDate: time.Now(),
|
||||
}
|
||||
|
||||
_, err = rpci.billingService.ProcessBilling(ctx, billingReq)
|
||||
if err != nil {
|
||||
rpci.logger.WithError(err).Error("Failed to process initial billing")
|
||||
// Don't fail the subscription if billing fails, but log it
|
||||
}
|
||||
|
||||
rpci.logger.WithFields(logrus.Fields{
|
||||
"profile_id": profileID,
|
||||
"plan_id": planID,
|
||||
"currency": targetCurrency,
|
||||
"subscription_id": createdSubscription.ID,
|
||||
}).Info("Rate plan subscription created with currency support")
|
||||
|
||||
return createdSubscription, nil
|
||||
}
|
||||
|
||||
// CalculatePlanCostInCurrency calculates the cost of a rate plan in a specific currency
|
||||
func (rpci *RatePlanCurrencyIntegrator) CalculatePlanCostInCurrency(ctx context.Context, planID string, targetCurrency string, usageData *rateplan.RatePlanUsage) (*currency.BillingSummary, error) {
|
||||
// Get the rate plan
|
||||
plan, err := rpci.ratePlanService.GetRatePlan(ctx, planID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get rate plan: %w", err)
|
||||
}
|
||||
|
||||
// Calculate base cost
|
||||
baseCost := plan.BasePrice
|
||||
|
||||
// Add overage costs if usage data is provided
|
||||
if usageData != nil {
|
||||
overageCost, err := rpci.calculateOverageCost(ctx, plan, usageData)
|
||||
if err != nil {
|
||||
rpci.logger.WithError(err).Warn("Failed to calculate overage cost")
|
||||
} else {
|
||||
baseCost += overageCost
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to requested currency
|
||||
convertedCost := baseCost
|
||||
exchangeRate := 1.0
|
||||
|
||||
if targetCurrency != plan.Currency {
|
||||
conversion, err := rpci.exchangeService.ConvertAmount(ctx, baseCost, plan.Currency, targetCurrency)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("currency conversion failed: %w", err)
|
||||
}
|
||||
convertedCost = conversion.ConvertedAmount
|
||||
exchangeRate = conversion.ExchangeRate
|
||||
}
|
||||
|
||||
// Create billing summary
|
||||
summary := ¤cy.BillingSummary{
|
||||
ProfileID: usageData.ProfileID,
|
||||
TotalAmount: convertedCost,
|
||||
Currency: targetCurrency,
|
||||
BaseTotalAmount: baseCost,
|
||||
BaseCurrency: plan.Currency,
|
||||
TransactionCount: 1,
|
||||
FromDate: time.Now().AddDate(0, -1, 0),
|
||||
ToDate: time.Now(),
|
||||
Breakdown: map[string]interface{}{
|
||||
"plan_id": planID,
|
||||
"plan_name": plan.Name,
|
||||
"base_cost": plan.BasePrice,
|
||||
"overage_cost": baseCost - plan.BasePrice,
|
||||
"exchange_rate": exchangeRate,
|
||||
"original_currency": plan.Currency,
|
||||
},
|
||||
}
|
||||
|
||||
return summary, nil
|
||||
}
|
||||
|
||||
// calculateOverageCost calculates overage costs for usage
|
||||
func (rpci *RatePlanCurrencyIntegrator) calculateOverageCost(ctx context.Context, plan *rateplan.RatePlan, usage *rateplan.RatePlanUsage) (float64, error) {
|
||||
overageCost := 0.0
|
||||
|
||||
// Calculate data overage
|
||||
if plan.DataAllowance != nil && usage.DataUsed > plan.DataAllowance.Amount {
|
||||
dataOverage := usage.DataUsed - plan.DataAllowance.Amount
|
||||
if plan.OverageRates != nil {
|
||||
overageCost += float64(dataOverage) * plan.OverageRates.DataRate
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate voice overage
|
||||
if plan.VoiceAllowance != nil && usage.VoiceUsed > plan.VoiceAllowance.Minutes {
|
||||
voiceOverage := usage.VoiceUsed - plan.VoiceAllowance.Minutes
|
||||
if plan.OverageRates != nil {
|
||||
overageCost += float64(voiceOverage) * plan.OverageRates.VoiceRate
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate SMS overage
|
||||
if plan.SMSAllowance != nil && usage.SMSUsed > plan.SMSAllowance.Messages {
|
||||
smsOverage := usage.SMSUsed - plan.SMSAllowance.Messages
|
||||
if plan.OverageRates != nil {
|
||||
overageCost += float64(smsOverage) * plan.OverageRates.SMSRate
|
||||
}
|
||||
}
|
||||
|
||||
return overageCost, nil
|
||||
}
|
||||
@@ -2,27 +2,23 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
|
||||
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/tenant"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// GormTenantRepository implements the tenant repository interface using GORM
|
||||
type GormTenantRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewGormTenantRepository creates a new GORM tenant repository
|
||||
func NewGormTenantRepository(db *gorm.DB) TenantRepository {
|
||||
return &GormTenantRepository{db: db}
|
||||
}
|
||||
|
||||
// CreateTenant creates a new tenant
|
||||
func (r *GormTenantRepository) CreateTenant(ctx context.Context, tenant *tenant.Tenant) error {
|
||||
return r.db.WithContext(ctx).Create(tenant).Error
|
||||
}
|
||||
|
||||
// GetTenant retrieves a tenant by ID
|
||||
func (r *GormTenantRepository) GetTenant(ctx context.Context, id string) (*tenant.Tenant, error) {
|
||||
var tenant tenant.Tenant
|
||||
err := r.db.WithContext(ctx).Where("id = ?", id).First(&tenant).Error
|
||||
@@ -56,7 +52,6 @@ func (r *GormTenantRepository) DeleteTenant(ctx context.Context, id string) erro
|
||||
func (r *GormTenantRepository) ListTenants(ctx context.Context, filter *tenant.TenantFilter) ([]*tenant.Tenant, error) {
|
||||
query := r.db.WithContext(ctx).Model(&tenant.Tenant{})
|
||||
|
||||
// Apply filters
|
||||
if filter.ID != "" {
|
||||
query = query.Where("id = ?", filter.ID)
|
||||
}
|
||||
@@ -97,11 +92,9 @@ func (r *GormTenantRepository) ListTenants(ctx context.Context, filter *tenant.T
|
||||
return tenants, err
|
||||
}
|
||||
|
||||
// CountTenants counts tenants with filtering
|
||||
func (r *GormTenantRepository) CountTenants(ctx context.Context, filter *tenant.TenantFilter) (int, error) {
|
||||
query := r.db.WithContext(ctx).Model(&tenant.Tenant{})
|
||||
|
||||
// Apply filters
|
||||
if filter.ID != "" {
|
||||
query = query.Where("id = ?", filter.ID)
|
||||
}
|
||||
@@ -123,12 +116,10 @@ func (r *GormTenantRepository) CountTenants(ctx context.Context, filter *tenant.
|
||||
return int(count), err
|
||||
}
|
||||
|
||||
// CreateTenantUser creates a new tenant user
|
||||
func (r *GormTenantRepository) CreateTenantUser(ctx context.Context, user *tenant.TenantUser) error {
|
||||
return r.db.WithContext(ctx).Create(user).Error
|
||||
}
|
||||
|
||||
// GetTenantUser retrieves a tenant user
|
||||
func (r *GormTenantRepository) GetTenantUser(ctx context.Context, tenantID, userID string) (*tenant.TenantUser, error) {
|
||||
var user tenant.TenantUser
|
||||
err := r.db.WithContext(ctx).Where("tenant_id = ? AND user_id = ?", tenantID, userID).First(&user).Error
|
||||
@@ -138,21 +129,17 @@ func (r *GormTenantRepository) GetTenantUser(ctx context.Context, tenantID, user
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// UpdateTenantUser updates a tenant user
|
||||
func (r *GormTenantRepository) UpdateTenantUser(ctx context.Context, user *tenant.TenantUser) error {
|
||||
return r.db.WithContext(ctx).Save(user).Error
|
||||
}
|
||||
|
||||
// DeleteTenantUser deletes a tenant user
|
||||
func (r *GormTenantRepository) DeleteTenantUser(ctx context.Context, tenantID, userID string) error {
|
||||
return r.db.WithContext(ctx).Delete(&tenant.TenantUser{}, "tenant_id = ? AND user_id = ?", tenantID, userID).Error
|
||||
}
|
||||
|
||||
// ListTenantUsers lists tenant users with filtering
|
||||
func (r *GormTenantRepository) ListTenantUsers(ctx context.Context, filter *tenant.TenantUserFilter) ([]*tenant.TenantUser, error) {
|
||||
query := r.db.WithContext(ctx).Model(&tenant.TenantUser{})
|
||||
|
||||
// Apply filters
|
||||
if filter.TenantID != "" {
|
||||
query = query.Where("tenant_id = ?", filter.TenantID)
|
||||
}
|
||||
@@ -181,3 +168,27 @@ func (r *GormTenantRepository) ListTenantUsers(ctx context.Context, filter *tena
|
||||
err := query.Find(&users).Error
|
||||
return users, err
|
||||
}
|
||||
|
||||
func (r *GormTenantRepository) CountTenantUsers(ctx context.Context, filter *tenant.TenantUserFilter) (int, error) {
|
||||
query := r.db.WithContext(ctx).Model(&tenant.TenantUser{})
|
||||
|
||||
if filter.TenantID != "" {
|
||||
query = query.Where("tenant_id = ?", filter.TenantID)
|
||||
}
|
||||
if filter.UserID != "" {
|
||||
query = query.Where("user_id = ?", filter.UserID)
|
||||
}
|
||||
if filter.Email != "" {
|
||||
query = query.Where("email ILIKE ?", "%"+filter.Email+"%")
|
||||
}
|
||||
if filter.Role != "" {
|
||||
query = query.Where("role = ?", filter.Role)
|
||||
}
|
||||
if filter.Status != "" {
|
||||
query = query.Where("status = ?", filter.Status)
|
||||
}
|
||||
|
||||
var count int64
|
||||
err := query.Count(&count).Error
|
||||
return int(count), err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user