|
|
@@ -0,0 +1,420 @@
|
|
|
+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 <pix_id>")
|
|
|
+ }
|
|
|
+
|
|
|
+ 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 <valor> <descrição opcional> <tempo em segundos>")
|
|
|
+ }
|
|
|
+ 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 <valor> <tipo de chave> <chave> <descrição opcional>")
|
|
|
+ }
|
|
|
+ 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 <valor> <descrição opcional> <tempo em segundos>
|
|
|
+ abacate-cli withdraw <valor> <tipo de chave> <chave> <descrição opcional>
|
|
|
+ abacate-cli checkpix <pix_id>
|
|
|
+
|
|
|
+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()
|
|
|
+}
|