From 1f5ad4f1944541d493996ce4e9e7b9b38324663d Mon Sep 17 00:00:00 2001 From: Eduardo Lauer Date: Tue, 7 Jul 2026 17:08:37 -0300 Subject: [PATCH 1/6] add imports --- src/crud.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/crud.py b/src/crud.py index 8fa0ce7..d035f26 100644 --- a/src/crud.py +++ b/src/crud.py @@ -4,12 +4,14 @@ from typing import Optional from sqlalchemy import select, and_, func +from sqlalchemy.dialects.postgresql import insert from sqlalchemy.sql import text from sqlalchemy.exc import IntegrityError from fastapi import HTTPException from sqlalchemy.ext.asyncio import AsyncSession import models, schemas from db_config import DbContextManager, sync_engine +from util import over_a_year async def get_plano_trabalho( From a63c45a89408557b0a5a12ff31060611cc30409e Mon Sep 17 00:00:00 2001 From: Eduardo Lauer Date: Tue, 7 Jul 2026 17:09:22 -0300 Subject: [PATCH 2/6] use upsert for plano_trabalho --- src/crud.py | 269 +++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 216 insertions(+), 53 deletions(-) diff --git a/src/crud.py b/src/crud.py index d035f26..fe51ce1 100644 --- a/src/crud.py +++ b/src/crud.py @@ -201,88 +201,251 @@ async def _build_plano_trabalho_model( return db_plano -async def create_plano_trabalho( +# async def create_plano_trabalho( +# db_session: DbContextManager, +# plano_trabalho: schemas.PlanoTrabalhoSchema, +# ) -> schemas.PlanoTrabalhoSchema: +# """Cria um plano de trabalho definido pelos dados do schema Pydantic +# plano_trabalho. + +# Args: +# db_session (DbContextManager): Context manager para a sessão +# async do SQL Alchemy. +# plano_trabalho (schemas.PlanoTrabalhoSchema): Dados do plano +# de trabalho como um esquema Pydantic. + +# Returns: +# schemas.PlanoTrabalhoSchema: Esquema Pydantic do Plano de Trabalho +# com os dados que foram gravados no banco. +# """ +# creation_timestamp = datetime.now() +# async with db_session as session: +# db_plano_trabalho = await _build_plano_trabalho_model( +# session, plano_trabalho, creation_timestamp +# ) +# session.add(db_plano_trabalho) +# try: +# await session.commit() +# except IntegrityError as e: +# raise HTTPException( +# status_code=422, +# detail="Alteração rejeitada por violar regras de integridade", +# ) from e +# await session.refresh(db_plano_trabalho) +# return schemas.PlanoTrabalhoSchema.model_validate(db_plano_trabalho) + + +async def upsert_plano_trabalho( db_session: DbContextManager, plano_trabalho: schemas.PlanoTrabalhoSchema, -) -> schemas.PlanoTrabalhoSchema: - """Cria um plano de trabalho definido pelos dados do schema Pydantic - plano_trabalho. - - Args: - db_session (DbContextManager): Context manager para a sessão - async do SQL Alchemy. - plano_trabalho (schemas.PlanoTrabalhoSchema): Dados do plano - de trabalho como um esquema Pydantic. - - Returns: - schemas.PlanoTrabalhoSchema: Esquema Pydantic do Plano de Trabalho - com os dados que foram gravados no banco. - """ - creation_timestamp = datetime.now() - async with db_session as session: - db_plano_trabalho = await _build_plano_trabalho_model( - session, plano_trabalho, creation_timestamp - ) - session.add(db_plano_trabalho) - try: - await session.commit() - except IntegrityError as e: - raise HTTPException( - status_code=422, - detail="Alteração rejeitada por violar regras de integridade", - ) from e - await session.refresh(db_plano_trabalho) - return schemas.PlanoTrabalhoSchema.model_validate(db_plano_trabalho) - - -async def update_plano_trabalho( - db_session: DbContextManager, - plano_trabalho: schemas.PlanoTrabalhoSchema, -) -> schemas.PlanoTrabalhoSchema: - """Atualiza um plano de trabalho conforme os dados recebidos no + update_year_validation_cutoff_date: date, +) -> tuple[schemas.PlanoTrabalhoSchema, bool]: + """Cria ou atualiza um plano de trabalho conforme os dados recebidos no esquema Pydantic em plano_trabalho. - Os dados existentes são primeiro apagados do banco para depois - inserir os dados recebidos. + O registro do plano é gravado com upsert para evitar conflitos de + integridade em transações concorrentes. As contribuições e avaliações + existentes são apagadas e recriadas dentro da mesma transação. Args: db_session (DbContextManager): Context manager para a sessão async do SQL Alchemy. plano_trabalho (schemas.PlanoTrabalhoSchema): Dados do plano de trabalho como um esquema Pydantic. + update_year_validation_cutoff_date (date): Data de corte para aplicar + a validação de período maior que 1 ano. Returns: - schemas.PlanoTrabalhoSchema: Esquema Pydantic do Plano de Trabalho - com o retorno de create_plano_trabalho. + tuple[schemas.PlanoTrabalhoSchema, bool]: Esquema Pydantic do Plano + de Trabalho e booleano indicando se foi criado. """ - creation_timestamp = datetime.now() + timestamp = datetime.now() + plano_values = plano_trabalho.model_dump( + exclude={"contribuicoes", "avaliacoes_registros_execucao"} + ) + plano_values["data_insercao"] = timestamp + plano_identity = { + "origem_unidade_pt": plano_trabalho.origem_unidade, + "cod_unidade_autorizadora_pt": plano_trabalho.cod_unidade_autorizadora, + "id_plano_trabalho": plano_trabalho.id_plano_trabalho, + } + contribuicoes_values = [ + { + **contribuicao.model_dump(), + **plano_identity, + "data_insercao": timestamp, + } + for contribuicao in (plano_trabalho.contribuicoes or []) + ] + avaliacoes_values = [ + { + **avaliacao.model_dump(), + **plano_identity, + "data_insercao": timestamp, + } + for avaliacao in (plano_trabalho.avaliacoes_registros_execucao or []) + ] + async with db_session as session: async with session.begin(): result = await session.execute( - select(models.PlanoTrabalho) + select(models.Participante.matricula_siape) .filter_by(origem_unidade=plano_trabalho.origem_unidade) .filter_by( cod_unidade_autorizadora=plano_trabalho.cod_unidade_autorizadora ) - .filter_by(id_plano_trabalho=plano_trabalho.id_plano_trabalho) + .filter_by(matricula_siape=plano_trabalho.matricula_siape) + .filter_by( + cod_unidade_lotacao=plano_trabalho.cod_unidade_lotacao_participante + ) ) - db_plano_trabalho = result.unique().scalar_one() - await session.delete(db_plano_trabalho) - await session.flush() + participante_exists = result.scalar_one_or_none() + if not participante_exists: + raise ValueError( + "Plano de Trabalho faz referência a participante inexistente.\n" + f" origem_unidade: {plano_trabalho.origem_unidade}\n" + f" cod_unidade_autorizadora: " + f"{plano_trabalho.cod_unidade_autorizadora}\n" + f" matricula_siape: {plano_trabalho.matricula_siape}\n" + f" cod_unidade_lotacao: " + f"{plano_trabalho.cod_unidade_lotacao_participante}" + ) + + for contribuicao in plano_trabalho.contribuicoes or []: + if ( + contribuicao.tipo_contribuicao == 1 + and contribuicao.id_plano_entregas + and contribuicao.id_entrega + ): + result = await session.execute( + select(models.Entrega) + .filter_by(origem_unidade=plano_trabalho.origem_unidade) + .filter_by( + cod_unidade_autorizadora=( + plano_trabalho.cod_unidade_autorizadora + ) + ) + .filter_by(id_plano_entregas=contribuicao.id_plano_entregas) + .filter_by(id_entrega=contribuicao.id_entrega) + ) + db_entrega = result.scalars().unique().one_or_none() + if not db_entrega: + raise ValueError( + "Contribuição do Plano de Trabalho faz referência a " + "entrega inexistente. " + f"origem_unidade: {plano_trabalho.origem_unidade} " + f"cod_unidade_autorizadora: " + f"{plano_trabalho.cod_unidade_autorizadora} " + f"id_plano_entregas: {contribuicao.id_plano_entregas} " + f"id_entrega: {contribuicao.id_entrega}" + ) - db_plano_atualizado = await _build_plano_trabalho_model( - session, plano_trabalho, creation_timestamp + result = await session.execute( + select(models.PlanoTrabalho.id_plano_trabalho) + .filter_by(origem_unidade=plano_trabalho.origem_unidade) + .filter_by( + cod_unidade_autorizadora=plano_trabalho.cod_unidade_autorizadora + ) + .filter_by(id_plano_trabalho=plano_trabalho.id_plano_trabalho) ) - session.add(db_plano_atualizado) + plano_exists = result.scalar_one_or_none() is not None + created = not plano_exists + if ( + over_a_year(plano_trabalho.data_inicio, plano_trabalho.data_termino) + == 1 + ): + if ( + not plano_exists + or plano_trabalho.data_inicio > update_year_validation_cutoff_date + ): + raise HTTPException( + status_code=422, + detail=( + "Plano de trabalho não pode abranger período " + "maior que 1 ano" + ), + ) + try: - await session.flush() + insert_stmt = insert(models.PlanoTrabalho).values(**plano_values) + update_values = { + column.name: insert_stmt.excluded[column.name] + for column in models.PlanoTrabalho.__table__.columns + if ( + not column.primary_key + and column.name not in ("data_insercao", "data_atualizacao") + ) + } + update_values["data_atualizacao"] = timestamp + await session.execute( + insert_stmt.on_conflict_do_update( + index_elements=[ + "origem_unidade", + "cod_unidade_autorizadora", + "id_plano_trabalho", + ], + set_=update_values, + ) + ) + + await session.execute( + models.Contribuicao.__table__.delete().where( + models.Contribuicao.origem_unidade_pt + == plano_trabalho.origem_unidade, + models.Contribuicao.cod_unidade_autorizadora_pt + == plano_trabalho.cod_unidade_autorizadora, + models.Contribuicao.id_plano_trabalho + == plano_trabalho.id_plano_trabalho, + ) + ) + await session.execute( + models.AvaliacaoRegistrosExecucao.__table__.delete().where( + models.AvaliacaoRegistrosExecucao.origem_unidade_pt + == plano_trabalho.origem_unidade, + models.AvaliacaoRegistrosExecucao.cod_unidade_autorizadora_pt + == plano_trabalho.cod_unidade_autorizadora, + models.AvaliacaoRegistrosExecucao.id_plano_trabalho + == plano_trabalho.id_plano_trabalho, + ) + ) + if contribuicoes_values: + await session.execute( + insert(models.Contribuicao), contribuicoes_values + ) + if avaliacoes_values: + await session.execute( + insert(models.AvaliacaoRegistrosExecucao), avaliacoes_values + ) except IntegrityError as e: raise HTTPException( status_code=422, detail="Alteração rejeitada por violar regras de integridade", ) from e - await session.refresh(db_plano_atualizado) - return schemas.PlanoTrabalhoSchema.model_validate(db_plano_atualizado) + + result = await session.execute( + select(models.PlanoTrabalho) + .filter_by(origem_unidade=plano_trabalho.origem_unidade) + .filter_by( + cod_unidade_autorizadora=plano_trabalho.cod_unidade_autorizadora + ) + .filter_by(id_plano_trabalho=plano_trabalho.id_plano_trabalho) + ) + db_plano_atualizado = result.unique().scalar_one() + + return schemas.PlanoTrabalhoSchema.model_validate(db_plano_atualizado), created + + +# async def update_plano_trabalho( +# db_session: DbContextManager, +# plano_trabalho: schemas.PlanoTrabalhoSchema, +# ) -> schemas.PlanoTrabalhoSchema: +# """Atualiza um plano de trabalho pelo fluxo único de upsert.""" +# db_plano_trabalho, _ = await upsert_plano_trabalho( +# db_session=db_session, +# plano_trabalho=plano_trabalho, +# update_year_validation_cutoff_date=date.max, +# ) +# return db_plano_trabalho async def get_plano_entregas( From f80008b871bb756837f862a753c9da042558ffbc Mon Sep 17 00:00:00 2001 From: Eduardo Lauer Date: Tue, 7 Jul 2026 17:09:31 -0300 Subject: [PATCH 3/6] use upsert for plano_entregas --- src/crud.py | 162 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 117 insertions(+), 45 deletions(-) diff --git a/src/crud.py b/src/crud.py index fe51ce1..e62d203 100644 --- a/src/crud.py +++ b/src/crud.py @@ -565,82 +565,154 @@ def _build_plano_entregas_model( return db_plano_entregas -async def create_plano_entregas( - db_session: DbContextManager, - plano_entregas: schemas.PlanoEntregasSchema, -) -> schemas.PlanoEntregasSchema: - """Cria um plano de trabalho definido pelos dados do schema Pydantic - plano_entregas. +# async def create_plano_entregas( +# db_session: DbContextManager, +# plano_entregas: schemas.PlanoEntregasSchema, +# ) -> schemas.PlanoEntregasSchema: +# """Cria um plano de trabalho definido pelos dados do schema Pydantic +# plano_entregas. - Args: - db_session (DbContextManager): Context manager para a sessão - async do SQL Alchemy. - plano_entregas (schemas.PlanoEntregasSchema): Dados do plano - de entregas como um esquema Pydantic. +# Args: +# db_session (DbContextManager): Context manager para a sessão +# async do SQL Alchemy. +# plano_entregas (schemas.PlanoEntregasSchema): Dados do plano +# de entregas como um esquema Pydantic. - Returns: - schemas.PlanoEntregasSchema: Esquema Pydantic do Plano de Entregas - com os dados que foram gravados no banco. - """ - creation_timestamp = datetime.now() - db_plano_entregas = _build_plano_entregas_model(plano_entregas, creation_timestamp) - async with db_session as session: - for entrega in db_plano_entregas.entregas: - session.add(entrega) - session.add(db_plano_entregas) - await session.commit() - await session.refresh(db_plano_entregas) - return schemas.PlanoEntregasSchema.model_validate(db_plano_entregas) +# Returns: +# schemas.PlanoEntregasSchema: Esquema Pydantic do Plano de Entregas +# com os dados que foram gravados no banco. +# """ +# creation_timestamp = datetime.now() +# db_plano_entregas = _build_plano_entregas_model(plano_entregas, creation_timestamp) +# async with db_session as session: +# for entrega in db_plano_entregas.entregas: +# session.add(entrega) +# session.add(db_plano_entregas) +# await session.commit() +# await session.refresh(db_plano_entregas) +# return schemas.PlanoEntregasSchema.model_validate(db_plano_entregas) -async def update_plano_entregas( +async def upsert_plano_entregas( db_session: DbContextManager, plano_entregas: schemas.PlanoEntregasSchema, -) -> schemas.PlanoEntregasSchema: - """Atualiza um plano de entregas conforme os dados recebidos no + update_year_validation_cutoff_date: date, +) -> tuple[schemas.PlanoEntregasSchema, bool]: + """Cria ou atualiza um plano de entregas conforme os dados recebidos no esquema Pydantic em plano_entregas. - Os dados existentes são primeiro apagados do banco para depois - inserir os dados recebidos. + O registro do plano é gravado com upsert para evitar conflitos de + integridade em transações concorrentes. As entregas existentes são + apagadas e recriadas dentro da mesma transação. Args: db_session (DbContextManager): Context manager para a sessão async do SQL Alchemy. plano_entregas (schemas.PlanoEntregasSchema): Dados do plano de entregas como um esquema Pydantic. + update_year_validation_cutoff_date (date): Data de corte para aplicar + a validação de período maior que 1 ano também nas atualizações. Returns: - schemas.PlanoEntregasSchema: Esquema Pydantic do Plano de Entregas - com o retorno de create_plano_entregas. + tuple[schemas.PlanoEntregasSchema, bool]: Esquema Pydantic do Plano + de Entregas e booleano indicando se foi criado. """ - creation_timestamp = datetime.now() - db_plano_entregas_atualizado = _build_plano_entregas_model( - plano_entregas, creation_timestamp - ) + timestamp = datetime.now() + plano_values = plano_entregas.model_dump(exclude={"entregas"}) + plano_values["data_insercao"] = timestamp + entrega_identity = { + "origem_unidade": plano_entregas.origem_unidade, + "cod_unidade_autorizadora": plano_entregas.cod_unidade_autorizadora, + "id_plano_entregas": plano_entregas.id_plano_entregas, + } + entregas_values = [ + { + **entrega.model_dump(), + **entrega_identity, + "data_insercao": timestamp, + } + for entrega in plano_entregas.entregas + ] async with db_session as session: async with session.begin(): result = await session.execute( - select(models.PlanoEntregas) + select(models.PlanoEntregas.id) .filter_by(origem_unidade=plano_entregas.origem_unidade) .filter_by( cod_unidade_autorizadora=plano_entregas.cod_unidade_autorizadora ) .filter_by(id_plano_entregas=plano_entregas.id_plano_entregas) ) - db_plano_entregas = result.unique().scalar_one() - await session.delete(db_plano_entregas) - await session.flush() + plano_exists = result.scalar_one_or_none() is not None + created = not plano_exists + if over_a_year(plano_entregas.data_inicio, plano_entregas.data_termino) == 1: + if ( + not plano_exists + or plano_entregas.data_inicio > update_year_validation_cutoff_date + ): + raise HTTPException( + status_code=422, + detail="Plano de entregas não pode abranger período maior que 1 ano", + ) + + insert_stmt = insert(models.PlanoEntregas).values(**plano_values) + update_values = { + column.name: insert_stmt.excluded[column.name] + for column in models.PlanoEntregas.__table__.columns + if column.name not in ("id", "data_insercao", "data_atualizacao") + } + update_values["data_atualizacao"] = timestamp + await session.execute( + insert_stmt.on_conflict_do_update( + index_elements=[ + "origem_unidade", + "cod_unidade_autorizadora", + "id_plano_entregas", + ], + set_=update_values, + ) + ) - for entrega in db_plano_entregas_atualizado.entregas: - session.add(entrega) + await session.execute( + models.Entrega.__table__.delete().where( + models.Entrega.origem_unidade == plano_entregas.origem_unidade, + models.Entrega.cod_unidade_autorizadora + == plano_entregas.cod_unidade_autorizadora, + models.Entrega.id_plano_entregas + == plano_entregas.id_plano_entregas, + ) + ) + if entregas_values: + await session.execute(insert(models.Entrega), entregas_values) + + result = await session.execute( + select(models.PlanoEntregas) + .filter_by(origem_unidade=plano_entregas.origem_unidade) + .filter_by( + cod_unidade_autorizadora=plano_entregas.cod_unidade_autorizadora + ) + .filter_by(id_plano_entregas=plano_entregas.id_plano_entregas) + ) + db_plano_entregas_atualizado = result.unique().scalar_one() - session.add(db_plano_entregas_atualizado) + return ( + schemas.PlanoEntregasSchema.model_validate(db_plano_entregas_atualizado), + created, + ) - await session.flush() - await session.refresh(db_plano_entregas_atualizado) - return schemas.PlanoEntregasSchema.model_validate(db_plano_entregas_atualizado) +# async def update_plano_entregas( +# db_session: DbContextManager, +# plano_entregas: schemas.PlanoEntregasSchema, +# ) -> schemas.PlanoEntregasSchema: +# """Atualiza um plano de entregas pelo fluxo único de upsert.""" +# db_plano_entregas, _ = await upsert_plano_entregas( +# db_session=db_session, +# plano_entregas=plano_entregas, +# update_year_validation_cutoff_date=date.max, +# ) +# return db_plano_entregas async def get_participante( From b31bbee7af6148df02b30f27227a842ebe822eae Mon Sep 17 00:00:00 2001 From: Eduardo Lauer Date: Tue, 7 Jul 2026 17:11:24 -0300 Subject: [PATCH 4/6] move validation rules to crud --- src/api.py | 78 +++++++++++------------------------------------------- 1 file changed, 16 insertions(+), 62 deletions(-) diff --git a/src/api.py b/src/api.py index 8d77e13..d17e0d6 100644 --- a/src/api.py +++ b/src/api.py @@ -29,7 +29,7 @@ import email_config import response_schemas import schemas -from util import check_permissions, over_a_year +from util import check_permissions DEFAULT_TOKEN_EXPIRE_MINS = 30 ACCESS_TOKEN_EXPIRE_MINUTES = int( @@ -580,38 +580,14 @@ async def create_or_update_plano_entregas( ) raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, detail=detail_msg) - # Verifica se já existe - db_plano_entregas = await crud.get_plano_entregas( - db_session=db, - origem_unidade=origem_unidade, - cod_unidade_autorizadora=cod_unidade_autorizadora, - id_plano_entregas=id_plano_entregas, - ) - try: - if not db_plano_entregas: # create - if over_a_year(plano_entregas.data_inicio, plano_entregas.data_termino) == 1: - detail_msg = ( - "Plano de entregas não pode abranger período maior que 1 ano" - ) - raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, detail=detail_msg) - - novo_plano_entregas = await crud.create_plano_entregas( - db_session=db, - plano_entregas=novo_plano_entregas, - ) + novo_plano_entregas, created = await crud.upsert_plano_entregas( + db_session=db, + plano_entregas=novo_plano_entregas, + update_year_validation_cutoff_date=PT_PE_UPDATE_YEAR_VALIDATION_CUTOFF_DATE, + ) + if created: response.status_code = status.HTTP_201_CREATED - else: # update - if over_a_year(plano_entregas.data_inicio, plano_entregas.data_termino) == 1 and \ - plano_entregas.data_inicio > PT_PE_UPDATE_YEAR_VALIDATION_CUTOFF_DATE: - detail_msg = ( - "Plano de entregas não pode abranger período maior que 1 ano" - ) - raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, detail=detail_msg) - novo_plano_entregas = await crud.update_plano_entregas( - db_session=db, - plano_entregas=novo_plano_entregas, - ) return novo_plano_entregas except IntegrityError as exception: raise HTTPException( @@ -725,39 +701,17 @@ async def create_or_update_plano_trabalho( ) raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, detail=detail_msg) - # Verifica se já existe - db_plano_trabalho = await crud.get_plano_trabalho( - db_session=db, - origem_unidade=origem_unidade, - cod_unidade_autorizadora=cod_unidade_autorizadora, - id_plano_trabalho=id_plano_trabalho, - ) - try: - if not db_plano_trabalho: # create - if over_a_year(plano_trabalho.data_inicio, plano_trabalho.data_termino) == 1: - detail_msg = ( - "Plano de trabalho não pode abranger período maior que 1 ano" - ) - raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, detail=detail_msg) - - novo_plano_trabalho = await crud.create_plano_trabalho( - db_session=db, - plano_trabalho=novo_plano_trabalho, - ) + novo_plano_trabalho, created = await crud.upsert_plano_trabalho( + db_session=db, + plano_trabalho=novo_plano_trabalho, + update_year_validation_cutoff_date=( + PT_PE_UPDATE_YEAR_VALIDATION_CUTOFF_DATE + ), + ) + if created: response.status_code = status.HTTP_201_CREATED - else: # update - if over_a_year(plano_trabalho.data_inicio, plano_trabalho.data_termino) == 1 and \ - plano_trabalho.data_inicio > PT_PE_UPDATE_YEAR_VALIDATION_CUTOFF_DATE: - detail_msg = ( - "Plano de trabalho não pode abranger período maior que 1 ano" - ) - raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, detail=detail_msg) - - novo_plano_trabalho = await crud.update_plano_trabalho( - db_session=db, - plano_trabalho=novo_plano_trabalho, - ) + else: response.status_code = status.HTTP_200_OK except IntegrityError as exception: raise HTTPException( From 8e8d6fbbba84309d2abad567a814ac2fa22208dd Mon Sep 17 00:00:00 2001 From: Eduardo Lauer Date: Tue, 7 Jul 2026 17:12:31 -0300 Subject: [PATCH 5/6] create test por PE data_insercao and data_atualizacao --- tests/plano_entregas/core_test.py | 43 +++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/plano_entregas/core_test.py b/tests/plano_entregas/core_test.py index 54ab1f2..d6256bf 100644 --- a/tests/plano_entregas/core_test.py +++ b/tests/plano_entregas/core_test.py @@ -9,7 +9,9 @@ from fastapi import status as http_status import pytest +from sqlalchemy import text +from db_config import sync_engine from util import assert_error_message from ..conftest import MAX_BIGINT @@ -232,6 +234,47 @@ def test_update_plano_entregas(self, example_pe): # pylint: disable=unused-argu assert response.json()["avaliacao"] == 3 assert response.json()["data_avaliacao"] == "2024-08-15" + def test_plano_entregas_datas_insercao_atualizacao(self): + """Verifica datas técnicas de criação e atualização do plano.""" + + response = self.put_plano_entregas(self.input_pe) + assert response.status_code == http_status.HTTP_201_CREATED + + query = text( + """ + SELECT data_insercao, data_atualizacao + FROM plano_entregas + WHERE origem_unidade = :origem_unidade + AND cod_unidade_autorizadora = :cod_unidade_autorizadora + AND id_plano_entregas = :id_plano_entregas + """ + ) + params = { + "origem_unidade": self.input_pe["origem_unidade"], + "cod_unidade_autorizadora": self.input_pe["cod_unidade_autorizadora"], + "id_plano_entregas": self.input_pe["id_plano_entregas"], + } + + with sync_engine.connect() as conn: + db_plano = conn.execute(query, params).mappings().one() + + data_insercao = db_plano["data_insercao"] + assert data_insercao is not None + assert db_plano["data_atualizacao"] is None + + input_pe = deepcopy(self.input_pe) + input_pe["avaliacao"] = 3 + input_pe["data_avaliacao"] = "2024-08-15" + response = self.put_plano_entregas(input_pe) + assert response.status_code == http_status.HTTP_200_OK + + with sync_engine.connect() as conn: + db_plano = conn.execute(query, params).mappings().one() + + assert db_plano["data_insercao"] == data_insercao + assert db_plano["data_atualizacao"] is not None + assert db_plano["data_atualizacao"] >= data_insercao + @pytest.mark.parametrize("omitted_fields", enumerate(FIELDS_ENTREGA["optional"])) def test_create_plano_entregas_entrega_omit_optional_fields(self, omitted_fields): """Tenta criar um novo Plano de Entregas omitindo campos opcionais. From 8cb4c49587247ae960e8f5a24ec4914db8a63e62 Mon Sep 17 00:00:00 2001 From: Eduardo Lauer Date: Tue, 7 Jul 2026 17:12:42 -0300 Subject: [PATCH 6/6] create test por PT data_insercao and data_atualizacao --- tests/plano_trabalho/core_test.py | 43 +++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/plano_trabalho/core_test.py b/tests/plano_trabalho/core_test.py index 852a9a0..64f61a8 100644 --- a/tests/plano_trabalho/core_test.py +++ b/tests/plano_trabalho/core_test.py @@ -10,7 +10,9 @@ from fastapi import status import pytest +from sqlalchemy import text +from db_config import sync_engine from util import assert_error_message from ..conftest import MAX_INT, MAX_BIGINT @@ -249,6 +251,47 @@ def test_completo(self): assert response.status_code == status.HTTP_200_OK self.assert_equal_plano_trabalho(response.json(), self.input_pt) + def test_plano_trabalho_datas_insercao_atualizacao(self): + """Verifica datas técnicas de criação e atualização do plano.""" + + response = self.put_plano_trabalho(self.input_pt) + assert response.status_code == status.HTTP_201_CREATED + + query = text( + """ + SELECT data_insercao, data_atualizacao + FROM plano_trabalho + WHERE origem_unidade = :origem_unidade + AND cod_unidade_autorizadora = :cod_unidade_autorizadora + AND id_plano_trabalho = :id_plano_trabalho + """ + ) + params = { + "origem_unidade": self.input_pt["origem_unidade"], + "cod_unidade_autorizadora": self.input_pt["cod_unidade_autorizadora"], + "id_plano_trabalho": self.input_pt["id_plano_trabalho"], + } + + with sync_engine.connect() as conn: + db_plano = conn.execute(query, params).mappings().one() + + data_insercao = db_plano["data_insercao"] + assert data_insercao is not None + assert db_plano["data_atualizacao"] is None + + input_pt = deepcopy(self.input_pt) + input_pt["status"] = 4 + input_pt["data_termino"] = "2024-06-30" + response = self.put_plano_trabalho(input_pt) + assert response.status_code == status.HTTP_200_OK + + with sync_engine.connect() as conn: + db_plano = conn.execute(query, params).mappings().one() + + assert db_plano["data_insercao"] == data_insercao + assert db_plano["data_atualizacao"] is not None + assert db_plano["data_atualizacao"] >= data_insercao + @pytest.mark.parametrize( "missing_fields", enumerate(FIELDS_PLANO_TRABALHO["mandatory"]) )