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:
Tiago Yamamoto 2025-12-17 14:26:26 -03:00 committed by GitHub
commit 470f8463b1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 2515 additions and 22 deletions

1
backend-go/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/bin

18
backend-go/Dockerfile Normal file
View 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
View 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
```

View 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
View 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)
}

View 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"
}
}
}
}
}

View 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
View 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
View 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=

View 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
}

View 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"
)

View 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
}
}

View 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)
}

View 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))
})
}

View 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
}

View 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
}

View 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
}

View 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)
}

View file

@ -1,30 +1,90 @@
import { useSignal } from "@preact/signals";
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) {
const count = useSignal(3);
ctx.state.title = count.value + " Fresh Counter" +
(Math.abs(count.value) === 1 ? "" : "s");
ctx.state.title = "SaveInMed | Marketplace B2B";
return (
<div class="px-4 py-8 mx-auto fresh-gradient min-h-screen">
<div class="max-w-screen-md mx-auto flex flex-col items-center justify-center">
<img
class="my-6"
src="/logo.svg"
width="128"
height="128"
alt="the Fresh logo: a sliced lemon dripping with juice"
/>
<h1 class="text-4xl font-bold">Welcome to Fresh</h1>
<p class="my-4">
Try updating this message in the
<code class="mx-2">./routes/index.tsx</code> file, and refresh.
</p>
<Counter count={count} />
</div>
<div class="min-h-screen bg-gradient-to-b from-sky-50 via-white to-sky-100 text-gray-900">
<header class="py-10 px-6 md:px-10">
<div class="max-w-5xl mx-auto grid gap-6 lg:grid-cols-2 items-center">
<div>
<p class="text-sm uppercase tracking-widest text-sky-700 font-semibold">
Planejamento B2B Farmacêutico
</p>
<h1 class="text-4xl md:text-5xl font-bold mt-2 leading-tight">
Nova fundação de Performance para o Marketplace SaveInMed
</h1>
<p class="mt-4 text-lg text-gray-700 leading-relaxed">
Arquitetura renovada com backend em Go ultrarrápido, Postgres 17 e Clean Architecture.
Ideal para transações de alto volume e integração nativa com Mercado Pago.
</p>
<div class="mt-6 grid gap-3 sm:grid-cols-2">
<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>
);
});