diff --git a/backend-go/.gitignore b/backend-go/.gitignore new file mode 100644 index 0000000..5e56e04 --- /dev/null +++ b/backend-go/.gitignore @@ -0,0 +1 @@ +/bin diff --git a/backend-go/Dockerfile b/backend-go/Dockerfile new file mode 100644 index 0000000..5854276 --- /dev/null +++ b/backend-go/Dockerfile @@ -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"] diff --git a/backend-go/README.md b/backend-go/README.md new file mode 100644 index 0000000..3647538 --- /dev/null +++ b/backend-go/README.md @@ -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 +``` diff --git a/backend-go/cmd/api/main.go b/backend-go/cmd/api/main.go new file mode 100644 index 0000000..7255717 --- /dev/null +++ b/backend-go/cmd/api/main.go @@ -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) + } +} diff --git a/backend-go/docs/docs.go b/backend-go/docs/docs.go new file mode 100644 index 0000000..a3ae9f7 --- /dev/null +++ b/backend-go/docs/docs.go @@ -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) +} diff --git a/backend-go/docs/swagger.json b/backend-go/docs/swagger.json new file mode 100644 index 0000000..26e570b --- /dev/null +++ b/backend-go/docs/swagger.json @@ -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" + } + } + } + } +} \ No newline at end of file diff --git a/backend-go/docs/swagger.yaml b/backend-go/docs/swagger.yaml new file mode 100644 index 0000000..684829f --- /dev/null +++ b/backend-go/docs/swagger.yaml @@ -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" diff --git a/backend-go/go.mod b/backend-go/go.mod new file mode 100644 index 0000000..5674bb3 --- /dev/null +++ b/backend-go/go.mod @@ -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 +) diff --git a/backend-go/go.sum b/backend-go/go.sum new file mode 100644 index 0000000..653c0df --- /dev/null +++ b/backend-go/go.sum @@ -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= diff --git a/backend-go/internal/config/config.go b/backend-go/internal/config/config.go new file mode 100644 index 0000000..d36bdf0 --- /dev/null +++ b/backend-go/internal/config/config.go @@ -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 +} diff --git a/backend-go/internal/domain/models.go b/backend-go/internal/domain/models.go new file mode 100644 index 0000000..ad48a5a --- /dev/null +++ b/backend-go/internal/domain/models.go @@ -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" +) diff --git a/backend-go/internal/http/handler/handler.go b/backend-go/internal/http/handler/handler.go new file mode 100644 index 0000000..aaa8c7d --- /dev/null +++ b/backend-go/internal/http/handler/handler.go @@ -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 + } +} diff --git a/backend-go/internal/http/middleware/compress.go b/backend-go/internal/http/middleware/compress.go new file mode 100644 index 0000000..397daaf --- /dev/null +++ b/backend-go/internal/http/middleware/compress.go @@ -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) +} diff --git a/backend-go/internal/http/middleware/logging.go b/backend-go/internal/http/middleware/logging.go new file mode 100644 index 0000000..ce77bb0 --- /dev/null +++ b/backend-go/internal/http/middleware/logging.go @@ -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)) + }) +} diff --git a/backend-go/internal/payments/mercadopago.go b/backend-go/internal/payments/mercadopago.go new file mode 100644 index 0000000..59a0795 --- /dev/null +++ b/backend-go/internal/payments/mercadopago.go @@ -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 +} diff --git a/backend-go/internal/repository/postgres/postgres.go b/backend-go/internal/repository/postgres/postgres.go new file mode 100644 index 0000000..71b0242 --- /dev/null +++ b/backend-go/internal/repository/postgres/postgres.go @@ -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 +} diff --git a/backend-go/internal/server/server.go b/backend-go/internal/server/server.go new file mode 100644 index 0000000..7fc5049 --- /dev/null +++ b/backend-go/internal/server/server.go @@ -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 +} diff --git a/backend-go/internal/usecase/usecase.go b/backend-go/internal/usecase/usecase.go new file mode 100644 index 0000000..2a3d0e0 --- /dev/null +++ b/backend-go/internal/usecase/usecase.go @@ -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) +} diff --git a/website/routes/index.tsx b/website/routes/index.tsx index b15b1f2..1e3130d 100644 --- a/website/routes/index.tsx +++ b/website/routes/index.tsx @@ -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 ( -
-
- the Fresh logo: a sliced lemon dripping with juice -

Welcome to Fresh

-

- Try updating this message in the - ./routes/index.tsx file, and refresh. -

- -
+
+
+
+
+

+ Planejamento B2B Farmacêutico +

+

+ Nova fundação de Performance para o Marketplace SaveInMed +

+

+ 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. +

+
+
+

Core em Go 1.24+

+

net/http, pgx, json-iter, gzip

+
+
+

Infra pronta para produção

+

Docker distroless + Swagger

+
+
+
+
+

Stack de diretórios

+
    +
  • /backend-go: Performance Core (Pagamentos)
  • +
  • /backend-nest: Gestão de usuários, CRM e regras de negócio
  • +
  • /frontend-market: App da farmácia em React + Vite
  • +
  • /website: Landing page institucional
  • +
  • /docker: Compose com Postgres e backends
  • +
+
+
+
+ +
+
+
+ {highlights.map((item) => ( +
+

{item.title}

+

{item.description}

+
+ ))} +
+ +
+

Foco em Observabilidade e Resiliência

+

+ 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. +

+
+
+
); });