Răsfoiți Sursa

add the abacate-cli to work with tooeasy

gdias 1 zi în urmă
comite
6a75bd269d
6 a modificat fișierele cu 463 adăugiri și 0 ștergeri
  1. 1 0
      .env.example
  2. 1 0
      .gitignore
  3. 38 0
      README.MD
  4. BIN
      abacate-cli
  5. 3 0
      go.mod
  6. 420 0
      main.go

+ 1 - 0
.env.example

@@ -0,0 +1 @@
+ABACATEPAY_API_KEY=xxxxxxxxxxxxxxxxxxxxx

+ 1 - 0
.gitignore

@@ -0,0 +1 @@
+.env

+ 38 - 0
README.MD

@@ -0,0 +1,38 @@
+# CLI AbacatePay – Exemplos de Uso
+
+## Saque PIX
+
+```bash
+./abacate-cli withdraw <valor> <tipo> <chave> "<descricao>"
+```
+
+Exemplo de saída:
+
+```
+SUCESSO tran_qJBuyPALbhghZdhSyP3Ludp6 status=PENDING amount=8.20 receipt=https://abacatepay.com/receipt/tran_qJBuyPALbhghZdhSyP3Ludp6
+```
+
+## QR Code PIX
+
+```bash
+./abacate-cli pix <valor> "<descricao>" <tempo>
+```
+
+Exemplo de saída:
+
+```
+00020126690014br.gov.bcb.pix0136ac834c7d-1e36-4f06-b468-3819bb0f95980207tooeasy520400005303986540510.805802BR592350755259 GABRIEL RODRIG6008Sao Jose622905255075525900000569556131ASA6304A523
+ID: pix_char_123456
+```
+
+## Check PIX
+
+```bash
+./abacate-cli checkpix <pix_id>
+```
+
+Exemplo de saída:
+
+```
+PIX status=PENDING
+```

BIN
abacate-cli


+ 3 - 0
go.mod

@@ -0,0 +1,3 @@
+module abacatecli
+
+go 1.21

+ 420 - 0
main.go

@@ -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()
+}