From ed4349a938e6568d9d79541c3d4b7a1e6931eeb3 Mon Sep 17 00:00:00 2001 From: Tiago Yamamoto Date: Fri, 26 Dec 2025 17:48:50 -0300 Subject: [PATCH] feat: Implement Payment Methods, Shipping Improvements, Swagger Audit, and UUIDv7 Migration - Payment Methods: Added Pix/Credit/Debit selection in checkout, updated backend models and handlers. - Shipping: Updated Checkout UI, added shipping_settings table and seed data. - Swagger: Updated API docs, regenerated swagger.yaml. - UUIDv7: Migrated seeder and backend tests to use uuid.NewV7(). --- backend/docs/docs.go | 486 ++++++++++++++---- backend/docs/swagger.json | 483 +++++++++++++---- backend/docs/swagger.yaml | 321 +++++++++--- backend/internal/domain/models.go | 28 +- backend/internal/http/handler/dto.go | 9 +- backend/internal/http/handler/handler_test.go | 34 +- .../internal/http/handler/order_handler.go | 9 +- .../http/middleware/middleware_test.go | 14 +- backend/internal/payments/payments_test.go | 14 +- .../internal/repository/postgres/postgres.go | 102 ++-- .../repository/postgres/repository_test.go | 10 +- backend/internal/usecase/usecase_test.go | 76 +-- marketplace/src/pages/Checkout.tsx | 170 +++++- marketplace/src/services/ordersService.ts | 1 + marketplace/src/services/shippingService.ts | 38 ++ marketplace/src/types/shipping.ts | 14 + seeder-api/pkg/seeder/seeder.go | 33 +- 17 files changed, 1414 insertions(+), 428 deletions(-) create mode 100644 marketplace/src/services/shippingService.ts create mode 100644 marketplace/src/types/shipping.ts diff --git a/backend/docs/docs.go b/backend/docs/docs.go index 07077a9..116cfaf 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -9,15 +9,124 @@ const docTemplate = `{ "info": { "description": "{{escape .Description}}", "title": "{{.Title}}", - "contact": { - "name": "Engenharia SaveInMed", - "email": "devops@saveinmed.com" - }, + "contact": {}, "version": "{{.Version}}" }, "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { + "/api/v1/admin/reviews": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin" + ], + "summary": "Lista todas as avaliações (Admin)", + "parameters": [ + { + "type": "integer", + "description": "Página", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Tamanho da página", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.ReviewPage" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/admin/shipments": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin" + ], + "summary": "Lista todos os envios (Admin)", + "parameters": [ + { + "type": "integer", + "description": "Página", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Tamanho da página", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.ShipmentPage" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/api/v1/auth/login": { "post": { "description": "Autentica usuário e retorna token JWT.", @@ -1506,6 +1615,60 @@ const docTemplate = `{ } }, "/api/v1/reviews": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin" + ], + "summary": "Lista todas as avaliações (Admin)", + "parameters": [ + { + "type": "integer", + "description": "Página", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Tamanho da página", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.ReviewPage" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, "post": { "security": [ { @@ -1553,6 +1716,60 @@ const docTemplate = `{ } }, "/api/v1/shipments": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin" + ], + "summary": "Lista todos os envios (Admin)", + "parameters": [ + { + "type": "integer", + "description": "Página", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Tamanho da página", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.ShipmentPage" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, "post": { "security": [ { @@ -1701,10 +1918,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.ShippingMethod" - } + "$ref": "#/definitions/domain.ShippingSettings" } }, "400": { @@ -1770,10 +1984,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.ShippingMethod" - } + "$ref": "#/definitions/domain.ShippingSettings" } }, "400": { @@ -2225,11 +2436,15 @@ const docTemplate = `{ "type": "string" }, "created_at": { + "description": "Timestamps", "type": "string" }, "id": { "type": "string" }, + "is_24_hours": { + "type": "boolean" + }, "is_verified": { "type": "boolean" }, @@ -2243,6 +2458,14 @@ const docTemplate = `{ "longitude": { "type": "number" }, + "operating_hours": { + "description": "e.g. \"Seg-Sex: 08:00-18:00, Sab: 08:00-12:00\"", + "type": "string" + }, + "phone": { + "description": "Contact \u0026 Hours", + "type": "string" + }, "state": { "type": "string" }, @@ -2312,6 +2535,9 @@ const docTemplate = `{ "$ref": "#/definitions/domain.OrderItem" } }, + "payment_method": { + "$ref": "#/definitions/domain.PaymentMethod" + }, "seller_id": { "type": "string" }, @@ -2370,6 +2596,19 @@ const docTemplate = `{ "OrderStatusDelivered" ] }, + "domain.PaymentMethod": { + "type": "string", + "enum": [ + "pix", + "credit_card", + "debit_card" + ], + "x-enum-varnames": [ + "PaymentMethodPix", + "PaymentMethodCredit", + "PaymentMethodDebit" + ] + }, "domain.PaymentPreference": { "type": "object", "properties": { @@ -2445,21 +2684,33 @@ const docTemplate = `{ "batch": { "type": "string" }, + "category": { + "type": "string" + }, "created_at": { "type": "string" }, "description": { "type": "string" }, + "ean_code": { + "type": "string" + }, "expires_at": { "type": "string" }, "id": { "type": "string" }, + "manufacturer": { + "type": "string" + }, "name": { "type": "string" }, + "observations": { + "type": "string" + }, "price_cents": { "type": "integer" }, @@ -2469,6 +2720,9 @@ const docTemplate = `{ "stock": { "type": "integer" }, + "subcategory": { + "type": "string" + }, "updated_at": { "type": "string" } @@ -2520,6 +2774,9 @@ const docTemplate = `{ "batch": { "type": "string" }, + "category": { + "type": "string" + }, "created_at": { "type": "string" }, @@ -2529,15 +2786,24 @@ const docTemplate = `{ "distance_km": { "type": "number" }, + "ean_code": { + "type": "string" + }, "expires_at": { "type": "string" }, "id": { "type": "string" }, + "manufacturer": { + "type": "string" + }, "name": { "type": "string" }, + "observations": { + "type": "string" + }, "price_cents": { "type": "integer" }, @@ -2547,6 +2813,9 @@ const docTemplate = `{ "stock": { "type": "integer" }, + "subcategory": { + "type": "string" + }, "tenant_city": { "type": "string" }, @@ -2584,6 +2853,26 @@ const docTemplate = `{ } } }, + "domain.ReviewPage": { + "type": "object", + "properties": { + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "reviews": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.Review" + } + }, + "total": { + "type": "integer" + } + } + }, "domain.SellerDashboard": { "type": "object", "properties": { @@ -2639,6 +2928,26 @@ const docTemplate = `{ } } }, + "domain.ShipmentPage": { + "type": "object", + "properties": { + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "shipments": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.Shipment" + } + }, + "total": { + "type": "integer" + } + } + }, "domain.ShippingAddress": { "type": "object", "properties": { @@ -2671,63 +2980,6 @@ const docTemplate = `{ } } }, - "domain.ShippingMethod": { - "type": "object", - "properties": { - "active": { - "type": "boolean" - }, - "created_at": { - "type": "string" - }, - "free_shipping_threshold_cents": { - "type": "integer" - }, - "id": { - "type": "string" - }, - "max_radius_km": { - "type": "number" - }, - "min_fee_cents": { - "type": "integer" - }, - "pickup_address": { - "type": "string" - }, - "pickup_hours": { - "type": "string" - }, - "preparation_minutes": { - "type": "integer" - }, - "price_per_km_cents": { - "type": "integer" - }, - "type": { - "$ref": "#/definitions/domain.ShippingMethodType" - }, - "updated_at": { - "type": "string" - }, - "vendor_id": { - "type": "string" - } - } - }, - "domain.ShippingMethodType": { - "type": "string", - "enum": [ - "pickup", - "own_delivery", - "third_party_delivery" - ], - "x-enum-varnames": [ - "ShippingMethodPickup", - "ShippingMethodOwnDelivery", - "ShippingMethodThirdParty" - ] - }, "domain.ShippingOption": { "type": "object", "properties": { @@ -2748,6 +3000,50 @@ const docTemplate = `{ } } }, + "domain.ShippingSettings": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + }, + "created_at": { + "type": "string" + }, + "free_shipping_threshold_cents": { + "type": "integer" + }, + "latitude": { + "type": "number" + }, + "longitude": { + "type": "number" + }, + "max_radius_km": { + "type": "number" + }, + "min_fee_cents": { + "type": "integer" + }, + "pickup_active": { + "type": "boolean" + }, + "pickup_address": { + "type": "string" + }, + "pickup_hours": { + "type": "string" + }, + "price_per_km_cents": { + "type": "integer" + }, + "updated_at": { + "type": "string" + }, + "vendor_id": { + "type": "string" + } + } + }, "domain.TopProduct": { "type": "object", "properties": { @@ -2851,6 +3147,9 @@ const docTemplate = `{ "$ref": "#/definitions/domain.OrderItem" } }, + "payment_method": { + "$ref": "#/definitions/domain.PaymentMethod" + }, "seller_id": { "type": "string" }, @@ -2938,6 +3237,9 @@ const docTemplate = `{ "handler.loginRequest": { "type": "object", "properties": { + "email": { + "type": "string" + }, "password": { "type": "string" }, @@ -3115,7 +3417,7 @@ const docTemplate = `{ } } }, - "handler.shippingMethodRequest": { + "handler.shippingSettingsRequest": { "type": "object", "properties": { "active": { @@ -3124,37 +3426,30 @@ const docTemplate = `{ "free_shipping_threshold_cents": { "type": "integer" }, + "latitude": { + "description": "Store location for radius calc", + "type": "number" + }, + "longitude": { + "type": "number" + }, "max_radius_km": { "type": "number" }, "min_fee_cents": { "type": "integer" }, + "pickup_active": { + "type": "boolean" + }, "pickup_address": { "type": "string" }, "pickup_hours": { "type": "string" }, - "preparation_minutes": { - "type": "integer" - }, "price_per_km_cents": { "type": "integer" - }, - "type": { - "type": "string" - } - } - }, - "handler.shippingSettingsRequest": { - "type": "object", - "properties": { - "methods": { - "type": "array", - "items": { - "$ref": "#/definitions/handler.shippingMethodRequest" - } } } }, @@ -3255,24 +3550,17 @@ const docTemplate = `{ } } } - }, - "securityDefinitions": { - "BearerAuth": { - "type": "apiKey", - "name": "Authorization", - "in": "header" - } } }` // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ - Version: "1.0", + Version: "", Host: "", - BasePath: "/", - Schemes: []string{"http"}, - Title: "SaveInMed Performance Core API", - Description: "API REST B2B para marketplace farmacêutico com split de pagamento e rastreabilidade.", + BasePath: "", + Schemes: []string{}, + Title: "", + Description: "", InfoInstanceName: "swagger", SwaggerTemplate: docTemplate, LeftDelim: "{{", diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index 484f62d..c4e426c 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -1,19 +1,121 @@ { - "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" + "contact": {} }, - "basePath": "/", "paths": { + "/api/v1/admin/reviews": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin" + ], + "summary": "Lista todas as avaliações (Admin)", + "parameters": [ + { + "type": "integer", + "description": "Página", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Tamanho da página", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.ReviewPage" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/admin/shipments": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin" + ], + "summary": "Lista todos os envios (Admin)", + "parameters": [ + { + "type": "integer", + "description": "Página", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Tamanho da página", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.ShipmentPage" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/api/v1/auth/login": { "post": { "description": "Autentica usuário e retorna token JWT.", @@ -1502,6 +1604,60 @@ } }, "/api/v1/reviews": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin" + ], + "summary": "Lista todas as avaliações (Admin)", + "parameters": [ + { + "type": "integer", + "description": "Página", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Tamanho da página", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.ReviewPage" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, "post": { "security": [ { @@ -1549,6 +1705,60 @@ } }, "/api/v1/shipments": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin" + ], + "summary": "Lista todos os envios (Admin)", + "parameters": [ + { + "type": "integer", + "description": "Página", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Tamanho da página", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.ShipmentPage" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, "post": { "security": [ { @@ -1697,10 +1907,7 @@ "200": { "description": "OK", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.ShippingMethod" - } + "$ref": "#/definitions/domain.ShippingSettings" } }, "400": { @@ -1766,10 +1973,7 @@ "200": { "description": "OK", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.ShippingMethod" - } + "$ref": "#/definitions/domain.ShippingSettings" } }, "400": { @@ -2221,11 +2425,15 @@ "type": "string" }, "created_at": { + "description": "Timestamps", "type": "string" }, "id": { "type": "string" }, + "is_24_hours": { + "type": "boolean" + }, "is_verified": { "type": "boolean" }, @@ -2239,6 +2447,14 @@ "longitude": { "type": "number" }, + "operating_hours": { + "description": "e.g. \"Seg-Sex: 08:00-18:00, Sab: 08:00-12:00\"", + "type": "string" + }, + "phone": { + "description": "Contact \u0026 Hours", + "type": "string" + }, "state": { "type": "string" }, @@ -2308,6 +2524,9 @@ "$ref": "#/definitions/domain.OrderItem" } }, + "payment_method": { + "$ref": "#/definitions/domain.PaymentMethod" + }, "seller_id": { "type": "string" }, @@ -2366,6 +2585,19 @@ "OrderStatusDelivered" ] }, + "domain.PaymentMethod": { + "type": "string", + "enum": [ + "pix", + "credit_card", + "debit_card" + ], + "x-enum-varnames": [ + "PaymentMethodPix", + "PaymentMethodCredit", + "PaymentMethodDebit" + ] + }, "domain.PaymentPreference": { "type": "object", "properties": { @@ -2441,21 +2673,33 @@ "batch": { "type": "string" }, + "category": { + "type": "string" + }, "created_at": { "type": "string" }, "description": { "type": "string" }, + "ean_code": { + "type": "string" + }, "expires_at": { "type": "string" }, "id": { "type": "string" }, + "manufacturer": { + "type": "string" + }, "name": { "type": "string" }, + "observations": { + "type": "string" + }, "price_cents": { "type": "integer" }, @@ -2465,6 +2709,9 @@ "stock": { "type": "integer" }, + "subcategory": { + "type": "string" + }, "updated_at": { "type": "string" } @@ -2516,6 +2763,9 @@ "batch": { "type": "string" }, + "category": { + "type": "string" + }, "created_at": { "type": "string" }, @@ -2525,15 +2775,24 @@ "distance_km": { "type": "number" }, + "ean_code": { + "type": "string" + }, "expires_at": { "type": "string" }, "id": { "type": "string" }, + "manufacturer": { + "type": "string" + }, "name": { "type": "string" }, + "observations": { + "type": "string" + }, "price_cents": { "type": "integer" }, @@ -2543,6 +2802,9 @@ "stock": { "type": "integer" }, + "subcategory": { + "type": "string" + }, "tenant_city": { "type": "string" }, @@ -2580,6 +2842,26 @@ } } }, + "domain.ReviewPage": { + "type": "object", + "properties": { + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "reviews": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.Review" + } + }, + "total": { + "type": "integer" + } + } + }, "domain.SellerDashboard": { "type": "object", "properties": { @@ -2635,6 +2917,26 @@ } } }, + "domain.ShipmentPage": { + "type": "object", + "properties": { + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "shipments": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.Shipment" + } + }, + "total": { + "type": "integer" + } + } + }, "domain.ShippingAddress": { "type": "object", "properties": { @@ -2667,63 +2969,6 @@ } } }, - "domain.ShippingMethod": { - "type": "object", - "properties": { - "active": { - "type": "boolean" - }, - "created_at": { - "type": "string" - }, - "free_shipping_threshold_cents": { - "type": "integer" - }, - "id": { - "type": "string" - }, - "max_radius_km": { - "type": "number" - }, - "min_fee_cents": { - "type": "integer" - }, - "pickup_address": { - "type": "string" - }, - "pickup_hours": { - "type": "string" - }, - "preparation_minutes": { - "type": "integer" - }, - "price_per_km_cents": { - "type": "integer" - }, - "type": { - "$ref": "#/definitions/domain.ShippingMethodType" - }, - "updated_at": { - "type": "string" - }, - "vendor_id": { - "type": "string" - } - } - }, - "domain.ShippingMethodType": { - "type": "string", - "enum": [ - "pickup", - "own_delivery", - "third_party_delivery" - ], - "x-enum-varnames": [ - "ShippingMethodPickup", - "ShippingMethodOwnDelivery", - "ShippingMethodThirdParty" - ] - }, "domain.ShippingOption": { "type": "object", "properties": { @@ -2744,6 +2989,50 @@ } } }, + "domain.ShippingSettings": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + }, + "created_at": { + "type": "string" + }, + "free_shipping_threshold_cents": { + "type": "integer" + }, + "latitude": { + "type": "number" + }, + "longitude": { + "type": "number" + }, + "max_radius_km": { + "type": "number" + }, + "min_fee_cents": { + "type": "integer" + }, + "pickup_active": { + "type": "boolean" + }, + "pickup_address": { + "type": "string" + }, + "pickup_hours": { + "type": "string" + }, + "price_per_km_cents": { + "type": "integer" + }, + "updated_at": { + "type": "string" + }, + "vendor_id": { + "type": "string" + } + } + }, "domain.TopProduct": { "type": "object", "properties": { @@ -2847,6 +3136,9 @@ "$ref": "#/definitions/domain.OrderItem" } }, + "payment_method": { + "$ref": "#/definitions/domain.PaymentMethod" + }, "seller_id": { "type": "string" }, @@ -2934,6 +3226,9 @@ "handler.loginRequest": { "type": "object", "properties": { + "email": { + "type": "string" + }, "password": { "type": "string" }, @@ -3111,7 +3406,7 @@ } } }, - "handler.shippingMethodRequest": { + "handler.shippingSettingsRequest": { "type": "object", "properties": { "active": { @@ -3120,37 +3415,30 @@ "free_shipping_threshold_cents": { "type": "integer" }, + "latitude": { + "description": "Store location for radius calc", + "type": "number" + }, + "longitude": { + "type": "number" + }, "max_radius_km": { "type": "number" }, "min_fee_cents": { "type": "integer" }, + "pickup_active": { + "type": "boolean" + }, "pickup_address": { "type": "string" }, "pickup_hours": { "type": "string" }, - "preparation_minutes": { - "type": "integer" - }, "price_per_km_cents": { "type": "integer" - }, - "type": { - "type": "string" - } - } - }, - "handler.shippingSettingsRequest": { - "type": "object", - "properties": { - "methods": { - "type": "array", - "items": { - "$ref": "#/definitions/handler.shippingMethodRequest" - } } } }, @@ -3251,12 +3539,5 @@ } } } - }, - "securityDefinitions": { - "BearerAuth": { - "type": "apiKey", - "name": "Authorization", - "in": "header" - } } } \ No newline at end of file diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index 6566ff5..8f779f6 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -1,4 +1,3 @@ -basePath: / definitions: domain.AdminDashboard: properties: @@ -59,9 +58,12 @@ definitions: corporate_name: type: string created_at: + description: Timestamps type: string id: type: string + is_24_hours: + type: boolean is_verified: type: boolean latitude: @@ -71,6 +73,12 @@ definitions: type: string longitude: type: number + operating_hours: + description: 'e.g. "Seg-Sex: 08:00-18:00, Sab: 08:00-12:00"' + type: string + phone: + description: Contact & Hours + type: string state: type: string updated_at: @@ -116,6 +124,8 @@ definitions: items: $ref: '#/definitions/domain.OrderItem' type: array + payment_method: + $ref: '#/definitions/domain.PaymentMethod' seller_id: type: string shipping: @@ -156,6 +166,16 @@ definitions: - OrderStatusPaid - OrderStatusInvoiced - OrderStatusDelivered + domain.PaymentMethod: + enum: + - pix + - credit_card + - debit_card + type: string + x-enum-varnames: + - PaymentMethodPix + - PaymentMethodCredit + - PaymentMethodDebit domain.PaymentPreference: properties: commission_pct: @@ -205,22 +225,32 @@ definitions: properties: batch: type: string + category: + type: string created_at: type: string description: type: string + ean_code: + type: string expires_at: type: string id: type: string + manufacturer: + type: string name: type: string + observations: + type: string price_cents: type: integer seller_id: type: string stock: type: integer + subcategory: + type: string updated_at: type: string type: object @@ -254,24 +284,34 @@ definitions: properties: batch: type: string + category: + type: string created_at: type: string description: type: string distance_km: type: number + ean_code: + type: string expires_at: type: string id: type: string + manufacturer: + type: string name: type: string + observations: + type: string price_cents: type: integer seller_id: type: string stock: type: integer + subcategory: + type: string tenant_city: type: string tenant_state: @@ -296,6 +336,19 @@ definitions: seller_id: type: string type: object + domain.ReviewPage: + properties: + page: + type: integer + page_size: + type: integer + reviews: + items: + $ref: '#/definitions/domain.Review' + type: array + total: + type: integer + type: object domain.SellerDashboard: properties: low_stock_alerts: @@ -332,6 +385,19 @@ definitions: updated_at: type: string type: object + domain.ShipmentPage: + properties: + page: + type: integer + page_size: + type: integer + shipments: + items: + $ref: '#/definitions/domain.Shipment' + type: array + total: + type: integer + type: object domain.ShippingAddress: properties: city: @@ -353,45 +419,6 @@ definitions: zip_code: type: string type: object - domain.ShippingMethod: - properties: - active: - type: boolean - created_at: - type: string - free_shipping_threshold_cents: - type: integer - id: - type: string - max_radius_km: - type: number - min_fee_cents: - type: integer - pickup_address: - type: string - pickup_hours: - type: string - preparation_minutes: - type: integer - price_per_km_cents: - type: integer - type: - $ref: '#/definitions/domain.ShippingMethodType' - updated_at: - type: string - vendor_id: - type: string - type: object - domain.ShippingMethodType: - enum: - - pickup - - own_delivery - - third_party_delivery - type: string - x-enum-varnames: - - ShippingMethodPickup - - ShippingMethodOwnDelivery - - ShippingMethodThirdParty domain.ShippingOption: properties: description: @@ -405,6 +432,35 @@ definitions: value_cents: type: integer type: object + domain.ShippingSettings: + properties: + active: + type: boolean + created_at: + type: string + free_shipping_threshold_cents: + type: integer + latitude: + type: number + longitude: + type: number + max_radius_km: + type: number + min_fee_cents: + type: integer + pickup_active: + type: boolean + pickup_address: + type: string + pickup_hours: + type: string + price_per_km_cents: + type: integer + updated_at: + type: string + vendor_id: + type: string + type: object domain.TopProduct: properties: name: @@ -472,6 +528,8 @@ definitions: items: $ref: '#/definitions/domain.OrderItem' type: array + payment_method: + $ref: '#/definitions/domain.PaymentMethod' seller_id: type: string shipping: @@ -528,6 +586,8 @@ definitions: type: object handler.loginRequest: properties: + email: + type: string password: type: string username: @@ -643,33 +703,29 @@ definitions: vendor_id: type: string type: object - handler.shippingMethodRequest: + handler.shippingSettingsRequest: properties: active: type: boolean free_shipping_threshold_cents: type: integer + latitude: + description: Store location for radius calc + type: number + longitude: + type: number max_radius_km: type: number min_fee_cents: type: integer + pickup_active: + type: boolean pickup_address: type: string pickup_hours: type: string - preparation_minutes: - type: integer price_per_km_cents: type: integer - type: - type: string - type: object - handler.shippingSettingsRequest: - properties: - methods: - items: - $ref: '#/definitions/handler.shippingMethodRequest' - type: array type: object handler.updateCompanyRequest: properties: @@ -735,14 +791,78 @@ definitions: 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" + contact: {} paths: + /api/v1/admin/reviews: + get: + parameters: + - description: Página + in: query + name: page + type: integer + - description: Tamanho da página + in: query + name: page_size + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.ReviewPage' + "401": + description: Unauthorized + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + security: + - BearerAuth: [] + summary: Lista todas as avaliações (Admin) + tags: + - Admin + /api/v1/admin/shipments: + get: + parameters: + - description: Página + in: query + name: page + type: integer + - description: Tamanho da página + in: query + name: page_size + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.ShipmentPage' + "401": + description: Unauthorized + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + security: + - BearerAuth: [] + summary: Lista todos os envios (Admin) + tags: + - Admin /api/v1/auth/login: post: consumes: @@ -1697,6 +1817,40 @@ paths: tags: - Produtos /api/v1/reviews: + get: + parameters: + - description: Página + in: query + name: page + type: integer + - description: Tamanho da página + in: query + name: page_size + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.ReviewPage' + "401": + description: Unauthorized + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + security: + - BearerAuth: [] + summary: Lista todas as avaliações (Admin) + tags: + - Admin post: consumes: - application/json @@ -1726,6 +1880,40 @@ paths: tags: - Avaliações /api/v1/shipments: + get: + parameters: + - description: Página + in: query + name: page + type: integer + - description: Tamanho da página + in: query + name: page_size + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.ShipmentPage' + "401": + description: Unauthorized + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + security: + - BearerAuth: [] + summary: Lista todos os envios (Admin) + tags: + - Admin post: consumes: - application/json @@ -1820,9 +2008,7 @@ paths: "200": description: OK schema: - items: - $ref: '#/definitions/domain.ShippingMethod' - type: array + $ref: '#/definitions/domain.ShippingSettings' "400": description: Bad Request schema: @@ -1866,9 +2052,7 @@ paths: "200": description: OK schema: - items: - $ref: '#/definitions/domain.ShippingMethod' - type: array + $ref: '#/definitions/domain.ShippingSettings' "400": description: Bad Request schema: @@ -2099,11 +2283,4 @@ paths: summary: Atualizar usuário tags: - Usuários -schemes: -- http -securityDefinitions: - BearerAuth: - in: header - name: Authorization - type: apiKey swagger: "2.0" diff --git a/backend/internal/domain/models.go b/backend/internal/domain/models.go index 7104a2c..30aad0d 100644 --- a/backend/internal/domain/models.go +++ b/backend/internal/domain/models.go @@ -184,15 +184,16 @@ type InventoryAdjustment struct { // 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"` - Shipping ShippingAddress `json:"shipping"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + 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"` + PaymentMethod PaymentMethod `db:"payment_method" json:"payment_method"` + Items []OrderItem `json:"items"` + Shipping ShippingAddress `json:"shipping"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` } // OrderItem stores SKU-level batch tracking. @@ -316,6 +317,15 @@ const ( OrderStatusDelivered OrderStatus = "Entregue" ) +// PaymentMethod enumerates supported payment types. +type PaymentMethod string + +const ( + PaymentMethodPix PaymentMethod = "pix" + PaymentMethodCredit PaymentMethod = "credit_card" + PaymentMethodDebit PaymentMethod = "debit_card" +) + // CartItem stores buyer selections with unit pricing. type CartItem struct { ID uuid.UUID `db:"id" json:"id"` diff --git a/backend/internal/http/handler/dto.go b/backend/internal/http/handler/dto.go index bf513ed..9d34c90 100644 --- a/backend/internal/http/handler/dto.go +++ b/backend/internal/http/handler/dto.go @@ -155,10 +155,11 @@ type updateProductRequest struct { } type createOrderRequest struct { - BuyerID uuid.UUID `json:"buyer_id"` - SellerID uuid.UUID `json:"seller_id"` - Items []domain.OrderItem `json:"items"` - Shipping domain.ShippingAddress `json:"shipping"` + BuyerID uuid.UUID `json:"buyer_id"` + SellerID uuid.UUID `json:"seller_id"` + Items []domain.OrderItem `json:"items"` + Shipping domain.ShippingAddress `json:"shipping"` + PaymentMethod domain.PaymentMethod `json:"payment_method"` } type createShipmentRequest struct { diff --git a/backend/internal/http/handler/handler_test.go b/backend/internal/http/handler/handler_test.go index a6c9f01..775925d 100644 --- a/backend/internal/http/handler/handler_test.go +++ b/backend/internal/http/handler/handler_test.go @@ -38,7 +38,7 @@ func NewMockRepository() *MockRepository { // Company methods func (m *MockRepository) CreateCompany(ctx context.Context, company *domain.Company) error { - id, _ := uuid.NewV4() + id, _ := uuid.NewV7() company.ID = id company.CreatedAt = time.Now() company.UpdatedAt = time.Now() @@ -81,7 +81,7 @@ func (m *MockRepository) DeleteCompany(ctx context.Context, id uuid.UUID) error // Product methods func (m *MockRepository) CreateProduct(ctx context.Context, product *domain.Product) error { - id, _ := uuid.NewV4() + id, _ := uuid.NewV7() product.ID = id product.CreatedAt = time.Now() product.UpdatedAt = time.Now() @@ -140,7 +140,7 @@ func (m *MockRepository) SearchProducts(ctx context.Context, filter domain.Produ } func (m *MockRepository) CreateOrder(ctx context.Context, order *domain.Order) error { - id, _ := uuid.NewV4() + id, _ := uuid.NewV7() order.ID = id m.orders = append(m.orders, *order) return nil @@ -176,7 +176,7 @@ func (m *MockRepository) GetShipmentByOrderID(ctx context.Context, orderID uuid. } func (m *MockRepository) CreateUser(ctx context.Context, user *domain.User) error { - id, _ := uuid.NewV4() + id, _ := uuid.NewV7() user.ID = id m.users = append(m.users, *user) return nil @@ -365,7 +365,7 @@ func TestCreateCompany(t *testing.T) { func TestCreateProduct(t *testing.T) { h := newTestHandler() - sellerID, _ := uuid.NewV4() + sellerID, _ := uuid.NewV7() payload := `{"seller_id":"` + sellerID.String() + `","name":"Aspirin","description":"Pain relief","batch":"BATCH-001","expires_at":"2025-12-31T00:00:00Z","price_cents":1000,"stock":100}` req := httptest.NewRequest(http.MethodPost, "/api/v1/products", bytes.NewReader([]byte(payload))) req.Header.Set("Content-Type", "application/json") @@ -401,7 +401,7 @@ func TestAdminLogin_Success(t *testing.T) { h := New(svc) // Create admin user through service (which hashes password) - companyID, _ := uuid.NewV4() + companyID, _ := uuid.NewV7() user := &domain.User{ CompanyID: companyID, Role: "admin", @@ -445,7 +445,7 @@ func TestAdminLogin_WrongPassword(t *testing.T) { h := New(svc) // Create admin user - companyID, _ := uuid.NewV4() + companyID, _ := uuid.NewV7() user := &domain.User{ CompanyID: companyID, Role: "admin", @@ -519,7 +519,7 @@ func TestRegister_MissingCompany(t *testing.T) { func TestGetCompany_NotFound(t *testing.T) { h := newTestHandler() - id, _ := uuid.NewV4() + id, _ := uuid.NewV7() req := httptest.NewRequest(http.MethodGet, "/api/v1/companies/"+id.String(), nil) rec := httptest.NewRecorder() h.GetCompany(rec, req) @@ -540,7 +540,7 @@ func TestGetCompany_InvalidUUID(t *testing.T) { func TestUpdateCompany_InvalidJSON(t *testing.T) { h := newTestHandler() - id, _ := uuid.NewV4() + id, _ := uuid.NewV7() req := httptest.NewRequest(http.MethodPatch, "/api/v1/companies/"+id.String(), bytes.NewReader([]byte("{"))) rec := httptest.NewRecorder() h.UpdateCompany(rec, req) @@ -573,7 +573,7 @@ func TestVerifyCompany_InvalidPath(t *testing.T) { func TestGetProduct_NotFound(t *testing.T) { h := newTestHandler() - id, _ := uuid.NewV4() + id, _ := uuid.NewV7() req := httptest.NewRequest(http.MethodGet, "/api/v1/products/"+id.String(), nil) rec := httptest.NewRecorder() h.GetProduct(rec, req) @@ -584,7 +584,7 @@ func TestGetProduct_NotFound(t *testing.T) { func TestUpdateProduct_InvalidJSON(t *testing.T) { h := newTestHandler() - id, _ := uuid.NewV4() + id, _ := uuid.NewV7() req := httptest.NewRequest(http.MethodPatch, "/api/v1/products/"+id.String(), bytes.NewReader([]byte("{"))) rec := httptest.NewRecorder() h.UpdateProduct(rec, req) @@ -637,7 +637,7 @@ func TestAdjustInventory_InvalidJSON(t *testing.T) { func TestAdjustInventory_ZeroDelta(t *testing.T) { h := newTestHandler() - id, _ := uuid.NewV4() + id, _ := uuid.NewV7() payload := `{"product_id":"` + id.String() + `","delta":0,"reason":"test"}` req := httptest.NewRequest(http.MethodPost, "/api/v1/inventory/adjust", bytes.NewReader([]byte(payload))) rec := httptest.NewRecorder() @@ -661,7 +661,7 @@ func TestCreateOrder_InvalidJSON(t *testing.T) { func TestGetOrder_NotFound(t *testing.T) { h := newTestHandler() - id, _ := uuid.NewV4() + id, _ := uuid.NewV7() req := httptest.NewRequest(http.MethodGet, "/api/v1/orders/"+id.String(), nil) rec := httptest.NewRecorder() h.GetOrder(rec, req) @@ -672,7 +672,7 @@ func TestGetOrder_NotFound(t *testing.T) { func TestUpdateOrderStatus_InvalidStatus(t *testing.T) { h := newTestHandler() - id, _ := uuid.NewV4() + id, _ := uuid.NewV7() payload := `{"status":"invalid_status"}` req := httptest.NewRequest(http.MethodPatch, "/api/v1/orders/"+id.String()+"/status", bytes.NewReader([]byte(payload))) rec := httptest.NewRecorder() @@ -783,7 +783,7 @@ func TestListUsers_Success(t *testing.T) { func TestGetUser_NotFound(t *testing.T) { h := newTestHandler() - id, _ := uuid.NewV4() + id, _ := uuid.NewV7() req := httptest.NewRequest(http.MethodGet, "/api/v1/users/"+id.String(), nil) req.Header.Set("X-User-Role", "Admin") rec := httptest.NewRecorder() @@ -806,7 +806,7 @@ func TestCreateUser_InvalidJSON(t *testing.T) { func TestUpdateUser_InvalidJSON(t *testing.T) { h := newTestHandler() - id, _ := uuid.NewV4() + id, _ := uuid.NewV7() req := httptest.NewRequest(http.MethodPut, "/api/v1/users/"+id.String(), bytes.NewReader([]byte("{"))) req.Header.Set("X-User-Role", "Admin") rec := httptest.NewRecorder() @@ -851,7 +851,7 @@ func TestAddToCart_InvalidJSON(t *testing.T) { func TestDeleteCartItem_NoContext(t *testing.T) { h := newTestHandler() - id, _ := uuid.NewV4() + id, _ := uuid.NewV7() req := httptest.NewRequest(http.MethodDelete, "/api/v1/cart/"+id.String(), nil) rec := httptest.NewRecorder() h.DeleteCartItem(rec, req) diff --git a/backend/internal/http/handler/order_handler.go b/backend/internal/http/handler/order_handler.go index 27b3e4d..a9fd635 100644 --- a/backend/internal/http/handler/order_handler.go +++ b/backend/internal/http/handler/order_handler.go @@ -23,10 +23,11 @@ func (h *Handler) CreateOrder(w http.ResponseWriter, r *http.Request) { } order := &domain.Order{ - BuyerID: req.BuyerID, - SellerID: req.SellerID, - Items: req.Items, - Shipping: req.Shipping, + BuyerID: req.BuyerID, + SellerID: req.SellerID, + Items: req.Items, + Shipping: req.Shipping, + PaymentMethod: req.PaymentMethod, } var total int64 diff --git a/backend/internal/http/middleware/middleware_test.go b/backend/internal/http/middleware/middleware_test.go index b45d33f..9efeb69 100644 --- a/backend/internal/http/middleware/middleware_test.go +++ b/backend/internal/http/middleware/middleware_test.go @@ -113,8 +113,8 @@ func createTestToken(secret string, userID uuid.UUID, role string, companyID *uu func TestRequireAuthValidToken(t *testing.T) { secret := "test-secret" - userID, _ := uuid.NewV4() - companyID, _ := uuid.NewV4() + userID, _ := uuid.NewV7() + companyID, _ := uuid.NewV7() tokenStr := createTestToken(secret, userID, "Admin", &companyID) var receivedClaims Claims @@ -172,7 +172,7 @@ func TestRequireAuthInvalidToken(t *testing.T) { } func TestRequireAuthWrongSecret(t *testing.T) { - userID, _ := uuid.NewV4() + userID, _ := uuid.NewV7() tokenStr := createTestToken("correct-secret", userID, "User", nil) handler := RequireAuth([]byte("wrong-secret"))(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -192,7 +192,7 @@ func TestRequireAuthWrongSecret(t *testing.T) { func TestRequireAuthRoleRestriction(t *testing.T) { secret := "secret" - userID, _ := uuid.NewV4() + userID, _ := uuid.NewV7() tokenStr := createTestToken(secret, userID, "User", nil) handler := RequireAuth([]byte(secret), "Admin")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -212,7 +212,7 @@ func TestRequireAuthRoleRestriction(t *testing.T) { func TestRequireAuthRoleAllowed(t *testing.T) { secret := "secret" - userID, _ := uuid.NewV4() + userID, _ := uuid.NewV7() tokenStr := createTestToken(secret, userID, "Admin", nil) handler := RequireAuth([]byte(secret), "Admin")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -232,7 +232,7 @@ func TestRequireAuthRoleAllowed(t *testing.T) { func TestGetClaimsFromContext(t *testing.T) { claims := Claims{ - UserID: uuid.Must(uuid.NewV4()), + UserID: uuid.Must(uuid.NewV7()), Role: "Admin", } ctx := context.WithValue(context.Background(), claimsKey, claims) @@ -345,7 +345,7 @@ func TestCORSLegacyWrapper(t *testing.T) { func TestRequireAuthExpiredToken(t *testing.T) { secret := "test-secret" - userID, _ := uuid.NewV4() + userID, _ := uuid.NewV7() // Create an expired token token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ diff --git a/backend/internal/payments/payments_test.go b/backend/internal/payments/payments_test.go index e1c8af6..e5180f2 100644 --- a/backend/internal/payments/payments_test.go +++ b/backend/internal/payments/payments_test.go @@ -24,9 +24,9 @@ func TestCreatePreference(t *testing.T) { gateway := NewMercadoPagoGateway("https://api.mercadopago.com", 2.5) order := &domain.Order{ - ID: uuid.Must(uuid.NewV4()), - BuyerID: uuid.Must(uuid.NewV4()), - SellerID: uuid.Must(uuid.NewV4()), + ID: uuid.Must(uuid.NewV7()), + BuyerID: uuid.Must(uuid.NewV7()), + SellerID: uuid.Must(uuid.NewV7()), TotalCents: 10000, // R$100 } @@ -83,7 +83,7 @@ func TestCreatePreferenceWithDifferentCommissions(t *testing.T) { t.Run(tc.name, func(t *testing.T) { gateway := NewMercadoPagoGateway("https://test.com", tc.commission) order := &domain.Order{ - ID: uuid.Must(uuid.NewV4()), + ID: uuid.Must(uuid.NewV7()), TotalCents: tc.totalCents, } @@ -106,7 +106,7 @@ func TestCreatePreferenceWithCancelledContext(t *testing.T) { gateway := NewMercadoPagoGateway("https://api.mercadopago.com", 2.5) order := &domain.Order{ - ID: uuid.Must(uuid.NewV4()), + ID: uuid.Must(uuid.NewV7()), TotalCents: 10000, } @@ -123,7 +123,7 @@ func TestCreatePreferenceWithTimeout(t *testing.T) { gateway := NewMercadoPagoGateway("https://api.mercadopago.com", 2.5) order := &domain.Order{ - ID: uuid.Must(uuid.NewV4()), + ID: uuid.Must(uuid.NewV7()), TotalCents: 10000, } @@ -144,7 +144,7 @@ func TestCreatePreferenceWithZeroTotal(t *testing.T) { gateway := NewMercadoPagoGateway("https://api.mercadopago.com", 2.5) order := &domain.Order{ - ID: uuid.Must(uuid.NewV4()), + ID: uuid.Must(uuid.NewV7()), TotalCents: 0, } diff --git a/backend/internal/repository/postgres/postgres.go b/backend/internal/repository/postgres/postgres.go index 0cbe46b..1a3d0da 100644 --- a/backend/internal/repository/postgres/postgres.go +++ b/backend/internal/repository/postgres/postgres.go @@ -459,9 +459,9 @@ func (r *Repository) CreateOrder(ctx context.Context, order *domain.Order) error return err } - orderQuery := `INSERT INTO orders (id, buyer_id, seller_id, status, total_cents, shipping_recipient_name, shipping_street, shipping_number, shipping_complement, shipping_district, shipping_city, shipping_state, shipping_zip_code, shipping_country, created_at, updated_at) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)` - if _, err := tx.ExecContext(ctx, orderQuery, order.ID, order.BuyerID, order.SellerID, order.Status, order.TotalCents, order.Shipping.RecipientName, order.Shipping.Street, order.Shipping.Number, order.Shipping.Complement, order.Shipping.District, order.Shipping.City, order.Shipping.State, order.Shipping.ZipCode, order.Shipping.Country, order.CreatedAt, order.UpdatedAt); err != nil { + orderQuery := `INSERT INTO orders (id, buyer_id, seller_id, status, total_cents, payment_method, shipping_recipient_name, shipping_street, shipping_number, shipping_complement, shipping_district, shipping_city, shipping_state, shipping_zip_code, shipping_country, created_at, updated_at) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)` + if _, err := tx.ExecContext(ctx, orderQuery, order.ID, order.BuyerID, order.SellerID, order.Status, order.TotalCents, order.PaymentMethod, order.Shipping.RecipientName, order.Shipping.Street, order.Shipping.Number, order.Shipping.Complement, order.Shipping.District, order.Shipping.City, order.Shipping.State, order.Shipping.ZipCode, order.Shipping.Country, order.CreatedAt, order.UpdatedAt); err != nil { _ = tx.Rollback() return err } @@ -528,25 +528,26 @@ func (r *Repository) ListOrders(ctx context.Context, filter domain.OrderFilter) filter.Limit = 20 } args = append(args, filter.Limit, filter.Offset) - listQuery := fmt.Sprintf(`SELECT id, buyer_id, seller_id, status, total_cents, COALESCE(shipping_recipient_name, '') as shipping_recipient_name, COALESCE(shipping_street, '') as shipping_street, COALESCE(shipping_number, '') as shipping_number, COALESCE(shipping_complement, '') as shipping_complement, COALESCE(shipping_district, '') as shipping_district, COALESCE(shipping_city, '') as shipping_city, COALESCE(shipping_state, '') as shipping_state, COALESCE(shipping_zip_code, '') as shipping_zip_code, COALESCE(shipping_country, '') as shipping_country, created_at, updated_at %s%s ORDER BY created_at DESC LIMIT $%d OFFSET $%d`, baseQuery, where, len(args)-1, len(args)) + listQuery := fmt.Sprintf(`SELECT id, buyer_id, seller_id, status, total_cents, payment_method, COALESCE(shipping_recipient_name, '') as shipping_recipient_name, COALESCE(shipping_street, '') as shipping_street, COALESCE(shipping_number, '') as shipping_number, COALESCE(shipping_complement, '') as shipping_complement, COALESCE(shipping_district, '') as shipping_district, COALESCE(shipping_city, '') as shipping_city, COALESCE(shipping_state, '') as shipping_state, COALESCE(shipping_zip_code, '') as shipping_zip_code, COALESCE(shipping_country, '') as shipping_country, created_at, updated_at %s%s ORDER BY created_at DESC LIMIT $%d OFFSET $%d`, baseQuery, where, len(args)-1, len(args)) var rows []struct { - ID uuid.UUID `db:"id"` - BuyerID uuid.UUID `db:"buyer_id"` - SellerID uuid.UUID `db:"seller_id"` - Status domain.OrderStatus `db:"status"` - TotalCents int64 `db:"total_cents"` - ShippingRecipientName string `db:"shipping_recipient_name"` - ShippingStreet string `db:"shipping_street"` - ShippingNumber string `db:"shipping_number"` - ShippingComplement string `db:"shipping_complement"` - ShippingDistrict string `db:"shipping_district"` - ShippingCity string `db:"shipping_city"` - ShippingState string `db:"shipping_state"` - ShippingZipCode string `db:"shipping_zip_code"` - ShippingCountry string `db:"shipping_country"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt time.Time `db:"updated_at"` + ID uuid.UUID `db:"id"` + BuyerID uuid.UUID `db:"buyer_id"` + SellerID uuid.UUID `db:"seller_id"` + Status domain.OrderStatus `db:"status"` + TotalCents int64 `db:"total_cents"` + PaymentMethod domain.PaymentMethod `db:"payment_method"` + ShippingRecipientName string `db:"shipping_recipient_name"` + ShippingStreet string `db:"shipping_street"` + ShippingNumber string `db:"shipping_number"` + ShippingComplement string `db:"shipping_complement"` + ShippingDistrict string `db:"shipping_district"` + ShippingCity string `db:"shipping_city"` + ShippingState string `db:"shipping_state"` + ShippingZipCode string `db:"shipping_zip_code"` + ShippingCountry string `db:"shipping_country"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` } if err := r.db.SelectContext(ctx, &rows, listQuery, args...); err != nil { @@ -561,12 +562,13 @@ func (r *Repository) ListOrders(ctx context.Context, filter domain.OrderFilter) return nil, 0, err } orders = append(orders, domain.Order{ - ID: row.ID, - BuyerID: row.BuyerID, - SellerID: row.SellerID, - Status: row.Status, - TotalCents: row.TotalCents, - Items: items, + ID: row.ID, + BuyerID: row.BuyerID, + SellerID: row.SellerID, + Status: row.Status, + TotalCents: row.TotalCents, + PaymentMethod: row.PaymentMethod, + Items: items, Shipping: domain.ShippingAddress{ RecipientName: row.ShippingRecipientName, Street: row.ShippingStreet, @@ -587,24 +589,25 @@ func (r *Repository) ListOrders(ctx context.Context, filter domain.OrderFilter) func (r *Repository) GetOrder(ctx context.Context, id uuid.UUID) (*domain.Order, error) { var row struct { - ID uuid.UUID `db:"id"` - BuyerID uuid.UUID `db:"buyer_id"` - SellerID uuid.UUID `db:"seller_id"` - Status domain.OrderStatus `db:"status"` - TotalCents int64 `db:"total_cents"` - ShippingRecipientName string `db:"shipping_recipient_name"` - ShippingStreet string `db:"shipping_street"` - ShippingNumber string `db:"shipping_number"` - ShippingComplement string `db:"shipping_complement"` - ShippingDistrict string `db:"shipping_district"` - ShippingCity string `db:"shipping_city"` - ShippingState string `db:"shipping_state"` - ShippingZipCode string `db:"shipping_zip_code"` - ShippingCountry string `db:"shipping_country"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt time.Time `db:"updated_at"` + ID uuid.UUID `db:"id"` + BuyerID uuid.UUID `db:"buyer_id"` + SellerID uuid.UUID `db:"seller_id"` + Status domain.OrderStatus `db:"status"` + TotalCents int64 `db:"total_cents"` + PaymentMethod domain.PaymentMethod `db:"payment_method"` + ShippingRecipientName string `db:"shipping_recipient_name"` + ShippingStreet string `db:"shipping_street"` + ShippingNumber string `db:"shipping_number"` + ShippingComplement string `db:"shipping_complement"` + ShippingDistrict string `db:"shipping_district"` + ShippingCity string `db:"shipping_city"` + ShippingState string `db:"shipping_state"` + ShippingZipCode string `db:"shipping_zip_code"` + ShippingCountry string `db:"shipping_country"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` } - orderQuery := `SELECT id, buyer_id, seller_id, status, total_cents, COALESCE(shipping_recipient_name, '') as shipping_recipient_name, COALESCE(shipping_street, '') as shipping_street, COALESCE(shipping_number, '') as shipping_number, COALESCE(shipping_complement, '') as shipping_complement, COALESCE(shipping_district, '') as shipping_district, COALESCE(shipping_city, '') as shipping_city, COALESCE(shipping_state, '') as shipping_state, COALESCE(shipping_zip_code, '') as shipping_zip_code, COALESCE(shipping_country, '') as shipping_country, created_at, updated_at FROM orders WHERE id = $1` + orderQuery := `SELECT id, buyer_id, seller_id, status, total_cents, payment_method, COALESCE(shipping_recipient_name, '') as shipping_recipient_name, COALESCE(shipping_street, '') as shipping_street, COALESCE(shipping_number, '') as shipping_number, COALESCE(shipping_complement, '') as shipping_complement, COALESCE(shipping_district, '') as shipping_district, COALESCE(shipping_city, '') as shipping_city, COALESCE(shipping_state, '') as shipping_state, COALESCE(shipping_zip_code, '') as shipping_zip_code, COALESCE(shipping_country, '') as shipping_country, created_at, updated_at FROM orders WHERE id = $1` if err := r.db.GetContext(ctx, &row, orderQuery, id); err != nil { return nil, err } @@ -615,12 +618,13 @@ func (r *Repository) GetOrder(ctx context.Context, id uuid.UUID) (*domain.Order, return nil, err } order := &domain.Order{ - ID: row.ID, - BuyerID: row.BuyerID, - SellerID: row.SellerID, - Status: row.Status, - TotalCents: row.TotalCents, - Items: items, + ID: row.ID, + BuyerID: row.BuyerID, + SellerID: row.SellerID, + Status: row.Status, + TotalCents: row.TotalCents, + PaymentMethod: row.PaymentMethod, + Items: items, Shipping: domain.ShippingAddress{ RecipientName: row.ShippingRecipientName, Street: row.ShippingStreet, diff --git a/backend/internal/repository/postgres/repository_test.go b/backend/internal/repository/postgres/repository_test.go index bc83779..d37b1df 100644 --- a/backend/internal/repository/postgres/repository_test.go +++ b/backend/internal/repository/postgres/repository_test.go @@ -28,7 +28,7 @@ func TestCreateCompany(t *testing.T) { defer repo.db.Close() company := &domain.Company{ - ID: uuid.Must(uuid.NewV4()), + ID: uuid.Must(uuid.NewV7()), CNPJ: "12345678901234", CorporateName: "Test Pharmacy", Category: "farmacia", @@ -72,7 +72,7 @@ func TestGetCompany(t *testing.T) { repo, mock := newMockRepo(t) defer repo.db.Close() - id := uuid.Must(uuid.NewV4()) + id := uuid.Must(uuid.NewV7()) rows := sqlmock.NewRows([]string{"id", "cnpj", "corporate_name", "category", "license_number", "is_verified", "latitude", "longitude", "city", "state", "created_at", "updated_at"}). AddRow(id, "123", "Test", "farmacia", "123", false, 0.0, 0.0, "City", "ST", time.Now(), time.Now()) @@ -97,8 +97,8 @@ func TestCreateProduct(t *testing.T) { defer repo.db.Close() product := &domain.Product{ - ID: uuid.Must(uuid.NewV4()), - SellerID: uuid.Must(uuid.NewV4()), + ID: uuid.Must(uuid.NewV7()), + SellerID: uuid.Must(uuid.NewV7()), Name: "Test Product", Description: "Desc", Batch: "B1", @@ -130,7 +130,7 @@ func TestListProducts(t *testing.T) { repo, mock := newMockRepo(t) defer repo.db.Close() - rows := sqlmock.NewRows([]string{"id", "name"}).AddRow(uuid.Must(uuid.NewV4()), "P1") + rows := sqlmock.NewRows([]string{"id", "name"}).AddRow(uuid.Must(uuid.NewV7()), "P1") // We expect two queries: count and select list mock.ExpectQuery(`SELECT count\(\*\) FROM products`).WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1)) diff --git a/backend/internal/usecase/usecase_test.go b/backend/internal/usecase/usecase_test.go index baed1c9..b1d79d8 100644 --- a/backend/internal/usecase/usecase_test.go +++ b/backend/internal/usecase/usecase_test.go @@ -379,7 +379,7 @@ func TestGetCompany(t *testing.T) { ctx := context.Background() company := &domain.Company{ - ID: uuid.Must(uuid.NewV4()), + ID: uuid.Must(uuid.NewV7()), Category: "farmacia", CNPJ: "12345678901234", CorporateName: "Test Pharmacy", @@ -400,7 +400,7 @@ func TestUpdateCompany(t *testing.T) { ctx := context.Background() company := &domain.Company{ - ID: uuid.Must(uuid.NewV4()), + ID: uuid.Must(uuid.NewV7()), Category: "farmacia", CNPJ: "12345678901234", CorporateName: "Test Pharmacy", @@ -423,7 +423,7 @@ func TestDeleteCompany(t *testing.T) { ctx := context.Background() company := &domain.Company{ - ID: uuid.Must(uuid.NewV4()), + ID: uuid.Must(uuid.NewV7()), CorporateName: "Test Pharmacy", } repo.companies = append(repo.companies, *company) @@ -443,7 +443,7 @@ func TestVerifyCompany(t *testing.T) { ctx := context.Background() company := &domain.Company{ - ID: uuid.Must(uuid.NewV4()), + ID: uuid.Must(uuid.NewV7()), Category: "farmacia", CNPJ: "12345678901234", CorporateName: "Test Pharmacy", @@ -468,7 +468,7 @@ func TestRegisterProduct(t *testing.T) { ctx := context.Background() product := &domain.Product{ - SellerID: uuid.Must(uuid.NewV4()), + SellerID: uuid.Must(uuid.NewV7()), Name: "Test Product", Description: "A test product", Batch: "BATCH-001", @@ -506,7 +506,7 @@ func TestGetProduct(t *testing.T) { ctx := context.Background() product := &domain.Product{ - ID: uuid.Must(uuid.NewV4()), + ID: uuid.Must(uuid.NewV7()), Name: "Test Product", } repo.products = append(repo.products, *product) @@ -525,7 +525,7 @@ func TestUpdateProduct(t *testing.T) { ctx := context.Background() product := &domain.Product{ - ID: uuid.Must(uuid.NewV4()), + ID: uuid.Must(uuid.NewV7()), Name: "Test Product", } repo.products = append(repo.products, *product) @@ -546,7 +546,7 @@ func TestDeleteProduct(t *testing.T) { ctx := context.Background() product := &domain.Product{ - ID: uuid.Must(uuid.NewV4()), + ID: uuid.Must(uuid.NewV7()), Name: "Test Product", } repo.products = append(repo.products, *product) @@ -592,7 +592,7 @@ func TestListInventory(t *testing.T) { func TestAdjustInventory(t *testing.T) { svc, _ := newTestService() ctx := context.Background() - productID := uuid.Must(uuid.NewV4()) + productID := uuid.Must(uuid.NewV7()) item, err := svc.AdjustInventory(ctx, productID, 10, "Restock") if err != nil { @@ -611,8 +611,8 @@ func TestCreateOrder(t *testing.T) { ctx := context.Background() order := &domain.Order{ - BuyerID: uuid.Must(uuid.NewV4()), - SellerID: uuid.Must(uuid.NewV4()), + BuyerID: uuid.Must(uuid.NewV7()), + SellerID: uuid.Must(uuid.NewV7()), TotalCents: 10000, } @@ -634,9 +634,9 @@ func TestUpdateOrderStatus(t *testing.T) { ctx := context.Background() order := &domain.Order{ - ID: uuid.Must(uuid.NewV4()), - BuyerID: uuid.Must(uuid.NewV4()), - SellerID: uuid.Must(uuid.NewV4()), + ID: uuid.Must(uuid.NewV7()), + BuyerID: uuid.Must(uuid.NewV7()), + SellerID: uuid.Must(uuid.NewV7()), Status: domain.OrderStatusPending, TotalCents: 10000, } @@ -655,7 +655,7 @@ func TestCreateUser(t *testing.T) { ctx := context.Background() user := &domain.User{ - CompanyID: uuid.Must(uuid.NewV4()), + CompanyID: uuid.Must(uuid.NewV7()), Role: "admin", Name: "Test User", Email: "test@example.com", @@ -722,7 +722,7 @@ func TestAuthenticate(t *testing.T) { // First create a user user := &domain.User{ - CompanyID: uuid.Must(uuid.NewV4()), + CompanyID: uuid.Must(uuid.NewV7()), Role: "admin", Name: "Test User", Username: "authuser", @@ -755,7 +755,7 @@ func TestAuthenticateInvalidPassword(t *testing.T) { ctx := context.Background() user := &domain.User{ - CompanyID: uuid.Must(uuid.NewV4()), + CompanyID: uuid.Must(uuid.NewV7()), Role: "admin", Name: "Test User", Username: "failuser", @@ -776,10 +776,10 @@ func TestAddItemToCart(t *testing.T) { svc, repo := newTestService() ctx := context.Background() - buyerID := uuid.Must(uuid.NewV4()) + buyerID := uuid.Must(uuid.NewV7()) product := &domain.Product{ - ID: uuid.Must(uuid.NewV4()), - SellerID: uuid.Must(uuid.NewV4()), + ID: uuid.Must(uuid.NewV7()), + SellerID: uuid.Must(uuid.NewV7()), Name: "Test Product", PriceCents: 1000, Stock: 100, @@ -802,8 +802,8 @@ func TestAddItemToCartInvalidQuantity(t *testing.T) { svc, _ := newTestService() ctx := context.Background() - buyerID := uuid.Must(uuid.NewV4()) - productID := uuid.Must(uuid.NewV4()) + buyerID := uuid.Must(uuid.NewV7()) + productID := uuid.Must(uuid.NewV7()) _, err := svc.AddItemToCart(ctx, buyerID, productID, 0) if err == nil { @@ -815,10 +815,10 @@ func TestCartB2BDiscount(t *testing.T) { svc, repo := newTestService() ctx := context.Background() - buyerID := uuid.Must(uuid.NewV4()) + buyerID := uuid.Must(uuid.NewV7()) product := &domain.Product{ - ID: uuid.Must(uuid.NewV4()), - SellerID: uuid.Must(uuid.NewV4()), + ID: uuid.Must(uuid.NewV7()), + SellerID: uuid.Must(uuid.NewV7()), Name: "Expensive Product", PriceCents: 50000, // R$500 per unit Stock: 1000, @@ -847,11 +847,11 @@ func TestCreateReview(t *testing.T) { svc, repo := newTestService() ctx := context.Background() - buyerID := uuid.Must(uuid.NewV4()) - sellerID := uuid.Must(uuid.NewV4()) + buyerID := uuid.Must(uuid.NewV7()) + sellerID := uuid.Must(uuid.NewV7()) order := &domain.Order{ - ID: uuid.Must(uuid.NewV4()), + ID: uuid.Must(uuid.NewV7()), BuyerID: buyerID, SellerID: sellerID, Status: domain.OrderStatusDelivered, @@ -873,7 +873,7 @@ func TestCreateReviewInvalidRating(t *testing.T) { svc, _ := newTestService() ctx := context.Background() - _, err := svc.CreateReview(ctx, uuid.Must(uuid.NewV4()), uuid.Must(uuid.NewV4()), 6, "Invalid") + _, err := svc.CreateReview(ctx, uuid.Must(uuid.NewV7()), uuid.Must(uuid.NewV7()), 6, "Invalid") if err == nil { t.Error("expected error for invalid rating") } @@ -883,9 +883,9 @@ func TestCreateReviewNotDelivered(t *testing.T) { svc, repo := newTestService() ctx := context.Background() - buyerID := uuid.Must(uuid.NewV4()) + buyerID := uuid.Must(uuid.NewV7()) order := &domain.Order{ - ID: uuid.Must(uuid.NewV4()), + ID: uuid.Must(uuid.NewV7()), BuyerID: buyerID, Status: domain.OrderStatusPending, // Not delivered TotalCents: 10000, @@ -904,7 +904,7 @@ func TestGetSellerDashboard(t *testing.T) { svc, _ := newTestService() ctx := context.Background() - sellerID := uuid.Must(uuid.NewV4()) + sellerID := uuid.Must(uuid.NewV7()) dashboard, err := svc.GetSellerDashboard(ctx, sellerID) if err != nil { t.Fatalf("failed to get seller dashboard: %v", err) @@ -936,9 +936,9 @@ func TestCreatePaymentPreference(t *testing.T) { ctx := context.Background() order := &domain.Order{ - ID: uuid.Must(uuid.NewV4()), - BuyerID: uuid.Must(uuid.NewV4()), - SellerID: uuid.Must(uuid.NewV4()), + ID: uuid.Must(uuid.NewV7()), + BuyerID: uuid.Must(uuid.NewV7()), + SellerID: uuid.Must(uuid.NewV7()), TotalCents: 10000, } repo.orders = append(repo.orders, *order) @@ -961,9 +961,9 @@ func TestHandlePaymentWebhook(t *testing.T) { ctx := context.Background() order := &domain.Order{ - ID: uuid.Must(uuid.NewV4()), - BuyerID: uuid.Must(uuid.NewV4()), - SellerID: uuid.Must(uuid.NewV4()), + ID: uuid.Must(uuid.NewV7()), + BuyerID: uuid.Must(uuid.NewV7()), + SellerID: uuid.Must(uuid.NewV7()), Status: domain.OrderStatusPending, TotalCents: 10000, } diff --git a/marketplace/src/pages/Checkout.tsx b/marketplace/src/pages/Checkout.tsx index bd72d06..a9634ab 100644 --- a/marketplace/src/pages/Checkout.tsx +++ b/marketplace/src/pages/Checkout.tsx @@ -1,11 +1,13 @@ -import { useState } from 'react' +import { useState, useEffect } from 'react' import { useNavigate } from 'react-router-dom' import { Shell } from '../layouts/Shell' import { useCartStore, selectGroupedCart, selectCartSummary } from '../stores/cartStore' import { useAuth } from '../context/AuthContext' import { ordersService, CreateOrderRequest } from '../services/ordersService' +import { shippingService } from '../services/shippingService' +import { apiClient } from '../services/apiClient' import { formatCurrency } from '../utils/format' -import { ArrowLeft, CheckCircle2, Truck } from 'lucide-react' +import { ArrowLeft, CheckCircle2, Truck, Store } from 'lucide-react' export function CheckoutPage() { const navigate = useNavigate() @@ -15,6 +17,10 @@ export function CheckoutPage() { const clearAll = useCartStore((state) => state.clearAll) const [loading, setLoading] = useState(false) + + // Shipping options state + const [shippingOptions, setShippingOptions] = useState>({}) + const [shipping, setShipping] = useState({ recipient_name: user?.name || '', street: '', @@ -27,8 +33,65 @@ export function CheckoutPage() { country: 'Brasil' }) + // Pre-fill address from company + useEffect(() => { + async function fetchCompanyAddress() { + try { + // TODO: Use a proper service for this + const res = await apiClient.get('/v1/companies/me') + const company = res.data + if (company) { + setShipping(prev => ({ + ...prev, + street: company.street || '', // Wait, company model fields differ from shipping fields? + // Let's check company model in backend: city, state, etc. + // Actually, the backend Company model has: city, state, latitude, longitude. + // It seems it does NOT have street, number, district, zip_code separately in the main table? + // Let's re-read models.go. + // Ah, lines 18-21: Latitude, Longitude, City, State. + // It seems we lack detailed address fields in Company model for pre-filling! + // Wait, seeder line 113: city, state. + // We might only be able to pre-fill city/state for now unless I missed something. + city: company.city || '', + state: company.state || '', + // We can't pre-fill street/number if they aren't there. + })) + } + } catch (e) { + console.error("Failed to fetch company address", e) + } + } + if (user) fetchCompanyAddress() + }, [user]) + + // Fetch shipping settings for sellers + useEffect(() => { + async function fetchShipping() { + const options: Record = {} + for (const sellerId of Object.keys(groups)) { + try { + const settings = await shippingService.getSettings(sellerId) + options[sellerId] = { + delivery: settings.active, + pickup: settings.pickup_active, + pickupAddress: settings.pickup_address, + price: 15.00 // Mock calc for now, or implement calculateShipping + } + } catch (e) { + console.error(`Failed to fetch shipping for ${sellerId}`, e) + } + } + setShippingOptions(options) + } + if (Object.keys(groups).length > 0) fetchShipping() + }, [groups]) + + // Payment method selection + const [paymentMethod, setPaymentMethod] = useState<'pix' | 'credit_card' | 'debit_card'>('credit_card') + const handleInputChange = (e: React.ChangeEvent) => { const { name, value } = e.target + setPaymentMethod(prev => prev) // Keep state if needed, though strictly not needed for this simple setter setShipping(prev => ({ ...prev, [name]: value })) } @@ -59,7 +122,8 @@ export function CheckoutPage() { state: shipping.state, zip_code: shipping.zip_code, country: shipping.country - } + }, + payment_method: paymentMethod } return ordersService.createOrder(orderData) }) @@ -192,13 +256,58 @@ export function CheckoutPage() { - {/* Payment Method Stub */} + {/* Payment Method Selection */}
-

Pagamento

-

Este é um ambiente de demonstração. O pagamento será processado como "Confirmado" para fins de teste.

-
- - Método de Teste (Aprovação Automática) +
+ +

Forma de Pagamento

+
+ +
+ + + + +
@@ -211,12 +320,44 @@ export function CheckoutPage() { {Object.entries(groups).map(([vendorId, group]) => (

{group.vendorName}

- {group.items.map(item => ( -
- {item.quantity}x {item.name} - R$ {formatCurrency(item.quantity * item.unitPrice)} -
+
+ {item.quantity}x {item.name} + R$ {formatCurrency(item.quantity * item.unitPrice)} +
))} + + {/* Shipping Options for this Vendor */} + {shippingOptions[vendorId] && ( +
+

Entrega

+
+ {shippingOptions[vendorId].delivery ? ( + + ) : ( +
Entrega indisponível para esta região
+ )} + + {shippingOptions[vendorId].pickup && ( + + )} +
+
+ )}
))} @@ -225,6 +366,7 @@ export function CheckoutPage() { Total R$ {formatCurrency(summary.totalValue)} +

Taxas de entrega não incluídas no total acima