Merge pull request #1 from rede5/codex/implement-b2b-marketplace-backend-in-go
Add Go performance core backend and refresh landing page
This commit is contained in:
commit
470f8463b1
19 changed files with 2515 additions and 22 deletions
1
backend-go/.gitignore
vendored
Normal file
1
backend-go/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/bin
|
||||||
18
backend-go/Dockerfile
Normal file
18
backend-go/Dockerfile
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
FROM golang:1.24 AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags "-s -w" -o /out/performance-core ./cmd/api
|
||||||
|
|
||||||
|
FROM gcr.io/distroless/base-debian12:nonroot
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=builder /out/performance-core /app/performance-core
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
ENTRYPOINT ["/app/performance-core"]
|
||||||
27
backend-go/README.md
Normal file
27
backend-go/README.md
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
# SaveInMed Performance Core (Go)
|
||||||
|
|
||||||
|
Backend em Go 1.24 focado em alta performance para o marketplace farmacêutico B2B.
|
||||||
|
|
||||||
|
## Funcionalidades
|
||||||
|
- Gestão de empresas com separação de papéis (farmácia, distribuidora, administrador).
|
||||||
|
- Catálogo de produtos com lote e validade obrigatórios.
|
||||||
|
- Pedidos com ciclo Pendente → Pago → Faturado → Entregue.
|
||||||
|
- Geração de preferência de pagamento Mercado Pago com split e retenção de comissão.
|
||||||
|
- Respostas JSON com `json-iterator` e compressão gzip.
|
||||||
|
- Swagger disponível em `/swagger/index.html`.
|
||||||
|
|
||||||
|
## Execução local
|
||||||
|
```bash
|
||||||
|
export DATABASE_URL=postgres://postgres:postgres@localhost:5432/saveinmed?sslmode=disable
|
||||||
|
cd backend-go
|
||||||
|
# gerar swagger (já versionado)
|
||||||
|
./bin/swag init --dir ./cmd/api,./internal/http/handler,./internal/domain --output ./docs --parseDependency --parseInternal
|
||||||
|
# executar API
|
||||||
|
go run ./cmd/api
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
```bash
|
||||||
|
docker build -t saveinmed-performance-core:dev .
|
||||||
|
docker run -p 8080:8080 -e DATABASE_URL=postgres://... saveinmed-performance-core:dev
|
||||||
|
```
|
||||||
43
backend-go/cmd/api/main.go
Normal file
43
backend-go/cmd/api/main.go
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
_ "github.com/jackc/pgx/v5/stdlib"
|
||||||
|
|
||||||
|
"github.com/saveinmed/backend-go/docs"
|
||||||
|
"github.com/saveinmed/backend-go/internal/config"
|
||||||
|
"github.com/saveinmed/backend-go/internal/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @title SaveInMed Performance Core API
|
||||||
|
// @version 1.0
|
||||||
|
// @description API REST B2B para marketplace farmacêutico com split de pagamento e rastreabilidade.
|
||||||
|
// @BasePath /
|
||||||
|
// @Schemes http
|
||||||
|
// @contact.name Engenharia SaveInMed
|
||||||
|
// @contact.email devops@saveinmed.com
|
||||||
|
func main() {
|
||||||
|
cfg := config.Load()
|
||||||
|
|
||||||
|
// swagger metadata overrides
|
||||||
|
docs.SwaggerInfo.Title = cfg.AppName
|
||||||
|
docs.SwaggerInfo.BasePath = "/"
|
||||||
|
|
||||||
|
srv, err := server.New(cfg)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("boot failure: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
if err := srv.Start(ctx); err != nil {
|
||||||
|
log.Printf("server stopped: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
501
backend-go/docs/docs.go
Normal file
501
backend-go/docs/docs.go
Normal file
|
|
@ -0,0 +1,501 @@
|
||||||
|
// Package docs Code generated by swaggo/swag. DO NOT EDIT
|
||||||
|
package docs
|
||||||
|
|
||||||
|
import "github.com/swaggo/swag"
|
||||||
|
|
||||||
|
const docTemplate = `{
|
||||||
|
"schemes": {{ marshal .Schemes }},
|
||||||
|
"swagger": "2.0",
|
||||||
|
"info": {
|
||||||
|
"description": "{{escape .Description}}",
|
||||||
|
"title": "{{.Title}}",
|
||||||
|
"contact": {
|
||||||
|
"name": "Engenharia SaveInMed",
|
||||||
|
"email": "devops@saveinmed.com"
|
||||||
|
},
|
||||||
|
"version": "{{.Version}}"
|
||||||
|
},
|
||||||
|
"host": "{{.Host}}",
|
||||||
|
"basePath": "{{.BasePath}}",
|
||||||
|
"paths": {
|
||||||
|
"/api/companies": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Empresas"
|
||||||
|
],
|
||||||
|
"summary": "Lista empresas",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/domain.Company"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"description": "Cadastra farmácia, distribuidora ou administrador com CNPJ e licença sanitária.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Empresas"
|
||||||
|
],
|
||||||
|
"summary": "Registro de empresas",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Dados da empresa",
|
||||||
|
"name": "company",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handler.registerCompanyRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "Created",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.Company"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/orders": {
|
||||||
|
"post": {
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Pedidos"
|
||||||
|
],
|
||||||
|
"summary": "Criação de pedido com split",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Pedido",
|
||||||
|
"name": "order",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handler.createOrderRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "Created",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.Order"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/orders/{id}": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Pedidos"
|
||||||
|
],
|
||||||
|
"summary": "Consulta pedido",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Order ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.Order"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/orders/{id}/payment": {
|
||||||
|
"post": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Pagamentos"
|
||||||
|
],
|
||||||
|
"summary": "Cria preferência de pagamento Mercado Pago com split nativo",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Order ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "Created",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.PaymentPreference"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/orders/{id}/status": {
|
||||||
|
"patch": {
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Pedidos"
|
||||||
|
],
|
||||||
|
"summary": "Atualiza status do pedido",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Order ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Novo status",
|
||||||
|
"name": "status",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handler.updateStatusRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/products": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Produtos"
|
||||||
|
],
|
||||||
|
"summary": "Lista catálogo com lote e validade",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/domain.Product"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Produtos"
|
||||||
|
],
|
||||||
|
"summary": "Cadastro de produto com rastreabilidade de lote",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Produto",
|
||||||
|
"name": "product",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handler.registerProductRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "Created",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.Product"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"definitions": {
|
||||||
|
"domain.Company": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"cnpj": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"corporate_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"description": "pharmacy, distributor, admin",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"sanitary_license": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"domain.Order": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"buyer_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/domain.OrderItem"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"seller_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"$ref": "#/definitions/domain.OrderStatus"
|
||||||
|
},
|
||||||
|
"total_cents": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"domain.OrderItem": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"batch": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"order_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"product_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"quantity": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"unit_cents": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"domain.OrderStatus": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"Pendente",
|
||||||
|
"Pago",
|
||||||
|
"Faturado",
|
||||||
|
"Entregue"
|
||||||
|
],
|
||||||
|
"x-enum-varnames": [
|
||||||
|
"OrderStatusPending",
|
||||||
|
"OrderStatusPaid",
|
||||||
|
"OrderStatusInvoiced",
|
||||||
|
"OrderStatusDelivered"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"domain.PaymentPreference": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"commission_pct": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"gateway": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"marketplace_fee": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"order_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"payment_url": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"seller_receivable": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"domain.Product": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"batch": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"price_cents": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"seller_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"stock": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handler.createOrderRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"buyer_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/domain.OrderItem"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"seller_id": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handler.registerCompanyRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"cnpj": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"corporate_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"sanitary_license": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handler.registerProductRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"batch": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"price_cents": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"seller_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"stock": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handler.updateStatusRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
// SwaggerInfo holds exported Swagger Info so clients can modify it
|
||||||
|
var SwaggerInfo = &swag.Spec{
|
||||||
|
Version: "1.0",
|
||||||
|
Host: "",
|
||||||
|
BasePath: "/",
|
||||||
|
Schemes: []string{"http"},
|
||||||
|
Title: "SaveInMed Performance Core API",
|
||||||
|
Description: "API REST B2B para marketplace farmacêutico com split de pagamento e rastreabilidade.",
|
||||||
|
InfoInstanceName: "swagger",
|
||||||
|
SwaggerTemplate: docTemplate,
|
||||||
|
LeftDelim: "{{",
|
||||||
|
RightDelim: "}}",
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
|
||||||
|
}
|
||||||
479
backend-go/docs/swagger.json
Normal file
479
backend-go/docs/swagger.json
Normal file
|
|
@ -0,0 +1,479 @@
|
||||||
|
{
|
||||||
|
"schemes": [
|
||||||
|
"http"
|
||||||
|
],
|
||||||
|
"swagger": "2.0",
|
||||||
|
"info": {
|
||||||
|
"description": "API REST B2B para marketplace farmacêutico com split de pagamento e rastreabilidade.",
|
||||||
|
"title": "SaveInMed Performance Core API",
|
||||||
|
"contact": {
|
||||||
|
"name": "Engenharia SaveInMed",
|
||||||
|
"email": "devops@saveinmed.com"
|
||||||
|
},
|
||||||
|
"version": "1.0"
|
||||||
|
},
|
||||||
|
"basePath": "/",
|
||||||
|
"paths": {
|
||||||
|
"/api/companies": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Empresas"
|
||||||
|
],
|
||||||
|
"summary": "Lista empresas",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/domain.Company"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"description": "Cadastra farmácia, distribuidora ou administrador com CNPJ e licença sanitária.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Empresas"
|
||||||
|
],
|
||||||
|
"summary": "Registro de empresas",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Dados da empresa",
|
||||||
|
"name": "company",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handler.registerCompanyRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "Created",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.Company"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/orders": {
|
||||||
|
"post": {
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Pedidos"
|
||||||
|
],
|
||||||
|
"summary": "Criação de pedido com split",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Pedido",
|
||||||
|
"name": "order",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handler.createOrderRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "Created",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.Order"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/orders/{id}": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Pedidos"
|
||||||
|
],
|
||||||
|
"summary": "Consulta pedido",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Order ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.Order"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/orders/{id}/payment": {
|
||||||
|
"post": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Pagamentos"
|
||||||
|
],
|
||||||
|
"summary": "Cria preferência de pagamento Mercado Pago com split nativo",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Order ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "Created",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.PaymentPreference"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/orders/{id}/status": {
|
||||||
|
"patch": {
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Pedidos"
|
||||||
|
],
|
||||||
|
"summary": "Atualiza status do pedido",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Order ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Novo status",
|
||||||
|
"name": "status",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handler.updateStatusRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/products": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Produtos"
|
||||||
|
],
|
||||||
|
"summary": "Lista catálogo com lote e validade",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/domain.Product"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Produtos"
|
||||||
|
],
|
||||||
|
"summary": "Cadastro de produto com rastreabilidade de lote",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Produto",
|
||||||
|
"name": "product",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handler.registerProductRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "Created",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.Product"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"definitions": {
|
||||||
|
"domain.Company": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"cnpj": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"corporate_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"description": "pharmacy, distributor, admin",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"sanitary_license": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"domain.Order": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"buyer_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/domain.OrderItem"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"seller_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"$ref": "#/definitions/domain.OrderStatus"
|
||||||
|
},
|
||||||
|
"total_cents": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"domain.OrderItem": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"batch": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"order_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"product_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"quantity": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"unit_cents": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"domain.OrderStatus": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"Pendente",
|
||||||
|
"Pago",
|
||||||
|
"Faturado",
|
||||||
|
"Entregue"
|
||||||
|
],
|
||||||
|
"x-enum-varnames": [
|
||||||
|
"OrderStatusPending",
|
||||||
|
"OrderStatusPaid",
|
||||||
|
"OrderStatusInvoiced",
|
||||||
|
"OrderStatusDelivered"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"domain.PaymentPreference": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"commission_pct": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"gateway": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"marketplace_fee": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"order_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"payment_url": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"seller_receivable": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"domain.Product": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"batch": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"price_cents": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"seller_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"stock": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handler.createOrderRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"buyer_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/domain.OrderItem"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"seller_id": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handler.registerCompanyRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"cnpj": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"corporate_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"sanitary_license": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handler.registerProductRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"batch": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"price_cents": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"seller_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"stock": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handler.updateStatusRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
315
backend-go/docs/swagger.yaml
Normal file
315
backend-go/docs/swagger.yaml
Normal file
|
|
@ -0,0 +1,315 @@
|
||||||
|
basePath: /
|
||||||
|
definitions:
|
||||||
|
domain.Company:
|
||||||
|
properties:
|
||||||
|
cnpj:
|
||||||
|
type: string
|
||||||
|
corporate_name:
|
||||||
|
type: string
|
||||||
|
created_at:
|
||||||
|
type: string
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
role:
|
||||||
|
description: pharmacy, distributor, admin
|
||||||
|
type: string
|
||||||
|
sanitary_license:
|
||||||
|
type: string
|
||||||
|
updated_at:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
domain.Order:
|
||||||
|
properties:
|
||||||
|
buyer_id:
|
||||||
|
type: string
|
||||||
|
created_at:
|
||||||
|
type: string
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
items:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/domain.OrderItem'
|
||||||
|
type: array
|
||||||
|
seller_id:
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
$ref: '#/definitions/domain.OrderStatus'
|
||||||
|
total_cents:
|
||||||
|
type: integer
|
||||||
|
updated_at:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
domain.OrderItem:
|
||||||
|
properties:
|
||||||
|
batch:
|
||||||
|
type: string
|
||||||
|
expires_at:
|
||||||
|
type: string
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
order_id:
|
||||||
|
type: string
|
||||||
|
product_id:
|
||||||
|
type: string
|
||||||
|
quantity:
|
||||||
|
type: integer
|
||||||
|
unit_cents:
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
|
domain.OrderStatus:
|
||||||
|
enum:
|
||||||
|
- Pendente
|
||||||
|
- Pago
|
||||||
|
- Faturado
|
||||||
|
- Entregue
|
||||||
|
type: string
|
||||||
|
x-enum-varnames:
|
||||||
|
- OrderStatusPending
|
||||||
|
- OrderStatusPaid
|
||||||
|
- OrderStatusInvoiced
|
||||||
|
- OrderStatusDelivered
|
||||||
|
domain.PaymentPreference:
|
||||||
|
properties:
|
||||||
|
commission_pct:
|
||||||
|
type: number
|
||||||
|
gateway:
|
||||||
|
type: string
|
||||||
|
marketplace_fee:
|
||||||
|
type: integer
|
||||||
|
order_id:
|
||||||
|
type: string
|
||||||
|
payment_url:
|
||||||
|
type: string
|
||||||
|
seller_receivable:
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
|
domain.Product:
|
||||||
|
properties:
|
||||||
|
batch:
|
||||||
|
type: string
|
||||||
|
created_at:
|
||||||
|
type: string
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
expires_at:
|
||||||
|
type: string
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
price_cents:
|
||||||
|
type: integer
|
||||||
|
seller_id:
|
||||||
|
type: string
|
||||||
|
stock:
|
||||||
|
type: integer
|
||||||
|
updated_at:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
handler.createOrderRequest:
|
||||||
|
properties:
|
||||||
|
buyer_id:
|
||||||
|
type: string
|
||||||
|
items:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/domain.OrderItem'
|
||||||
|
type: array
|
||||||
|
seller_id:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
handler.registerCompanyRequest:
|
||||||
|
properties:
|
||||||
|
cnpj:
|
||||||
|
type: string
|
||||||
|
corporate_name:
|
||||||
|
type: string
|
||||||
|
role:
|
||||||
|
type: string
|
||||||
|
sanitary_license:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
handler.registerProductRequest:
|
||||||
|
properties:
|
||||||
|
batch:
|
||||||
|
type: string
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
expires_at:
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
price_cents:
|
||||||
|
type: integer
|
||||||
|
seller_id:
|
||||||
|
type: string
|
||||||
|
stock:
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
|
handler.updateStatusRequest:
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
info:
|
||||||
|
contact:
|
||||||
|
email: devops@saveinmed.com
|
||||||
|
name: Engenharia SaveInMed
|
||||||
|
description: API REST B2B para marketplace farmacêutico com split de pagamento e
|
||||||
|
rastreabilidade.
|
||||||
|
title: SaveInMed Performance Core API
|
||||||
|
version: "1.0"
|
||||||
|
paths:
|
||||||
|
/api/companies:
|
||||||
|
get:
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/domain.Company'
|
||||||
|
type: array
|
||||||
|
summary: Lista empresas
|
||||||
|
tags:
|
||||||
|
- Empresas
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Cadastra farmácia, distribuidora ou administrador com CNPJ e licença
|
||||||
|
sanitária.
|
||||||
|
parameters:
|
||||||
|
- description: Dados da empresa
|
||||||
|
in: body
|
||||||
|
name: company
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/handler.registerCompanyRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"201":
|
||||||
|
description: Created
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.Company'
|
||||||
|
summary: Registro de empresas
|
||||||
|
tags:
|
||||||
|
- Empresas
|
||||||
|
/api/orders:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
parameters:
|
||||||
|
- description: Pedido
|
||||||
|
in: body
|
||||||
|
name: order
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/handler.createOrderRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"201":
|
||||||
|
description: Created
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.Order'
|
||||||
|
summary: Criação de pedido com split
|
||||||
|
tags:
|
||||||
|
- Pedidos
|
||||||
|
/api/orders/{id}:
|
||||||
|
get:
|
||||||
|
parameters:
|
||||||
|
- description: Order ID
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.Order'
|
||||||
|
summary: Consulta pedido
|
||||||
|
tags:
|
||||||
|
- Pedidos
|
||||||
|
/api/orders/{id}/payment:
|
||||||
|
post:
|
||||||
|
parameters:
|
||||||
|
- description: Order ID
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"201":
|
||||||
|
description: Created
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.PaymentPreference'
|
||||||
|
summary: Cria preferência de pagamento Mercado Pago com split nativo
|
||||||
|
tags:
|
||||||
|
- Pagamentos
|
||||||
|
/api/orders/{id}/status:
|
||||||
|
patch:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
parameters:
|
||||||
|
- description: Order ID
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: Novo status
|
||||||
|
in: body
|
||||||
|
name: status
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/handler.updateStatusRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: ""
|
||||||
|
summary: Atualiza status do pedido
|
||||||
|
tags:
|
||||||
|
- Pedidos
|
||||||
|
/api/products:
|
||||||
|
get:
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/domain.Product'
|
||||||
|
type: array
|
||||||
|
summary: Lista catálogo com lote e validade
|
||||||
|
tags:
|
||||||
|
- Produtos
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
parameters:
|
||||||
|
- description: Produto
|
||||||
|
in: body
|
||||||
|
name: product
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/handler.registerProductRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"201":
|
||||||
|
description: Created
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.Product'
|
||||||
|
summary: Cadastro de produto com rastreabilidade de lote
|
||||||
|
tags:
|
||||||
|
- Produtos
|
||||||
|
schemes:
|
||||||
|
- http
|
||||||
|
swagger: "2.0"
|
||||||
36
backend-go/go.mod
Normal file
36
backend-go/go.mod
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
module github.com/saveinmed/backend-go
|
||||||
|
|
||||||
|
go 1.24.3
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gofrs/uuid/v5 v5.4.0
|
||||||
|
github.com/jackc/pgx/v5 v5.7.6
|
||||||
|
github.com/jmoiron/sqlx v1.4.0
|
||||||
|
github.com/json-iterator/go v1.1.12
|
||||||
|
github.com/swaggo/http-swagger v1.3.4
|
||||||
|
github.com/swaggo/swag v1.16.6
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||||
|
github.com/go-openapi/jsonreference v0.20.0 // indirect
|
||||||
|
github.com/go-openapi/spec v0.20.6 // indirect
|
||||||
|
github.com/go-openapi/swag v0.19.15 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
|
github.com/mailru/easyjson v0.7.6 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
|
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect
|
||||||
|
golang.org/x/crypto v0.37.0 // indirect
|
||||||
|
golang.org/x/mod v0.21.0 // indirect
|
||||||
|
golang.org/x/net v0.34.0 // indirect
|
||||||
|
golang.org/x/sync v0.13.0 // indirect
|
||||||
|
golang.org/x/text v0.24.0 // indirect
|
||||||
|
golang.org/x/tools v0.26.0 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
)
|
||||||
103
backend-go/go.sum
Normal file
103
backend-go/go.sum
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
|
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||||
|
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||||
|
github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA=
|
||||||
|
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
|
||||||
|
github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ=
|
||||||
|
github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
|
||||||
|
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||||
|
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
|
||||||
|
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
|
github.com/gofrs/uuid/v5 v5.4.0 h1:EfbpCTjqMuGyq5ZJwxqzn3Cbr2d0rUZU7v5ycAk/e/0=
|
||||||
|
github.com/gofrs/uuid/v5 v5.4.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
|
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
|
||||||
|
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
|
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||||
|
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||||
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||||
|
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||||
|
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe h1:K8pHPVoTgxFJt1lXuIzzOX7zZhZFldJQK/CgKx9BFIc=
|
||||||
|
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
|
||||||
|
github.com/swaggo/http-swagger v1.3.4 h1:q7t/XLx0n15H1Q9/tk3Y9L4n210XzJF5WtnDX64a5ww=
|
||||||
|
github.com/swaggo/http-swagger v1.3.4/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4UbucIg1MFkQ=
|
||||||
|
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
|
||||||
|
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
|
||||||
|
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||||
|
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||||
|
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
||||||
|
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||||
|
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||||
|
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||||
|
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||||
|
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||||
|
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
||||||
|
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
63
backend-go/internal/config/config.go
Normal file
63
backend-go/internal/config/config.go
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config centralizes runtime configuration loaded from the environment.
|
||||||
|
type Config struct {
|
||||||
|
AppName string
|
||||||
|
Port string
|
||||||
|
DatabaseURL string
|
||||||
|
MaxOpenConns int
|
||||||
|
MaxIdleConns int
|
||||||
|
ConnMaxIdle time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load reads configuration from environment variables and applies sane defaults
|
||||||
|
// for local development.
|
||||||
|
func Load() Config {
|
||||||
|
cfg := Config{
|
||||||
|
AppName: getEnv("APP_NAME", "saveinmed-performance-core"),
|
||||||
|
Port: getEnv("PORT", "8080"),
|
||||||
|
DatabaseURL: getEnv("DATABASE_URL", "postgres://postgres:postgres@localhost:5432/saveinmed?sslmode=disable"),
|
||||||
|
MaxOpenConns: getEnvInt("DB_MAX_OPEN_CONNS", 15),
|
||||||
|
MaxIdleConns: getEnvInt("DB_MAX_IDLE_CONNS", 5),
|
||||||
|
ConnMaxIdle: getEnvDuration("DB_CONN_MAX_IDLE", 5*time.Minute),
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
// Addr returns the address binding for the HTTP server.
|
||||||
|
func (c Config) Addr() string {
|
||||||
|
return fmt.Sprintf(":%s", c.Port)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnv(key, fallback string) string {
|
||||||
|
if value := os.Getenv(key); value != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnvInt(key string, fallback int) int {
|
||||||
|
if value := os.Getenv(key); value != "" {
|
||||||
|
if parsed, err := strconv.Atoi(value); err == nil {
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnvDuration(key string, fallback time.Duration) time.Duration {
|
||||||
|
if value := os.Getenv(key); value != "" {
|
||||||
|
if parsed, err := time.ParseDuration(value); err == nil {
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
75
backend-go/internal/domain/models.go
Normal file
75
backend-go/internal/domain/models.go
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofrs/uuid/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Company represents a B2B actor in the marketplace.
|
||||||
|
type Company struct {
|
||||||
|
ID uuid.UUID `db:"id" json:"id"`
|
||||||
|
Role string `db:"role" json:"role"` // pharmacy, distributor, admin
|
||||||
|
CNPJ string `db:"cnpj" json:"cnpj"`
|
||||||
|
CorporateName string `db:"corporate_name" json:"corporate_name"`
|
||||||
|
SanitaryLicense string `db:"sanitary_license" json:"sanitary_license"`
|
||||||
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Product represents a medicine SKU with batch tracking.
|
||||||
|
type Product struct {
|
||||||
|
ID uuid.UUID `db:"id" json:"id"`
|
||||||
|
SellerID uuid.UUID `db:"seller_id" json:"seller_id"`
|
||||||
|
Name string `db:"name" json:"name"`
|
||||||
|
Description string `db:"description" json:"description"`
|
||||||
|
Batch string `db:"batch" json:"batch"`
|
||||||
|
ExpiresAt time.Time `db:"expires_at" json:"expires_at"`
|
||||||
|
PriceCents int64 `db:"price_cents" json:"price_cents"`
|
||||||
|
Stock int64 `db:"stock" json:"stock"`
|
||||||
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Order captures the status lifecycle and payment intent.
|
||||||
|
type Order struct {
|
||||||
|
ID uuid.UUID `db:"id" json:"id"`
|
||||||
|
BuyerID uuid.UUID `db:"buyer_id" json:"buyer_id"`
|
||||||
|
SellerID uuid.UUID `db:"seller_id" json:"seller_id"`
|
||||||
|
Status OrderStatus `db:"status" json:"status"`
|
||||||
|
TotalCents int64 `db:"total_cents" json:"total_cents"`
|
||||||
|
Items []OrderItem `json:"items"`
|
||||||
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// OrderItem stores SKU-level batch tracking.
|
||||||
|
type OrderItem struct {
|
||||||
|
ID uuid.UUID `db:"id" json:"id"`
|
||||||
|
OrderID uuid.UUID `db:"order_id" json:"order_id"`
|
||||||
|
ProductID uuid.UUID `db:"product_id" json:"product_id"`
|
||||||
|
Quantity int64 `db:"quantity" json:"quantity"`
|
||||||
|
UnitCents int64 `db:"unit_cents" json:"unit_cents"`
|
||||||
|
Batch string `db:"batch" json:"batch"`
|
||||||
|
ExpiresAt time.Time `db:"expires_at" json:"expires_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaymentPreference wraps the data needed for Mercado Pago split payments.
|
||||||
|
type PaymentPreference struct {
|
||||||
|
OrderID uuid.UUID `json:"order_id"`
|
||||||
|
Gateway string `json:"gateway"`
|
||||||
|
CommissionPct float64 `json:"commission_pct"`
|
||||||
|
MarketplaceFee int64 `json:"marketplace_fee"`
|
||||||
|
SellerReceivable int64 `json:"seller_receivable"`
|
||||||
|
PaymentURL string `json:"payment_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// OrderStatus enumerates supported transitions.
|
||||||
|
type OrderStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
OrderStatusPending OrderStatus = "Pendente"
|
||||||
|
OrderStatusPaid OrderStatus = "Pago"
|
||||||
|
OrderStatusInvoiced OrderStatus = "Faturado"
|
||||||
|
OrderStatusDelivered OrderStatus = "Entregue"
|
||||||
|
)
|
||||||
326
backend-go/internal/http/handler/handler.go
Normal file
326
backend-go/internal/http/handler/handler.go
Normal file
|
|
@ -0,0 +1,326 @@
|
||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
jsoniter "github.com/json-iterator/go"
|
||||||
|
|
||||||
|
"github.com/gofrs/uuid/v5"
|
||||||
|
|
||||||
|
"github.com/saveinmed/backend-go/internal/domain"
|
||||||
|
"github.com/saveinmed/backend-go/internal/usecase"
|
||||||
|
)
|
||||||
|
|
||||||
|
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
svc *usecase.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(svc *usecase.Service) *Handler {
|
||||||
|
return &Handler{svc: svc}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateCompany godoc
|
||||||
|
// @Summary Registro de empresas
|
||||||
|
// @Description Cadastra farmácia, distribuidora ou administrador com CNPJ e licença sanitária.
|
||||||
|
// @Tags Empresas
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param company body registerCompanyRequest true "Dados da empresa"
|
||||||
|
// @Success 201 {object} domain.Company
|
||||||
|
// @Router /api/companies [post]
|
||||||
|
func (h *Handler) CreateCompany(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req registerCompanyRequest
|
||||||
|
if err := decodeJSON(r.Context(), r, &req); err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
company := &domain.Company{
|
||||||
|
Role: req.Role,
|
||||||
|
CNPJ: req.CNPJ,
|
||||||
|
CorporateName: req.CorporateName,
|
||||||
|
SanitaryLicense: req.SanitaryLicense,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.svc.RegisterCompany(r.Context(), company); err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusCreated, company)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListCompanies godoc
|
||||||
|
// @Summary Lista empresas
|
||||||
|
// @Tags Empresas
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {array} domain.Company
|
||||||
|
// @Router /api/companies [get]
|
||||||
|
func (h *Handler) ListCompanies(w http.ResponseWriter, r *http.Request) {
|
||||||
|
companies, err := h.svc.ListCompanies(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, companies)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateProduct godoc
|
||||||
|
// @Summary Cadastro de produto com rastreabilidade de lote
|
||||||
|
// @Tags Produtos
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param product body registerProductRequest true "Produto"
|
||||||
|
// @Success 201 {object} domain.Product
|
||||||
|
// @Router /api/products [post]
|
||||||
|
func (h *Handler) CreateProduct(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req registerProductRequest
|
||||||
|
if err := decodeJSON(r.Context(), r, &req); err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
product := &domain.Product{
|
||||||
|
SellerID: req.SellerID,
|
||||||
|
Name: req.Name,
|
||||||
|
Description: req.Description,
|
||||||
|
Batch: req.Batch,
|
||||||
|
ExpiresAt: req.ExpiresAt,
|
||||||
|
PriceCents: req.PriceCents,
|
||||||
|
Stock: req.Stock,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.svc.RegisterProduct(r.Context(), product); err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusCreated, product)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListProducts godoc
|
||||||
|
// @Summary Lista catálogo com lote e validade
|
||||||
|
// @Tags Produtos
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {array} domain.Product
|
||||||
|
// @Router /api/products [get]
|
||||||
|
func (h *Handler) ListProducts(w http.ResponseWriter, r *http.Request) {
|
||||||
|
products, err := h.svc.ListProducts(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, products)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateOrder godoc
|
||||||
|
// @Summary Criação de pedido com split
|
||||||
|
// @Tags Pedidos
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param order body createOrderRequest true "Pedido"
|
||||||
|
// @Success 201 {object} domain.Order
|
||||||
|
// @Router /api/orders [post]
|
||||||
|
func (h *Handler) CreateOrder(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req createOrderRequest
|
||||||
|
if err := decodeJSON(r.Context(), r, &req); err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
order := &domain.Order{
|
||||||
|
BuyerID: req.BuyerID,
|
||||||
|
SellerID: req.SellerID,
|
||||||
|
Items: req.Items,
|
||||||
|
}
|
||||||
|
|
||||||
|
var total int64
|
||||||
|
for _, item := range req.Items {
|
||||||
|
total += item.UnitCents * item.Quantity
|
||||||
|
}
|
||||||
|
order.TotalCents = total
|
||||||
|
|
||||||
|
if err := h.svc.CreateOrder(r.Context(), order); err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusCreated, order)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOrder godoc
|
||||||
|
// @Summary Consulta pedido
|
||||||
|
// @Tags Pedidos
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Order ID"
|
||||||
|
// @Success 200 {object} domain.Order
|
||||||
|
// @Router /api/orders/{id} [get]
|
||||||
|
func (h *Handler) GetOrder(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, err := parseUUIDFromPath(r.URL.Path)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
order, err := h.svc.GetOrder(r.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusNotFound, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusOK, order)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateOrderStatus godoc
|
||||||
|
// @Summary Atualiza status do pedido
|
||||||
|
// @Tags Pedidos
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Order ID"
|
||||||
|
// @Param status body updateStatusRequest true "Novo status"
|
||||||
|
// @Success 204 ""
|
||||||
|
// @Router /api/orders/{id}/status [patch]
|
||||||
|
func (h *Handler) UpdateOrderStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, err := parseUUIDFromPath(r.URL.Path)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req updateStatusRequest
|
||||||
|
if err := decodeJSON(r.Context(), r, &req); err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isValidStatus(req.Status) {
|
||||||
|
writeError(w, http.StatusBadRequest, errors.New("invalid status"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.svc.UpdateOrderStatus(r.Context(), id, domain.OrderStatus(req.Status)); err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatePaymentPreference godoc
|
||||||
|
// @Summary Cria preferência de pagamento Mercado Pago com split nativo
|
||||||
|
// @Tags Pagamentos
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Order ID"
|
||||||
|
// @Success 201 {object} domain.PaymentPreference
|
||||||
|
// @Router /api/orders/{id}/payment [post]
|
||||||
|
func (h *Handler) CreatePaymentPreference(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !strings.HasSuffix(r.URL.Path, "/payment") {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := parseUUIDFromPath(r.URL.Path)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pref, err := h.svc.CreatePaymentPreference(r.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusCreated, pref)
|
||||||
|
}
|
||||||
|
|
||||||
|
type registerCompanyRequest struct {
|
||||||
|
Role string `json:"role"`
|
||||||
|
CNPJ string `json:"cnpj"`
|
||||||
|
CorporateName string `json:"corporate_name"`
|
||||||
|
SanitaryLicense string `json:"sanitary_license"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type registerProductRequest struct {
|
||||||
|
SellerID uuid.UUID `json:"seller_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Batch string `json:"batch"`
|
||||||
|
ExpiresAt time.Time `json:"expires_at"`
|
||||||
|
PriceCents int64 `json:"price_cents"`
|
||||||
|
Stock int64 `json:"stock"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type createOrderRequest struct {
|
||||||
|
BuyerID uuid.UUID `json:"buyer_id"`
|
||||||
|
SellerID uuid.UUID `json:"seller_id"`
|
||||||
|
Items []domain.OrderItem `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type updateStatusRequest struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeJSON(w http.ResponseWriter, status int, v any) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(status)
|
||||||
|
_ = json.NewEncoder(w).Encode(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeError(w http.ResponseWriter, status int, err error) {
|
||||||
|
writeJSON(w, status, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeJSON(ctx context.Context, r *http.Request, v any) error {
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
dec := json.NewDecoder(r.Body)
|
||||||
|
dec.DisallowUnknownFields()
|
||||||
|
if err := dec.Decode(v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseUUIDFromPath(path string) (uuid.UUID, error) {
|
||||||
|
parts := splitPath(path)
|
||||||
|
for i := len(parts) - 1; i >= 0; i-- {
|
||||||
|
if id, err := uuid.FromString(parts[i]); err == nil {
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uuid.UUID{}, errors.New("missing resource id")
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitPath(p string) []string {
|
||||||
|
var parts []string
|
||||||
|
start := 0
|
||||||
|
for i := 0; i < len(p); i++ {
|
||||||
|
if p[i] == '/' {
|
||||||
|
if i > start {
|
||||||
|
parts = append(parts, p[start:i])
|
||||||
|
}
|
||||||
|
start = i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if start < len(p) {
|
||||||
|
parts = append(parts, p[start:])
|
||||||
|
}
|
||||||
|
return parts
|
||||||
|
}
|
||||||
|
|
||||||
|
func isValidStatus(status string) bool {
|
||||||
|
switch domain.OrderStatus(status) {
|
||||||
|
case domain.OrderStatusPending, domain.OrderStatusPaid, domain.OrderStatusInvoiced, domain.OrderStatusDelivered:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
36
backend-go/internal/http/middleware/compress.go
Normal file
36
backend-go/internal/http/middleware/compress.go
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"compress/gzip"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Gzip wraps HTTP responses with gzip compression when supported by the client.
|
||||||
|
func Gzip(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gz := gzip.NewWriter(w)
|
||||||
|
defer gz.Close()
|
||||||
|
|
||||||
|
w.Header().Set("Content-Encoding", "gzip")
|
||||||
|
w.Header().Add("Vary", "Accept-Encoding")
|
||||||
|
|
||||||
|
gzr := gzipResponseWriter{Writer: gz, ResponseWriter: w}
|
||||||
|
next.ServeHTTP(gzr, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type gzipResponseWriter struct {
|
||||||
|
io.Writer
|
||||||
|
http.ResponseWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w gzipResponseWriter) Write(b []byte) (int, error) {
|
||||||
|
return w.Writer.Write(b)
|
||||||
|
}
|
||||||
16
backend-go/internal/http/middleware/logging.go
Normal file
16
backend-go/internal/http/middleware/logging.go
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Logger records basic request information.
|
||||||
|
func Logger(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
start := time.Now()
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start))
|
||||||
|
})
|
||||||
|
}
|
||||||
44
backend-go/internal/payments/mercadopago.go
Normal file
44
backend-go/internal/payments/mercadopago.go
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
package payments
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/saveinmed/backend-go/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MercadoPagoGateway fakes the split configuration while keeping the API contract ready for the SDK.
|
||||||
|
type MercadoPagoGateway struct {
|
||||||
|
MarketplaceCommission float64
|
||||||
|
BaseURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMercadoPagoGateway() *MercadoPagoGateway {
|
||||||
|
return &MercadoPagoGateway{
|
||||||
|
MarketplaceCommission: 2.5,
|
||||||
|
BaseURL: "https://api.mercadopago.com",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *MercadoPagoGateway) CreatePreference(ctx context.Context, order *domain.Order) (*domain.PaymentPreference, error) {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
fee := int64(float64(order.TotalCents) * (g.MarketplaceCommission / 100))
|
||||||
|
pref := &domain.PaymentPreference{
|
||||||
|
OrderID: order.ID,
|
||||||
|
Gateway: "mercadopago",
|
||||||
|
CommissionPct: g.MarketplaceCommission,
|
||||||
|
MarketplaceFee: fee,
|
||||||
|
SellerReceivable: order.TotalCents - fee,
|
||||||
|
PaymentURL: fmt.Sprintf("%s/checkout/v1/redirect?order_id=%s", g.BaseURL, order.ID.String()),
|
||||||
|
}
|
||||||
|
|
||||||
|
// In a real gateway this is where the SDK call would run; we preserve latency budgets.
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
return pref, nil
|
||||||
|
}
|
||||||
181
backend-go/internal/repository/postgres/postgres.go
Normal file
181
backend-go/internal/repository/postgres/postgres.go
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofrs/uuid/v5"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
|
||||||
|
"github.com/saveinmed/backend-go/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Repository implements the data access layer using sqlx + pgx.
|
||||||
|
type Repository struct {
|
||||||
|
db *sqlx.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a Postgres-backed repository and configures pooling.
|
||||||
|
func New(db *sqlx.DB) *Repository {
|
||||||
|
return &Repository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) CreateCompany(ctx context.Context, company *domain.Company) error {
|
||||||
|
now := time.Now().UTC()
|
||||||
|
company.CreatedAt = now
|
||||||
|
company.UpdatedAt = now
|
||||||
|
|
||||||
|
query := `INSERT INTO companies (id, role, cnpj, corporate_name, sanitary_license, created_at, updated_at)
|
||||||
|
VALUES (:id, :role, :cnpj, :corporate_name, :sanitary_license, :created_at, :updated_at)`
|
||||||
|
|
||||||
|
_, err := r.db.NamedExecContext(ctx, query, company)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) ListCompanies(ctx context.Context) ([]domain.Company, error) {
|
||||||
|
var companies []domain.Company
|
||||||
|
query := `SELECT id, role, cnpj, corporate_name, sanitary_license, created_at, updated_at FROM companies ORDER BY created_at DESC`
|
||||||
|
if err := r.db.SelectContext(ctx, &companies, query); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return companies, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) CreateProduct(ctx context.Context, product *domain.Product) error {
|
||||||
|
now := time.Now().UTC()
|
||||||
|
product.CreatedAt = now
|
||||||
|
product.UpdatedAt = now
|
||||||
|
|
||||||
|
query := `INSERT INTO products (id, seller_id, name, description, batch, expires_at, price_cents, stock, created_at, updated_at)
|
||||||
|
VALUES (:id, :seller_id, :name, :description, :batch, :expires_at, :price_cents, :stock, :created_at, :updated_at)`
|
||||||
|
|
||||||
|
_, err := r.db.NamedExecContext(ctx, query, product)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) ListProducts(ctx context.Context) ([]domain.Product, error) {
|
||||||
|
var products []domain.Product
|
||||||
|
query := `SELECT id, seller_id, name, description, batch, expires_at, price_cents, stock, created_at, updated_at FROM products ORDER BY created_at DESC`
|
||||||
|
if err := r.db.SelectContext(ctx, &products, query); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return products, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) CreateOrder(ctx context.Context, order *domain.Order) error {
|
||||||
|
now := time.Now().UTC()
|
||||||
|
order.CreatedAt = now
|
||||||
|
order.UpdatedAt = now
|
||||||
|
|
||||||
|
tx, err := r.db.BeginTxx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
orderQuery := `INSERT INTO orders (id, buyer_id, seller_id, status, total_cents, created_at, updated_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7)`
|
||||||
|
if _, err := tx.ExecContext(ctx, orderQuery, order.ID, order.BuyerID, order.SellerID, order.Status, order.TotalCents, order.CreatedAt, order.UpdatedAt); err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
itemQuery := `INSERT INTO order_items (id, order_id, product_id, quantity, unit_cents, batch, expires_at) VALUES ($1, $2, $3, $4, $5, $6, $7)`
|
||||||
|
for i := range order.Items {
|
||||||
|
item := &order.Items[i]
|
||||||
|
item.ID = uuid.Must(uuid.NewV7())
|
||||||
|
item.OrderID = order.ID
|
||||||
|
if _, err := tx.ExecContext(ctx, itemQuery, item.ID, item.OrderID, item.ProductID, item.Quantity, item.UnitCents, item.Batch, item.ExpiresAt); err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) GetOrder(ctx context.Context, id uuid.UUID) (*domain.Order, error) {
|
||||||
|
var order domain.Order
|
||||||
|
orderQuery := `SELECT id, buyer_id, seller_id, status, total_cents, created_at, updated_at FROM orders WHERE id = $1`
|
||||||
|
if err := r.db.GetContext(ctx, &order, orderQuery, id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var items []domain.OrderItem
|
||||||
|
itemQuery := `SELECT id, order_id, product_id, quantity, unit_cents, batch, expires_at FROM order_items WHERE order_id = $1`
|
||||||
|
if err := r.db.SelectContext(ctx, &items, itemQuery, id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
order.Items = items
|
||||||
|
return &order, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) UpdateOrderStatus(ctx context.Context, id uuid.UUID, status domain.OrderStatus) error {
|
||||||
|
query := `UPDATE orders SET status = $1, updated_at = $2 WHERE id = $3`
|
||||||
|
res, err := r.db.ExecContext(ctx, query, status, time.Now().UTC(), id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rows, err := res.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if rows == 0 {
|
||||||
|
return errors.New("order not found")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitSchema applies a minimal schema for development environments.
|
||||||
|
func (r *Repository) InitSchema(ctx context.Context) error {
|
||||||
|
schema := `
|
||||||
|
CREATE TABLE IF NOT EXISTS companies (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
role TEXT NOT NULL,
|
||||||
|
cnpj TEXT NOT NULL UNIQUE,
|
||||||
|
corporate_name TEXT NOT NULL,
|
||||||
|
sanitary_license TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL,
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS products (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
seller_id UUID NOT NULL REFERENCES companies(id),
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
batch TEXT NOT NULL,
|
||||||
|
expires_at DATE NOT NULL,
|
||||||
|
price_cents BIGINT NOT NULL,
|
||||||
|
stock BIGINT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL,
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS orders (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
buyer_id UUID NOT NULL REFERENCES companies(id),
|
||||||
|
seller_id UUID NOT NULL REFERENCES companies(id),
|
||||||
|
status TEXT NOT NULL,
|
||||||
|
total_cents BIGINT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL,
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS order_items (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
order_id UUID NOT NULL REFERENCES orders(id),
|
||||||
|
product_id UUID NOT NULL REFERENCES products(id),
|
||||||
|
quantity BIGINT NOT NULL,
|
||||||
|
unit_cents BIGINT NOT NULL,
|
||||||
|
batch TEXT NOT NULL,
|
||||||
|
expires_at DATE NOT NULL
|
||||||
|
);
|
||||||
|
`
|
||||||
|
|
||||||
|
if _, err := r.db.ExecContext(ctx, schema); err != nil {
|
||||||
|
return fmt.Errorf("apply schema: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
92
backend-go/internal/server/server.go
Normal file
92
backend-go/internal/server/server.go
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "github.com/jackc/pgx/v5/stdlib"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
httpSwagger "github.com/swaggo/http-swagger"
|
||||||
|
|
||||||
|
"github.com/saveinmed/backend-go/internal/config"
|
||||||
|
"github.com/saveinmed/backend-go/internal/http/handler"
|
||||||
|
"github.com/saveinmed/backend-go/internal/http/middleware"
|
||||||
|
"github.com/saveinmed/backend-go/internal/payments"
|
||||||
|
"github.com/saveinmed/backend-go/internal/repository/postgres"
|
||||||
|
"github.com/saveinmed/backend-go/internal/usecase"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Server wires the infrastructure and exposes HTTP handlers.
|
||||||
|
type Server struct {
|
||||||
|
cfg config.Config
|
||||||
|
db *sqlx.DB
|
||||||
|
mux *http.ServeMux
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg config.Config) (*Server, error) {
|
||||||
|
db, err := sqlx.Open("pgx", cfg.DatabaseURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
db.SetMaxOpenConns(cfg.MaxOpenConns)
|
||||||
|
db.SetMaxIdleConns(cfg.MaxIdleConns)
|
||||||
|
db.SetConnMaxIdleTime(cfg.ConnMaxIdle)
|
||||||
|
|
||||||
|
repo := postgres.New(db)
|
||||||
|
gateway := payments.NewMercadoPagoGateway()
|
||||||
|
svc := usecase.NewService(repo, gateway)
|
||||||
|
h := handler.New(svc)
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write([]byte("ok"))
|
||||||
|
})
|
||||||
|
|
||||||
|
mux.Handle("POST /api/companies", chain(http.HandlerFunc(h.CreateCompany), middleware.Logger, middleware.Gzip))
|
||||||
|
mux.Handle("GET /api/companies", chain(http.HandlerFunc(h.ListCompanies), middleware.Logger, middleware.Gzip))
|
||||||
|
|
||||||
|
mux.Handle("POST /api/products", chain(http.HandlerFunc(h.CreateProduct), middleware.Logger, middleware.Gzip))
|
||||||
|
mux.Handle("GET /api/products", chain(http.HandlerFunc(h.ListProducts), middleware.Logger, middleware.Gzip))
|
||||||
|
|
||||||
|
mux.Handle("POST /api/orders", chain(http.HandlerFunc(h.CreateOrder), middleware.Logger, middleware.Gzip))
|
||||||
|
mux.Handle("GET /api/orders/", chain(http.HandlerFunc(h.GetOrder), middleware.Logger, middleware.Gzip))
|
||||||
|
mux.Handle("PATCH /api/orders/", chain(http.HandlerFunc(h.UpdateOrderStatus), middleware.Logger, middleware.Gzip))
|
||||||
|
mux.Handle("POST /api/orders/", chain(http.HandlerFunc(h.CreatePaymentPreference), middleware.Logger, middleware.Gzip))
|
||||||
|
|
||||||
|
mux.Handle("GET /swagger/", httpSwagger.Handler(httpSwagger.URL("/swagger/doc.json")))
|
||||||
|
|
||||||
|
return &Server{cfg: cfg, db: db, mux: mux}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start runs the HTTP server and ensures the database is reachable.
|
||||||
|
func (s *Server) Start(ctx context.Context) error {
|
||||||
|
if err := s.db.PingContext(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
repo := postgres.New(s.db)
|
||||||
|
if err := repo.InitSchema(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: s.cfg.Addr(),
|
||||||
|
Handler: s.mux,
|
||||||
|
ReadHeaderTimeout: 5 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("starting %s on %s", s.cfg.AppName, s.cfg.Addr())
|
||||||
|
return srv.ListenAndServe()
|
||||||
|
}
|
||||||
|
|
||||||
|
func chain(h http.Handler, middlewareFns ...func(http.Handler) http.Handler) http.Handler {
|
||||||
|
for i := len(middlewareFns) - 1; i >= 0; i-- {
|
||||||
|
h = middlewareFns[i](h)
|
||||||
|
}
|
||||||
|
return h
|
||||||
|
}
|
||||||
77
backend-go/internal/usecase/usecase.go
Normal file
77
backend-go/internal/usecase/usecase.go
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
package usecase
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/gofrs/uuid/v5"
|
||||||
|
|
||||||
|
"github.com/saveinmed/backend-go/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Repository defines DB contract for the core entities.
|
||||||
|
type Repository interface {
|
||||||
|
CreateCompany(ctx context.Context, company *domain.Company) error
|
||||||
|
ListCompanies(ctx context.Context) ([]domain.Company, error)
|
||||||
|
|
||||||
|
CreateProduct(ctx context.Context, product *domain.Product) error
|
||||||
|
ListProducts(ctx context.Context) ([]domain.Product, error)
|
||||||
|
|
||||||
|
CreateOrder(ctx context.Context, order *domain.Order) error
|
||||||
|
GetOrder(ctx context.Context, id uuid.UUID) (*domain.Order, error)
|
||||||
|
UpdateOrderStatus(ctx context.Context, id uuid.UUID, status domain.OrderStatus) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaymentGateway abstracts Mercado Pago integration.
|
||||||
|
type PaymentGateway interface {
|
||||||
|
CreatePreference(ctx context.Context, order *domain.Order) (*domain.PaymentPreference, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
repo Repository
|
||||||
|
pay PaymentGateway
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewService wires use cases together.
|
||||||
|
func NewService(repo Repository, pay PaymentGateway) *Service {
|
||||||
|
return &Service{repo: repo, pay: pay}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) RegisterCompany(ctx context.Context, company *domain.Company) error {
|
||||||
|
company.ID = uuid.Must(uuid.NewV7())
|
||||||
|
return s.repo.CreateCompany(ctx, company)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListCompanies(ctx context.Context) ([]domain.Company, error) {
|
||||||
|
return s.repo.ListCompanies(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) RegisterProduct(ctx context.Context, product *domain.Product) error {
|
||||||
|
product.ID = uuid.Must(uuid.NewV7())
|
||||||
|
return s.repo.CreateProduct(ctx, product)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListProducts(ctx context.Context) ([]domain.Product, error) {
|
||||||
|
return s.repo.ListProducts(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) CreateOrder(ctx context.Context, order *domain.Order) error {
|
||||||
|
order.ID = uuid.Must(uuid.NewV7())
|
||||||
|
order.Status = domain.OrderStatusPending
|
||||||
|
return s.repo.CreateOrder(ctx, order)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetOrder(ctx context.Context, id uuid.UUID) (*domain.Order, error) {
|
||||||
|
return s.repo.GetOrder(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) UpdateOrderStatus(ctx context.Context, id uuid.UUID, status domain.OrderStatus) error {
|
||||||
|
return s.repo.UpdateOrderStatus(ctx, id, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) CreatePaymentPreference(ctx context.Context, id uuid.UUID) (*domain.PaymentPreference, error) {
|
||||||
|
order, err := s.repo.GetOrder(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return s.pay.CreatePreference(ctx, order)
|
||||||
|
}
|
||||||
|
|
@ -1,30 +1,90 @@
|
||||||
import { useSignal } from "@preact/signals";
|
|
||||||
import { define } from "../utils.ts";
|
import { define } from "../utils.ts";
|
||||||
import Counter from "../islands/Counter.tsx";
|
|
||||||
|
const highlights = [
|
||||||
|
{
|
||||||
|
title: "Hierarquia de Permissões",
|
||||||
|
description:
|
||||||
|
"Perfis separados para Farmácia (comprador), Distribuidora (vendedor) e Administrador do marketplace com trilhas dedicadas.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Rastreabilidade de Lote",
|
||||||
|
description:
|
||||||
|
"Produtos carregam lote e validade obrigatórios em todo pedido, garantindo conformidade sanitária de ponta a ponta.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Split nativo Mercado Pago",
|
||||||
|
description:
|
||||||
|
"Preferências de pagamento prontas para retenção automática de comissão do marketplace e repasse ao seller.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Impostos por UF",
|
||||||
|
description:
|
||||||
|
"Camada de cálculo preparada para tabelas de substituição tributária estadual, simplificando faturamento fiscal.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export default define.page(function Home(ctx) {
|
export default define.page(function Home(ctx) {
|
||||||
const count = useSignal(3);
|
ctx.state.title = "SaveInMed | Marketplace B2B";
|
||||||
|
|
||||||
ctx.state.title = count.value + " Fresh Counter" +
|
|
||||||
(Math.abs(count.value) === 1 ? "" : "s");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="px-4 py-8 mx-auto fresh-gradient min-h-screen">
|
<div class="min-h-screen bg-gradient-to-b from-sky-50 via-white to-sky-100 text-gray-900">
|
||||||
<div class="max-w-screen-md mx-auto flex flex-col items-center justify-center">
|
<header class="py-10 px-6 md:px-10">
|
||||||
<img
|
<div class="max-w-5xl mx-auto grid gap-6 lg:grid-cols-2 items-center">
|
||||||
class="my-6"
|
<div>
|
||||||
src="/logo.svg"
|
<p class="text-sm uppercase tracking-widest text-sky-700 font-semibold">
|
||||||
width="128"
|
Planejamento B2B Farmacêutico
|
||||||
height="128"
|
</p>
|
||||||
alt="the Fresh logo: a sliced lemon dripping with juice"
|
<h1 class="text-4xl md:text-5xl font-bold mt-2 leading-tight">
|
||||||
/>
|
Nova fundação de Performance para o Marketplace SaveInMed
|
||||||
<h1 class="text-4xl font-bold">Welcome to Fresh</h1>
|
</h1>
|
||||||
<p class="my-4">
|
<p class="mt-4 text-lg text-gray-700 leading-relaxed">
|
||||||
Try updating this message in the
|
Arquitetura renovada com backend em Go ultrarrápido, Postgres 17 e Clean Architecture.
|
||||||
<code class="mx-2">./routes/index.tsx</code> file, and refresh.
|
Ideal para transações de alto volume e integração nativa com Mercado Pago.
|
||||||
</p>
|
</p>
|
||||||
<Counter count={count} />
|
<div class="mt-6 grid gap-3 sm:grid-cols-2">
|
||||||
</div>
|
<div class="p-4 rounded-xl bg-white shadow-sm border border-sky-100">
|
||||||
|
<p class="text-sm text-gray-600">Core em Go 1.24+</p>
|
||||||
|
<p class="font-semibold">net/http, pgx, json-iter, gzip</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 rounded-xl bg-white shadow-sm border border-sky-100">
|
||||||
|
<p class="text-sm text-gray-600">Infra pronta para produção</p>
|
||||||
|
<p class="font-semibold">Docker distroless + Swagger</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white shadow-md rounded-2xl border border-sky-100 p-6 space-y-3">
|
||||||
|
<h2 class="text-xl font-semibold">Stack de diretórios</h2>
|
||||||
|
<ul class="text-sm text-gray-700 space-y-2">
|
||||||
|
<li><strong>/backend-go</strong>: Performance Core (Pagamentos)</li>
|
||||||
|
<li><strong>/backend-nest</strong>: Gestão de usuários, CRM e regras de negócio</li>
|
||||||
|
<li><strong>/frontend-market</strong>: App da farmácia em React + Vite</li>
|
||||||
|
<li><strong>/website</strong>: Landing page institucional</li>
|
||||||
|
<li><strong>/docker</strong>: Compose com Postgres e backends</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="px-6 md:px-10 pb-16">
|
||||||
|
<div class="max-w-5xl mx-auto">
|
||||||
|
<section class="grid md:grid-cols-2 gap-6">
|
||||||
|
{highlights.map((item) => (
|
||||||
|
<div class="p-6 bg-white rounded-2xl shadow-sm border border-sky-100" key={item.title}>
|
||||||
|
<h3 class="font-semibold text-lg text-sky-800">{item.title}</h3>
|
||||||
|
<p class="mt-2 text-gray-700 leading-relaxed">{item.description}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mt-10 p-6 bg-white rounded-2xl shadow-sm border border-emerald-100">
|
||||||
|
<h3 class="text-xl font-semibold text-emerald-700">Foco em Observabilidade e Resiliência</h3>
|
||||||
|
<p class="mt-2 text-gray-700 leading-relaxed">
|
||||||
|
Conexões pgx otimizadas, middlewares de compressão e roteamento do Go 1.22+ garantem baixa latência.
|
||||||
|
O repositório vem com schema base e UUID v7 ordenável para índices mais eficientes.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue