diff --git a/backend/docs/docs.go b/backend/docs/docs.go index c257e09..f48efe5 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -38,7 +38,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_http_handler.loginRequest" + "$ref": "#/definitions/handler.loginRequest" } } ], @@ -46,7 +46,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_http_handler.authResponse" + "$ref": "#/definitions/handler.authResponse" } }, "400": { @@ -70,6 +70,159 @@ const docTemplate = `{ } } }, + "/api/v1/auth/logout": { + "post": { + "description": "Endpoint para logout (invalidação client-side).", + "tags": [ + "Autenticação" + ], + "summary": "Logout", + "responses": { + "204": { + "description": "No Content", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/auth/password/forgot": { + "post": { + "description": "Gera um token de redefinição de senha.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Autenticação" + ], + "summary": "Solicitar redefinição de senha", + "parameters": [ + { + "description": "Email do usuário", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.forgotPasswordRequest" + } + } + ], + "responses": { + "202": { + "description": "Accepted", + "schema": { + "$ref": "#/definitions/handler.resetTokenResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/auth/password/reset": { + "post": { + "description": "Atualiza a senha usando o token de redefinição.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Autenticação" + ], + "summary": "Redefinir senha", + "parameters": [ + { + "description": "Token e nova senha", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.resetPasswordRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.messageResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/auth/refresh-token": { + "post": { + "description": "Gera um novo JWT a partir de um token válido.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Autenticação" + ], + "summary": "Atualizar token", + "parameters": [ + { + "type": "string", + "description": "Bearer token", + "name": "Authorization", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.authResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/api/v1/auth/register": { "post": { "description": "Cria um usuário e opcionalmente uma empresa, retornando token JWT.", @@ -90,7 +243,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_http_handler.registerAuthRequest" + "$ref": "#/definitions/handler.registerAuthRequest" } } ], @@ -98,7 +251,7 @@ const docTemplate = `{ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/internal_http_handler.authResponse" + "$ref": "#/definitions/handler.authResponse" } }, "400": { @@ -122,6 +275,162 @@ const docTemplate = `{ } } }, + "/api/v1/auth/register/customer": { + "post": { + "description": "Cria um usuário do tipo cliente e opcionalmente uma empresa, retornando token JWT.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Autenticação" + ], + "summary": "Cadastro de cliente", + "parameters": [ + { + "description": "Dados do usuário e empresa", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.registerAuthRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/handler.authResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/auth/register/tenant": { + "post": { + "description": "Cria um usuário do tipo tenant e opcionalmente uma empresa, retornando token JWT.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Autenticação" + ], + "summary": "Cadastro de tenant", + "parameters": [ + { + "description": "Dados do usuário e empresa", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.registerAuthRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/handler.authResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/auth/verify-email": { + "post": { + "description": "Marca o email como verificado usando um token JWT.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Autenticação" + ], + "summary": "Verificar email", + "parameters": [ + { + "description": "Token de verificação", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.verifyEmailRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.messageResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/api/v1/cart": { "get": { "security": [ @@ -140,7 +449,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.CartSummary" + "$ref": "#/definitions/domain.CartSummary" } } } @@ -168,7 +477,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_http_handler.addCartItemRequest" + "$ref": "#/definitions/handler.addCartItemRequest" } } ], @@ -176,7 +485,7 @@ const docTemplate = `{ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.CartSummary" + "$ref": "#/definitions/domain.CartSummary" } }, "400": { @@ -215,7 +524,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.CartSummary" + "$ref": "#/definitions/domain.CartSummary" } }, "400": { @@ -245,7 +554,7 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.Company" + "$ref": "#/definitions/domain.Company" } } } @@ -270,7 +579,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_http_handler.registerCompanyRequest" + "$ref": "#/definitions/handler.registerCompanyRequest" } } ], @@ -278,7 +587,7 @@ const docTemplate = `{ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.Company" + "$ref": "#/definitions/domain.Company" } } } @@ -302,7 +611,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.Company" + "$ref": "#/definitions/domain.Company" } } } @@ -330,7 +639,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.Company" + "$ref": "#/definitions/domain.Company" } }, "404": { @@ -407,7 +716,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_http_handler.updateCompanyRequest" + "$ref": "#/definitions/handler.updateCompanyRequest" } } ], @@ -415,7 +724,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.Company" + "$ref": "#/definitions/domain.Company" } }, "400": { @@ -461,7 +770,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.CompanyRating" + "$ref": "#/definitions/domain.CompanyRating" } } } @@ -491,7 +800,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.Company" + "$ref": "#/definitions/domain.Company" } } } @@ -515,7 +824,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.AdminDashboard" + "$ref": "#/definitions/domain.AdminDashboard" } } } @@ -547,7 +856,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.SellerDashboard" + "$ref": "#/definitions/domain.SellerDashboard" } } } @@ -581,7 +890,7 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.InventoryItem" + "$ref": "#/definitions/domain.InventoryItem" } } } @@ -612,7 +921,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_http_handler.inventoryAdjustRequest" + "$ref": "#/definitions/handler.inventoryAdjustRequest" } } ], @@ -620,7 +929,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.InventoryItem" + "$ref": "#/definitions/domain.InventoryItem" } }, "400": { @@ -635,6 +944,69 @@ const docTemplate = `{ } } }, + "/api/v1/marketplace/records": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Marketplace" + ], + "summary": "Busca avançada no marketplace", + "parameters": [ + { + "type": "string", + "description": "Busca textual", + "name": "query", + "in": "query" + }, + { + "type": "string", + "description": "Campo de ordenação (created_at|updated_at)", + "name": "sort_by", + "in": "query" + }, + { + "type": "string", + "description": "Direção (asc|desc)", + "name": "sort_order", + "in": "query" + }, + { + "type": "string", + "description": "Data mínima (RFC3339)", + "name": "created_after", + "in": "query" + }, + { + "type": "string", + "description": "Data máxima (RFC3339)", + "name": "created_before", + "in": "query" + }, + { + "type": "integer", + "description": "Página", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Itens por página", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.ProductPaginationResponse" + } + } + } + } + }, "/api/v1/orders": { "get": { "security": [ @@ -655,7 +1027,7 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.Order" + "$ref": "#/definitions/domain.Order" } } } @@ -679,7 +1051,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_http_handler.createOrderRequest" + "$ref": "#/definitions/handler.createOrderRequest" } } ], @@ -687,7 +1059,7 @@ const docTemplate = `{ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.Order" + "$ref": "#/definitions/domain.Order" } } } @@ -720,7 +1092,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.Order" + "$ref": "#/definitions/domain.Order" } } } @@ -796,7 +1168,7 @@ const docTemplate = `{ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.PaymentPreference" + "$ref": "#/definitions/domain.PaymentPreference" } } } @@ -833,7 +1205,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_http_handler.updateStatusRequest" + "$ref": "#/definitions/handler.updateStatusRequest" } } ], @@ -863,7 +1235,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.PaymentWebhookEvent" + "$ref": "#/definitions/domain.PaymentWebhookEvent" } } ], @@ -871,7 +1243,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.PaymentSplitResult" + "$ref": "#/definitions/domain.PaymentSplitResult" } } } @@ -892,7 +1264,7 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.Product" + "$ref": "#/definitions/domain.Product" } } } @@ -916,7 +1288,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_http_handler.registerProductRequest" + "$ref": "#/definitions/handler.registerProductRequest" } } ], @@ -924,7 +1296,79 @@ const docTemplate = `{ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.Product" + "$ref": "#/definitions/domain.Product" + } + } + } + } + }, + "/api/v1/products/search": { + "get": { + "description": "Retorna produtos ordenados por validade, com distância aproximada. Vendedor anônimo até checkout.", + "produces": [ + "application/json" + ], + "tags": [ + "Produtos" + ], + "summary": "Busca avançada de produtos com filtros e distância", + "parameters": [ + { + "type": "string", + "description": "Termo de busca", + "name": "search", + "in": "query" + }, + { + "type": "integer", + "description": "Preço mínimo em centavos", + "name": "min_price", + "in": "query" + }, + { + "type": "integer", + "description": "Preço máximo em centavos", + "name": "max_price", + "in": "query" + }, + { + "type": "number", + "description": "Distância máxima em km", + "name": "max_distance", + "in": "query" + }, + { + "type": "number", + "description": "Latitude do comprador", + "name": "lat", + "in": "query", + "required": true + }, + { + "type": "number", + "description": "Longitude do comprador", + "name": "lng", + "in": "query", + "required": true + }, + { + "type": "integer", + "description": "Página", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Itens por página", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.ProductSearchPage" } } } @@ -952,7 +1396,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.Product" + "$ref": "#/definitions/domain.Product" } }, "404": { @@ -1029,7 +1473,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_http_handler.updateProductRequest" + "$ref": "#/definitions/handler.updateProductRequest" } } ], @@ -1037,7 +1481,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.Product" + "$ref": "#/definitions/domain.Product" } }, "400": { @@ -1085,7 +1529,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_http_handler.createReviewRequest" + "$ref": "#/definitions/handler.createReviewRequest" } } ], @@ -1093,7 +1537,7 @@ const docTemplate = `{ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.Review" + "$ref": "#/definitions/domain.Review" } }, "400": { @@ -1132,7 +1576,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_http_handler.createShipmentRequest" + "$ref": "#/definitions/handler.createShipmentRequest" } } ], @@ -1140,7 +1584,7 @@ const docTemplate = `{ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.Shipment" + "$ref": "#/definitions/domain.Shipment" } } } @@ -1173,7 +1617,190 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.Shipment" + "$ref": "#/definitions/domain.Shipment" + } + } + } + } + }, + "/api/v1/shipping/calculate": { + "post": { + "description": "Calculates shipping or pickup options based on vendor config and buyer location.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Shipping" + ], + "summary": "Calculate shipping options", + "parameters": [ + { + "description": "Calculation inputs", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.shippingCalculateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.ShippingOption" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/shipping/settings/{vendor_id}": { + "get": { + "description": "Returns pickup and delivery settings for a vendor.", + "produces": [ + "application/json" + ], + "tags": [ + "Shipping" + ], + "summary": "Get vendor shipping settings", + "parameters": [ + { + "type": "string", + "description": "Vendor ID", + "name": "vendor_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.ShippingMethod" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "put": { + "description": "Stores pickup and delivery settings for a vendor.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Shipping" + ], + "summary": "Update vendor shipping settings", + "parameters": [ + { + "type": "string", + "description": "Vendor ID", + "name": "vendor_id", + "in": "path", + "required": true + }, + { + "description": "Shipping settings", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.shippingSettingsRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.ShippingMethod" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } } } } @@ -1217,7 +1844,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.UserPage" + "$ref": "#/definitions/domain.UserPage" } }, "400": { @@ -1263,7 +1890,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_http_handler.createUserRequest" + "$ref": "#/definitions/handler.createUserRequest" } } ], @@ -1271,7 +1898,7 @@ const docTemplate = `{ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.User" + "$ref": "#/definitions/domain.User" } }, "400": { @@ -1331,7 +1958,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.User" + "$ref": "#/definitions/domain.User" } }, "400": { @@ -1393,7 +2020,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_http_handler.updateUserRequest" + "$ref": "#/definitions/handler.updateUserRequest" } } ], @@ -1401,7 +2028,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.User" + "$ref": "#/definitions/domain.User" } }, "400": { @@ -1509,7 +2136,7 @@ const docTemplate = `{ } }, "definitions": { - "github_com_saveinmed_backend-go_internal_domain.AdminDashboard": { + "domain.AdminDashboard": { "type": "object", "properties": { "gmv_cents": { @@ -1523,7 +2150,7 @@ const docTemplate = `{ } } }, - "github_com_saveinmed_backend-go_internal_domain.CartItem": { + "domain.CartItem": { "type": "object", "properties": { "batch": { @@ -1558,7 +2185,7 @@ const docTemplate = `{ } } }, - "github_com_saveinmed_backend-go_internal_domain.CartSummary": { + "domain.CartSummary": { "type": "object", "properties": { "discount_cents": { @@ -1570,7 +2197,7 @@ const docTemplate = `{ "items": { "type": "array", "items": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.CartItem" + "$ref": "#/definitions/domain.CartItem" } }, "subtotal_cents": { @@ -1581,9 +2208,16 @@ const docTemplate = `{ } } }, - "github_com_saveinmed_backend-go_internal_domain.Company": { + "domain.Company": { "type": "object", "properties": { + "category": { + "description": "farmacia, distribuidora", + "type": "string" + }, + "city": { + "type": "string" + }, "cnpj": { "type": "string" }, @@ -1599,11 +2233,17 @@ const docTemplate = `{ "is_verified": { "type": "boolean" }, + "latitude": { + "description": "Location", + "type": "number" + }, "license_number": { "type": "string" }, - "role": { - "description": "pharmacy, distributor, admin", + "longitude": { + "type": "number" + }, + "state": { "type": "string" }, "updated_at": { @@ -1611,7 +2251,7 @@ const docTemplate = `{ } } }, - "github_com_saveinmed_backend-go_internal_domain.CompanyRating": { + "domain.CompanyRating": { "type": "object", "properties": { "average_score": { @@ -1625,7 +2265,7 @@ const docTemplate = `{ } } }, - "github_com_saveinmed_backend-go_internal_domain.InventoryItem": { + "domain.InventoryItem": { "type": "object", "properties": { "batch": { @@ -1654,7 +2294,7 @@ const docTemplate = `{ } } }, - "github_com_saveinmed_backend-go_internal_domain.Order": { + "domain.Order": { "type": "object", "properties": { "buyer_id": { @@ -1669,17 +2309,17 @@ const docTemplate = `{ "items": { "type": "array", "items": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.OrderItem" + "$ref": "#/definitions/domain.OrderItem" } }, "seller_id": { "type": "string" }, "shipping": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.ShippingAddress" + "$ref": "#/definitions/domain.ShippingAddress" }, "status": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.OrderStatus" + "$ref": "#/definitions/domain.OrderStatus" }, "total_cents": { "type": "integer" @@ -1689,7 +2329,7 @@ const docTemplate = `{ } } }, - "github_com_saveinmed_backend-go_internal_domain.OrderItem": { + "domain.OrderItem": { "type": "object", "properties": { "batch": { @@ -1715,7 +2355,7 @@ const docTemplate = `{ } } }, - "github_com_saveinmed_backend-go_internal_domain.OrderStatus": { + "domain.OrderStatus": { "type": "string", "enum": [ "Pendente", @@ -1730,7 +2370,7 @@ const docTemplate = `{ "OrderStatusDelivered" ] }, - "github_com_saveinmed_backend-go_internal_domain.PaymentPreference": { + "domain.PaymentPreference": { "type": "object", "properties": { "commission_pct": { @@ -1753,7 +2393,7 @@ const docTemplate = `{ } } }, - "github_com_saveinmed_backend-go_internal_domain.PaymentSplitResult": { + "domain.PaymentSplitResult": { "type": "object", "properties": { "marketplace_fee": { @@ -1776,7 +2416,7 @@ const docTemplate = `{ } } }, - "github_com_saveinmed_backend-go_internal_domain.PaymentWebhookEvent": { + "domain.PaymentWebhookEvent": { "type": "object", "properties": { "marketplace_fee": { @@ -1799,7 +2439,7 @@ const docTemplate = `{ } } }, - "github_com_saveinmed_backend-go_internal_domain.Product": { + "domain.Product": { "type": "object", "properties": { "batch": { @@ -1834,7 +2474,91 @@ const docTemplate = `{ } } }, - "github_com_saveinmed_backend-go_internal_domain.Review": { + "domain.ProductPaginationResponse": { + "type": "object", + "properties": { + "current_page": { + "type": "integer" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.Product" + } + }, + "total_count": { + "type": "integer" + }, + "total_pages": { + "type": "integer" + } + } + }, + "domain.ProductSearchPage": { + "type": "object", + "properties": { + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "products": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.ProductWithDistance" + } + }, + "total": { + "type": "integer" + } + } + }, + "domain.ProductWithDistance": { + "type": "object", + "properties": { + "batch": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "distance_km": { + "type": "number" + }, + "expires_at": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "price_cents": { + "type": "integer" + }, + "seller_id": { + "type": "string" + }, + "stock": { + "type": "integer" + }, + "tenant_city": { + "type": "string" + }, + "tenant_state": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "domain.Review": { "type": "object", "properties": { "buyer_id": { @@ -1860,13 +2584,13 @@ const docTemplate = `{ } } }, - "github_com_saveinmed_backend-go_internal_domain.SellerDashboard": { + "domain.SellerDashboard": { "type": "object", "properties": { "low_stock_alerts": { "type": "array", "items": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.Product" + "$ref": "#/definitions/domain.Product" } }, "orders_count": { @@ -1878,7 +2602,7 @@ const docTemplate = `{ "top_products": { "type": "array", "items": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.TopProduct" + "$ref": "#/definitions/domain.TopProduct" } }, "total_sales_cents": { @@ -1886,7 +2610,7 @@ const docTemplate = `{ } } }, - "github_com_saveinmed_backend-go_internal_domain.Shipment": { + "domain.Shipment": { "type": "object", "properties": { "carrier": { @@ -1915,7 +2639,7 @@ const docTemplate = `{ } } }, - "github_com_saveinmed_backend-go_internal_domain.ShippingAddress": { + "domain.ShippingAddress": { "type": "object", "properties": { "city": { @@ -1947,7 +2671,84 @@ const docTemplate = `{ } } }, - "github_com_saveinmed_backend-go_internal_domain.TopProduct": { + "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": { + "description": { + "type": "string" + }, + "distance_km": { + "type": "number" + }, + "estimated_minutes": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "value_cents": { + "type": "integer" + } + } + }, + "domain.TopProduct": { "type": "object", "properties": { "name": { @@ -1964,7 +2765,7 @@ const docTemplate = `{ } } }, - "github_com_saveinmed_backend-go_internal_domain.User": { + "domain.User": { "type": "object", "properties": { "company_id": { @@ -1976,6 +2777,9 @@ const docTemplate = `{ "email": { "type": "string" }, + "email_verified": { + "type": "boolean" + }, "id": { "type": "string" }, @@ -1990,7 +2794,7 @@ const docTemplate = `{ } } }, - "github_com_saveinmed_backend-go_internal_domain.UserPage": { + "domain.UserPage": { "type": "object", "properties": { "page": { @@ -2005,12 +2809,12 @@ const docTemplate = `{ "users": { "type": "array", "items": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.User" + "$ref": "#/definitions/domain.User" } } } }, - "internal_http_handler.addCartItemRequest": { + "handler.addCartItemRequest": { "type": "object", "properties": { "product_id": { @@ -2021,7 +2825,7 @@ const docTemplate = `{ } } }, - "internal_http_handler.authResponse": { + "handler.authResponse": { "type": "object", "properties": { "expires_at": { @@ -2032,7 +2836,7 @@ const docTemplate = `{ } } }, - "internal_http_handler.createOrderRequest": { + "handler.createOrderRequest": { "type": "object", "properties": { "buyer_id": { @@ -2041,18 +2845,18 @@ const docTemplate = `{ "items": { "type": "array", "items": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.OrderItem" + "$ref": "#/definitions/domain.OrderItem" } }, "seller_id": { "type": "string" }, "shipping": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.ShippingAddress" + "$ref": "#/definitions/domain.ShippingAddress" } } }, - "internal_http_handler.createReviewRequest": { + "handler.createReviewRequest": { "type": "object", "properties": { "comment": { @@ -2066,7 +2870,7 @@ const docTemplate = `{ } } }, - "internal_http_handler.createShipmentRequest": { + "handler.createShipmentRequest": { "type": "object", "properties": { "carrier": { @@ -2083,7 +2887,7 @@ const docTemplate = `{ } } }, - "internal_http_handler.createUserRequest": { + "handler.createUserRequest": { "type": "object", "properties": { "company_id": { @@ -2103,7 +2907,15 @@ const docTemplate = `{ } } }, - "internal_http_handler.inventoryAdjustRequest": { + "handler.forgotPasswordRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + } + } + }, + "handler.inventoryAdjustRequest": { "type": "object", "properties": { "delta": { @@ -2117,7 +2929,7 @@ const docTemplate = `{ } } }, - "internal_http_handler.loginRequest": { + "handler.loginRequest": { "type": "object", "properties": { "email": { @@ -2128,11 +2940,19 @@ const docTemplate = `{ } } }, - "internal_http_handler.registerAuthRequest": { + "handler.messageResponse": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + }, + "handler.registerAuthRequest": { "type": "object", "properties": { "company": { - "$ref": "#/definitions/internal_http_handler.registerCompanyTarget" + "$ref": "#/definitions/handler.registerCompanyTarget" }, "company_id": { "type": "string" @@ -2151,26 +2971,44 @@ const docTemplate = `{ } } }, - "internal_http_handler.registerCompanyRequest": { + "handler.registerCompanyRequest": { "type": "object", "properties": { + "category": { + "type": "string" + }, + "city": { + "type": "string" + }, "cnpj": { "type": "string" }, "corporate_name": { "type": "string" }, + "latitude": { + "type": "number" + }, "license_number": { "type": "string" }, - "role": { + "longitude": { + "type": "number" + }, + "state": { "type": "string" } } }, - "internal_http_handler.registerCompanyTarget": { + "handler.registerCompanyTarget": { "type": "object", "properties": { + "category": { + "type": "string" + }, + "city": { + "type": "string" + }, "cnpj": { "type": "string" }, @@ -2180,15 +3018,21 @@ const docTemplate = `{ "id": { "type": "string" }, + "latitude": { + "type": "number" + }, "license_number": { "type": "string" }, - "role": { + "longitude": { + "type": "number" + }, + "state": { "type": "string" } } }, - "internal_http_handler.registerProductRequest": { + "handler.registerProductRequest": { "type": "object", "properties": { "batch": { @@ -2214,9 +3058,106 @@ const docTemplate = `{ } } }, - "internal_http_handler.updateCompanyRequest": { + "handler.resetPasswordRequest": { "type": "object", "properties": { + "password": { + "type": "string" + }, + "token": { + "type": "string" + } + } + }, + "handler.resetTokenResponse": { + "type": "object", + "properties": { + "expires_at": { + "type": "string" + }, + "message": { + "type": "string" + }, + "reset_token": { + "type": "string" + } + } + }, + "handler.shippingCalculateRequest": { + "type": "object", + "properties": { + "address_id": { + "type": "string" + }, + "buyer_latitude": { + "type": "number" + }, + "buyer_longitude": { + "type": "number" + }, + "cart_total_cents": { + "type": "integer" + }, + "postal_code": { + "type": "string" + }, + "vendor_id": { + "type": "string" + } + } + }, + "handler.shippingMethodRequest": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + }, + "free_shipping_threshold_cents": { + "type": "integer" + }, + "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": { + "type": "string" + } + } + }, + "handler.shippingSettingsRequest": { + "type": "object", + "properties": { + "methods": { + "type": "array", + "items": { + "$ref": "#/definitions/handler.shippingMethodRequest" + } + } + } + }, + "handler.updateCompanyRequest": { + "type": "object", + "properties": { + "category": { + "type": "string" + }, + "city": { + "type": "string" + }, "cnpj": { "type": "string" }, @@ -2226,15 +3167,21 @@ const docTemplate = `{ "is_verified": { "type": "boolean" }, + "latitude": { + "type": "number" + }, "license_number": { "type": "string" }, - "role": { + "longitude": { + "type": "number" + }, + "state": { "type": "string" } } }, - "internal_http_handler.updateProductRequest": { + "handler.updateProductRequest": { "type": "object", "properties": { "batch": { @@ -2260,7 +3207,7 @@ const docTemplate = `{ } } }, - "internal_http_handler.updateStatusRequest": { + "handler.updateStatusRequest": { "type": "object", "properties": { "status": { @@ -2268,7 +3215,7 @@ const docTemplate = `{ } } }, - "internal_http_handler.updateUserRequest": { + "handler.updateUserRequest": { "type": "object", "properties": { "company_id": { @@ -2287,6 +3234,14 @@ const docTemplate = `{ "type": "string" } } + }, + "handler.verifyEmailRequest": { + "type": "object", + "properties": { + "token": { + "type": "string" + } + } } }, "securityDefinitions": { diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index a6a4a43..d220144 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -34,7 +34,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_http_handler.loginRequest" + "$ref": "#/definitions/handler.loginRequest" } } ], @@ -42,7 +42,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal_http_handler.authResponse" + "$ref": "#/definitions/handler.authResponse" } }, "400": { @@ -66,6 +66,159 @@ } } }, + "/api/v1/auth/logout": { + "post": { + "description": "Endpoint para logout (invalidação client-side).", + "tags": [ + "Autenticação" + ], + "summary": "Logout", + "responses": { + "204": { + "description": "No Content", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/auth/password/forgot": { + "post": { + "description": "Gera um token de redefinição de senha.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Autenticação" + ], + "summary": "Solicitar redefinição de senha", + "parameters": [ + { + "description": "Email do usuário", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.forgotPasswordRequest" + } + } + ], + "responses": { + "202": { + "description": "Accepted", + "schema": { + "$ref": "#/definitions/handler.resetTokenResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/auth/password/reset": { + "post": { + "description": "Atualiza a senha usando o token de redefinição.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Autenticação" + ], + "summary": "Redefinir senha", + "parameters": [ + { + "description": "Token e nova senha", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.resetPasswordRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.messageResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/auth/refresh-token": { + "post": { + "description": "Gera um novo JWT a partir de um token válido.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Autenticação" + ], + "summary": "Atualizar token", + "parameters": [ + { + "type": "string", + "description": "Bearer token", + "name": "Authorization", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.authResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/api/v1/auth/register": { "post": { "description": "Cria um usuário e opcionalmente uma empresa, retornando token JWT.", @@ -86,7 +239,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_http_handler.registerAuthRequest" + "$ref": "#/definitions/handler.registerAuthRequest" } } ], @@ -94,7 +247,7 @@ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/internal_http_handler.authResponse" + "$ref": "#/definitions/handler.authResponse" } }, "400": { @@ -118,6 +271,162 @@ } } }, + "/api/v1/auth/register/customer": { + "post": { + "description": "Cria um usuário do tipo cliente e opcionalmente uma empresa, retornando token JWT.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Autenticação" + ], + "summary": "Cadastro de cliente", + "parameters": [ + { + "description": "Dados do usuário e empresa", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.registerAuthRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/handler.authResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/auth/register/tenant": { + "post": { + "description": "Cria um usuário do tipo tenant e opcionalmente uma empresa, retornando token JWT.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Autenticação" + ], + "summary": "Cadastro de tenant", + "parameters": [ + { + "description": "Dados do usuário e empresa", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.registerAuthRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/handler.authResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/auth/verify-email": { + "post": { + "description": "Marca o email como verificado usando um token JWT.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Autenticação" + ], + "summary": "Verificar email", + "parameters": [ + { + "description": "Token de verificação", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.verifyEmailRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.messageResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/api/v1/cart": { "get": { "security": [ @@ -136,7 +445,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.CartSummary" + "$ref": "#/definitions/domain.CartSummary" } } } @@ -164,7 +473,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_http_handler.addCartItemRequest" + "$ref": "#/definitions/handler.addCartItemRequest" } } ], @@ -172,7 +481,7 @@ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.CartSummary" + "$ref": "#/definitions/domain.CartSummary" } }, "400": { @@ -211,7 +520,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.CartSummary" + "$ref": "#/definitions/domain.CartSummary" } }, "400": { @@ -241,7 +550,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.Company" + "$ref": "#/definitions/domain.Company" } } } @@ -266,7 +575,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_http_handler.registerCompanyRequest" + "$ref": "#/definitions/handler.registerCompanyRequest" } } ], @@ -274,7 +583,7 @@ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.Company" + "$ref": "#/definitions/domain.Company" } } } @@ -298,7 +607,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.Company" + "$ref": "#/definitions/domain.Company" } } } @@ -326,7 +635,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.Company" + "$ref": "#/definitions/domain.Company" } }, "404": { @@ -403,7 +712,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_http_handler.updateCompanyRequest" + "$ref": "#/definitions/handler.updateCompanyRequest" } } ], @@ -411,7 +720,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.Company" + "$ref": "#/definitions/domain.Company" } }, "400": { @@ -457,7 +766,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.CompanyRating" + "$ref": "#/definitions/domain.CompanyRating" } } } @@ -487,7 +796,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.Company" + "$ref": "#/definitions/domain.Company" } } } @@ -511,7 +820,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.AdminDashboard" + "$ref": "#/definitions/domain.AdminDashboard" } } } @@ -543,7 +852,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.SellerDashboard" + "$ref": "#/definitions/domain.SellerDashboard" } } } @@ -577,7 +886,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.InventoryItem" + "$ref": "#/definitions/domain.InventoryItem" } } } @@ -608,7 +917,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_http_handler.inventoryAdjustRequest" + "$ref": "#/definitions/handler.inventoryAdjustRequest" } } ], @@ -616,7 +925,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.InventoryItem" + "$ref": "#/definitions/domain.InventoryItem" } }, "400": { @@ -631,6 +940,69 @@ } } }, + "/api/v1/marketplace/records": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Marketplace" + ], + "summary": "Busca avançada no marketplace", + "parameters": [ + { + "type": "string", + "description": "Busca textual", + "name": "query", + "in": "query" + }, + { + "type": "string", + "description": "Campo de ordenação (created_at|updated_at)", + "name": "sort_by", + "in": "query" + }, + { + "type": "string", + "description": "Direção (asc|desc)", + "name": "sort_order", + "in": "query" + }, + { + "type": "string", + "description": "Data mínima (RFC3339)", + "name": "created_after", + "in": "query" + }, + { + "type": "string", + "description": "Data máxima (RFC3339)", + "name": "created_before", + "in": "query" + }, + { + "type": "integer", + "description": "Página", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Itens por página", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.ProductPaginationResponse" + } + } + } + } + }, "/api/v1/orders": { "get": { "security": [ @@ -651,7 +1023,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.Order" + "$ref": "#/definitions/domain.Order" } } } @@ -675,7 +1047,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_http_handler.createOrderRequest" + "$ref": "#/definitions/handler.createOrderRequest" } } ], @@ -683,7 +1055,7 @@ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.Order" + "$ref": "#/definitions/domain.Order" } } } @@ -716,7 +1088,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.Order" + "$ref": "#/definitions/domain.Order" } } } @@ -792,7 +1164,7 @@ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.PaymentPreference" + "$ref": "#/definitions/domain.PaymentPreference" } } } @@ -829,7 +1201,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_http_handler.updateStatusRequest" + "$ref": "#/definitions/handler.updateStatusRequest" } } ], @@ -859,7 +1231,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.PaymentWebhookEvent" + "$ref": "#/definitions/domain.PaymentWebhookEvent" } } ], @@ -867,7 +1239,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.PaymentSplitResult" + "$ref": "#/definitions/domain.PaymentSplitResult" } } } @@ -888,7 +1260,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.Product" + "$ref": "#/definitions/domain.Product" } } } @@ -912,7 +1284,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_http_handler.registerProductRequest" + "$ref": "#/definitions/handler.registerProductRequest" } } ], @@ -920,7 +1292,79 @@ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.Product" + "$ref": "#/definitions/domain.Product" + } + } + } + } + }, + "/api/v1/products/search": { + "get": { + "description": "Retorna produtos ordenados por validade, com distância aproximada. Vendedor anônimo até checkout.", + "produces": [ + "application/json" + ], + "tags": [ + "Produtos" + ], + "summary": "Busca avançada de produtos com filtros e distância", + "parameters": [ + { + "type": "string", + "description": "Termo de busca", + "name": "search", + "in": "query" + }, + { + "type": "integer", + "description": "Preço mínimo em centavos", + "name": "min_price", + "in": "query" + }, + { + "type": "integer", + "description": "Preço máximo em centavos", + "name": "max_price", + "in": "query" + }, + { + "type": "number", + "description": "Distância máxima em km", + "name": "max_distance", + "in": "query" + }, + { + "type": "number", + "description": "Latitude do comprador", + "name": "lat", + "in": "query", + "required": true + }, + { + "type": "number", + "description": "Longitude do comprador", + "name": "lng", + "in": "query", + "required": true + }, + { + "type": "integer", + "description": "Página", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Itens por página", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.ProductSearchPage" } } } @@ -948,7 +1392,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.Product" + "$ref": "#/definitions/domain.Product" } }, "404": { @@ -1025,7 +1469,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_http_handler.updateProductRequest" + "$ref": "#/definitions/handler.updateProductRequest" } } ], @@ -1033,7 +1477,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.Product" + "$ref": "#/definitions/domain.Product" } }, "400": { @@ -1081,7 +1525,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_http_handler.createReviewRequest" + "$ref": "#/definitions/handler.createReviewRequest" } } ], @@ -1089,7 +1533,7 @@ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.Review" + "$ref": "#/definitions/domain.Review" } }, "400": { @@ -1128,7 +1572,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_http_handler.createShipmentRequest" + "$ref": "#/definitions/handler.createShipmentRequest" } } ], @@ -1136,7 +1580,7 @@ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.Shipment" + "$ref": "#/definitions/domain.Shipment" } } } @@ -1169,7 +1613,190 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.Shipment" + "$ref": "#/definitions/domain.Shipment" + } + } + } + } + }, + "/api/v1/shipping/calculate": { + "post": { + "description": "Calculates shipping or pickup options based on vendor config and buyer location.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Shipping" + ], + "summary": "Calculate shipping options", + "parameters": [ + { + "description": "Calculation inputs", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.shippingCalculateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.ShippingOption" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/shipping/settings/{vendor_id}": { + "get": { + "description": "Returns pickup and delivery settings for a vendor.", + "produces": [ + "application/json" + ], + "tags": [ + "Shipping" + ], + "summary": "Get vendor shipping settings", + "parameters": [ + { + "type": "string", + "description": "Vendor ID", + "name": "vendor_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.ShippingMethod" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "put": { + "description": "Stores pickup and delivery settings for a vendor.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Shipping" + ], + "summary": "Update vendor shipping settings", + "parameters": [ + { + "type": "string", + "description": "Vendor ID", + "name": "vendor_id", + "in": "path", + "required": true + }, + { + "description": "Shipping settings", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.shippingSettingsRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.ShippingMethod" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } } } } @@ -1213,7 +1840,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.UserPage" + "$ref": "#/definitions/domain.UserPage" } }, "400": { @@ -1259,7 +1886,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_http_handler.createUserRequest" + "$ref": "#/definitions/handler.createUserRequest" } } ], @@ -1267,7 +1894,7 @@ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.User" + "$ref": "#/definitions/domain.User" } }, "400": { @@ -1327,7 +1954,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.User" + "$ref": "#/definitions/domain.User" } }, "400": { @@ -1389,7 +2016,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_http_handler.updateUserRequest" + "$ref": "#/definitions/handler.updateUserRequest" } } ], @@ -1397,7 +2024,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.User" + "$ref": "#/definitions/domain.User" } }, "400": { @@ -1505,7 +2132,7 @@ } }, "definitions": { - "github_com_saveinmed_backend-go_internal_domain.AdminDashboard": { + "domain.AdminDashboard": { "type": "object", "properties": { "gmv_cents": { @@ -1519,7 +2146,7 @@ } } }, - "github_com_saveinmed_backend-go_internal_domain.CartItem": { + "domain.CartItem": { "type": "object", "properties": { "batch": { @@ -1554,7 +2181,7 @@ } } }, - "github_com_saveinmed_backend-go_internal_domain.CartSummary": { + "domain.CartSummary": { "type": "object", "properties": { "discount_cents": { @@ -1566,7 +2193,7 @@ "items": { "type": "array", "items": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.CartItem" + "$ref": "#/definitions/domain.CartItem" } }, "subtotal_cents": { @@ -1577,9 +2204,16 @@ } } }, - "github_com_saveinmed_backend-go_internal_domain.Company": { + "domain.Company": { "type": "object", "properties": { + "category": { + "description": "farmacia, distribuidora", + "type": "string" + }, + "city": { + "type": "string" + }, "cnpj": { "type": "string" }, @@ -1595,11 +2229,17 @@ "is_verified": { "type": "boolean" }, + "latitude": { + "description": "Location", + "type": "number" + }, "license_number": { "type": "string" }, - "role": { - "description": "pharmacy, distributor, admin", + "longitude": { + "type": "number" + }, + "state": { "type": "string" }, "updated_at": { @@ -1607,7 +2247,7 @@ } } }, - "github_com_saveinmed_backend-go_internal_domain.CompanyRating": { + "domain.CompanyRating": { "type": "object", "properties": { "average_score": { @@ -1621,7 +2261,7 @@ } } }, - "github_com_saveinmed_backend-go_internal_domain.InventoryItem": { + "domain.InventoryItem": { "type": "object", "properties": { "batch": { @@ -1650,7 +2290,7 @@ } } }, - "github_com_saveinmed_backend-go_internal_domain.Order": { + "domain.Order": { "type": "object", "properties": { "buyer_id": { @@ -1665,17 +2305,17 @@ "items": { "type": "array", "items": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.OrderItem" + "$ref": "#/definitions/domain.OrderItem" } }, "seller_id": { "type": "string" }, "shipping": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.ShippingAddress" + "$ref": "#/definitions/domain.ShippingAddress" }, "status": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.OrderStatus" + "$ref": "#/definitions/domain.OrderStatus" }, "total_cents": { "type": "integer" @@ -1685,7 +2325,7 @@ } } }, - "github_com_saveinmed_backend-go_internal_domain.OrderItem": { + "domain.OrderItem": { "type": "object", "properties": { "batch": { @@ -1711,7 +2351,7 @@ } } }, - "github_com_saveinmed_backend-go_internal_domain.OrderStatus": { + "domain.OrderStatus": { "type": "string", "enum": [ "Pendente", @@ -1726,7 +2366,7 @@ "OrderStatusDelivered" ] }, - "github_com_saveinmed_backend-go_internal_domain.PaymentPreference": { + "domain.PaymentPreference": { "type": "object", "properties": { "commission_pct": { @@ -1749,7 +2389,7 @@ } } }, - "github_com_saveinmed_backend-go_internal_domain.PaymentSplitResult": { + "domain.PaymentSplitResult": { "type": "object", "properties": { "marketplace_fee": { @@ -1772,7 +2412,7 @@ } } }, - "github_com_saveinmed_backend-go_internal_domain.PaymentWebhookEvent": { + "domain.PaymentWebhookEvent": { "type": "object", "properties": { "marketplace_fee": { @@ -1795,7 +2435,7 @@ } } }, - "github_com_saveinmed_backend-go_internal_domain.Product": { + "domain.Product": { "type": "object", "properties": { "batch": { @@ -1830,7 +2470,91 @@ } } }, - "github_com_saveinmed_backend-go_internal_domain.Review": { + "domain.ProductPaginationResponse": { + "type": "object", + "properties": { + "current_page": { + "type": "integer" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.Product" + } + }, + "total_count": { + "type": "integer" + }, + "total_pages": { + "type": "integer" + } + } + }, + "domain.ProductSearchPage": { + "type": "object", + "properties": { + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "products": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.ProductWithDistance" + } + }, + "total": { + "type": "integer" + } + } + }, + "domain.ProductWithDistance": { + "type": "object", + "properties": { + "batch": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "distance_km": { + "type": "number" + }, + "expires_at": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "price_cents": { + "type": "integer" + }, + "seller_id": { + "type": "string" + }, + "stock": { + "type": "integer" + }, + "tenant_city": { + "type": "string" + }, + "tenant_state": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "domain.Review": { "type": "object", "properties": { "buyer_id": { @@ -1856,13 +2580,13 @@ } } }, - "github_com_saveinmed_backend-go_internal_domain.SellerDashboard": { + "domain.SellerDashboard": { "type": "object", "properties": { "low_stock_alerts": { "type": "array", "items": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.Product" + "$ref": "#/definitions/domain.Product" } }, "orders_count": { @@ -1874,7 +2598,7 @@ "top_products": { "type": "array", "items": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.TopProduct" + "$ref": "#/definitions/domain.TopProduct" } }, "total_sales_cents": { @@ -1882,7 +2606,7 @@ } } }, - "github_com_saveinmed_backend-go_internal_domain.Shipment": { + "domain.Shipment": { "type": "object", "properties": { "carrier": { @@ -1911,7 +2635,7 @@ } } }, - "github_com_saveinmed_backend-go_internal_domain.ShippingAddress": { + "domain.ShippingAddress": { "type": "object", "properties": { "city": { @@ -1943,7 +2667,84 @@ } } }, - "github_com_saveinmed_backend-go_internal_domain.TopProduct": { + "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": { + "description": { + "type": "string" + }, + "distance_km": { + "type": "number" + }, + "estimated_minutes": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "value_cents": { + "type": "integer" + } + } + }, + "domain.TopProduct": { "type": "object", "properties": { "name": { @@ -1960,7 +2761,7 @@ } } }, - "github_com_saveinmed_backend-go_internal_domain.User": { + "domain.User": { "type": "object", "properties": { "company_id": { @@ -1972,6 +2773,9 @@ "email": { "type": "string" }, + "email_verified": { + "type": "boolean" + }, "id": { "type": "string" }, @@ -1986,7 +2790,7 @@ } } }, - "github_com_saveinmed_backend-go_internal_domain.UserPage": { + "domain.UserPage": { "type": "object", "properties": { "page": { @@ -2001,12 +2805,12 @@ "users": { "type": "array", "items": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.User" + "$ref": "#/definitions/domain.User" } } } }, - "internal_http_handler.addCartItemRequest": { + "handler.addCartItemRequest": { "type": "object", "properties": { "product_id": { @@ -2017,7 +2821,7 @@ } } }, - "internal_http_handler.authResponse": { + "handler.authResponse": { "type": "object", "properties": { "expires_at": { @@ -2028,7 +2832,7 @@ } } }, - "internal_http_handler.createOrderRequest": { + "handler.createOrderRequest": { "type": "object", "properties": { "buyer_id": { @@ -2037,18 +2841,18 @@ "items": { "type": "array", "items": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.OrderItem" + "$ref": "#/definitions/domain.OrderItem" } }, "seller_id": { "type": "string" }, "shipping": { - "$ref": "#/definitions/github_com_saveinmed_backend-go_internal_domain.ShippingAddress" + "$ref": "#/definitions/domain.ShippingAddress" } } }, - "internal_http_handler.createReviewRequest": { + "handler.createReviewRequest": { "type": "object", "properties": { "comment": { @@ -2062,7 +2866,7 @@ } } }, - "internal_http_handler.createShipmentRequest": { + "handler.createShipmentRequest": { "type": "object", "properties": { "carrier": { @@ -2079,7 +2883,7 @@ } } }, - "internal_http_handler.createUserRequest": { + "handler.createUserRequest": { "type": "object", "properties": { "company_id": { @@ -2099,7 +2903,15 @@ } } }, - "internal_http_handler.inventoryAdjustRequest": { + "handler.forgotPasswordRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + } + } + }, + "handler.inventoryAdjustRequest": { "type": "object", "properties": { "delta": { @@ -2113,7 +2925,7 @@ } } }, - "internal_http_handler.loginRequest": { + "handler.loginRequest": { "type": "object", "properties": { "email": { @@ -2124,11 +2936,19 @@ } } }, - "internal_http_handler.registerAuthRequest": { + "handler.messageResponse": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + }, + "handler.registerAuthRequest": { "type": "object", "properties": { "company": { - "$ref": "#/definitions/internal_http_handler.registerCompanyTarget" + "$ref": "#/definitions/handler.registerCompanyTarget" }, "company_id": { "type": "string" @@ -2147,26 +2967,44 @@ } } }, - "internal_http_handler.registerCompanyRequest": { + "handler.registerCompanyRequest": { "type": "object", "properties": { + "category": { + "type": "string" + }, + "city": { + "type": "string" + }, "cnpj": { "type": "string" }, "corporate_name": { "type": "string" }, + "latitude": { + "type": "number" + }, "license_number": { "type": "string" }, - "role": { + "longitude": { + "type": "number" + }, + "state": { "type": "string" } } }, - "internal_http_handler.registerCompanyTarget": { + "handler.registerCompanyTarget": { "type": "object", "properties": { + "category": { + "type": "string" + }, + "city": { + "type": "string" + }, "cnpj": { "type": "string" }, @@ -2176,15 +3014,21 @@ "id": { "type": "string" }, + "latitude": { + "type": "number" + }, "license_number": { "type": "string" }, - "role": { + "longitude": { + "type": "number" + }, + "state": { "type": "string" } } }, - "internal_http_handler.registerProductRequest": { + "handler.registerProductRequest": { "type": "object", "properties": { "batch": { @@ -2210,9 +3054,106 @@ } } }, - "internal_http_handler.updateCompanyRequest": { + "handler.resetPasswordRequest": { "type": "object", "properties": { + "password": { + "type": "string" + }, + "token": { + "type": "string" + } + } + }, + "handler.resetTokenResponse": { + "type": "object", + "properties": { + "expires_at": { + "type": "string" + }, + "message": { + "type": "string" + }, + "reset_token": { + "type": "string" + } + } + }, + "handler.shippingCalculateRequest": { + "type": "object", + "properties": { + "address_id": { + "type": "string" + }, + "buyer_latitude": { + "type": "number" + }, + "buyer_longitude": { + "type": "number" + }, + "cart_total_cents": { + "type": "integer" + }, + "postal_code": { + "type": "string" + }, + "vendor_id": { + "type": "string" + } + } + }, + "handler.shippingMethodRequest": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + }, + "free_shipping_threshold_cents": { + "type": "integer" + }, + "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": { + "type": "string" + } + } + }, + "handler.shippingSettingsRequest": { + "type": "object", + "properties": { + "methods": { + "type": "array", + "items": { + "$ref": "#/definitions/handler.shippingMethodRequest" + } + } + } + }, + "handler.updateCompanyRequest": { + "type": "object", + "properties": { + "category": { + "type": "string" + }, + "city": { + "type": "string" + }, "cnpj": { "type": "string" }, @@ -2222,15 +3163,21 @@ "is_verified": { "type": "boolean" }, + "latitude": { + "type": "number" + }, "license_number": { "type": "string" }, - "role": { + "longitude": { + "type": "number" + }, + "state": { "type": "string" } } }, - "internal_http_handler.updateProductRequest": { + "handler.updateProductRequest": { "type": "object", "properties": { "batch": { @@ -2256,7 +3203,7 @@ } } }, - "internal_http_handler.updateStatusRequest": { + "handler.updateStatusRequest": { "type": "object", "properties": { "status": { @@ -2264,7 +3211,7 @@ } } }, - "internal_http_handler.updateUserRequest": { + "handler.updateUserRequest": { "type": "object", "properties": { "company_id": { @@ -2283,6 +3230,14 @@ "type": "string" } } + }, + "handler.verifyEmailRequest": { + "type": "object", + "properties": { + "token": { + "type": "string" + } + } } }, "securityDefinitions": { diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index 3dcc215..8c40051 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -1,6 +1,6 @@ basePath: / definitions: - github_com_saveinmed_backend-go_internal_domain.AdminDashboard: + domain.AdminDashboard: properties: gmv_cents: type: integer @@ -9,7 +9,7 @@ definitions: window_start_at: type: string type: object - github_com_saveinmed_backend-go_internal_domain.CartItem: + domain.CartItem: properties: batch: type: string @@ -32,7 +32,7 @@ definitions: updated_at: type: string type: object - github_com_saveinmed_backend-go_internal_domain.CartSummary: + domain.CartSummary: properties: discount_cents: type: integer @@ -40,15 +40,20 @@ definitions: type: string items: items: - $ref: '#/definitions/github_com_saveinmed_backend-go_internal_domain.CartItem' + $ref: '#/definitions/domain.CartItem' type: array subtotal_cents: type: integer total_cents: type: integer type: object - github_com_saveinmed_backend-go_internal_domain.Company: + domain.Company: properties: + category: + description: farmacia, distribuidora + type: string + city: + type: string cnpj: type: string corporate_name: @@ -59,15 +64,19 @@ definitions: type: string is_verified: type: boolean + latitude: + description: Location + type: number license_number: type: string - role: - description: pharmacy, distributor, admin + longitude: + type: number + state: type: string updated_at: type: string type: object - github_com_saveinmed_backend-go_internal_domain.CompanyRating: + domain.CompanyRating: properties: average_score: type: number @@ -76,7 +85,7 @@ definitions: total_reviews: type: integer type: object - github_com_saveinmed_backend-go_internal_domain.InventoryItem: + domain.InventoryItem: properties: batch: type: string @@ -95,7 +104,7 @@ definitions: updated_at: type: string type: object - github_com_saveinmed_backend-go_internal_domain.Order: + domain.Order: properties: buyer_id: type: string @@ -105,20 +114,20 @@ definitions: type: string items: items: - $ref: '#/definitions/github_com_saveinmed_backend-go_internal_domain.OrderItem' + $ref: '#/definitions/domain.OrderItem' type: array seller_id: type: string shipping: - $ref: '#/definitions/github_com_saveinmed_backend-go_internal_domain.ShippingAddress' + $ref: '#/definitions/domain.ShippingAddress' status: - $ref: '#/definitions/github_com_saveinmed_backend-go_internal_domain.OrderStatus' + $ref: '#/definitions/domain.OrderStatus' total_cents: type: integer updated_at: type: string type: object - github_com_saveinmed_backend-go_internal_domain.OrderItem: + domain.OrderItem: properties: batch: type: string @@ -135,7 +144,7 @@ definitions: unit_cents: type: integer type: object - github_com_saveinmed_backend-go_internal_domain.OrderStatus: + domain.OrderStatus: enum: - Pendente - Pago @@ -147,7 +156,7 @@ definitions: - OrderStatusPaid - OrderStatusInvoiced - OrderStatusDelivered - github_com_saveinmed_backend-go_internal_domain.PaymentPreference: + domain.PaymentPreference: properties: commission_pct: type: number @@ -162,7 +171,7 @@ definitions: seller_receivable: type: integer type: object - github_com_saveinmed_backend-go_internal_domain.PaymentSplitResult: + domain.PaymentSplitResult: properties: marketplace_fee: type: integer @@ -177,7 +186,7 @@ definitions: total_paid_amount: type: integer type: object - github_com_saveinmed_backend-go_internal_domain.PaymentWebhookEvent: + domain.PaymentWebhookEvent: properties: marketplace_fee: type: integer @@ -192,7 +201,7 @@ definitions: total_paid_amount: type: integer type: object - github_com_saveinmed_backend-go_internal_domain.Product: + domain.Product: properties: batch: type: string @@ -215,7 +224,62 @@ definitions: updated_at: type: string type: object - github_com_saveinmed_backend-go_internal_domain.Review: + domain.ProductPaginationResponse: + properties: + current_page: + type: integer + items: + items: + $ref: '#/definitions/domain.Product' + type: array + total_count: + type: integer + total_pages: + type: integer + type: object + domain.ProductSearchPage: + properties: + page: + type: integer + page_size: + type: integer + products: + items: + $ref: '#/definitions/domain.ProductWithDistance' + type: array + total: + type: integer + type: object + domain.ProductWithDistance: + properties: + batch: + type: string + created_at: + type: string + description: + type: string + distance_km: + type: number + expires_at: + type: string + id: + type: string + name: + type: string + price_cents: + type: integer + seller_id: + type: string + stock: + type: integer + tenant_city: + type: string + tenant_state: + type: string + updated_at: + type: string + type: object + domain.Review: properties: buyer_id: type: string @@ -232,11 +296,11 @@ definitions: seller_id: type: string type: object - github_com_saveinmed_backend-go_internal_domain.SellerDashboard: + domain.SellerDashboard: properties: low_stock_alerts: items: - $ref: '#/definitions/github_com_saveinmed_backend-go_internal_domain.Product' + $ref: '#/definitions/domain.Product' type: array orders_count: type: integer @@ -244,12 +308,12 @@ definitions: type: string top_products: items: - $ref: '#/definitions/github_com_saveinmed_backend-go_internal_domain.TopProduct' + $ref: '#/definitions/domain.TopProduct' type: array total_sales_cents: type: integer type: object - github_com_saveinmed_backend-go_internal_domain.Shipment: + domain.Shipment: properties: carrier: type: string @@ -268,7 +332,7 @@ definitions: updated_at: type: string type: object - github_com_saveinmed_backend-go_internal_domain.ShippingAddress: + domain.ShippingAddress: properties: city: type: string @@ -289,7 +353,59 @@ definitions: zip_code: type: string type: object - github_com_saveinmed_backend-go_internal_domain.TopProduct: + 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: + type: string + distance_km: + type: number + estimated_minutes: + type: integer + type: + type: string + value_cents: + type: integer + type: object + domain.TopProduct: properties: name: type: string @@ -300,7 +416,7 @@ definitions: total_quantity: type: integer type: object - github_com_saveinmed_backend-go_internal_domain.User: + domain.User: properties: company_id: type: string @@ -308,6 +424,8 @@ definitions: type: string email: type: string + email_verified: + type: boolean id: type: string name: @@ -317,7 +435,7 @@ definitions: updated_at: type: string type: object - github_com_saveinmed_backend-go_internal_domain.UserPage: + domain.UserPage: properties: page: type: integer @@ -327,37 +445,37 @@ definitions: type: integer users: items: - $ref: '#/definitions/github_com_saveinmed_backend-go_internal_domain.User' + $ref: '#/definitions/domain.User' type: array type: object - internal_http_handler.addCartItemRequest: + handler.addCartItemRequest: properties: product_id: type: string quantity: type: integer type: object - internal_http_handler.authResponse: + handler.authResponse: properties: expires_at: type: string token: type: string type: object - internal_http_handler.createOrderRequest: + handler.createOrderRequest: properties: buyer_id: type: string items: items: - $ref: '#/definitions/github_com_saveinmed_backend-go_internal_domain.OrderItem' + $ref: '#/definitions/domain.OrderItem' type: array seller_id: type: string shipping: - $ref: '#/definitions/github_com_saveinmed_backend-go_internal_domain.ShippingAddress' + $ref: '#/definitions/domain.ShippingAddress' type: object - internal_http_handler.createReviewRequest: + handler.createReviewRequest: properties: comment: type: string @@ -366,7 +484,7 @@ definitions: rating: type: integer type: object - internal_http_handler.createShipmentRequest: + handler.createShipmentRequest: properties: carrier: type: string @@ -377,7 +495,7 @@ definitions: tracking_code: type: string type: object - internal_http_handler.createUserRequest: + handler.createUserRequest: properties: company_id: type: string @@ -390,7 +508,12 @@ definitions: role: type: string type: object - internal_http_handler.inventoryAdjustRequest: + handler.forgotPasswordRequest: + properties: + email: + type: string + type: object + handler.inventoryAdjustRequest: properties: delta: type: integer @@ -399,17 +522,22 @@ definitions: reason: type: string type: object - internal_http_handler.loginRequest: + handler.loginRequest: properties: email: type: string password: type: string type: object - internal_http_handler.registerAuthRequest: + handler.messageResponse: + properties: + message: + type: string + type: object + handler.registerAuthRequest: properties: company: - $ref: '#/definitions/internal_http_handler.registerCompanyTarget' + $ref: '#/definitions/handler.registerCompanyTarget' company_id: type: string email: @@ -421,31 +549,47 @@ definitions: role: type: string type: object - internal_http_handler.registerCompanyRequest: + handler.registerCompanyRequest: properties: + category: + type: string + city: + type: string cnpj: type: string corporate_name: type: string + latitude: + type: number license_number: type: string - role: + longitude: + type: number + state: type: string type: object - internal_http_handler.registerCompanyTarget: + handler.registerCompanyTarget: properties: + category: + type: string + city: + type: string cnpj: type: string corporate_name: type: string id: type: string + latitude: + type: number license_number: type: string - role: + longitude: + type: number + state: type: string type: object - internal_http_handler.registerProductRequest: + handler.registerProductRequest: properties: batch: type: string @@ -462,20 +606,87 @@ definitions: stock: type: integer type: object - internal_http_handler.updateCompanyRequest: + handler.resetPasswordRequest: properties: + password: + type: string + token: + type: string + type: object + handler.resetTokenResponse: + properties: + expires_at: + type: string + message: + type: string + reset_token: + type: string + type: object + handler.shippingCalculateRequest: + properties: + address_id: + type: string + buyer_latitude: + type: number + buyer_longitude: + type: number + cart_total_cents: + type: integer + postal_code: + type: string + vendor_id: + type: string + type: object + handler.shippingMethodRequest: + properties: + active: + type: boolean + free_shipping_threshold_cents: + type: integer + 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: + type: string + type: object + handler.shippingSettingsRequest: + properties: + methods: + items: + $ref: '#/definitions/handler.shippingMethodRequest' + type: array + type: object + handler.updateCompanyRequest: + properties: + category: + type: string + city: + type: string cnpj: type: string corporate_name: type: string is_verified: type: boolean + latitude: + type: number license_number: type: string - role: + longitude: + type: number + state: type: string type: object - internal_http_handler.updateProductRequest: + handler.updateProductRequest: properties: batch: type: string @@ -492,12 +703,12 @@ definitions: stock: type: integer type: object - internal_http_handler.updateStatusRequest: + handler.updateStatusRequest: properties: status: type: string type: object - internal_http_handler.updateUserRequest: + handler.updateUserRequest: properties: company_id: type: string @@ -510,6 +721,11 @@ definitions: role: type: string type: object + handler.verifyEmailRequest: + properties: + token: + type: string + type: object info: contact: email: devops@saveinmed.com @@ -530,14 +746,14 @@ paths: name: payload required: true schema: - $ref: '#/definitions/internal_http_handler.loginRequest' + $ref: '#/definitions/handler.loginRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/internal_http_handler.authResponse' + $ref: '#/definitions/handler.authResponse' "400": description: Bad Request schema: @@ -553,6 +769,106 @@ paths: summary: Login tags: - Autenticação + /api/v1/auth/logout: + post: + description: Endpoint para logout (invalidação client-side). + responses: + "204": + description: No Content + schema: + type: string + summary: Logout + tags: + - Autenticação + /api/v1/auth/password/forgot: + post: + consumes: + - application/json + description: Gera um token de redefinição de senha. + parameters: + - description: Email do usuário + in: body + name: payload + required: true + schema: + $ref: '#/definitions/handler.forgotPasswordRequest' + produces: + - application/json + responses: + "202": + description: Accepted + schema: + $ref: '#/definitions/handler.resetTokenResponse' + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + summary: Solicitar redefinição de senha + tags: + - Autenticação + /api/v1/auth/password/reset: + post: + consumes: + - application/json + description: Atualiza a senha usando o token de redefinição. + parameters: + - description: Token e nova senha + in: body + name: payload + required: true + schema: + $ref: '#/definitions/handler.resetPasswordRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.messageResponse' + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "401": + description: Unauthorized + schema: + additionalProperties: + type: string + type: object + summary: Redefinir senha + tags: + - Autenticação + /api/v1/auth/refresh-token: + post: + consumes: + - application/json + description: Gera um novo JWT a partir de um token válido. + parameters: + - description: Bearer token + in: header + name: Authorization + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.authResponse' + "401": + description: Unauthorized + schema: + additionalProperties: + type: string + type: object + summary: Atualizar token + tags: + - Autenticação /api/v1/auth/register: post: consumes: @@ -564,14 +880,14 @@ paths: name: payload required: true schema: - $ref: '#/definitions/internal_http_handler.registerAuthRequest' + $ref: '#/definitions/handler.registerAuthRequest' produces: - application/json responses: "201": description: Created schema: - $ref: '#/definitions/internal_http_handler.authResponse' + $ref: '#/definitions/handler.authResponse' "400": description: Bad Request schema: @@ -587,6 +903,110 @@ paths: summary: Cadastro de usuário tags: - Autenticação + /api/v1/auth/register/customer: + post: + consumes: + - application/json + description: Cria um usuário do tipo cliente e opcionalmente uma empresa, retornando + token JWT. + parameters: + - description: Dados do usuário e empresa + in: body + name: payload + required: true + schema: + $ref: '#/definitions/handler.registerAuthRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/handler.authResponse' + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + summary: Cadastro de cliente + tags: + - Autenticação + /api/v1/auth/register/tenant: + post: + consumes: + - application/json + description: Cria um usuário do tipo tenant e opcionalmente uma empresa, retornando + token JWT. + parameters: + - description: Dados do usuário e empresa + in: body + name: payload + required: true + schema: + $ref: '#/definitions/handler.registerAuthRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/handler.authResponse' + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + summary: Cadastro de tenant + tags: + - Autenticação + /api/v1/auth/verify-email: + post: + consumes: + - application/json + description: Marca o email como verificado usando um token JWT. + parameters: + - description: Token de verificação + in: body + name: payload + required: true + schema: + $ref: '#/definitions/handler.verifyEmailRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.messageResponse' + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "401": + description: Unauthorized + schema: + additionalProperties: + type: string + type: object + summary: Verificar email + tags: + - Autenticação /api/v1/cart: get: produces: @@ -595,7 +1015,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/github_com_saveinmed_backend-go_internal_domain.CartSummary' + $ref: '#/definitions/domain.CartSummary' security: - BearerAuth: [] summary: Obter carrinho @@ -610,14 +1030,14 @@ paths: name: payload required: true schema: - $ref: '#/definitions/internal_http_handler.addCartItemRequest' + $ref: '#/definitions/handler.addCartItemRequest' produces: - application/json responses: "201": description: Created schema: - $ref: '#/definitions/github_com_saveinmed_backend-go_internal_domain.CartSummary' + $ref: '#/definitions/domain.CartSummary' "400": description: Bad Request schema: @@ -641,7 +1061,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/github_com_saveinmed_backend-go_internal_domain.CartSummary' + $ref: '#/definitions/domain.CartSummary' "400": description: Bad Request schema: @@ -662,7 +1082,7 @@ paths: description: OK schema: items: - $ref: '#/definitions/github_com_saveinmed_backend-go_internal_domain.Company' + $ref: '#/definitions/domain.Company' type: array summary: Lista empresas tags: @@ -678,14 +1098,14 @@ paths: name: company required: true schema: - $ref: '#/definitions/internal_http_handler.registerCompanyRequest' + $ref: '#/definitions/handler.registerCompanyRequest' produces: - application/json responses: "201": description: Created schema: - $ref: '#/definitions/github_com_saveinmed_backend-go_internal_domain.Company' + $ref: '#/definitions/domain.Company' summary: Registro de empresas tags: - Empresas @@ -728,7 +1148,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/github_com_saveinmed_backend-go_internal_domain.Company' + $ref: '#/definitions/domain.Company' "404": description: Not Found schema: @@ -752,14 +1172,14 @@ paths: name: payload required: true schema: - $ref: '#/definitions/internal_http_handler.updateCompanyRequest' + $ref: '#/definitions/handler.updateCompanyRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/github_com_saveinmed_backend-go_internal_domain.Company' + $ref: '#/definitions/domain.Company' "400": description: Bad Request schema: @@ -789,7 +1209,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/github_com_saveinmed_backend-go_internal_domain.CompanyRating' + $ref: '#/definitions/domain.CompanyRating' summary: Obter avaliação da empresa tags: - Empresas @@ -805,7 +1225,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/github_com_saveinmed_backend-go_internal_domain.Company' + $ref: '#/definitions/domain.Company' security: - BearerAuth: [] summary: Verificar empresa @@ -819,7 +1239,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/github_com_saveinmed_backend-go_internal_domain.Company' + $ref: '#/definitions/domain.Company' security: - BearerAuth: [] summary: Obter minha empresa @@ -833,7 +1253,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/github_com_saveinmed_backend-go_internal_domain.AdminDashboard' + $ref: '#/definitions/domain.AdminDashboard' security: - BearerAuth: [] summary: Dashboard do administrador @@ -852,7 +1272,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/github_com_saveinmed_backend-go_internal_domain.SellerDashboard' + $ref: '#/definitions/domain.SellerDashboard' security: - BearerAuth: [] summary: Dashboard do vendedor @@ -872,7 +1292,7 @@ paths: description: OK schema: items: - $ref: '#/definitions/github_com_saveinmed_backend-go_internal_domain.InventoryItem' + $ref: '#/definitions/domain.InventoryItem' type: array security: - BearerAuth: [] @@ -889,14 +1309,14 @@ paths: name: payload required: true schema: - $ref: '#/definitions/internal_http_handler.inventoryAdjustRequest' + $ref: '#/definitions/handler.inventoryAdjustRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/github_com_saveinmed_backend-go_internal_domain.InventoryItem' + $ref: '#/definitions/domain.InventoryItem' "400": description: Bad Request schema: @@ -908,6 +1328,47 @@ paths: summary: Ajustar estoque tags: - Estoque + /api/v1/marketplace/records: + get: + parameters: + - description: Busca textual + in: query + name: query + type: string + - description: Campo de ordenação (created_at|updated_at) + in: query + name: sort_by + type: string + - description: Direção (asc|desc) + in: query + name: sort_order + type: string + - description: Data mínima (RFC3339) + in: query + name: created_after + type: string + - description: Data máxima (RFC3339) + in: query + name: created_before + type: string + - description: Página + in: query + name: page + type: integer + - description: Itens por página + in: query + name: page_size + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.ProductPaginationResponse' + summary: Busca avançada no marketplace + tags: + - Marketplace /api/v1/orders: get: produces: @@ -917,7 +1378,7 @@ paths: description: OK schema: items: - $ref: '#/definitions/github_com_saveinmed_backend-go_internal_domain.Order' + $ref: '#/definitions/domain.Order' type: array security: - BearerAuth: [] @@ -933,14 +1394,14 @@ paths: name: order required: true schema: - $ref: '#/definitions/internal_http_handler.createOrderRequest' + $ref: '#/definitions/handler.createOrderRequest' produces: - application/json responses: "201": description: Created schema: - $ref: '#/definitions/github_com_saveinmed_backend-go_internal_domain.Order' + $ref: '#/definitions/domain.Order' summary: Criação de pedido com split tags: - Pedidos @@ -985,7 +1446,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/github_com_saveinmed_backend-go_internal_domain.Order' + $ref: '#/definitions/domain.Order' security: - BearerAuth: [] summary: Consulta pedido @@ -1005,7 +1466,7 @@ paths: "201": description: Created schema: - $ref: '#/definitions/github_com_saveinmed_backend-go_internal_domain.PaymentPreference' + $ref: '#/definitions/domain.PaymentPreference' security: - BearerAuth: [] summary: Cria preferência de pagamento Mercado Pago com split nativo @@ -1026,7 +1487,7 @@ paths: name: status required: true schema: - $ref: '#/definitions/internal_http_handler.updateStatusRequest' + $ref: '#/definitions/handler.updateStatusRequest' produces: - application/json responses: @@ -1047,14 +1508,14 @@ paths: name: notification required: true schema: - $ref: '#/definitions/github_com_saveinmed_backend-go_internal_domain.PaymentWebhookEvent' + $ref: '#/definitions/domain.PaymentWebhookEvent' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/github_com_saveinmed_backend-go_internal_domain.PaymentSplitResult' + $ref: '#/definitions/domain.PaymentSplitResult' summary: Recebe notificações do Mercado Pago tags: - Pagamentos @@ -1067,7 +1528,7 @@ paths: description: OK schema: items: - $ref: '#/definitions/github_com_saveinmed_backend-go_internal_domain.Product' + $ref: '#/definitions/domain.Product' type: array summary: Lista catálogo com lote e validade tags: @@ -1081,14 +1542,14 @@ paths: name: product required: true schema: - $ref: '#/definitions/internal_http_handler.registerProductRequest' + $ref: '#/definitions/handler.registerProductRequest' produces: - application/json responses: "201": description: Created schema: - $ref: '#/definitions/github_com_saveinmed_backend-go_internal_domain.Product' + $ref: '#/definitions/domain.Product' summary: Cadastro de produto com rastreabilidade de lote tags: - Produtos @@ -1131,7 +1592,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/github_com_saveinmed_backend-go_internal_domain.Product' + $ref: '#/definitions/domain.Product' "404": description: Not Found schema: @@ -1155,14 +1616,14 @@ paths: name: payload required: true schema: - $ref: '#/definitions/internal_http_handler.updateProductRequest' + $ref: '#/definitions/handler.updateProductRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/github_com_saveinmed_backend-go_internal_domain.Product' + $ref: '#/definitions/domain.Product' "400": description: Bad Request schema: @@ -1178,6 +1639,55 @@ paths: summary: Atualizar produto tags: - Produtos + /api/v1/products/search: + get: + description: Retorna produtos ordenados por validade, com distância aproximada. + Vendedor anônimo até checkout. + parameters: + - description: Termo de busca + in: query + name: search + type: string + - description: Preço mínimo em centavos + in: query + name: min_price + type: integer + - description: Preço máximo em centavos + in: query + name: max_price + type: integer + - description: Distância máxima em km + in: query + name: max_distance + type: number + - description: Latitude do comprador + in: query + name: lat + required: true + type: number + - description: Longitude do comprador + in: query + name: lng + required: true + type: number + - description: Página + in: query + name: page + type: integer + - description: Itens por página + in: query + name: page_size + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.ProductSearchPage' + summary: Busca avançada de produtos com filtros e distância + tags: + - Produtos /api/v1/reviews: post: consumes: @@ -1188,14 +1698,14 @@ paths: name: payload required: true schema: - $ref: '#/definitions/internal_http_handler.createReviewRequest' + $ref: '#/definitions/handler.createReviewRequest' produces: - application/json responses: "201": description: Created schema: - $ref: '#/definitions/github_com_saveinmed_backend-go_internal_domain.Review' + $ref: '#/definitions/domain.Review' "400": description: Bad Request schema: @@ -1217,14 +1727,14 @@ paths: name: shipment required: true schema: - $ref: '#/definitions/internal_http_handler.createShipmentRequest' + $ref: '#/definitions/handler.createShipmentRequest' produces: - application/json responses: "201": description: Created schema: - $ref: '#/definitions/github_com_saveinmed_backend-go_internal_domain.Shipment' + $ref: '#/definitions/domain.Shipment' security: - BearerAuth: [] summary: Gera guia de postagem/transporte @@ -1244,12 +1754,134 @@ paths: "200": description: OK schema: - $ref: '#/definitions/github_com_saveinmed_backend-go_internal_domain.Shipment' + $ref: '#/definitions/domain.Shipment' security: - BearerAuth: [] summary: Rastreia entrega tags: - Logistica + /api/v1/shipping/calculate: + post: + consumes: + - application/json + description: Calculates shipping or pickup options based on vendor config and + buyer location. + parameters: + - description: Calculation inputs + in: body + name: payload + required: true + schema: + $ref: '#/definitions/handler.shippingCalculateRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/domain.ShippingOption' + type: array + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + summary: Calculate shipping options + tags: + - Shipping + /api/v1/shipping/settings/{vendor_id}: + get: + description: Returns pickup and delivery settings for a vendor. + parameters: + - description: Vendor ID + in: path + name: vendor_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/domain.ShippingMethod' + type: array + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "403": + description: Forbidden + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + summary: Get vendor shipping settings + tags: + - Shipping + put: + consumes: + - application/json + description: Stores pickup and delivery settings for a vendor. + parameters: + - description: Vendor ID + in: path + name: vendor_id + required: true + type: string + - description: Shipping settings + in: body + name: payload + required: true + schema: + $ref: '#/definitions/handler.shippingSettingsRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/domain.ShippingMethod' + type: array + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "403": + description: Forbidden + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + summary: Update vendor shipping settings + tags: + - Shipping /api/v1/users: get: parameters: @@ -1271,7 +1903,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/github_com_saveinmed_backend-go_internal_domain.UserPage' + $ref: '#/definitions/domain.UserPage' "400": description: Bad Request schema: @@ -1298,14 +1930,14 @@ paths: name: payload required: true schema: - $ref: '#/definitions/internal_http_handler.createUserRequest' + $ref: '#/definitions/handler.createUserRequest' produces: - application/json responses: "201": description: Created schema: - $ref: '#/definitions/github_com_saveinmed_backend-go_internal_domain.User' + $ref: '#/definitions/domain.User' "400": description: Bad Request schema: @@ -1384,7 +2016,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/github_com_saveinmed_backend-go_internal_domain.User' + $ref: '#/definitions/domain.User' "400": description: Bad Request schema: @@ -1422,14 +2054,14 @@ paths: name: payload required: true schema: - $ref: '#/definitions/internal_http_handler.updateUserRequest' + $ref: '#/definitions/handler.updateUserRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/github_com_saveinmed_backend-go_internal_domain.User' + $ref: '#/definitions/domain.User' "400": description: Bad Request schema: diff --git a/backend/go.mod b/backend/go.mod index ca29efc..a4c9a70 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -17,6 +17,7 @@ require ( require ( github.com/KyleBanks/depth v1.2.1 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.20.0 // indirect @@ -31,7 +32,10 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/russross/blackfriday/v2 v2.0.1 // indirect + github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect + github.com/urfave/cli/v2 v2.3.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 @@ -39,4 +43,5 @@ require ( golang.org/x/tools v0.26.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index c1ad7eb..44481d4 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,9 +1,12 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 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= @@ -64,6 +67,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb 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/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -80,6 +87,8 @@ github.com/swaggo/http-swagger v1.3.4 h1:q7t/XLx0n15H1Q9/tk3Y9L4n210XzJF5WtnDX64 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= +github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= 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= @@ -104,9 +113,12 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8 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.2.3/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= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/backend/internal/domain/models.go b/backend/internal/domain/models.go index 82af30f..855c4e0 100644 --- a/backend/internal/domain/models.go +++ b/backend/internal/domain/models.go @@ -28,14 +28,15 @@ type Company = Tenant // User represents an authenticated actor inside a company. type User struct { - ID uuid.UUID `db:"id" json:"id"` - CompanyID uuid.UUID `db:"company_id" json:"company_id"` - Role string `db:"role" json:"role"` - Name string `db:"name" json:"name"` - Email string `db:"email" json:"email"` - PasswordHash string `db:"password_hash" json:"-"` - 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"` + CompanyID uuid.UUID `db:"company_id" json:"company_id"` + Role string `db:"role" json:"role"` + Name string `db:"name" json:"name"` + Email string `db:"email" json:"email"` + EmailVerified bool `db:"email_verified" json:"email_verified"` + PasswordHash string `db:"password_hash" json:"-"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` } // UserFilter captures listing constraints. diff --git a/backend/internal/domain/search.go b/backend/internal/domain/search.go index 2cbb6bd..f9b4245 100644 --- a/backend/internal/domain/search.go +++ b/backend/internal/domain/search.go @@ -31,3 +31,11 @@ type PaginationResponse[T any] struct { CurrentPage int `json:"current_page"` TotalPages int `json:"total_pages"` } + +// ProductPaginationResponse is a swagger-friendly pagination response for products. +type ProductPaginationResponse struct { + Items []Product `json:"items"` + TotalCount int64 `json:"total_count"` + CurrentPage int `json:"current_page"` + TotalPages int `json:"total_pages"` +} diff --git a/backend/internal/http/handler/dto.go b/backend/internal/http/handler/dto.go index fd6dca6..278704b 100644 --- a/backend/internal/http/handler/dto.go +++ b/backend/internal/http/handler/dto.go @@ -5,6 +5,7 @@ import ( "errors" "net/http" "strconv" + "strings" "time" "github.com/gofrs/uuid/v5" @@ -48,11 +49,34 @@ type loginRequest struct { Password string `json:"password"` } +type forgotPasswordRequest struct { + Email string `json:"email"` +} + +type resetPasswordRequest struct { + Token string `json:"token"` + Password string `json:"password"` +} + +type verifyEmailRequest struct { + Token string `json:"token"` +} + type authResponse struct { Token string `json:"token"` ExpiresAt time.Time `json:"expires_at"` } +type messageResponse struct { + Message string `json:"message"` +} + +type resetTokenResponse struct { + Message string `json:"message"` + ResetToken string `json:"reset_token,omitempty"` + ExpiresAt *time.Time `json:"expires_at,omitempty"` +} + type inventoryAdjustRequest struct { ProductID uuid.UUID `json:"product_id"` Delta int64 `json:"delta"` @@ -268,3 +292,19 @@ func getRequester(r *http.Request) (requester, error) { return requester{Role: role, CompanyID: companyID}, nil } + +func parseBearerToken(r *http.Request) (string, error) { + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + return "", errors.New("missing Authorization header") + } + parts := strings.SplitN(authHeader, " ", 2) + if len(parts) != 2 || !strings.EqualFold(parts[0], "bearer") { + return "", errors.New("invalid Authorization header") + } + token := strings.TrimSpace(parts[1]) + if token == "" { + return "", errors.New("token is required") + } + return token, nil +} diff --git a/backend/internal/http/handler/handler.go b/backend/internal/http/handler/handler.go index c126699..523b1ff 100644 --- a/backend/internal/http/handler/handler.go +++ b/backend/internal/http/handler/handler.go @@ -1,6 +1,7 @@ package handler import ( + "database/sql" "errors" "net/http" @@ -112,3 +113,230 @@ func (h *Handler) Login(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, authResponse{Token: token, ExpiresAt: exp}) } + +// RegisterCustomer godoc +// @Summary Cadastro de cliente +// @Description Cria um usuário do tipo cliente e opcionalmente uma empresa, retornando token JWT. +// @Tags Autenticação +// @Accept json +// @Produce json +// @Param payload body registerAuthRequest true "Dados do usuário e empresa" +// @Success 201 {object} authResponse +// @Failure 400 {object} map[string]string +// @Failure 500 {object} map[string]string +// @Router /api/v1/auth/register/customer [post] +func (h *Handler) RegisterCustomer(w http.ResponseWriter, r *http.Request) { + var req registerAuthRequest + if err := decodeJSON(r.Context(), r, &req); err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + + req.Role = "Customer" + h.registerWithPayload(w, r, req) +} + +// RegisterTenant godoc +// @Summary Cadastro de tenant +// @Description Cria um usuário do tipo tenant e opcionalmente uma empresa, retornando token JWT. +// @Tags Autenticação +// @Accept json +// @Produce json +// @Param payload body registerAuthRequest true "Dados do usuário e empresa" +// @Success 201 {object} authResponse +// @Failure 400 {object} map[string]string +// @Failure 500 {object} map[string]string +// @Router /api/v1/auth/register/tenant [post] +func (h *Handler) RegisterTenant(w http.ResponseWriter, r *http.Request) { + var req registerAuthRequest + if err := decodeJSON(r.Context(), r, &req); err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + + req.Role = "Seller" + h.registerWithPayload(w, r, req) +} + +// RefreshToken godoc +// @Summary Atualizar token +// @Description Gera um novo JWT a partir de um token válido. +// @Tags Autenticação +// @Accept json +// @Produce json +// @Param Authorization header string true "Bearer token" +// @Success 200 {object} authResponse +// @Failure 401 {object} map[string]string +// @Router /api/v1/auth/refresh-token [post] +func (h *Handler) RefreshToken(w http.ResponseWriter, r *http.Request) { + tokenStr, err := parseBearerToken(r) + if err != nil { + writeError(w, http.StatusUnauthorized, err) + return + } + + token, exp, err := h.svc.RefreshToken(r.Context(), tokenStr) + if err != nil { + writeError(w, http.StatusUnauthorized, err) + return + } + + writeJSON(w, http.StatusOK, authResponse{Token: token, ExpiresAt: exp}) +} + +// Logout godoc +// @Summary Logout +// @Description Endpoint para logout (invalidação client-side). +// @Tags Autenticação +// @Success 204 {string} string "No Content" +// @Router /api/v1/auth/logout [post] +func (h *Handler) Logout(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNoContent) +} + +// ForgotPassword godoc +// @Summary Solicitar redefinição de senha +// @Description Gera um token de redefinição de senha. +// @Tags Autenticação +// @Accept json +// @Produce json +// @Param payload body forgotPasswordRequest true "Email do usuário" +// @Success 202 {object} resetTokenResponse +// @Failure 400 {object} map[string]string +// @Router /api/v1/auth/password/forgot [post] +func (h *Handler) ForgotPassword(w http.ResponseWriter, r *http.Request) { + var req forgotPasswordRequest + if err := decodeJSON(r.Context(), r, &req); err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + if req.Email == "" { + writeError(w, http.StatusBadRequest, errors.New("email is required")) + return + } + + token, exp, err := h.svc.CreatePasswordResetToken(r.Context(), req.Email) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + writeJSON(w, http.StatusAccepted, resetTokenResponse{ + Message: "Se existir uma conta, enviaremos instruções de redefinição.", + }) + return + } + writeError(w, http.StatusInternalServerError, err) + return + } + + writeJSON(w, http.StatusAccepted, resetTokenResponse{ + Message: "Token de redefinição gerado.", + ResetToken: token, + ExpiresAt: &exp, + }) +} + +// ResetPassword godoc +// @Summary Redefinir senha +// @Description Atualiza a senha usando o token de redefinição. +// @Tags Autenticação +// @Accept json +// @Produce json +// @Param payload body resetPasswordRequest true "Token e nova senha" +// @Success 200 {object} messageResponse +// @Failure 400 {object} map[string]string +// @Failure 401 {object} map[string]string +// @Router /api/v1/auth/password/reset [post] +func (h *Handler) ResetPassword(w http.ResponseWriter, r *http.Request) { + var req resetPasswordRequest + if err := decodeJSON(r.Context(), r, &req); err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + if req.Token == "" || req.Password == "" { + writeError(w, http.StatusBadRequest, errors.New("token and password are required")) + return + } + + if err := h.svc.ResetPassword(r.Context(), req.Token, req.Password); err != nil { + writeError(w, http.StatusUnauthorized, err) + return + } + + writeJSON(w, http.StatusOK, messageResponse{Message: "Senha atualizada com sucesso."}) +} + +// VerifyEmail godoc +// @Summary Verificar email +// @Description Marca o email como verificado usando um token JWT. +// @Tags Autenticação +// @Accept json +// @Produce json +// @Param payload body verifyEmailRequest true "Token de verificação" +// @Success 200 {object} messageResponse +// @Failure 400 {object} map[string]string +// @Failure 401 {object} map[string]string +// @Router /api/v1/auth/verify-email [post] +func (h *Handler) VerifyEmail(w http.ResponseWriter, r *http.Request) { + var req verifyEmailRequest + if err := decodeJSON(r.Context(), r, &req); err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + if req.Token == "" { + writeError(w, http.StatusBadRequest, errors.New("token is required")) + return + } + + if _, err := h.svc.VerifyEmail(r.Context(), req.Token); err != nil { + writeError(w, http.StatusUnauthorized, err) + return + } + + writeJSON(w, http.StatusOK, messageResponse{Message: "E-mail verificado com sucesso."}) +} + +func (h *Handler) registerWithPayload(w http.ResponseWriter, r *http.Request, req registerAuthRequest) { + var company *domain.Company + if req.Company != nil { + company = &domain.Company{ + ID: req.Company.ID, + Category: req.Company.Category, + CNPJ: req.Company.CNPJ, + CorporateName: req.Company.CorporateName, + LicenseNumber: req.Company.LicenseNumber, + Latitude: req.Company.Latitude, + Longitude: req.Company.Longitude, + City: req.Company.City, + State: req.Company.State, + } + } + + var companyID uuid.UUID + if req.CompanyID != nil { + companyID = *req.CompanyID + } + + user := &domain.User{ + CompanyID: companyID, + Role: req.Role, + Name: req.Name, + Email: req.Email, + } + + if user.CompanyID == uuid.Nil && company == nil { + writeError(w, http.StatusBadRequest, errors.New("company_id or company payload is required")) + return + } + + if err := h.svc.RegisterAccount(r.Context(), company, user, req.Password); err != nil { + writeError(w, http.StatusInternalServerError, err) + return + } + + token, exp, err := h.svc.Authenticate(r.Context(), user.Email, req.Password) + if err != nil { + writeError(w, http.StatusInternalServerError, err) + return + } + + writeJSON(w, http.StatusCreated, authResponse{Token: token, ExpiresAt: exp}) +} diff --git a/backend/internal/http/handler/marketplace_handler.go b/backend/internal/http/handler/marketplace_handler.go index 6a2c536..af4360b 100644 --- a/backend/internal/http/handler/marketplace_handler.go +++ b/backend/internal/http/handler/marketplace_handler.go @@ -18,7 +18,7 @@ import ( // @Param created_before query string false "Data máxima (RFC3339)" // @Param page query integer false "Página" // @Param page_size query integer false "Itens por página" -// @Success 200 {object} domain.PaginationResponse[domain.Product] +// @Success 200 {object} domain.ProductPaginationResponse // @Router /api/v1/marketplace/records [get] func (h *Handler) ListMarketplaceRecords(w http.ResponseWriter, r *http.Request) { page, pageSize := parsePagination(r) diff --git a/backend/internal/repository/postgres/migrations/0004_users_email_verified.sql b/backend/internal/repository/postgres/migrations/0004_users_email_verified.sql new file mode 100644 index 0000000..f189f97 --- /dev/null +++ b/backend/internal/repository/postgres/migrations/0004_users_email_verified.sql @@ -0,0 +1,2 @@ +ALTER TABLE users +ADD COLUMN IF NOT EXISTS email_verified BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/backend/internal/repository/postgres/postgres.go b/backend/internal/repository/postgres/postgres.go index c9046ef..75d2e22 100644 --- a/backend/internal/repository/postgres/postgres.go +++ b/backend/internal/repository/postgres/postgres.go @@ -785,8 +785,8 @@ func (r *Repository) CreateUser(ctx context.Context, user *domain.User) error { user.CreatedAt = now user.UpdatedAt = now - query := `INSERT INTO users (id, company_id, role, name, email, password_hash, created_at, updated_at) -VALUES (:id, :company_id, :role, :name, :email, :password_hash, :created_at, :updated_at)` + query := `INSERT INTO users (id, company_id, role, name, email, email_verified, password_hash, created_at, updated_at) +VALUES (:id, :company_id, :role, :name, :email, :email_verified, :password_hash, :created_at, :updated_at)` _, err := r.db.NamedExecContext(ctx, query, user) return err @@ -814,7 +814,7 @@ func (r *Repository) ListUsers(ctx context.Context, filter domain.UserFilter) ([ } args = append(args, filter.Limit, filter.Offset) - listQuery := fmt.Sprintf("SELECT id, company_id, role, name, email, password_hash, 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, company_id, role, name, email, email_verified, password_hash, created_at, updated_at %s%s ORDER BY created_at DESC LIMIT $%d OFFSET $%d", baseQuery, where, len(args)-1, len(args)) var users []domain.User if err := r.db.SelectContext(ctx, &users, listQuery, args...); err != nil { @@ -826,7 +826,7 @@ func (r *Repository) ListUsers(ctx context.Context, filter domain.UserFilter) ([ func (r *Repository) GetUser(ctx context.Context, id uuid.UUID) (*domain.User, error) { var user domain.User - query := `SELECT id, company_id, role, name, email, password_hash, created_at, updated_at FROM users WHERE id = $1` + query := `SELECT id, company_id, role, name, email, email_verified, password_hash, created_at, updated_at FROM users WHERE id = $1` if err := r.db.GetContext(ctx, &user, query, id); err != nil { return nil, err } @@ -835,7 +835,7 @@ func (r *Repository) GetUser(ctx context.Context, id uuid.UUID) (*domain.User, e func (r *Repository) GetUserByEmail(ctx context.Context, email string) (*domain.User, error) { var user domain.User - query := `SELECT id, company_id, role, name, email, password_hash, created_at, updated_at FROM users WHERE email = $1` + query := `SELECT id, company_id, role, name, email, email_verified, password_hash, created_at, updated_at FROM users WHERE email = $1` if err := r.db.GetContext(ctx, &user, query, email); err != nil { return nil, err } @@ -846,7 +846,7 @@ func (r *Repository) UpdateUser(ctx context.Context, user *domain.User) error { user.UpdatedAt = time.Now().UTC() query := `UPDATE users -SET company_id = :company_id, role = :role, name = :name, email = :email, password_hash = :password_hash, updated_at = :updated_at +SET company_id = :company_id, role = :role, name = :name, email = :email, email_verified = :email_verified, password_hash = :password_hash, updated_at = :updated_at WHERE id = :id` res, err := r.db.NamedExecContext(ctx, query, user) diff --git a/backend/internal/server/server.go b/backend/internal/server/server.go index 5a07a81..3748cc5 100644 --- a/backend/internal/server/server.go +++ b/backend/internal/server/server.go @@ -100,7 +100,14 @@ func New(cfg config.Config) (*Server, error) { mux.Handle("GET /api/v1/dashboard/admin", chain(http.HandlerFunc(h.GetAdminDashboard), middleware.Logger, middleware.Gzip, adminOnly)) mux.Handle("POST /api/v1/auth/register", chain(http.HandlerFunc(h.Register), middleware.Logger, middleware.Gzip)) + mux.Handle("POST /api/v1/auth/register/customer", chain(http.HandlerFunc(h.RegisterCustomer), middleware.Logger, middleware.Gzip)) + mux.Handle("POST /api/v1/auth/register/tenant", chain(http.HandlerFunc(h.RegisterTenant), middleware.Logger, middleware.Gzip)) mux.Handle("POST /api/v1/auth/login", chain(http.HandlerFunc(h.Login), middleware.Logger, middleware.Gzip)) + mux.Handle("POST /api/v1/auth/logout", chain(http.HandlerFunc(h.Logout), middleware.Logger, middleware.Gzip)) + mux.Handle("POST /api/v1/auth/password/forgot", chain(http.HandlerFunc(h.ForgotPassword), middleware.Logger, middleware.Gzip)) + mux.Handle("POST /api/v1/auth/password/reset", chain(http.HandlerFunc(h.ResetPassword), middleware.Logger, middleware.Gzip)) + mux.Handle("POST /api/v1/auth/refresh-token", chain(http.HandlerFunc(h.RefreshToken), middleware.Logger, middleware.Gzip)) + mux.Handle("POST /api/v1/auth/verify-email", chain(http.HandlerFunc(h.VerifyEmail), middleware.Logger, middleware.Gzip)) mux.Handle("POST /api/v1/users", chain(http.HandlerFunc(h.CreateUser), middleware.Logger, middleware.Gzip, auth)) mux.Handle("GET /api/v1/users", chain(http.HandlerFunc(h.ListUsers), middleware.Logger, middleware.Gzip, auth)) diff --git a/backend/internal/usecase/usecase.go b/backend/internal/usecase/usecase.go index 153595d..7930506 100644 --- a/backend/internal/usecase/usecase.go +++ b/backend/internal/usecase/usecase.go @@ -75,6 +75,10 @@ type Service struct { passwordPepper string } +const ( + passwordResetTTL = 30 * time.Minute +) + // NewService wires use cases together. func NewService(repo Repository, pay PaymentGateway, commissionPct float64, jwtSecret string, tokenTTL time.Duration, passwordPepper string) *Service { return &Service{ @@ -629,21 +633,7 @@ func (s *Service) Authenticate(ctx context.Context, email, password string) (str return "", time.Time{}, errors.New("invalid credentials") } - expiresAt := time.Now().Add(s.tokenTTL) - claims := jwt.MapClaims{ - "sub": user.ID.String(), - "role": user.Role, - "company_id": user.CompanyID.String(), - "exp": expiresAt.Unix(), - } - - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - signed, err := token.SignedString(s.jwtSecret) - if err != nil { - return "", time.Time{}, err - } - - return signed, expiresAt, nil + return s.issueAccessToken(user) } func (s *Service) pepperPassword(password string) string { @@ -653,6 +643,156 @@ func (s *Service) pepperPassword(password string) string { return password + s.passwordPepper } +// RefreshToken validates the provided JWT and issues a new access token. +func (s *Service) RefreshToken(ctx context.Context, tokenStr string) (string, time.Time, error) { + claims, err := s.parseToken(tokenStr) + if err != nil { + return "", time.Time{}, err + } + + if scope, ok := claims["scope"].(string); ok && scope != "" { + return "", time.Time{}, errors.New("invalid token scope") + } + + sub, ok := claims["sub"].(string) + if !ok || sub == "" { + return "", time.Time{}, errors.New("invalid token subject") + } + + userID, err := uuid.FromString(sub) + if err != nil { + return "", time.Time{}, errors.New("invalid token subject") + } + + user, err := s.repo.GetUser(ctx, userID) + if err != nil { + return "", time.Time{}, err + } + + return s.issueAccessToken(user) +} + +// CreatePasswordResetToken generates a short-lived token for password reset. +func (s *Service) CreatePasswordResetToken(ctx context.Context, email string) (string, time.Time, error) { + user, err := s.repo.GetUserByEmail(ctx, email) + if err != nil { + return "", time.Time{}, err + } + + expiresAt := time.Now().Add(passwordResetTTL) + claims := jwt.MapClaims{ + "sub": user.ID.String(), + "scope": "password_reset", + } + signed, err := s.signToken(claims, expiresAt) + if err != nil { + return "", time.Time{}, err + } + return signed, expiresAt, nil +} + +// ResetPassword validates the reset token and updates the user password. +func (s *Service) ResetPassword(ctx context.Context, tokenStr, newPassword string) error { + claims, err := s.parseToken(tokenStr) + if err != nil { + return err + } + + scope, _ := claims["scope"].(string) + if scope != "password_reset" { + return errors.New("invalid token scope") + } + + sub, ok := claims["sub"].(string) + if !ok || sub == "" { + return errors.New("invalid token subject") + } + + userID, err := uuid.FromString(sub) + if err != nil { + return errors.New("invalid token subject") + } + + user, err := s.repo.GetUser(ctx, userID) + if err != nil { + return err + } + + return s.UpdateUser(ctx, user, newPassword) +} + +// VerifyEmail marks the user email as verified based on a JWT token. +func (s *Service) VerifyEmail(ctx context.Context, tokenStr string) (*domain.User, error) { + claims, err := s.parseToken(tokenStr) + if err != nil { + return nil, err + } + + if scope, ok := claims["scope"].(string); ok && scope != "" && scope != "email_verify" { + return nil, errors.New("invalid token scope") + } + + sub, ok := claims["sub"].(string) + if !ok || sub == "" { + return nil, errors.New("invalid token subject") + } + + userID, err := uuid.FromString(sub) + if err != nil { + return nil, errors.New("invalid token subject") + } + + user, err := s.repo.GetUser(ctx, userID) + if err != nil { + return nil, err + } + + if !user.EmailVerified { + user.EmailVerified = true + if err := s.repo.UpdateUser(ctx, user); err != nil { + return nil, err + } + } + + return user, nil +} + +func (s *Service) issueAccessToken(user *domain.User) (string, time.Time, error) { + expiresAt := time.Now().Add(s.tokenTTL) + claims := jwt.MapClaims{ + "sub": user.ID.String(), + "role": user.Role, + "company_id": user.CompanyID.String(), + } + signed, err := s.signToken(claims, expiresAt) + if err != nil { + return "", time.Time{}, err + } + return signed, expiresAt, nil +} + +func (s *Service) signToken(claims jwt.MapClaims, expiresAt time.Time) (string, error) { + claims["exp"] = expiresAt.Unix() + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(s.jwtSecret) +} + +func (s *Service) parseToken(tokenStr string) (jwt.MapClaims, error) { + if strings.TrimSpace(tokenStr) == "" { + return nil, errors.New("token is required") + } + + claims := jwt.MapClaims{} + parser := jwt.NewParser(jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()})) + token, err := parser.ParseWithClaims(tokenStr, claims, func(token *jwt.Token) (any, error) { + return s.jwtSecret, nil + }) + if err != nil || !token.Valid { + return nil, errors.New("invalid token") + } + return claims, nil +} + // VerifyCompany marks a company as verified. func (s *Service) VerifyCompany(ctx context.Context, id uuid.UUID) (*domain.Company, error) { company, err := s.repo.GetCompany(ctx, id)