termcloud/internal/cli/account.go

280 lines
7.8 KiB
Go

package cli
import (
"context"
"fmt"
"os"
"strconv"
"strings"
"text/tabwriter"
"github.com/spf13/cobra"
)
func NewAccountCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "account",
Short: "Manage your TermCloud account",
Long: "Create, authenticate, and manage your TermCloud account",
}
cmd.AddCommand(newAccountCreateCmd())
cmd.AddCommand(newAccountLoginCmd())
cmd.AddCommand(newAccountInfoCmd())
cmd.AddCommand(newAccountTopUpCmd())
cmd.AddCommand(newAccountPaymentsCmd())
cmd.AddCommand(newAccountUsageCmd())
return cmd
}
func newAccountCreateCmd() *cobra.Command {
return &cobra.Command{
Use: "create",
Short: "Create a new TermCloud account",
RunE: func(cmd *cobra.Command, args []string) error {
config, err := LoadConfig()
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
client := NewClient(config.ServerURL, "")
ctx := context.Background()
account, err := client.CreateAccount(ctx)
if err != nil {
return fmt.Errorf("failed to create account: %w", err)
}
config.AccountNumber = account.AccountNumber
config.AccessToken = account.AccessToken
if err := config.Save(); err != nil {
return fmt.Errorf("failed to save config: %w", err)
}
fmt.Printf("Account created successfully!\n")
fmt.Printf("Account Number: %s\n", account.AccountNumber)
fmt.Printf("Access Token: %s\n", account.AccessToken)
fmt.Printf("Status: %s\n", getAccountStatus(account))
fmt.Printf("\nIMPORTANT: Save your account number and access token!\n")
fmt.Printf("You are now logged in. Add funds using: tcman account top-up 1\n")
return nil
},
}
}
func newAccountLoginCmd() *cobra.Command {
return &cobra.Command{
Use: "login <account-number> <access-token>",
Short: "Login to an existing account",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
accountNumber := args[0]
accessToken := args[1]
config, err := LoadConfig()
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
client := NewClient(config.ServerURL, accessToken)
ctx := context.Background()
account, err := client.GetAccount(ctx)
if err != nil {
return fmt.Errorf("failed to authenticate: %w", err)
}
if account.AccountNumber != accountNumber {
return fmt.Errorf("account number mismatch")
}
config.AccountNumber = accountNumber
config.AccessToken = accessToken
if err := config.Save(); err != nil {
return fmt.Errorf("failed to save config: %w", err)
}
fmt.Printf("Successfully logged in!\n")
fmt.Printf("Account: %s\n", account.AccountNumber)
fmt.Printf("Status: %s\n", getAccountStatus(account))
fmt.Printf("Balance: $%.2f USD\n", account.BalanceUSD)
return nil
},
}
}
func newAccountInfoCmd() *cobra.Command {
return &cobra.Command{
Use: "info",
Short: "Show account information",
RunE: func(cmd *cobra.Command, args []string) error {
config, err := LoadConfig()
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
if !config.IsAuthenticated() {
return fmt.Errorf("not logged in. Use 'tcman account login' or 'tcman account create'")
}
client := NewClient(config.ServerURL, config.AccessToken)
ctx := context.Background()
account, err := client.GetAccount(ctx)
if err != nil {
return fmt.Errorf("failed to get account info: %w", err)
}
fmt.Printf("Account Information:\n")
fmt.Printf(" Number: %s\n", account.AccountNumber)
fmt.Printf(" Status: %s\n", getAccountStatus(account))
fmt.Printf(" Balance: $%.2f USD\n", account.BalanceUSD)
fmt.Printf(" Created: %s\n", account.CreatedAt.Format("2006-01-02 15:04:05"))
if account.ActivatedAt != nil {
fmt.Printf(" Activated: %s\n", account.ActivatedAt.Format("2006-01-02 15:04:05"))
}
fmt.Printf(" Last Billing: %s\n", account.LastBillingDate.Format("2006-01-02"))
return nil
},
}
}
func newAccountTopUpCmd() *cobra.Command {
return &cobra.Command{
Use: "top-up <amount> [account-number]",
Short: "Add funds to your account via Bitcoin",
Long: "Add funds to your account. If logged in, uses your account. If not logged in, provide account number.",
Args: cobra.RangeArgs(1, 2),
RunE: func(cmd *cobra.Command, args []string) error {
amount, err := strconv.ParseFloat(args[0], 64)
if err != nil {
return fmt.Errorf("invalid amount: %w", err)
}
if amount < 1.0 {
return fmt.Errorf("minimum top-up amount is $1.00 USD")
}
config, err := LoadConfig()
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
var accountNumber string
if len(args) == 2 {
accountNumber = args[1]
} else if config.IsAuthenticated() {
accountNumber = config.AccountNumber
} else if config.AccountNumber != "" {
accountNumber = config.AccountNumber
} else {
return fmt.Errorf("account number required. Usage: tcman account top-up <amount> <account-number>")
}
client := NewClient(config.ServerURL, "")
ctx := context.Background()
payment, err := client.CreatePaymentByAccountNumber(ctx, accountNumber, amount)
if err != nil {
return fmt.Errorf("failed to create payment: %w", err)
}
fmt.Printf("Bitcoin Payment Created:\n")
fmt.Printf(" Payment ID: %d\n", payment.ID)
fmt.Printf(" Account: %s\n", accountNumber)
fmt.Printf(" Amount: $%.2f USD (%.8f BTC)\n", payment.USDAmount, payment.BTCAmount)
fmt.Printf(" Address: %s\n", payment.BTCAddress)
fmt.Printf(" Status: %s\n", strings.ToUpper(payment.Status))
if payment.ExpiresAt != nil {
fmt.Printf(" Expires: %s\n", payment.ExpiresAt.Format("2006-01-02 15:04:05"))
}
fmt.Printf("\nSend exactly %.8f BTC to the address above.\n", payment.BTCAmount)
fmt.Printf("Your payment will be confirmed manually after Bitcoin is received.\n")
return nil
},
}
}
func newAccountPaymentsCmd() *cobra.Command {
return &cobra.Command{
Use: "payments",
Short: "List payment history",
RunE: func(cmd *cobra.Command, args []string) error {
config, err := LoadConfig()
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
if !config.IsAuthenticated() {
return fmt.Errorf("not logged in. Use 'tcman account login' or 'tcman account create'")
}
client := NewClient(config.ServerURL, config.AccessToken)
ctx := context.Background()
payments, err := client.GetPayments(ctx)
if err != nil {
return fmt.Errorf("failed to get payments: %w", err)
}
if len(payments) == 0 {
fmt.Printf("No payments found.\n")
return nil
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "ID\tAmount\tBTC Amount\tStatus\tCreated\tConfirmed")
fmt.Fprintln(w, "---\t------\t----------\t------\t-------\t---------")
for _, payment := range payments {
confirmedStr := "N/A"
if payment.ConfirmedAt != nil {
confirmedStr = payment.ConfirmedAt.Format("2006-01-02 15:04")
}
fmt.Fprintf(w, "%d\t$%.2f\t%.8f\t%s\t%s\t%s\n",
payment.ID,
payment.USDAmount,
payment.BTCAmount,
strings.ToUpper(payment.Status),
payment.CreatedAt.Format("2006-01-02 15:04"),
confirmedStr,
)
}
w.Flush()
return nil
},
}
}
func newAccountUsageCmd() *cobra.Command {
return &cobra.Command{
Use: "usage",
Short: "Show billing and usage history",
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Printf("Usage tracking is handled automatically.\n")
fmt.Printf("You are charged monthly based on peak storage usage.\n")
fmt.Printf("Current rate: $0.50 per GB per month.\n")
fmt.Printf("\nUse 'tcman account info' to see your current balance.\n")
return nil
},
}
}
func getAccountStatus(account *Account) string {
if !account.IsActive {
return "INACTIVE (needs funds)"
}
if account.BalanceUSD <= 0 {
return "LOW BALANCE"
}
return "ACTIVE"
}