Skip to content

Commit c321ecf

Browse files
author
Thiago Zilli
committed
Add CLI, HTTP server, parser and dynamic Pix
- Add cmd/pixgen CLI with generate and serve commands and /pix, /healthz handlers - Add Dockerfile, Makefile, bin/.gitkeep and update README and examples - Implement Pix parser, dynamic payload fetching, validations and unit tests - Add internal lightweight cobra substitute and replace in go.mod
1 parent 2aa783c commit c321ecf

File tree

17 files changed

+1932
-69
lines changed

17 files changed

+1932
-69
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.gocache
2+
3+
bin/*
4+
!bin/.gitkeep

Dockerfile

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
FROM golang:1.22-bullseye AS builder
2+
3+
WORKDIR /app
4+
5+
COPY go.mod go.sum ./
6+
RUN go mod download
7+
8+
COPY . .
9+
10+
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/pixgen ./cmd/pixgen
11+
12+
FROM gcr.io/distroless/base-debian12
13+
14+
COPY --from=builder /out/pixgen /usr/local/bin/pixgen
15+
16+
USER 65532:65532
17+
18+
ENTRYPOINT ["/usr/local/bin/pixgen"]
19+
CMD ["serve", "--addr", ":8080"]

Makefile

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
SHELL := /bin/bash
2+
3+
APP_NAME := pixgen
4+
CLI_MAIN := ./cmd/pixgen
5+
CLI_BINARY := bin/$(APP_NAME)
6+
DOCKER_IMAGE := $(APP_NAME):latest
7+
GO_FILES := $(shell find . -name '*.go' -not -path "./.gocache/*" -not -path "./internal/cobra/testdata/*")
8+
ARGS ?=
9+
10+
.PHONY: help build test fmt cli run docker-build docker-run clean
11+
12+
help:
13+
@echo "Available targets:"
14+
@echo " make build - Compile all Go packages"
15+
@echo " make test - Run unit tests"
16+
@echo " make fmt - Format Go code with gofmt"
17+
@echo " make cli - Build CLI binary into $(CLI_BINARY)"
18+
@echo " make run ARGS='...' - Run CLI with optional arguments (e.g., ARGS=\"serve --addr :8080\")"
19+
@echo " make docker-build - Build CLI Docker image ($(DOCKER_IMAGE))"
20+
@echo " make docker-run - Run CLI Docker image"
21+
@echo " make clean - Remove build artifacts and Docker image"
22+
23+
build:
24+
@echo "==> Building Go packages"
25+
go build ./...
26+
27+
test:
28+
@echo "==> Running tests"
29+
GOCACHE=$$(pwd)/.gocache go test ./...
30+
31+
fmt:
32+
@echo "==> Formatting code"
33+
gofmt -w $(GO_FILES)
34+
35+
cli: $(CLI_BINARY)
36+
37+
$(CLI_BINARY): $(GO_FILES)
38+
@echo "==> Building CLI binary $@"
39+
@mkdir -p $(@D)
40+
CGO_ENABLED=0 go build -o $@ $(CLI_MAIN)
41+
42+
run:
43+
@echo "==> Running CLI $(ARGS)"
44+
go run $(CLI_MAIN) $(ARGS)
45+
46+
docker-build:
47+
@echo "==> Building Docker image $(DOCKER_IMAGE)"
48+
docker build -t $(DOCKER_IMAGE) .
49+
50+
docker-run:
51+
@echo "==> Running Docker image $(DOCKER_IMAGE)"
52+
docker run --rm -p 8080:8080 $(DOCKER_IMAGE)
53+
54+
clean:
55+
@echo "==> Cleaning artifacts"
56+
rm -rf $(CLI_BINARY) .gocache
57+
-@docker rmi $(DOCKER_IMAGE) >/dev/null 2>&1 || true

README.md

Lines changed: 192 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,59 +2,210 @@
22

33
<p align="center"><img alt="pix-utils" src="https://raw.githubusercontent.com/thiagozs/go-pixgen/main/assets/logo-pix.png" width="128px" /></p>
44

5-
Generate ~~and validate~~ payments of Brazil Instant Payment System (Pix), making fast and simple to handle charges and proccess then in your project.
5+
Generate and validate payments of Brazil Instant Payment System (Pix), making fast and simple to handle charges and proccess then in your project. Além da lib, o repositório oferece CLI e serviço REST para gerar payloads Pix estáticos ou dinâmicos, incluindo QR Code.
66

7-
**UNDER DEVELOPMENT - Becareful**
7+
## Features
8+
9+
- Static (copy & paste) and dynamic (URL-based) Pix payload generation.
10+
- CLI `pixgen` (comandos `generate` e `serve`) para uso local ou como serviço.
11+
- REST service (`POST /pix`) retornando payload + QR Code (base64) e `GET /healthz`.
12+
- QR Code byte encoding built-in via `github.com/skip2/go-qrcode`.
13+
- EMV parsing helpers to inspect payload tags and metadata.
14+
- Field normalization (Pix keys, amount, TXID) with BACEN-aligned validation.
15+
- Dynamic Pix resolver with JSON/plain-text support, expiration awareness and mock friendly interface.
16+
17+
## Installation
18+
19+
```bash
20+
go get github.com/thiagozs/go-pixgen
21+
```
22+
23+
Go 1.17 or newer is required.
24+
25+
## Quick Start
26+
27+
### CLI – gerar payload no stdout
28+
29+
```bash
30+
make cli
31+
32+
bin/pixgen generate \
33+
--kind static \
34+
--key +5511999999999 \
35+
--merchant-name "Thiago Zilli Sarmento" \
36+
--merchant-city ARARANGUA \
37+
--amount 10.00 \
38+
--description "Pedido 123" \
39+
--txid PEDIDO-123
40+
```
41+
42+
Saída inclui o código copia-e-cola, campos relevantes e o QR Code em base64.
43+
44+
### REST service
45+
46+
```bash
47+
make run ARGS="serve --addr :8080"
48+
49+
curl -X POST http://localhost:8080/pix \
50+
-H "Content-Type: application/json" \
51+
-d '{
52+
"kind": "static",
53+
"pixKey": "+5511999999999",
54+
"merchantName": "Thiago Zilli Sarmento",
55+
"merchantCity": "ARARANGUA",
56+
"amount": "10.00",
57+
"description": "Pedido 123",
58+
"txid": "PEDIDO-123"
59+
}'
60+
```
61+
62+
```json
63+
{
64+
"payload": "000201...",
65+
"qrCode": "iVBORw0KGgoAAA...",
66+
"kind": "STATIC",
67+
"txid": "PEDIDO-123"
68+
}
69+
```
70+
71+
Endpoint `GET /healthz` retorna `200 OK` para checagens.
72+
73+
Exemplo rápido com `curl` + `jq` para visualizar a resposta formatada:
74+
75+
```bash
76+
curl -sS -X POST http://localhost:8080/pix \
77+
-H 'Content-Type: application/json' \
78+
-d '{
79+
"kind": "dynamic",
80+
"url": "https://example.com/api/pix/abc",
81+
"merchantName": "Fulano de Tal",
82+
"merchantCity": "CURITIBA",
83+
"amount": "25.00",
84+
"txid": "DINAMICO-001"
85+
}' | jq
86+
```
87+
88+
## Usage
89+
90+
### Generate a Pix payload programaticamente
891

9-
Example implementation.
1092
```golang
1193
opts := []pix.Options{
12-
pix.OptPixKey("11955555555"),
13-
pix.OptDescription("Teste"),
14-
pix.OptMerchantName("Thiago Zilli Sarmento"),
15-
pix.OptMerchantCity("Ararangua"),
16-
pix.OptAmount("1.00"),
17-
pix.OptAditionalInfo("gerado por go-pixgen"),
18-
pix.OptKind(pix.STATIC),
19-
}
20-
21-
p, err := pix.New(opts...)
22-
if err != nil {
23-
fmt.Println(err.Error())
24-
return
25-
}
26-
27-
if err := p.Validates(); err != nil {
28-
fmt.Println(err.Error())
29-
return
30-
}
31-
32-
cpy := p.GenPayload()
33-
34-
fmt.Printf("Copy and Paste: %s\n", cpy)
35-
36-
bts, err := p.GenQRCode()
37-
if err != nil {
38-
fmt.Println(err.Error())
39-
return
40-
}
41-
42-
fmt.Printf("QRCode: %b\n", bts)
94+
pix.OptPixKey("+5511955555555"),
95+
pix.OptDescription("Teste"),
96+
pix.OptMerchantName("Thiago Zilli Sarmento"),
97+
pix.OptMerchantCity("Ararangua"),
98+
pix.OptAmount("1.00"),
99+
pix.OptAdditionalInfo("gerado por go-pixgen"),
100+
pix.OptKind(pix.STATIC),
101+
}
102+
103+
p, err := pix.New(opts...)
104+
if err != nil {
105+
fmt.Println(err.Error())
106+
return
107+
}
108+
109+
cpy := p.GenPayload()
110+
fmt.Printf("Copy and Paste: %s\n", cpy)
111+
112+
qrPNG, err := p.GenQRCode()
113+
if err != nil {
114+
fmt.Println(err.Error())
115+
return
116+
}
117+
118+
fmt.Printf("QRCode bytes: %d\n", len(qrPNG))
119+
120+
parsed, err := pix.ParsePayload(cpy)
121+
if err != nil {
122+
fmt.Println(err.Error())
123+
return
124+
}
125+
126+
fmt.Printf("Kind: %s | TxID: %s\n", parsed.Kind(), parsed.AdditionalDataField.TxID)
43127
```
44128

129+
### Fetch a dynamic Pix payload
130+
131+
```golang
132+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
133+
defer cancel()
134+
135+
dynamicOpts := []pix.Options{
136+
pix.OptKind(pix.DYNAMIC),
137+
pix.OptUrl("https://my-provider.example/api/pix/123"),
138+
pix.OptMerchantName("Fulano de Tal"),
139+
pix.OptMerchantCity("CURITIBA"),
140+
}
141+
142+
dyn, err := pix.New(dynamicOpts...)
143+
if err != nil {
144+
fmt.Println(err)
145+
return
146+
}
147+
148+
payload, err := dyn.FetchDynamicPayload(ctx, nil)
149+
if err != nil {
150+
fmt.Println(err)
151+
return
152+
}
153+
154+
fmt.Printf("Remote Pix expires at: %v\n", payload.ExpiresAt)
155+
```
156+
157+
## API Highlights
158+
159+
- `pix.New(opts...) (*pix.Pix, error)` - build a Pix generator using functional options.
160+
- `(*Pix).GenPayload()` - returns the EMV string and caches it for `GenQRCode()`.
161+
- `pix.ParsePayload(string) (*ParsedPayload, error)` - converts an EMV payload back into structured fields and validates the CRC.
162+
- `(*Pix).FetchDynamicPayload(ctx, client)` - downloads a dynamic Pix payload, parses it and checks expiration. Works with `http.Client` stubs for tests.
163+
- `(*Pix).Validates()` - called automatically by `pix.New`, and can be invoked manually to re-check mutated parameters.
164+
165+
### Supported Pix key formats
166+
167+
- Random EVP UUID (case-normalized to lowercase).
168+
- Phone (`+55DDDUSER`), accepting raw digits with optional `+55` prefix.
169+
- CPF/CNPJ (digits only, validated with check digits).
170+
- Email addresses (validated using `net/mail`).
171+
172+
### Amount & TxID
173+
174+
- Amount accepts up to 13 digits before the separator and up to 2 decimals (`9999999999999.99` cap).
175+
- TxID allows uppercase/lowercase letters, digits, `.` and `-` up to 35 characters. Value is uppercased when stored.
176+
177+
### Dynamic payload fetching
178+
179+
`FetchDynamicPayload` understands responses as:
180+
181+
- Raw EMV payload (any `text/*` content type).
182+
- JSON containing fields `pixCopyPaste`, `pix`, `payload`, `pixCopiaECola`, etc.
183+
- Optional expiration keys such as `expiresAt`, `expiration`, `expiry`. Parsed values support RFC3339 and similar layouts.
184+
185+
Local testing (HTTP URLs) is accepted when targeting `localhost` or `127.*`. Production URLs must be HTTPS.
186+
187+
## Build & Docker
188+
189+
- `make build`, `make test`, `make fmt`
190+
- `make cli` compila o binário em `bin/pixgen`
191+
- `make run ARGS="serve --addr :8080"` executa o servidor REST local
192+
- `make docker-build && make docker-run` constroem e sobem a imagem (porta 8080)
193+
45194
## Roadmap
46195

47196
- [x] Generate payments based on parameters
48197
- [x] Static
49198
- [x] Dynamic
50-
- [ ] Parse and validate EMV Codes
199+
- [x] Parse and validate EMV Codes
51200
- [x] Export generated/parsed payment to Image
52201
- [x] Export generated/parsed payment to EMV Code
53-
- [ ] Fetch, parse and validate remote payloads from dynamic payments
54-
- [ ] Verify if has already expired
55-
- [ ] Improve tests
56-
- [ ] Doccumentation with all methods, parameters and some examples
57-
- [ ] Add dynamic payment tests
202+
- [x] Fetch, parse and validate remote payloads from dynamic payments
203+
- [x] Verify if has already expired
204+
- [x] Improve tests
205+
- [ ] Documentation with all methods, parameters and more examples
206+
- [x] Add dynamic payment tests
207+
- [x] CLI tooling for payload inspection
208+
- [ ] REST API hardening & auth helpers
58209

59210
## Contributing
60211

@@ -64,4 +215,4 @@ Please contribute using [GitHub Flow](https://guides.github.com/introduction/flo
64215

65216
Our version numbers follow the [semantic versioning specification](http://semver.org/). You can see the available versions by checking the [tags on this repository](https://github.com/thiagozs/go-pixgen/tags). For more details about our license model, please take a look at the [LICENSE](LICENSE) file.
66217

67-
**2022**, thiagozs
218+
**2022**, Thiago Zilli Sarmento :heart:

bin/.gitkeep

Whitespace-only changes.

0 commit comments

Comments
 (0)