From d921270f67ccc7eb6925e4f5964917fe905a7e87 Mon Sep 17 00:00:00 2001 From: nutcas3 Date: Fri, 17 Apr 2026 16:13:11 +0300 Subject: [PATCH] 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 --- apps/cli/internal/api/client.go | 205 ++++++++++++++++++++++++++ apps/cli/internal/commands/billing.go | 151 +++++++++++++++---- 2 files changed, 326 insertions(+), 30 deletions(-) create mode 100644 apps/cli/internal/api/client.go diff --git a/apps/cli/internal/api/client.go b/apps/cli/internal/api/client.go new file mode 100644 index 0000000..06ac21d --- /dev/null +++ b/apps/cli/internal/api/client.go @@ -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 +} diff --git a/apps/cli/internal/commands/billing.go b/apps/cli/internal/commands/billing.go index a64d3b1..ae94b09 100644 --- a/apps/cli/internal/commands/billing.go +++ b/apps/cli/internal/commands/billing.go @@ -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 [options]") - fmt.Println("\nAvailable commands:") - fmt.Println(" invoices - List all invoices") - fmt.Println(" payments - List all payments") - fmt.Println(" generate - 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 [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 ", "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 ") + errorMsg := colorizer.Colorize("Error: Subscriber ID is required", ui.StyleError) + usageMsg := colorizer.Colorize("Usage: telecom-cli billing generate ", 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()) }