package main import ( "bufio" "bytes" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "os" "strconv" "strings" "time" ) const ( baseURL = "https://api.abacatepay.com/v1" defaultTimeout = 15 * time.Second minWithdrawCents = 350 ) type apiEnvelope[T any] struct { Data T `json:"data"` Error json.RawMessage `json:"error"` } func getJSON[T any](path string, out *apiEnvelope[T]) error { apiKey := strings.TrimSpace(os.Getenv("ABACATEPAY_API_KEY")) if apiKey == "" { return errors.New("variável de ambiente ABACATEPAY_API_KEY não definida") } req, err := http.NewRequest(http.MethodGet, baseURL+path, nil) if err != nil { return err } req.Header.Set("Authorization", "Bearer "+apiKey) req.Header.Set("Accept", "application/json") client := &http.Client{Timeout: defaultTimeout} resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { return err } if resp.StatusCode >= 400 { return fmt.Errorf("requisição falhou (%d): %s", resp.StatusCode, strings.TrimSpace(string(respBody))) } if err := json.Unmarshal(respBody, out); err != nil { return fmt.Errorf("falha ao parsear resposta: %w", err) } return nil } func runCheckPixCommand(args []string) error { if len(args) != 1 { return errors.New("uso: abacate-cli checkpix ") } pixID := strings.TrimSpace(args[0]) if pixID == "" { return errors.New("o id do QRCode PIX não pode ser vazio") } query := url.Values{} query.Set("id", pixID) var envelope apiEnvelope[pixQrCodeStatusResponse] if err := getJSON("/pixQrCode/check?"+query.Encode(), &envelope); err != nil { return err } if len(envelope.Error) > 0 && string(envelope.Error) != "null" { return fmt.Errorf("erro ao checar QRCode: %s", sanitizeAPIError(envelope.Error)) } fmt.Printf("status=%s\n", envelope.Data.Status) return nil } type pixQrCodeRequest struct { Amount int `json:"amount"` ExpiresIn int `json:"expiresIn,omitempty"` Description string `json:"description,omitempty"` Customer *pixCustomerInput `json:"customer,omitempty"` Metadata map[string]string `json:"metadata,omitempty"` } type pixCustomerInput struct { Name string `json:"name"` Cellphone string `json:"cellphone"` Email string `json:"email"` TaxID string `json:"taxId"` } type pixQrCodeResponse struct { ID string `json:"id"` Amount int `json:"amount"` Status string `json:"status"` BRCode string `json:"brCode"` BRCodeBase string `json:"brCodeBase64"` PlatformFee int `json:"platformFee"` CreatedAt string `json:"createdAt"` ExpiresAt string `json:"expiresAt"` } type pixQrCodeStatusResponse struct { Status string `json:"status"` ExpiresAt string `json:"expiresAt"` } type withdrawRequest struct { ExternalID string `json:"externalId"` Method string `json:"method"` Amount int `json:"amount"` Description string `json:"description,omitempty"` Pix withdrawPixData `json:"pix"` } type withdrawPixData struct { Type string `json:"type"` Key string `json:"key"` } type withdrawResponse struct { ID string `json:"id"` Status string `json:"status"` Kind string `json:"kind"` Amount int `json:"amount"` PlatformFee int `json:"platformFee"` ReceiptURL string `json:"receiptUrl"` ExternalID string `json:"externalId"` } func main() { if err := loadEnvFile(".env"); err != nil { exitWithError(fmt.Errorf("falha ao carregar .env: %w", err)) } if len(os.Args) < 2 { printUsage() os.Exit(1) } cmd := os.Args[1] switch cmd { case "pix": if err := runPixCommand(os.Args[2:]); err != nil { exitWithError(err) } case "withdraw": if err := runWithdrawCommand(os.Args[2:]); err != nil { exitWithError(err) } case "checkpix": if err := runCheckPixCommand(os.Args[2:]); err != nil { exitWithError(err) } case "help", "-h", "--help": printUsage() default: exitWithError(fmt.Errorf("comando desconhecido: %s", cmd)) } } func runPixCommand(args []string) error { if len(args) < 1 { return errors.New("uso: abacate-cli pix ") } if len(args) > 3 { return errors.New("muitos argumentos para o comando pix") } amountStr := args[0] description := "" expires := 1800 if len(args) >= 2 { description = args[1] } if len(args) == 3 { secs, err := strconv.Atoi(args[2]) if err != nil || secs <= 0 { return fmt.Errorf("tempo inválido: %q", args[2]) } expires = secs } valueInCents, err := parseAmountToCents(amountStr) if err != nil { return err } req := pixQrCodeRequest{ Amount: valueInCents, ExpiresIn: expires, Description: description, } var envelope apiEnvelope[pixQrCodeResponse] if err := postJSON("/pixQrCode/create", req, &envelope); err != nil { return err } if len(envelope.Error) > 0 && string(envelope.Error) != "null" { return fmt.Errorf("erro ao criar QRCode: %s", sanitizeAPIError(envelope.Error)) } if envelope.Data.BRCode == "" || envelope.Data.ID == "" { return errors.New("resposta inválida: QRCode sem dados essenciais") } fmt.Println(envelope.Data.BRCode) fmt.Println("ID:", envelope.Data.ID) return nil } func runWithdrawCommand(args []string) error { if len(args) < 3 { return errors.New("uso: abacate-cli withdraw ") } if len(args) > 4 { return errors.New("muitos argumentos para o comando withdraw") } amountStr := args[0] pixType := strings.ToUpper(args[1]) pixKey := args[2] description := "" if len(args) == 4 { description = args[3] } valueInCents, err := parseAmountToCents(amountStr) if err != nil { return err } if valueInCents < minWithdrawCents { return fmt.Errorf("valor mínimo para saque é R$3,50") } req := withdrawRequest{ ExternalID: fmt.Sprintf("withdraw-%d", time.Now().Unix()), Method: "PIX", Amount: valueInCents, Description: description, Pix: withdrawPixData{ Type: pixType, Key: pixKey, }, } var envelope apiEnvelope[withdrawResponse] if err := postJSON("/withdraw/create", req, &envelope); err != nil { return err } if len(envelope.Error) > 0 && string(envelope.Error) != "null" { return fmt.Errorf("erro ao criar saque: %s", sanitizeAPIError(envelope.Error)) } resp := envelope.Data if resp.ID == "" { return errors.New("resposta inválida: saque sem identificador") } fmt.Printf("SUCESSO %s status=%s amount=%0.2f receipt=%s\n", resp.ID, resp.Status, float64(resp.Amount)/100, resp.ReceiptURL, ) return nil } func parseAmountToCents(amount string) (int, error) { s := strings.TrimSpace(amount) if s == "" { return 0, errors.New("valor vazio") } s = strings.ReplaceAll(s, ",", ".") value, err := strconv.ParseFloat(s, 64) if err != nil { return 0, fmt.Errorf("valor inválido: %w", err) } if value <= 0 { return 0, errors.New("o valor precisa ser maior que zero") } cents := int(value*100 + 0.5) return cents, nil } func postJSON[T any](path string, payload any, out *apiEnvelope[T]) error { apiKey := strings.TrimSpace(os.Getenv("ABACATEPAY_API_KEY")) if apiKey == "" { return errors.New("variável de ambiente ABACATEPAY_API_KEY não definida") } body, err := json.Marshal(payload) if err != nil { return err } req, err := http.NewRequest(http.MethodPost, baseURL+path, bytes.NewBuffer(body)) if err != nil { return err } req.Header.Set("Authorization", "Bearer "+apiKey) req.Header.Set("Content-Type", "application/json") client := &http.Client{Timeout: defaultTimeout} resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { return err } if resp.StatusCode >= 400 { return fmt.Errorf("requisição falhou (%d): %s", resp.StatusCode, strings.TrimSpace(string(respBody))) } if err := json.Unmarshal(respBody, out); err != nil { return fmt.Errorf("falha ao parsear resposta: %w", err) } return nil } func sanitizeAPIError(raw json.RawMessage) string { if len(raw) == 0 || string(raw) == "null" { return "" } var asObj map[string]any if err := json.Unmarshal(raw, &asObj); err == nil { if msg, ok := asObj["message"].(string); ok && msg != "" { if code, ok := asObj["code"].(string); ok && code != "" { return fmt.Sprintf("%s (%s)", msg, code) } return msg } return string(raw) } var asStr string if err := json.Unmarshal(raw, &asStr); err == nil && asStr != "" { return asStr } return string(raw) } func printUsage() { fmt.Println(`Uso: abacate-cli pix abacate-cli withdraw abacate-cli checkpix Certifique-se de definir ABACATEPAY_API_KEY no ambiente.`) } func exitWithError(err error) { fmt.Fprintln(os.Stderr, "Erro:", err) os.Exit(1) } func loadEnvFile(path string) error { file, err := os.Open(path) if errors.Is(err, os.ErrNotExist) { return nil } if err != nil { return err } defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } key, value, found := strings.Cut(line, "=") if !found { continue } key = strings.TrimSpace(key) if key == "" { continue } value = strings.TrimSpace(value) value = strings.Trim(value, `"'`) if err := os.Setenv(key, value); err != nil { return err } } return scanner.Err() }