From 8e67dcfd49ce143cde3ae50644ae648dd3bba03c Mon Sep 17 00:00:00 2001 From: NANDO9322 Date: Wed, 10 Dec 2025 11:59:18 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20Auto-Migra=C3=A7=C3=A3o,=20Seeding=20de?= =?UTF-8?q?=20Pre=C3=A7os=20e=20CRUD=20de=20Eventos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/cmd/api/main.go | 9 +- backend/docs/docs.go | 82 ++++++++++++ backend/docs/swagger.json | 82 ++++++++++++ backend/docs/swagger.yaml | 51 ++++++++ backend/internal/db/schema.sql | 144 +++++++++++++++++++++- backend/internal/tipos_eventos/handler.go | 82 +++++++++++- backend/internal/tipos_eventos/service.go | 9 +- 7 files changed, 447 insertions(+), 12 deletions(-) diff --git a/backend/cmd/api/main.go b/backend/cmd/api/main.go index a8900c8..2d11d6b 100644 --- a/backend/cmd/api/main.go +++ b/backend/cmd/api/main.go @@ -85,10 +85,15 @@ func main() { configCors.AllowHeaders = []string{"Origin", "Content-Length", "Content-Type", "Authorization"} r.Use(cors.New(configCors)) + // Swagger // Swagger // Dynamically update Swagger Info docs.SwaggerInfo.Host = cfg.SwaggerHost - docs.SwaggerInfo.Schemes = []string{"https", "http"} + if cfg.AppEnv == "production" { + docs.SwaggerInfo.Schemes = []string{"https", "http"} + } else { + docs.SwaggerInfo.Schemes = []string{"http", "https"} + } r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) @@ -158,6 +163,8 @@ func main() { api.DELETE("/tipos-servicos/:id", tiposServicosHandler.Delete) api.POST("/tipos-eventos", tiposEventosHandler.Create) + api.PUT("/tipos-eventos/:id", tiposEventosHandler.Update) + api.DELETE("/tipos-eventos/:id", tiposEventosHandler.Delete) api.POST("/tipos-eventos/precos", tiposEventosHandler.SetPrice) } diff --git a/backend/docs/docs.go b/backend/docs/docs.go index 9d604b8..d94d876 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -1116,6 +1116,82 @@ const docTemplate = `{ } } }, + "/api/tipos-eventos/{id}": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "tipos_eventos" + ], + "summary": "Update an event type", + "parameters": [ + { + "type": "string", + "description": "Event ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Event Data", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/tipos_eventos.CreateEventoRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/tipos_eventos.EventoResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "tipos_eventos" + ], + "summary": "Delete an event type", + "parameters": [ + { + "type": "string", + "description": "Event ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, "/api/tipos-eventos/{id}/precos": { "get": { "security": [ @@ -1888,6 +1964,12 @@ const docTemplate = `{ }, "nome": { "type": "string" + }, + "precos": { + "type": "array", + "items": { + "$ref": "#/definitions/tipos_eventos.PrecoResponse" + } } } }, diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index 1759bb7..bd2b29b 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -1110,6 +1110,82 @@ } } }, + "/api/tipos-eventos/{id}": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "tipos_eventos" + ], + "summary": "Update an event type", + "parameters": [ + { + "type": "string", + "description": "Event ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Event Data", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/tipos_eventos.CreateEventoRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/tipos_eventos.EventoResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "tipos_eventos" + ], + "summary": "Delete an event type", + "parameters": [ + { + "type": "string", + "description": "Event ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, "/api/tipos-eventos/{id}/precos": { "get": { "security": [ @@ -1882,6 +1958,12 @@ }, "nome": { "type": "string" + }, + "precos": { + "type": "array", + "items": { + "$ref": "#/definitions/tipos_eventos.PrecoResponse" + } } } }, diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index 169bdcb..a127059 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -254,6 +254,10 @@ definitions: type: string nome: type: string + precos: + items: + $ref: '#/definitions/tipos_eventos.PrecoResponse' + type: array type: object tipos_eventos.PrecoResponse: properties: @@ -966,6 +970,53 @@ paths: summary: Create a new event type tags: - tipos_eventos + /api/tipos-eventos/{id}: + delete: + consumes: + - application/json + parameters: + - description: Event ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "204": + description: No Content + security: + - BearerAuth: [] + summary: Delete an event type + tags: + - tipos_eventos + put: + consumes: + - application/json + parameters: + - description: Event ID + in: path + name: id + required: true + type: string + - description: Event Data + in: body + name: request + required: true + schema: + $ref: '#/definitions/tipos_eventos.CreateEventoRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/tipos_eventos.EventoResponse' + security: + - BearerAuth: [] + summary: Update an event type + tags: + - tipos_eventos /api/tipos-eventos/{id}/precos: get: consumes: diff --git a/backend/internal/db/schema.sql b/backend/internal/db/schema.sql index d85144b..84e3e57 100644 --- a/backend/internal/db/schema.sql +++ b/backend/internal/db/schema.sql @@ -145,8 +145,142 @@ CREATE TABLE IF NOT EXISTS precos_tipos_eventos ( UNIQUE(tipo_evento_id, funcao_profissional_id) ); --- Initial Pricing Seed (Examples based on image, requires joining IDs which makes raw SQL insert hard without known UUIDs. --- For simplicity in schema.sql, we'll skip complex dynamic inserts. --- The user can populate via API or we can write a more complex PL/pgSQL block if absolutely necessary, --- but usually schema.sql is structure + static data. Dynamic pricing is better handled via admin or separate migration script. --- Leaving table empty for now, or adding a comment.) +-- Seed Pricing Data +DO $$ +DECLARE + -- IDs for Functions + id_foto UUID; + id_cine UUID; + id_recep UUID; + + -- IDs for Events + id_identificacao UUID; + id_baile UUID; + id_colacao UUID; + id_col_baile_mesmo UUID; + id_col_baile_dif UUID; + id_missa UUID; + id_churrasco UUID; + id_trote UUID; + id_outro UUID; + id_balada UUID; + id_jantar UUID; + id_festa_junina UUID; + id_colacao_oficial UUID; + id_family_day UUID; + id_refeicao UUID; + id_estudio_id UUID; + id_estudio_col UUID; +BEGIN + -- Fetch Function IDs + SELECT id INTO id_foto FROM funcoes_profissionais WHERE nome = 'Fotógrafo'; + SELECT id INTO id_cine FROM funcoes_profissionais WHERE nome = 'Cinegrafista'; + SELECT id INTO id_recep FROM funcoes_profissionais WHERE nome = 'Recepcionista'; + + -- Fetch Event IDs + SELECT id INTO id_identificacao FROM tipos_eventos WHERE nome = 'Identificação'; + SELECT id INTO id_baile FROM tipos_eventos WHERE nome = 'Baile'; + SELECT id INTO id_colacao FROM tipos_eventos WHERE nome = 'Colação'; + SELECT id INTO id_col_baile_mesmo FROM tipos_eventos WHERE nome = 'Col/Baile (mesmo local)'; + SELECT id INTO id_col_baile_dif FROM tipos_eventos WHERE nome = 'Col/Baile (local diferente)'; + SELECT id INTO id_missa FROM tipos_eventos WHERE nome = 'Missa / Culto'; + SELECT id INTO id_churrasco FROM tipos_eventos WHERE nome = 'Churrasco'; + SELECT id INTO id_trote FROM tipos_eventos WHERE nome = 'Trote'; + SELECT id INTO id_outro FROM tipos_eventos WHERE nome = 'Outro'; + SELECT id INTO id_balada FROM tipos_eventos WHERE nome = 'Balada'; + SELECT id INTO id_jantar FROM tipos_eventos WHERE nome = 'Jantar'; + SELECT id INTO id_festa_junina FROM tipos_eventos WHERE nome = 'Festa Junina'; + SELECT id INTO id_colacao_oficial FROM tipos_eventos WHERE nome = 'Colação Oficial'; + SELECT id INTO id_family_day FROM tipos_eventos WHERE nome = 'Family Day'; + SELECT id INTO id_refeicao FROM tipos_eventos WHERE nome = 'Refeição'; + SELECT id INTO id_estudio_id FROM tipos_eventos WHERE nome = 'Estudio ID e Family Day'; + SELECT id INTO id_estudio_col FROM tipos_eventos WHERE nome = 'Estudio Colação / Baile'; + + -- Helper temp table for inserts to keep code clean? No, direct inserts are clearer. + + -- Identificação: Rec 90, Foto 160 + INSERT INTO precos_tipos_eventos (tipo_evento_id, funcao_profissional_id, valor) VALUES + (id_identificacao, id_recep, 90.00), (id_identificacao, id_foto, 160.00) + ON CONFLICT (tipo_evento_id, funcao_profissional_id) DO UPDATE SET valor = EXCLUDED.valor; + + -- Baile: Rec 115, Cin 450, Foto 310 + INSERT INTO precos_tipos_eventos (tipo_evento_id, funcao_profissional_id, valor) VALUES + (id_baile, id_recep, 115.00), (id_baile, id_cine, 450.00), (id_baile, id_foto, 310.00) + ON CONFLICT (tipo_evento_id, funcao_profissional_id) DO UPDATE SET valor = EXCLUDED.valor; + + -- Colação: Rec 115, Cin 450, Foto 280 + INSERT INTO precos_tipos_eventos (tipo_evento_id, funcao_profissional_id, valor) VALUES + (id_colacao, id_recep, 115.00), (id_colacao, id_cine, 450.00), (id_colacao, id_foto, 280.00) + ON CONFLICT (tipo_evento_id, funcao_profissional_id) DO UPDATE SET valor = EXCLUDED.valor; + + -- Col/Baile (mesmo local): Rec 140, Cin 510, Foto 350 + INSERT INTO precos_tipos_eventos (tipo_evento_id, funcao_profissional_id, valor) VALUES + (id_col_baile_mesmo, id_recep, 140.00), (id_col_baile_mesmo, id_cine, 510.00), (id_col_baile_mesmo, id_foto, 350.00) + ON CONFLICT (tipo_evento_id, funcao_profissional_id) DO UPDATE SET valor = EXCLUDED.valor; + + -- Col/Baile (local diferente): Rec 150, Cin 610, Foto 390 + INSERT INTO precos_tipos_eventos (tipo_evento_id, funcao_profissional_id, valor) VALUES + (id_col_baile_dif, id_recep, 150.00), (id_col_baile_dif, id_cine, 610.00), (id_col_baile_dif, id_foto, 390.00) + ON CONFLICT (tipo_evento_id, funcao_profissional_id) DO UPDATE SET valor = EXCLUDED.valor; + + -- Missa / Culto: Rec 115, Cin 440, Foto 230 + INSERT INTO precos_tipos_eventos (tipo_evento_id, funcao_profissional_id, valor) VALUES + (id_missa, id_recep, 115.00), (id_missa, id_cine, 440.00), (id_missa, id_foto, 230.00) + ON CONFLICT (tipo_evento_id, funcao_profissional_id) DO UPDATE SET valor = EXCLUDED.valor; + + -- Churrasco: Rec 90, Cin 440, Foto 250 + INSERT INTO precos_tipos_eventos (tipo_evento_id, funcao_profissional_id, valor) VALUES + (id_churrasco, id_recep, 90.00), (id_churrasco, id_cine, 440.00), (id_churrasco, id_foto, 250.00) + ON CONFLICT (tipo_evento_id, funcao_profissional_id) DO UPDATE SET valor = EXCLUDED.valor; + + -- Trote: Rec 70, Foto 100 + INSERT INTO precos_tipos_eventos (tipo_evento_id, funcao_profissional_id, valor) VALUES + (id_trote, id_recep, 70.00), (id_trote, id_foto, 100.00) + ON CONFLICT (tipo_evento_id, funcao_profissional_id) DO UPDATE SET valor = EXCLUDED.valor; + + -- Outro: Foto 250 + INSERT INTO precos_tipos_eventos (tipo_evento_id, funcao_profissional_id, valor) VALUES + (id_outro, id_foto, 250.00) + ON CONFLICT (tipo_evento_id, funcao_profissional_id) DO UPDATE SET valor = EXCLUDED.valor; + + -- Balada: Rec 115, Cin 440, Foto 250 + INSERT INTO precos_tipos_eventos (tipo_evento_id, funcao_profissional_id, valor) VALUES + (id_balada, id_recep, 115.00), (id_balada, id_cine, 440.00), (id_balada, id_foto, 250.00) + ON CONFLICT (tipo_evento_id, funcao_profissional_id) DO UPDATE SET valor = EXCLUDED.valor; + + -- Jantar: Rec 115, Cin 440, Foto 310 + INSERT INTO precos_tipos_eventos (tipo_evento_id, funcao_profissional_id, valor) VALUES + (id_jantar, id_recep, 115.00), (id_jantar, id_cine, 440.00), (id_jantar, id_foto, 310.00) + ON CONFLICT (tipo_evento_id, funcao_profissional_id) DO UPDATE SET valor = EXCLUDED.valor; + + -- Festa Junina: Foto 200 + INSERT INTO precos_tipos_eventos (tipo_evento_id, funcao_profissional_id, valor) VALUES + (id_festa_junina, id_foto, 200.00) + ON CONFLICT (tipo_evento_id, funcao_profissional_id) DO UPDATE SET valor = EXCLUDED.valor; + + -- Colação Oficial: Rec 115, Cin 450, Foto 280 + INSERT INTO precos_tipos_eventos (tipo_evento_id, funcao_profissional_id, valor) VALUES + (id_colacao_oficial, id_recep, 115.00), (id_colacao_oficial, id_cine, 450.00), (id_colacao_oficial, id_foto, 280.00) + ON CONFLICT (tipo_evento_id, funcao_profissional_id) DO UPDATE SET valor = EXCLUDED.valor; + + -- Family Day: Rec 210, Cin 670, Foto 490 + INSERT INTO precos_tipos_eventos (tipo_evento_id, funcao_profissional_id, valor) VALUES + (id_family_day, id_recep, 210.00), (id_family_day, id_cine, 670.00), (id_family_day, id_foto, 490.00) + ON CONFLICT (tipo_evento_id, funcao_profissional_id) DO UPDATE SET valor = EXCLUDED.valor; + + -- Refeição: Rec 35 (Assuming Rec column corresponds to Refeição cost based on position, or just mapping single value) + INSERT INTO precos_tipos_eventos (tipo_evento_id, funcao_profissional_id, valor) VALUES + (id_refeicao, id_recep, 35.00) + ON CONFLICT (tipo_evento_id, funcao_profissional_id) DO UPDATE SET valor = EXCLUDED.valor; + + -- Estudio ID e Family Day: Rec 80 + INSERT INTO precos_tipos_eventos (tipo_evento_id, funcao_profissional_id, valor) VALUES + (id_estudio_id, id_recep, 80.00) + ON CONFLICT (tipo_evento_id, funcao_profissional_id) DO UPDATE SET valor = EXCLUDED.valor; + + -- Estudio Colação / Baile: Rec 50 + INSERT INTO precos_tipos_eventos (tipo_evento_id, funcao_profissional_id, valor) VALUES + (id_estudio_col, id_recep, 50.00) + ON CONFLICT (tipo_evento_id, funcao_profissional_id) DO UPDATE SET valor = EXCLUDED.valor; + +END $$; diff --git a/backend/internal/tipos_eventos/handler.go b/backend/internal/tipos_eventos/handler.go index e491aea..68fd38c 100644 --- a/backend/internal/tipos_eventos/handler.go +++ b/backend/internal/tipos_eventos/handler.go @@ -19,8 +19,9 @@ func NewHandler(service *Service) *Handler { } type EventoResponse struct { - ID string `json:"id"` - Nome string `json:"nome"` + ID string `json:"id"` + Nome string `json:"nome"` + Precos []PrecoResponse `json:"precos"` } type PrecoResponse struct { @@ -87,9 +88,33 @@ func (h *Handler) List(c *gin.Context) { return } - var response []EventoResponse + response := []EventoResponse{} for _, e := range events { - response = append(response, toEventoResponse(e)) + eventID := uuid.UUID(e.ID.Bytes).String() + + // Fetch prices for this event + prices, err := h.service.ListPrices(c.Request.Context(), eventID) + if err != nil { + // If fetching prices fails, we log/error (or just return empty prices depending on requirement) + // For now, failing the request ensures data consistency + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + pricesResp := []PrecoResponse{} + for _, p := range prices { + pricesResp = append(pricesResp, PrecoResponse{ + ID: uuid.UUID(p.ID.Bytes).String(), + TipoEventoID: uuid.UUID(p.TipoEventoID.Bytes).String(), + FuncaoProfissionalID: uuid.UUID(p.FuncaoProfissionalID.Bytes).String(), + FuncaoNome: p.FuncaoNome, + Valor: fromPgNumeric(p.Valor), + }) + } + + evtResp := toEventoResponse(e) + evtResp.Precos = pricesResp + response = append(response, evtResp) } c.JSON(http.StatusOK, response) } @@ -136,7 +161,7 @@ func (h *Handler) ListPrices(c *gin.Context) { return } - var response []PrecoResponse + response := []PrecoResponse{} for _, p := range prices { response = append(response, PrecoResponse{ ID: uuid.UUID(p.ID.Bytes).String(), @@ -148,3 +173,50 @@ func (h *Handler) ListPrices(c *gin.Context) { } c.JSON(http.StatusOK, response) } + +// Update godoc +// @Summary Update an event type +// @Tags tipos_eventos +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param id path string true "Event ID" +// @Param request body CreateEventoRequest true "Event Data" +// @Success 200 {object} EventoResponse +// @Router /api/tipos-eventos/{id} [put] +func (h *Handler) Update(c *gin.Context) { + id := c.Param("id") + var req CreateEventoRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + event, err := h.service.Update(c.Request.Context(), id, req.Nome) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, toEventoResponse(*event)) +} + +// Delete godoc +// @Summary Delete an event type +// @Tags tipos_eventos +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param id path string true "Event ID" +// @Success 204 +// @Router /api/tipos-eventos/{id} [delete] +func (h *Handler) Delete(c *gin.Context) { + id := c.Param("id") + err := h.service.Delete(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusNoContent, nil) +} diff --git a/backend/internal/tipos_eventos/service.go b/backend/internal/tipos_eventos/service.go index e8f9d8e..f4466e1 100644 --- a/backend/internal/tipos_eventos/service.go +++ b/backend/internal/tipos_eventos/service.go @@ -3,6 +3,7 @@ package tipos_eventos import ( "context" "errors" + "strconv" "photum-backend/internal/db/generated" @@ -107,6 +108,12 @@ func (s *Service) ListPrices(ctx context.Context, eventoID string) ([]generated. // Helper (Assuming user doesn't have it in shared utils or similar) func toPgNumeric(f float64) pgtype.Numeric { var n pgtype.Numeric - n.Scan(f) + // Convert to string first to ensure precise scanning + s := strconv.FormatFloat(f, 'f', -1, 64) + if err := n.Scan(s); err != nil { + // Fallback or log if needed, but for valid floats this should work. + // Returning invalid (NULL) numeric if scan fails. + return pgtype.Numeric{Valid: false} + } return n }