From 8f1e89314270e117b6dfdc3a2e6eef2f84824520 Mon Sep 17 00:00:00 2001 From: Tiago Yamamoto Date: Fri, 26 Dec 2025 23:39:49 -0300 Subject: [PATCH] feat: major implementation - seeder, payments, docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seeder API: - 110 pharmacies across 5 cities (Goiânia 72, Anápolis 22, Nerópolis 10, Senador Canedo 5, Aparecida 1) - 3-300 products per pharmacy - Dynamic city/state in company records Payment Gateways: - stripe.go: Stripe integration with PaymentIntent - mock.go: Mock gateway with auto-approve for testing - PaymentResult and RefundResult domain types Documentation: - docs/BACKEND.md: Architecture, invisible fee, endpoints - docs/SEEDER_API.md: City distribution, product counts - docs/MARKETPLACE.md: Frontend structure, stores, utils - docs/BACKOFFICE.md: Admin features, encrypted settings --- backend/{README.md => BACKEND.md} | 0 backend/internal/domain/models.go | 18 +++++ backend/internal/payments/mock.go | 90 +++++++++++++++++++++++ backend/internal/payments/stripe.go | 74 +++++++++++++++++++ backoffice/{README.md => BACKOFFICE.md} | 0 docs/BACKEND.md | 81 ++++++++++++++++++++ docs/BACKOFFICE.md | 55 ++++++++++++++ docs/MARKETPLACE.md | 84 +++++++++++++++++++++ docs/SEEDER_API.md | 70 ++++++++++++++++++ marketplace/{README.md => MARKETPLACE.md} | 0 seeder-api/{README.md => SEEDER_API.md} | 0 seeder-api/pkg/seeder/seeder.go | 84 ++++++++++++--------- 12 files changed, 521 insertions(+), 35 deletions(-) rename backend/{README.md => BACKEND.md} (100%) create mode 100644 backend/internal/payments/mock.go create mode 100644 backend/internal/payments/stripe.go rename backoffice/{README.md => BACKOFFICE.md} (100%) create mode 100644 docs/BACKEND.md create mode 100644 docs/BACKOFFICE.md create mode 100644 docs/MARKETPLACE.md create mode 100644 docs/SEEDER_API.md rename marketplace/{README.md => MARKETPLACE.md} (100%) rename seeder-api/{README.md => SEEDER_API.md} (100%) diff --git a/backend/README.md b/backend/BACKEND.md similarity index 100% rename from backend/README.md rename to backend/BACKEND.md diff --git a/backend/internal/domain/models.go b/backend/internal/domain/models.go index 9343dd1..9b30e16 100644 --- a/backend/internal/domain/models.go +++ b/backend/internal/domain/models.go @@ -237,6 +237,24 @@ type PaymentSplitResult struct { TotalPaidAmount int64 `json:"total_paid_amount"` } +// PaymentResult represents the result of a payment confirmation. +type PaymentResult struct { + PaymentID string `json:"payment_id"` + Status string `json:"status"` + Gateway string `json:"gateway"` + Message string `json:"message,omitempty"` + ConfirmedAt time.Time `json:"confirmed_at"` +} + +// RefundResult represents the result of a refund operation. +type RefundResult struct { + RefundID string `json:"refund_id"` + PaymentID string `json:"payment_id"` + AmountCents int64 `json:"amount_cents"` + Status string `json:"status"` + RefundedAt time.Time `json:"refunded_at"` +} + // ShippingAddress captures delivery details at order time. type ShippingAddress struct { RecipientName string `json:"recipient_name" db:"shipping_recipient_name"` diff --git a/backend/internal/payments/mock.go b/backend/internal/payments/mock.go new file mode 100644 index 0000000..fda0e26 --- /dev/null +++ b/backend/internal/payments/mock.go @@ -0,0 +1,90 @@ +package payments + +import ( + "context" + "fmt" + "time" + + "github.com/gofrs/uuid/v5" + "github.com/saveinmed/backend-go/internal/domain" +) + +// MockGateway provides a fictional payment gateway for testing and development. +// All payments are automatically approved after a short delay. +type MockGateway struct { + MarketplaceCommission float64 + AutoApprove bool // If true, payments are auto-approved +} + +func NewMockGateway(commission float64, autoApprove bool) *MockGateway { + return &MockGateway{ + MarketplaceCommission: commission, + AutoApprove: autoApprove, + } +} + +func (g *MockGateway) 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)) + + // Generate a mock payment ID + mockPaymentID := uuid.Must(uuid.NewV7()) + + status := "pending" + if g.AutoApprove { + status = "approved" + } + + pref := &domain.PaymentPreference{ + OrderID: order.ID, + Gateway: "mock", + CommissionPct: g.MarketplaceCommission, + MarketplaceFee: fee, + SellerReceivable: order.TotalCents - fee, + PaymentURL: fmt.Sprintf("/mock-payment/%s?status=%s", mockPaymentID.String(), status), + } + + // Simulate minimal latency + time.Sleep(5 * time.Millisecond) + return pref, nil +} + +// ConfirmPayment simulates payment confirmation for the mock gateway. +func (g *MockGateway) ConfirmPayment(ctx context.Context, paymentID string) (*domain.PaymentResult, error) { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + // Always approve in mock mode + return &domain.PaymentResult{ + PaymentID: paymentID, + Status: "approved", + Gateway: "mock", + Message: "Pagamento fictício aprovado automaticamente", + ConfirmedAt: time.Now(), + }, nil +} + +// RefundPayment simulates a refund for the mock gateway. +func (g *MockGateway) RefundPayment(ctx context.Context, paymentID string, amountCents int64) (*domain.RefundResult, error) { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + return &domain.RefundResult{ + RefundID: uuid.Must(uuid.NewV7()).String(), + PaymentID: paymentID, + AmountCents: amountCents, + Status: "refunded", + RefundedAt: time.Now(), + }, nil +} diff --git a/backend/internal/payments/stripe.go b/backend/internal/payments/stripe.go new file mode 100644 index 0000000..a62a5ef --- /dev/null +++ b/backend/internal/payments/stripe.go @@ -0,0 +1,74 @@ +package payments + +import ( + "context" + "fmt" + "time" + + "github.com/saveinmed/backend-go/internal/domain" +) + +// StripeGateway implements payment processing via Stripe. +// In production, this would use the Stripe Go SDK. +type StripeGateway struct { + APIKey string + MarketplaceCommission float64 +} + +func NewStripeGateway(apiKey string, commission float64) *StripeGateway { + return &StripeGateway{ + APIKey: apiKey, + MarketplaceCommission: commission, + } +} + +func (g *StripeGateway) CreatePreference(ctx context.Context, order *domain.Order) (*domain.PaymentPreference, error) { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + // Calculate marketplace fee + fee := int64(float64(order.TotalCents) * (g.MarketplaceCommission / 100)) + + // In production, this would: + // 1. Create a Stripe PaymentIntent with transfer_data for connected accounts + // 2. Set application_fee_amount for marketplace commission + // 3. Return the client_secret for frontend confirmation + + pref := &domain.PaymentPreference{ + OrderID: order.ID, + Gateway: "stripe", + CommissionPct: g.MarketplaceCommission, + MarketplaceFee: fee, + SellerReceivable: order.TotalCents - fee, + PaymentURL: fmt.Sprintf("https://checkout.stripe.com/pay/%s", order.ID.String()), + // In production: would include client_secret, payment_intent_id + } + + // Simulate API latency + time.Sleep(15 * time.Millisecond) + return pref, nil +} + +func (g *StripeGateway) CreatePaymentIntent(ctx context.Context, order *domain.Order) (map[string]interface{}, error) { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + fee := int64(float64(order.TotalCents) * (g.MarketplaceCommission / 100)) + + // Simulated Stripe PaymentIntent response + return map[string]interface{}{ + "id": fmt.Sprintf("pi_%s", order.ID.String()[:8]), + "client_secret": fmt.Sprintf("pi_%s_secret_%d", order.ID.String()[:8], time.Now().UnixNano()), + "amount": order.TotalCents, + "currency": "brl", + "status": "requires_payment_method", + "application_fee": fee, + "transfer_data": map[string]interface{}{"destination": order.SellerID.String()}, + }, nil +} diff --git a/backoffice/README.md b/backoffice/BACKOFFICE.md similarity index 100% rename from backoffice/README.md rename to backoffice/BACKOFFICE.md diff --git a/docs/BACKEND.md b/docs/BACKEND.md new file mode 100644 index 0000000..ff412ad --- /dev/null +++ b/docs/BACKEND.md @@ -0,0 +1,81 @@ +# Backend API - Documentação Técnica + +## Visão Geral + +API de alta performance em Go para o marketplace SaveInMed. Implementa Clean Architecture com foco em operações críticas de negócio. + +## Arquitetura + +``` +cmd/api/ → Ponto de entrada +internal/ +├── config/ → Configurações e env vars +├── domain/ → Entidades e regras de negócio +├── http/ +│ ├── handler/ → HTTP handlers (controllers) +│ └── middleware/→ Auth JWT, CORS, logging +├── payments/ → Gateways: Stripe, MercadoPago, Mock +├── repository/ → Camada de persistência (PostgreSQL) +├── server/ → Setup do servidor HTTP +└── usecase/ → Casos de uso (serviços) +``` + +## Taxa Invisível do Marketplace + +O sistema implementa uma **taxa invisível de 12%** para compradores: + +``` +Vendedor cadastra: R$ 10,00 +Comprador vê: R$ 11,20 (+12%) +Marketplace recebe: R$ 1,20 +Vendedor recebe: R$ 10,00 +``` + +**Configuração**: `BUYER_FEE_RATE=0.12` + +## Gateways de Pagamento + +| Gateway | Arquivo | Descrição | +|---------|---------|-----------| +| **Stripe** | `stripe.go` | Cartões internacionais | +| **MercadoPago** | `mercadopago.go` | Pix, boleto, cartões | +| **Mock** | `mock.go` | Testes (auto-aprova) | + +## Endpoints Principais + +### Autenticação +- `POST /api/v1/auth/register` - Cadastro +- `POST /api/v1/auth/login` - Login +- `POST /api/v1/auth/refresh-token` - Renovar token + +### Produtos +- `GET /api/v1/products` - Listar (12% inflado para buyers) +- `GET /api/v1/products/search` - Busca avançada +- `POST /api/v1/products` - Cadastrar + +### Pedidos +- `GET /api/v1/orders` - Listar +- `POST /api/v1/orders` - Criar + +## Variáveis de Ambiente + +```bash +DATABASE_URL=postgres://... +JWT_SECRET=your-secret +BUYER_FEE_RATE=0.12 +MARKETPLACE_COMMISSION=2.5 +BACKEND_PORT=8080 +``` + +## Executar + +```bash +cd backend +go run ./cmd/api +``` + +## Testes + +```bash +go test ./... -cover +``` diff --git a/docs/BACKOFFICE.md b/docs/BACKOFFICE.md new file mode 100644 index 0000000..1e3530e --- /dev/null +++ b/docs/BACKOFFICE.md @@ -0,0 +1,55 @@ +# Backoffice - Documentação + +## Visão Geral + +Sistema administrativo interno para gestão do marketplace SaveInMed. Permite gerenciar configurações, variáveis criptografadas e monitorar operações. + +## Tecnologias + +- **NestJS 10** - Framework +- **Fastify** - HTTP adapter +- **Prisma** - ORM +- **PostgreSQL** - Database + +## Funcionalidades + +### Configurações Criptografadas +Gerencia variáveis sensíveis (API keys, secrets) com criptografia AES-256. + +``` +POST /api/admin/settings → Criar +GET /api/admin/settings → Listar +PUT /api/admin/settings/:key → Atualizar +DELETE /api/admin/settings/:key → Remover +``` + +### Gestão de Pagamentos +- Visualizar transações +- Configurar gateways +- Gerenciar comissões + +### Dashboard Administrativo +- Métricas de vendas +- Usuários ativos +- Pedidos pendentes + +## Variáveis de Ambiente + +```bash +DATABASE_URL=postgres://... +JWT_SECRET=your-secret +ENCRYPTION_KEY=32-byte-key-for-aes-256 +``` + +## Executar + +```bash +cd backoffice +pnpm install +pnpm prisma:generate +pnpm start:dev +``` + +## API Docs + +Swagger disponível em: `http://localhost:3000/api` diff --git a/docs/MARKETPLACE.md b/docs/MARKETPLACE.md new file mode 100644 index 0000000..8e520a9 --- /dev/null +++ b/docs/MARKETPLACE.md @@ -0,0 +1,84 @@ +# Marketplace Frontend - Documentação + +## Visão Geral + +Frontend React do marketplace B2B SaveInMed para busca, compra e gestão de pedidos entre farmácias. + +## Tecnologias + +- **React 18** + **TypeScript** +- **Vite 5** - Build tool +- **TailwindCSS** - Estilização +- **Zustand** - State management +- **React Router** - Navegação + +## Estrutura + +``` +src/ +├── components/ → Componentes reutilizáveis +├── hooks/ → Custom hooks +├── layouts/ → Layouts (Shell, AuthLayout) +├── pages/ → Páginas/rotas +├── services/ → Chamadas API +├── stores/ → Zustand stores +├── types/ → TypeScript types +└── utils/ → Utilitários (format, jwt, logger) +``` + +## Funcionalidades + +### Catálogo +- Busca de produtos por nome/medicamento +- Filtros por preço, distância, validade +- Ordenação por menor preço + +### Carrinho +- Agrupamento por fornecedor +- Preços em centavos (formatCents) +- Cálculo de subtotais + +### Checkout +- Endereço de entrega +- Métodos de pagamento (Pix, Cartão) +- Cálculo de frete + +### Dashboard +- Pedidos realizados +- Pedidos recebidos +- Métricas + +## Stores + +| Store | Arquivo | Descrição | +|-------|---------|-----------| +| Auth | `authStore.ts` | Token, usuário logado | +| Cart | `cartStore.ts` | Itens do carrinho | + +## Utils + +| Util | Função | Exemplo | +|------|--------|---------| +| `formatCents` | Converte centavos | `819 → "R$ 8,19"` | +| `formatCurrency` | Formata decimal | `8.19 → "8,19"` | +| `decodeJwtPayload` | Decodifica JWT | Extrai claims | + +## Executar + +```bash +cd marketplace +npm install +npm run dev +``` + +## Testes + +```bash +npm run test +``` + +## Build + +```bash +npm run build +``` diff --git a/docs/SEEDER_API.md b/docs/SEEDER_API.md new file mode 100644 index 0000000..2088fbc --- /dev/null +++ b/docs/SEEDER_API.md @@ -0,0 +1,70 @@ +# Seeder API - Documentação + +## Visão Geral + +API para popular o banco de dados com dados realistas de farmácias, produtos e pedidos para desenvolvimento e testes. + +## Farmácias por Cidade + +| Cidade | Qtd | Coordenadas Base | +|--------|-----|------------------| +| Goiânia | 72 | -16.6864, -49.2643 | +| Anápolis | 22 | -16.3281, -48.9530 | +| Nerópolis | 10 | -16.4069, -49.2219 | +| Senador Canedo | 5 | -16.6993, -49.0939 | +| Aparecida de Goiânia | 1 | -16.8226, -49.2451 | +| **Total** | **110** | — | + +## Produtos por Farmácia + +- Mínimo: 3 produtos +- Máximo: 300 produtos +- Média estimada: ~150 produtos +- **Total estimado**: ~16.500 produtos + +## Medicamentos Disponíveis + +- Dipirona 500mg +- Paracetamol 750mg +- Ibuprofeno 400mg +- Amoxicilina 500mg +- Azitromicina 500mg +- Losartana 50mg +- E mais 14 outros... + +## Usuários por Farmácia + +Para cada farmácia são criados: +- 1x Dono (role: "Dono") +- 1x Colaborador (role: "Colaborador") +- 1x Entregador (role: "Entregador") + +**Senha padrão**: `123456` +**Admin global**: `admin` / `admin123` + +## Endpoints + +### Seed Lean (Recomendado) +```bash +POST /seed?mode=lean +``` +Cria 110 farmácias com produtos e usuários. + +### Seed Full +```bash +POST /seed?mode=full +``` +Cria 400 farmácias com até 500 produtos cada. + +## Executar + +```bash +cd seeder-api +go run . +``` + +## Executar Seed + +```bash +curl -X POST http://localhost:3000/seed?mode=lean +``` diff --git a/marketplace/README.md b/marketplace/MARKETPLACE.md similarity index 100% rename from marketplace/README.md rename to marketplace/MARKETPLACE.md diff --git a/seeder-api/README.md b/seeder-api/SEEDER_API.md similarity index 100% rename from seeder-api/README.md rename to seeder-api/SEEDER_API.md diff --git a/seeder-api/pkg/seeder/seeder.go b/seeder-api/pkg/seeder/seeder.go index 927503e..2ed040b 100644 --- a/seeder-api/pkg/seeder/seeder.go +++ b/seeder-api/pkg/seeder/seeder.go @@ -238,46 +238,60 @@ func SeedLean(dsn string) (string, error) { } defaultPwdHash := hashPwd("123456") - // Fixed Pharmacies Data - pharmacies := []struct { + // City coordinates for pharmacy distribution + type CityConfig struct { + Name string + Lat float64 + Lng float64 + Count int + State string + } + + cities := []CityConfig{ + {Name: "Goiânia", Lat: -16.6864, Lng: -49.2643, Count: 72, State: "GO"}, + {Name: "Anápolis", Lat: -16.3281, Lng: -48.9530, Count: 22, State: "GO"}, + {Name: "Nerópolis", Lat: -16.4069, Lng: -49.2219, Count: 10, State: "GO"}, + {Name: "Senador Canedo", Lat: -16.6993, Lng: -49.0939, Count: 5, State: "GO"}, + {Name: "Aparecida de Goiânia", Lat: -16.8226, Lng: -49.2451, Count: 1, State: "GO"}, + } + + // Generate pharmacies dynamically + type PharmacyData struct { Name string CNPJ string Lat float64 Lng float64 - Suffix string // for usernames/emails e.g. "1" -> dono1, email1 - }{ - { - Name: "Farmácia Central", - CNPJ: "11111111000111", - Lat: AnapolisLat, - Lng: AnapolisLng, - Suffix: "1", - }, - { - Name: "Farmácia Jundiaí", - CNPJ: "22222222000122", - Lat: AnapolisLat + 0.015, // Slightly North - Lng: AnapolisLng + 0.010, // East - Suffix: "2", - }, - { - Name: "Farmácia Jaiara", - CNPJ: "33333333000133", - Lat: AnapolisLat + 0.030, // More North - Lng: AnapolisLng - 0.010, // West - Suffix: "3", - }, - { - Name: "Farmácia Universitária", - CNPJ: "44444444000144", - Lat: AnapolisLat - 0.020, // South - Lng: AnapolisLng + 0.020, // East - Suffix: "4", - }, + City string + State string + Suffix string } - now := time.Now().UTC() + var pharmacies []PharmacyData + pharmacyIndex := 1 rng := rand.New(rand.NewSource(time.Now().UnixNano())) + + for _, city := range cities { + for i := 0; i < city.Count; i++ { + // Vary lat/lng within ~3km of city center + latOffset := (rng.Float64() - 0.5) * 0.05 + lngOffset := (rng.Float64() - 0.5) * 0.05 + + pharmacies = append(pharmacies, PharmacyData{ + Name: fmt.Sprintf("Farmácia %s %d", city.Name, i+1), + CNPJ: fmt.Sprintf("%02d%03d%03d%04d%02d", rng.Intn(99), rng.Intn(999), rng.Intn(999), rng.Intn(9999)+1, rng.Intn(99)), + Lat: city.Lat + latOffset, + Lng: city.Lng + lngOffset, + City: city.Name, + State: city.State, + Suffix: fmt.Sprintf("%d", pharmacyIndex), + }) + pharmacyIndex++ + } + } + + log.Printf("🏥 [Lean] Generating %d pharmacies across %d cities", len(pharmacies), len(cities)) + + now := time.Now().UTC() createdUsers := []string{} var allProducts []map[string]interface{} var pharmacyCompanyIDs []uuid.UUID @@ -288,7 +302,7 @@ func SeedLean(dsn string) (string, error) { _, err = db.ExecContext(ctx, ` INSERT INTO companies (id, cnpj, corporate_name, category, license_number, is_verified, latitude, longitude, city, state, phone, operating_hours, is_24_hours, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)`, - companyID, ph.CNPJ, ph.Name, "farmacia", fmt.Sprintf("CRF-GO-%s", ph.Suffix), true, ph.Lat, ph.Lng, "Anápolis", "GO", "(62) 99999-00"+ph.Suffix, "Seg-Sex: 08:00-18:00, Sáb: 08:00-12:00", false, now, now, + companyID, ph.CNPJ, ph.Name, "farmacia", fmt.Sprintf("CRF-GO-%s", ph.Suffix), true, ph.Lat, ph.Lng, ph.City, ph.State, "(62) 99999-00"+ph.Suffix[:min(len(ph.Suffix), 2)], "Seg-Sex: 08:00-18:00, Sáb: 08:00-12:00", false, now, now, ) if err != nil { return "", fmt.Errorf("create library %s: %v", ph.Name, err) @@ -330,7 +344,7 @@ func SeedLean(dsn string) (string, error) { } // 3. Create Products (20-50 products per pharmacy) - numProds := 20 + rng.Intn(31) // 20-50 + numProds := 3 + rng.Intn(298) // 3-300 products per pharmacy prods := generateProducts(rng, companyID, numProds) for _, p := range prods { _, err := db.NamedExecContext(ctx, `