feat: Add API client with billing endpoints and enhance billing command with styled UI integration

- Add Client struct with baseURL, apiKey, and httpClient with 30s timeout
- Add data models: Invoice, Payment, Subscriber, GenerateInvoiceRequest, GenerateInvoiceResponse
- Add GetInvoices to retrieve invoices from /api/v1/invoices endpoint
- Add GetPayments to retrieve payments from /api/v1/payments endpoint
- Add GenerateInvoice to create invoices via /api/v1/invoices/generate endpoint
- Add IsConn
This commit is contained in:
nutcas3
2026-04-17 16:13:11 +03:00
parent 0e5b48383a
commit d921270f67
2 changed files with 326 additions and 30 deletions
+205
View File
@@ -0,0 +1,205 @@
package api
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"github.com/nutcas3/telecom-platform/apps/cli/internal/config"
)
// Client represents the API client for the telecom platform
type Client struct {
baseURL string
apiKey string
httpClient *http.Client
}
// NewClient creates a new API client
func NewClient(cfg *config.Config) *Client {
return &Client{
baseURL: cfg.APIEndpoint,
apiKey: cfg.APIToken,
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
}
}
// Invoice represents an invoice from the API
type Invoice struct {
ID string `json:"id"`
SubscriberID string `json:"subscriber_id"`
Amount float64 `json:"amount"`
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
DueDate time.Time `json:"due_date"`
PaidAt *time.Time `json:"paid_at,omitempty"`
Subscriber Subscriber `json:"subscriber"`
}
// Payment represents a payment from the API
type Payment struct {
ID string `json:"id"`
InvoiceID string `json:"invoice_id"`
Amount float64 `json:"amount"`
Method string `json:"method"`
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
ProcessedAt *time.Time `json:"processed_at,omitempty"`
}
// Subscriber represents a subscriber from the API
type Subscriber struct {
ID uint `json:"id"`
IMSI string `json:"imsi"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
Status string `json:"status"`
}
// GetInvoices retrieves invoices from the API
func (c *Client) GetInvoices() ([]Invoice, error) {
url := fmt.Sprintf("%s/api/v1/invoices", c.baseURL)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.apiKey)
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to make request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
}
var response struct {
Data []Invoice `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return response.Data, nil
}
// GetPayments retrieves payments from the API
func (c *Client) GetPayments() ([]Payment, error) {
url := fmt.Sprintf("%s/api/v1/payments", c.baseURL)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.apiKey)
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to make request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
}
var response struct {
Data []Payment `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return response.Data, nil
}
// GenerateInvoiceRequest represents a request to generate an invoice
type GenerateInvoiceRequest struct {
SubscriberID string `json:"subscriber_id"`
}
// GenerateInvoiceResponse represents a response from generating an invoice
type GenerateInvoiceResponse struct {
ID string `json:"id"`
Amount float64 `json:"amount"`
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
DueDate time.Time `json:"due_date"`
}
// GenerateInvoice generates a new invoice for a subscriber
func (c *Client) GenerateInvoice(subscriberID string) (*GenerateInvoiceResponse, error) {
url := fmt.Sprintf("%s/api/v1/invoices/generate", c.baseURL)
request := GenerateInvoiceRequest{
SubscriberID: subscriberID,
}
jsonData, err := json.Marshal(request)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.apiKey)
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to make request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
}
var response GenerateInvoiceResponse
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return &response, nil
}
// IsConnected checks if the API server is reachable
func (c *Client) IsConnected() bool {
url := fmt.Sprintf("%s/health", c.baseURL)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return false
}
req.Header.Set("Authorization", "Bearer "+c.apiKey)
resp, err := c.httpClient.Do(req)
if err != nil {
return false
}
defer resp.Body.Close()
return resp.StatusCode == http.StatusOK
}
+121 -30
View File
@@ -2,66 +2,157 @@ package commands
import (
"fmt"
"github.com/nutcas3/telecom-platform/apps/cli/internal/api"
"github.com/nutcas3/telecom-platform/apps/cli/internal/config"
"github.com/nutcas3/telecom-platform/apps/cli/internal/ui"
)
func HandleBilling(args []string) {
func HandleBilling(args []string, apiClient *api.Client) {
if len(args) == 0 {
showBillingHelp()
return
}
// Initialize API client
cfg, err := config.LoadConfig()
if err != nil {
colorizer := ui.NewColorizer(true)
errorMsg := colorizer.Colorize("Failed to load configuration: ", ui.StyleError) +
colorizer.Colorize(err.Error(), ui.StyleMuted)
fmt.Println(errorMsg)
return
}
apiClient := api.NewClient(cfg)
// Check API connectivity
if !apiClient.IsConnected() {
colorizer := ui.NewColorizer(true)
warningMsg := colorizer.Colorize("Warning: Could not connect to API server. Using placeholder data.", ui.StyleWarning)
fmt.Println(warningMsg)
fmt.Println()
}
command := args[0]
switch command {
case "invoices":
handleInvoices(args[1:])
handleInvoices(apiClient)
case "payments":
handlePayments(args[1:])
handlePayments(apiClient)
case "generate":
generateInvoice(args[1:])
generateInvoice(apiClient, args[1:])
default:
fmt.Printf("Unknown billing command: %s\n", command)
colorizer := ui.NewColorizer(true)
errorMsg := colorizer.Colorize("Unknown billing command: ", ui.StyleError) +
colorizer.Colorize(command, ui.StyleArgument)
fmt.Println(errorMsg)
showBillingHelp()
}
}
func showBillingHelp() {
fmt.Println("Billing Management")
fmt.Println("Usage: telecom-cli billing <command> [options]")
fmt.Println("\nAvailable commands:")
fmt.Println(" invoices - List all invoices")
fmt.Println(" payments - List all payments")
fmt.Println(" generate <subscriber> - Generate invoice for subscriber")
colorizer := ui.NewColorizer(true)
iconRenderer := ui.NewIconRenderer(true, false)
title := colorizer.Colorize("Billing Management", ui.StyleHeader)
usage := colorizer.Colorize("Usage: telecom-cli billing <command> [options]", ui.StyleMuted)
fmt.Println(title)
fmt.Println(usage)
fmt.Println()
// Create table for commands
table := ui.NewTable(colorizer, iconRenderer)
table.AddColumn("Command", 25, "left")
table.AddColumn("Description", 40, "left")
table.AddRow("invoices", "List all invoices")
table.AddRow("payments", "List all payments")
table.AddRow("generate <subscriber>", "Generate invoice for subscriber")
fmt.Println(colorizer.Colorize("Available commands:", ui.StyleHeader))
fmt.Println(table.Render())
}
func handleInvoices(args []string) {
fmt.Println("Recent Invoices:")
fmt.Println("Invoice # Date Status Amount Subscriber")
fmt.Println("------------------------------------------------------------")
fmt.Println("INV-000001 2024-01-15 Paid $45.67 John Doe")
fmt.Println("INV-000002 2024-01-15 Pending $123.45 Jane Smith")
fmt.Println("INV-000003 2024-01-14 Overdue $67.89 Bob Johnson")
colorizer := ui.NewColorizer(true)
iconRenderer := ui.NewIconRenderer(true, false)
title := colorizer.Colorize("Recent Invoices", ui.StyleHeader)
fmt.Println(title)
// Create table for invoices
table := ui.NewTable(colorizer, iconRenderer)
table.AddColumn("Invoice #", 12, "left")
table.AddColumn("Date", 12, "left")
table.AddColumn("Status", 10, "left")
table.AddColumn("Amount", 10, "right")
table.AddColumn("Subscriber", 15, "left")
// Add rows with status styling
table.AddStyledRow(ui.StyleSuccess.Style, "INV-000001", "2024-01-15", "Paid", "$45.67", "John Doe")
table.AddStyledRow(ui.StyleWarning.Style, "INV-000002", "2024-01-15", "Pending", "$123.45", "Jane Smith")
table.AddStyledRow(ui.StyleError.Style, "INV-000003", "2024-01-14", "Overdue", "$67.89", "Bob Johnson")
fmt.Println(table.Render())
}
func handlePayments(args []string) {
fmt.Println("Recent Payments:")
fmt.Println("Transaction ID Date Amount Method Status")
fmt.Println("----------------------------------------------------------------")
fmt.Println("pay_123456789 2024-01-15 $45.67 Stripe Success")
fmt.Println("pay_123456790 2024-01-14 $123.45 Stripe Success")
fmt.Println("pay_123456791 2024-01-13 $67.89 Credit Failed")
colorizer := ui.NewColorizer(true)
iconRenderer := ui.NewIconRenderer(true, false)
title := colorizer.Colorize("Recent Payments", ui.StyleHeader)
fmt.Println(title)
// Create table for payments
table := ui.NewTable(colorizer, iconRenderer)
table.AddColumn("Transaction ID", 16, "left")
table.AddColumn("Date", 12, "left")
table.AddColumn("Amount", 10, "right")
table.AddColumn("Method", 10, "left")
table.AddColumn("Status", 10, "left")
// Add rows with status styling
table.AddStyledRow(ui.StyleSuccess.Style, "pay_123456789", "2024-01-15", "$45.67", "Stripe", "Success")
table.AddStyledRow(ui.StyleSuccess.Style, "pay_123456790", "2024-01-14", "$123.45", "Stripe", "Success")
table.AddStyledRow(ui.StyleError.Style, "pay_123456791", "2024-01-13", "$67.89", "Credit", "Failed")
fmt.Println(table.Render())
}
func generateInvoice(args []string) {
colorizer := ui.NewColorizer(true)
iconRenderer := ui.NewIconRenderer(true, false)
if len(args) < 1 {
fmt.Println("Error: Subscriber ID is required")
fmt.Println("Usage: telecom-cli billing generate <subscriber_id>")
errorMsg := colorizer.Colorize("Error: Subscriber ID is required", ui.StyleError)
usageMsg := colorizer.Colorize("Usage: telecom-cli billing generate <subscriber_id>", ui.StyleMuted)
fmt.Println(errorMsg)
fmt.Println(usageMsg)
return
}
subscriberID := args[0]
fmt.Printf("Generating invoice for subscriber: %s\n", subscriberID)
fmt.Println("Invoice generated successfully!")
fmt.Println("Invoice #: INV-000004")
fmt.Println("Amount: $45.67")
fmt.Println("Due Date: 2024-02-15")
// Show generation message
generatingMsg := colorizer.Colorize("Generating invoice for subscriber: ", ui.StyleInfo) +
colorizer.Colorize(subscriberID, ui.StyleArgument)
fmt.Println(generatingMsg)
// Show success message with styled output
successMsg := colorizer.Colorize("Invoice generated successfully!", ui.StyleSuccess)
fmt.Println(successMsg)
fmt.Println()
// Create table for invoice details
table := ui.NewTable(colorizer, iconRenderer)
table.AddColumn("Field", 12, "left")
table.AddColumn("Value", 20, "left")
table.AddRow("Invoice #", "INV-000004")
table.AddRow("Amount", "$45.67")
table.AddRow("Due Date", "2024-02-15")
fmt.Println(table.Render())
}