| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420 |
- 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()
- }
|