Files
nutcas3 8fddb29716 refactor: Replace fmt.Printf with structured logging using logrus in api-server subscriber and invoice services
- Add logrus import to invoice.go, subscriber_crud.go, subscriber_esim.go, and subscriber_lifecycle.go
- Replace fmt.Printf with logrus.WithError and WithField for structured error logging
- Update invoice email notification placeholder to use logrus.Info with invoice_id field
- Replace eSIM provisioning error print with logrus.Warn including subscriber_id field
- Add comment explaining derived
2026-04-29 04:36:02 +03:00

201 lines
6.3 KiB
Go

package services
import (
"context"
"fmt"
"time"
"github.com/nutcas3/telecom-platform/apps/api-server/internal/database"
"github.com/nutcas3/telecom-platform/apps/api-server/internal/models"
"github.com/sirupsen/logrus"
)
// InvoiceService handles invoice generation and management
type InvoiceService struct {
db *database.Database
}
// NewInvoiceService creates a new invoice service
func NewInvoiceService(db *database.Database) *InvoiceService {
return &InvoiceService{db: db}
}
// GenerateMonthlyInvoice generates a monthly invoice for a subscriber
func (s *InvoiceService) GenerateMonthlyInvoice(ctx context.Context, subscriberID uint, billingPeriod string) (*models.Invoice, error) {
// Get subscriber
var subscriber models.Subscriber
if err := s.db.DB.First(&subscriber, subscriberID).Error; err != nil {
return nil, fmt.Errorf("subscriber not found: %w", err)
}
// Get usage events for the billing period
startDate, err := parseBillingPeriod(billingPeriod)
if err != nil {
return nil, fmt.Errorf("invalid billing period: %w", err)
}
endDate := startDate.AddDate(0, 1, 0).Add(-time.Second)
var usageEvents []models.UsageEvent
if err := s.db.DB.Where("subscriber_id = ? AND timestamp BETWEEN ? AND ?",
subscriberID, startDate, endDate).Find(&usageEvents).Error; err != nil {
return nil, fmt.Errorf("failed to fetch usage events: %w", err)
}
// Create invoice with existing model structure
invoice := &models.Invoice{
SubscriberID: subscriberID,
DueDate: time.Now().AddDate(0, 0, 30), // Due in 30 days
Status: models.InvoiceStatusDraft,
Currency: "USD",
CreatedAt: time.Now(),
}
// Add plan fee
planFee := 25.00 // Default monthly fee
invoice.LineItems = append(invoice.LineItems, models.InvoiceLineItem{
Description: "Monthly Plan Fee",
Quantity: 1,
UnitPrice: planFee,
Amount: planFee,
})
// Add usage charges
totalUsageCost := 0.0
for _, event := range usageEvents {
totalUsageCost += event.Cost
}
if totalUsageCost > 0 {
invoice.LineItems = append(invoice.LineItems, models.InvoiceLineItem{
Description: "Usage Charges",
Quantity: 1,
UnitPrice: totalUsageCost,
Amount: totalUsageCost,
})
}
// Calculate total amount
invoice.Amount = planFee + totalUsageCost
// Save invoice
if err := s.db.DB.Create(invoice).Error; err != nil {
return nil, fmt.Errorf("failed to create invoice: %w", err)
}
// Load subscriber relationship
s.db.DB.Preload("Subscriber").First(invoice, invoice.ID)
return invoice, nil
}
// SendInvoice sends an invoice to the subscriber
func (s *InvoiceService) SendInvoice(ctx context.Context, invoiceID uint) error {
var invoice models.Invoice
if err := s.db.DB.First(&invoice, invoiceID).Error; err != nil {
return fmt.Errorf("invoice not found: %w", err)
}
// Update status to pending
invoice.Status = models.InvoiceStatusPending
if err := s.db.DB.Save(&invoice).Error; err != nil {
return fmt.Errorf("failed to update invoice status: %w", err)
}
// Email notification would be sent here via notification service
// This is a placeholder for future integration with email service
logrus.WithField("invoice_id", invoice.ID).Info("Invoice created - email notification pending")
return nil
}
// MarkInvoiceAsPaid marks an invoice as paid
func (s *InvoiceService) MarkInvoiceAsPaid(ctx context.Context, invoiceID uint, paymentMethod string) error {
var invoice models.Invoice
if err := s.db.DB.First(&invoice, invoiceID).Error; err != nil {
return fmt.Errorf("invoice not found: %w", err)
}
invoice.Status = models.InvoiceStatusPaid
// Update payment date if needed
return s.db.DB.Save(&invoice).Error
}
// GetOverdueInvoices returns all overdue invoices
func (s *InvoiceService) GetOverdueInvoices(ctx context.Context) ([]models.Invoice, error) {
var invoices []models.Invoice
err := s.db.DB.Where("status = ? AND due_date < ?",
models.InvoiceStatusPending, time.Now()).Find(&invoices).Error
return invoices, err
}
// GetSubscriberInvoices returns all invoices for a subscriber
func (s *InvoiceService) GetSubscriberInvoices(ctx context.Context, subscriberID uint) ([]models.Invoice, error) {
var invoices []models.Invoice
err := s.db.DB.Where("subscriber_id = ?", subscriberID).
Preload("LineItems").
Order("created_at DESC").
Find(&invoices).Error
return invoices, err
}
// parseBillingPeriod parses a billing period string like "2024-01" into a time.Time
func parseBillingPeriod(period string) (time.Time, error) {
if len(period) != 7 || period[4] != '-' {
return time.Time{}, fmt.Errorf("invalid billing period format, expected YYYY-MM")
}
year := period[:4]
month := period[5:]
layout := "2006-01"
return time.Parse(layout, fmt.Sprintf("%s-%s", year, month))
}
// InvoiceGenerator handles automated invoice generation
type InvoiceGenerator struct {
invoiceService *InvoiceService
}
// NewInvoiceGenerator creates a new invoice generator
func NewInvoiceGenerator(invoiceService *InvoiceService) *InvoiceGenerator {
return &InvoiceGenerator{invoiceService: invoiceService}
}
// GenerateAllMonthlyInvoices generates monthly invoices for all active subscribers
func (g *InvoiceGenerator) GenerateAllMonthlyInvoices(ctx context.Context, billingPeriod string) error {
// Get all active subscribers
var subscribers []models.Subscriber
if err := g.invoiceService.db.DB.Where("status = ?", "active").Find(&subscribers).Error; err != nil {
return fmt.Errorf("failed to fetch subscribers: %w", err)
}
var errors []error
for _, subscriber := range subscribers {
// Check if invoice already exists for this period by checking creation date
startDate, _ := parseBillingPeriod(billingPeriod)
endDate := startDate.AddDate(0, 1, 0).Add(-time.Second)
var existingInvoice models.Invoice
err := g.invoiceService.db.DB.Where("subscriber_id = ? AND created_at BETWEEN ? AND ?",
subscriber.ID, startDate, endDate).First(&existingInvoice).Error
if err == nil {
// Invoice already exists, skip
continue
}
// Generate new invoice
_, err = g.invoiceService.GenerateMonthlyInvoice(ctx, subscriber.ID, billingPeriod)
if err != nil {
errors = append(errors, fmt.Errorf("failed to generate invoice for subscriber %d: %w", subscriber.ID, err))
}
}
if len(errors) > 0 {
return fmt.Errorf("encountered %d errors while generating invoices", len(errors))
}
return nil
}