From dd44e50fb0b3ec09f2dcc29b59540dd0fc1d333f Mon Sep 17 00:00:00 2001 From: Wellyson Freitas Date: Fri, 26 Jul 2024 13:28:12 +0200 Subject: [PATCH] Add unit & integration tests, OpenAPI, minor fixes --- README.md | 28 +- docs/003-bancos-de-dados.md | 4 +- docs/004-estrategia-de-testes.md | 2 +- docs/README.md | 32 +- docs/openapi.json | 1 + docs/openapi.yaml | 577 ++++++++++++++++++ pom.xml | 4 +- .../kotlin/com/fiap/healthmed/HealthMedApp.kt | 6 +- .../config/ControllerExceptionHandler.kt | 2 +- .../adapter/gateway/DoctorGateway.kt | 2 +- .../adapter/gateway/impl/DoctorGatewayImpl.kt | 2 +- .../impl/MedicalAppointmentGatewayImpl.kt | 2 +- .../healthmed/domain/MedicalAppointment.kt | 2 +- .../fiap/healthmed/domain/errors/ErrorType.kt | 9 +- .../mapper/MedicalAppointmentMapper.kt | 1 - .../persistence/mapper/MedicalRecordMapper.kt | 1 - .../fiap/healthmed/driver/web/DoctorApi.kt | 7 +- .../fiap/healthmed/driver/web/PatientApi.kt | 4 +- .../service/MedicalAppointmentService.kt | 1 + .../usecases/service/MedicalRecordService.kt | 4 +- ...itional-spring-configuration-metadata.json | 30 - src/main/resources/application-openapi.yml | 3 - src/main/resources/application-test.yml | 3 - .../com/fiap/healthmed/TestAnnotations.kt | 10 + .../kotlin/com/fiap/healthmed/TestFixtures.kt | 84 +++ .../com/fiap/healthmed/it/CommonSteps.kt | 33 + .../com/fiap/healthmed/it/IntegrationTest.kt | 12 + .../healthmed/it/IntegrationTestFixtures.kt | 65 ++ .../it/PostgresContainerInitiliazer.kt | 29 + .../it/RegisterDoctorIntegrationTest.kt | 75 +++ .../it/RegisterPatientIntegrationTest.kt | 66 ++ .../com/fiap/healthmed/it/RunCucumberIT.kt | 16 + .../usecases/service/DoctorServiceTest.kt | 131 ++++ .../service/MedicalAppointmentServiceTest.kt | 143 +++++ .../service/MedicalRecordServiceTest.kt | 62 ++ .../usecases/service/PatientServiceTest.kt | 45 ++ src/test/resources/features/Doctor.feature | 7 + src/test/resources/features/Patient.feature | 7 + 38 files changed, 1442 insertions(+), 70 deletions(-) create mode 100644 docs/openapi.json create mode 100644 docs/openapi.yaml create mode 100644 src/test/kotlin/com/fiap/healthmed/TestAnnotations.kt create mode 100644 src/test/kotlin/com/fiap/healthmed/TestFixtures.kt create mode 100644 src/test/kotlin/com/fiap/healthmed/it/CommonSteps.kt create mode 100644 src/test/kotlin/com/fiap/healthmed/it/IntegrationTest.kt create mode 100644 src/test/kotlin/com/fiap/healthmed/it/IntegrationTestFixtures.kt create mode 100644 src/test/kotlin/com/fiap/healthmed/it/PostgresContainerInitiliazer.kt create mode 100644 src/test/kotlin/com/fiap/healthmed/it/RegisterDoctorIntegrationTest.kt create mode 100644 src/test/kotlin/com/fiap/healthmed/it/RegisterPatientIntegrationTest.kt create mode 100644 src/test/kotlin/com/fiap/healthmed/it/RunCucumberIT.kt create mode 100644 src/test/kotlin/com/fiap/healthmed/usecases/service/DoctorServiceTest.kt create mode 100644 src/test/kotlin/com/fiap/healthmed/usecases/service/MedicalAppointmentServiceTest.kt create mode 100644 src/test/kotlin/com/fiap/healthmed/usecases/service/MedicalRecordServiceTest.kt create mode 100644 src/test/kotlin/com/fiap/healthmed/usecases/service/PatientServiceTest.kt create mode 100644 src/test/resources/features/Doctor.feature create mode 100644 src/test/resources/features/Patient.feature diff --git a/README.md b/README.md index 8748bb6..075448b 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ > Acesse o [website](http://fiap-3soat-g15-healthmed.s3-website-us-east-1.amazonaws.com) da nossa documentação! ✨ > -> Publicamos nossa documentação automaticamente em nosso [website]((http://fiap-3soat-g15-healthmed.s3-website-us-east-1.amazonaws.com)). Lá você encontrará todas as informações, documentos e diagramas a respeito da nossa documentação e do nosso MVP de uma forma intuituva, agradável, e indexada, permitindo buscas eficientes. Boa leitura! +> Publicamos nossa documentação automaticamente em nosso [website](http://fiap-3soat-g15-healthmed.s3-website-us-east-1.amazonaws.com). Lá você encontrará todas as informações, documentos e diagramas a respeito da nossa documentação e do nosso MVP de uma forma intuituva, agradável, e indexada, permitindo buscas eficientes. Boa leitura! -Grupo 15: +**Grupo 15:** - Bleno Humberto Claus - Giovanni Di Luca Evangelista @@ -21,9 +21,9 @@ Grupo 15: [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=FIAP-3SOAT-G15_healthmed-app&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=FIAP-3SOAT-G15_healthmed-app) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=FIAP-3SOAT-G15_healthmed-app&metric=coverage)](https://sonarcloud.io/summary/new_code?id=FIAP-3SOAT-G15_healthmed-app) -Nosso MVP é uma versão simplificada para fins de demonstração dos requisitos atendidos durante o Hackathon. Compreende um monolito com um banco de dados relacional e uso de serviços serverless na AWS. Toda a infraestrutura descrita em Terraform e a imagem da aplicação são automatizados em nossas pipelines com GitHub Actions, que inclui verificação dos testes e análise estática no SonarQube. +Nosso MVP é uma versão simplificada para fins de demonstração dos requisitos atendidos durante o Hackathon. Compreende um monolito com um banco de dados relacional e uso de serviços serverless na AWS. Toda a infraestrutura descrita em Terraform e a imagem da aplicação são automatizados em nossas pipelines com GitHub Actions, que inclui verificação dos testes e análise estática no Sonar. -Projeto no SonarCloud: https://sonarcloud.io/project/overview?id=FIAP-3SOAT-G15_healthmed-app +Projeto no SonarCloud: [https://sonarcloud.io/project/overview?id=FIAP-3SOAT-G15_healthmed-app](https://sonarcloud.io/project/overview?id=FIAP-3SOAT-G15_healthmed-app) Também criamos uma extensa documentação para suportar uma aplicação escalável e resiliente que atenda a todos os requisitos não funcionais. @@ -45,6 +45,12 @@ Para viabilizar o desenvolvimento de um sistema que esteja em conformidade com a - [Storytelling](/docs/ddd.md#storytelling) - [Event Storming](/docs/ddd.md#event-storming) +## Volumetria Estimada + +- **usuários:** 20.000 usuários simultâneos usando em horários de pico sem problemas na experiência do usuário. +- **consultas:** 10.000 consultas diárias (2 consultas x 5000 médicos x dia), cada consulta gerando um link de reunião online com duração padrão de 50 minutos, suportando centenas de sessões simultâneas. +- **prontuário eletrônico:** milhões de documentos variados, de até 100 MB por arquivo. + ## Decisões de Arquitetura 1. [Infraestrutura](/docs/001-infraestrutura.md) @@ -135,3 +141,17 @@ mvn clean verify -DskipITs=false ``` mvn antrun:run@ktlint-format ``` + +## OpenAPI (Swagger) + +Acesse localmente: + +- UI: [http://localhost:8080/swagger-ui/index.html]() +- JSON: [http://localhost:8080/v3/api-docs]() ([baixar](docs/openapi.json)) +- YAML: [http://localhost:8080/v3/api-docs.yaml]() ([baixar](docs/openapi.yaml)) + +Geração da documentação: + +``` +mvn -B verify -DskipOpenAPIGen=false +``` diff --git a/docs/003-bancos-de-dados.md b/docs/003-bancos-de-dados.md index 0aeeeb4..2aa4679 100644 --- a/docs/003-bancos-de-dados.md +++ b/docs/003-bancos-de-dados.md @@ -12,7 +12,7 @@ Utilizaremos Amazon Aurora com PostgreSQL para os serviços de cadastro e agenda ### Positivas -### Amazon Aurora (PostgreSQL): +#### Amazon Aurora (PostgreSQL): - melhor custo-benefício em comparação com RDS de acordo com a calculadora de custo da AWS. - alta disponibilidade e desempenho com replicação automática. @@ -20,7 +20,7 @@ Utilizaremos Amazon Aurora com PostgreSQL para os serviços de cadastro e agenda - ACID. - indexes, sharding, e outras opções para performance. -### Amazon DynamoDB: +#### Amazon DynamoDB: - escalabilidade horizontal automática. - baixa latência e alta performance. diff --git a/docs/004-estrategia-de-testes.md b/docs/004-estrategia-de-testes.md index 4f44e25..a17ee0a 100644 --- a/docs/004-estrategia-de-testes.md +++ b/docs/004-estrategia-de-testes.md @@ -13,7 +13,7 @@ Adotaremos uma abordagem de testes baseada na pirâmide de testes, que inclui: - testes end-to-end - testes de performance / carga (ex.: usando Gatling) -Os testes unitários e de integração serão executados nas pipelines de CI, e um quality gate de 80% será definido (com o SonarQube) como requisito. BDD também será utilizado. +Os testes unitários e de integração serão executados nas pipelines de CI, e um quality gate de 80% será definido (com o Sonar) como requisito. BDD também será utilizado. - Implementação Testes Unitários: diff --git a/docs/README.md b/docs/README.md index d00ab98..fbb533c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,7 +2,7 @@ -Grupo 15: +**Grupo 15:** - Bleno Humberto Claus - Giovanni Di Luca Evangelista @@ -15,9 +15,9 @@ Grupo 15: [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=FIAP-3SOAT-G15_healthmed-app&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=FIAP-3SOAT-G15_healthmed-app) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=FIAP-3SOAT-G15_healthmed-app&metric=coverage)](https://sonarcloud.io/summary/new_code?id=FIAP-3SOAT-G15_healthmed-app) -Nosso MVP é uma versão simplificada para fins de demonstração dos requisitos atendidos durante o Hackathon. Compreende um monolito com um banco de dados relacional e uso de serviços serverless na AWS. Toda a infraestrutura descrita em Terraform e a imagem da aplicação são automatizados em nossas pipelines com GitHub Actions, que inclui verificação dos testes e análise estática no SonarQube. +Nosso MVP é uma versão simplificada para fins de demonstração dos requisitos atendidos durante o Hackathon. Compreende um monolito com um banco de dados relacional e uso de serviços serverless na AWS. Toda a infraestrutura descrita em Terraform e a imagem da aplicação são automatizados em nossas pipelines com GitHub Actions, que inclui verificação dos testes e análise estática no Sonar. -Projeto no SonarCloud: https://sonarcloud.io/project/overview?id=FIAP-3SOAT-G15_healthmed-app +Projeto no SonarCloud: [https://sonarcloud.io/project/overview?id=FIAP-3SOAT-G15_healthmed-app](https://sonarcloud.io/project/overview?id=FIAP-3SOAT-G15_healthmed-app) Também criamos uma extensa documentação para suportar uma aplicação escalável e resiliente que atenda a todos os requisitos não funcionais. @@ -123,6 +123,18 @@ Itens adicionais que serão diferenciais para a classificação dos grupos: - Documentação abrangente de todos os componentes e níveis da solução. - Automatização do processo de gerenciamento e alteração de infraestrutura. +## DDD + +- [Glossário Ubíquo](ddd.md#glossario-ubiquo) +- [Storytelling](ddd.md#storytelling) +- [Event Storming](ddd.md#event-storming) + +## Volumetria Estimada + +- **usuários:** 20.000 usuários simultâneos usando em horários de pico sem problemas na experiência do usuário. +- **consultas:** 10.000 consultas diárias (2 consultas x 5000 médicos x dia), cada consulta gerando um link de reunião online com duração padrão de 50 minutos, suportando centenas de sessões simultâneas. +- **prontuário eletrônico:** milhões de documentos variados, de até 100 MB por arquivo. + ## Decisões de Arquitetura 1. [Infraestrutura](001-infraestrutura.md) @@ -207,3 +219,17 @@ mvn clean verify -DskipITs=false ``` mvn antrun:run@ktlint-format ``` + +## OpenAPI (Swagger) + +Acesse localmente: + +- UI: [http://localhost:8080/swagger-ui/index.html]() +- JSON: [http://localhost:8080/v3/api-docs]() ([baixar](openapi.json)) +- YAML: [http://localhost:8080/v3/api-docs.yaml]() ([baixar](openapi.yaml)) + +Geração da documentação: + +``` +mvn -B verify -DskipOpenAPIGen=false +``` diff --git a/docs/openapi.json b/docs/openapi.json new file mode 100644 index 0000000..785a1de --- /dev/null +++ b/docs/openapi.json @@ -0,0 +1 @@ +{"openapi":"3.0.1","info":{"title":"Health&Med MVP","description":"Demo API para sistema de consultas médicas online","contact":{"name":"Grupo 15","url":"http://fiap-3soat-g15-healthmed.s3-website-us-east-1.amazonaws.com"},"version":"1.0.0"},"servers":[{"url":"/"}],"paths":{"/healthmed/patient/{document}":{"put":{"tags":["patient-controller"],"summary":"Atualizar um cadastro de um paciente","operationId":"update","parameters":[{"name":"document","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatientRequest"}}},"required":true},"responses":{"200":{"description":"Operação bem-sucedida","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Patient"}}}}}}},"/healthmed/doctor/{crm}":{"get":{"tags":["doctor-controller"],"operationId":"get","parameters":[{"name":"crm","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Doctor"}}}}}},"put":{"tags":["doctor-controller"],"summary":"Atualizar um cadastro de um médico","operationId":"update_1","parameters":[{"name":"crm","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DoctorRequest"}}},"required":true},"responses":{"200":{"description":"Operação bem-sucedida","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Doctor"}}}}}}},"/healthmed/patient":{"post":{"tags":["patient-controller"],"summary":"Cria um cadastro de um paciente","operationId":"create","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatientRequest"}}},"required":true},"responses":{"200":{"description":"Operação bem-sucedida","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Patient"}}}}}}},"/healthmed/medical-record/{appointmentNumber}/append":{"post":{"tags":["medical-record-controller"],"operationId":"appendAnnotations","parameters":[{"name":"appointmentNumber","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MedicalRecordContentRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MedicalAppointment"}}}}}}},"/healthmed/medical-appointment/{document}/{crm}":{"post":{"tags":["medical-appointment-by-patient-controller"],"operationId":"scheduleAppointment","parameters":[{"name":"document","in":"path","required":true,"schema":{"type":"string"}},{"name":"crm","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeAndDateToScheduleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MedicalAppointment"}}}}}}},"/healthmed/medical-appointment/{document}/cancel/{appointmentNumber}":{"post":{"tags":["medical-appointment-by-patient-controller"],"operationId":"cancelAppointment","parameters":[{"name":"document","in":"path","required":true,"schema":{"type":"string"}},{"name":"appointmentNumber","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JustificationCancellationRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MedicalAppointment"}}}}}}},"/healthmed/medical-appointment/{crm}/reject/{appointmentNumber}":{"post":{"tags":["medical-appointment-by-doctor-controller"],"operationId":"rejectAppointment","parameters":[{"name":"crm","in":"path","required":true,"schema":{"type":"string"}},{"name":"appointmentNumber","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MedicalAppointment"}}}}}}},"/healthmed/medical-appointment/{crm}/cancel/{appointmentNumber}":{"post":{"tags":["medical-appointment-by-doctor-controller"],"operationId":"acceptAppointment","parameters":[{"name":"crm","in":"path","required":true,"schema":{"type":"string"}},{"name":"appointmentNumber","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MedicalAppointment"}}}}}}},"/healthmed/doctor":{"post":{"tags":["doctor-controller"],"summary":"Cria um cadastro de um médico","operationId":"create_1","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DoctorRequest"}}},"required":true},"responses":{"200":{"description":"Operação bem-sucedida","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Doctor"}}}}}}},"/healthmed/doctor/{crm}/update-availability":{"patch":{"tags":["doctor-controller"],"summary":"Atualizar os horarios disponiveis do medico","operationId":"updateAvailableTimes","parameters":[{"name":"crm","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AvailableTimesRequest"}}},"required":true},"responses":{"200":{"description":"Operação bem-sucedida","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Doctor"}}}}}}},"/healthmed/medical-appointment/{document}":{"get":{"tags":["medical-appointment-by-patient-controller"],"operationId":"getAllMyAppointments","parameters":[{"name":"document","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/MedicalAppointment"}}}}}}}},"/healthmed/medical-appointment/{crm}":{"get":{"tags":["medical-appointment-by-doctor-controller"],"operationId":"getAllMyAppointments_1","parameters":[{"name":"crm","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/MedicalAppointment"}}}}}}}},"/healthmed/doctor/search":{"get":{"tags":["doctor-controller"],"operationId":"search","parameters":[{"name":"query","in":"query","required":true,"schema":{"type":"object","additionalProperties":{"type":"string"}}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Doctor"}}}}}}}}},"components":{"schemas":{"PatientRequest":{"type":"object","properties":{"document":{"type":"string","description":"CPF","example":"12345678900"},"name":{"type":"string","description":"Name","example":"Foo Bar"},"email":{"type":"string","description":"Email","example":"foo_bar@gmail.com"},"phoneNumber":{"type":"string","description":"Phone number","example":"+5511999999999"},"zipCode":{"type":"string","description":"ZIP code","example":"12345678"},"address":{"type":"string","description":"Address","example":"123, Main St."}}},"Patient":{"required":["address","document","email","name","phoneNumber","zipCode"],"type":"object","properties":{"document":{"type":"string"},"name":{"type":"string"},"email":{"type":"string"},"phoneNumber":{"type":"string"},"zipCode":{"type":"string"},"address":{"type":"string"}}},"AvailablePeriodsRequest":{"type":"object","properties":{"start":{"type":"string","description":"Start time","format":"date-time"},"end":{"type":"string","description":"End time","format":"date-time"}},"description":"Available slots","example":{"MONDAY":[{"start":"2022-12-31T00:00:00","end":"2022-12-31T23:59:59"}]}},"AvailableTimesRequest":{"type":"object","properties":{"slots":{"type":"object","additionalProperties":{"type":"array","description":"Available slots","example":{"MONDAY":[{"start":"2022-12-31T00:00:00","end":"2022-12-31T23:59:59"}]},"items":{"$ref":"#/components/schemas/AvailablePeriodsRequest"}},"description":"Available slots","example":{"MONDAY":[{"start":"2022-12-31T00:00:00","end":"2022-12-31T23:59:59"}]}}},"description":"Available times"},"DoctorRequest":{"type":"object","properties":{"crm":{"type":"string","description":"CRM","example":"123456"},"document":{"type":"string","description":"CPF","example":"28685216907"},"specialty":{"type":"string","description":"Specialty","example":"Cardiologist"},"name":{"type":"string","description":"Name","example":"John Doe"},"email":{"type":"string","description":"Email","example":"john_doe@gmail.com"},"phoneNumber":{"type":"string","description":"Phone number","example":"+5511999999999"},"serviceZipCode":{"type":"string","description":"Service ZIP code","example":"12345678"},"serviceAddress":{"type":"string","description":"Service address","example":"123, Main St."},"availableTimes":{"$ref":"#/components/schemas/AvailableTimesRequest"},"appointmentPrice":{"type":"number","description":"Appointment price","example":100.0}}},"AvailablePeriods":{"required":["end","start"],"type":"object","properties":{"start":{"type":"string","format":"date-time"},"end":{"type":"string","format":"date-time"}}},"AvailableTimes":{"required":["slots"],"type":"object","properties":{"slots":{"type":"object","additionalProperties":{"type":"array","items":{"$ref":"#/components/schemas/AvailablePeriods"}}}}},"Doctor":{"required":["appointmentPrice","availableTimes","crm","document","email","name","phoneNumber","serviceAddress","serviceZipCode","specialty"],"type":"object","properties":{"crm":{"type":"string"},"document":{"type":"string"},"specialty":{"type":"string"},"name":{"type":"string"},"email":{"type":"string"},"phoneNumber":{"type":"string"},"serviceZipCode":{"type":"string"},"serviceAddress":{"type":"string"},"availableTimes":{"$ref":"#/components/schemas/AvailableTimes"},"appointmentPrice":{"type":"number"}}},"MedicalRecordContentRequest":{"type":"object","properties":{"content":{"type":"string","description":"Medical record content","example":"Patient has a fever"}}},"MedicalAppointment":{"required":["doctor","estimatedTimeSpentInMinutes","expectedStartTime","patient","status"],"type":"object","properties":{"number":{"type":"integer","format":"int64"},"doctor":{"$ref":"#/components/schemas/Doctor"},"patient":{"$ref":"#/components/schemas/Patient"},"expectedStartTime":{"type":"string","format":"date-time"},"estimatedTimeSpentInMinutes":{"type":"integer","format":"int32"},"status":{"type":"string","enum":["SCHEDULED","CANCELED","ACCEPTED","REJECTED","FINISHED"]},"statusChangedAt":{"type":"string","format":"date-time"},"justificationCancellationByPatient":{"type":"string"}}},"TimeAndDateToScheduleRequest":{"type":"object","properties":{"scheduleAt":{"type":"string","description":"scheduleAt","format":"date-time"}}},"JustificationCancellationRequest":{"type":"object","properties":{"text":{"type":"string","description":"Justification text","example":"I'm not feeling well"}}}},"securitySchemes":{"Bearer Authentication":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}}}} diff --git a/docs/openapi.yaml b/docs/openapi.yaml new file mode 100644 index 0000000..fd7bd7d --- /dev/null +++ b/docs/openapi.yaml @@ -0,0 +1,577 @@ +openapi: 3.0.1 +info: + title: Health&Med MVP + description: Demo API para sistema de consultas médicas online + contact: + name: Grupo 15 + url: http://fiap-3soat-g15-healthmed.s3-website-us-east-1.amazonaws.com + version: 1.0.0 +servers: + - url: / +paths: + /healthmed/patient/{document}: + put: + tags: + - patient-controller + summary: Atualizar um cadastro de um paciente + operationId: update + parameters: + - name: document + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/PatientRequest" + required: true + responses: + "200": + description: Operação bem-sucedida + content: + '*/*': + schema: + $ref: "#/components/schemas/Patient" + /healthmed/doctor/{crm}: + get: + tags: + - doctor-controller + operationId: get + parameters: + - name: crm + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: + '*/*': + schema: + $ref: "#/components/schemas/Doctor" + put: + tags: + - doctor-controller + summary: Atualizar um cadastro de um médico + operationId: update_1 + parameters: + - name: crm + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/DoctorRequest" + required: true + responses: + "200": + description: Operação bem-sucedida + content: + '*/*': + schema: + $ref: "#/components/schemas/Doctor" + /healthmed/patient: + post: + tags: + - patient-controller + summary: Cria um cadastro de um paciente + operationId: create + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/PatientRequest" + required: true + responses: + "200": + description: Operação bem-sucedida + content: + '*/*': + schema: + $ref: "#/components/schemas/Patient" + /healthmed/medical-record/{appointmentNumber}/append: + post: + tags: + - medical-record-controller + operationId: appendAnnotations + parameters: + - name: appointmentNumber + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/MedicalRecordContentRequest" + required: true + responses: + "200": + description: OK + content: + '*/*': + schema: + $ref: "#/components/schemas/MedicalAppointment" + /healthmed/medical-appointment/{document}/{crm}: + post: + tags: + - medical-appointment-by-patient-controller + operationId: scheduleAppointment + parameters: + - name: document + in: path + required: true + schema: + type: string + - name: crm + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/TimeAndDateToScheduleRequest" + required: true + responses: + "200": + description: OK + content: + '*/*': + schema: + $ref: "#/components/schemas/MedicalAppointment" + /healthmed/medical-appointment/{document}/cancel/{appointmentNumber}: + post: + tags: + - medical-appointment-by-patient-controller + operationId: cancelAppointment + parameters: + - name: document + in: path + required: true + schema: + type: string + - name: appointmentNumber + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/JustificationCancellationRequest" + required: true + responses: + "200": + description: OK + content: + '*/*': + schema: + $ref: "#/components/schemas/MedicalAppointment" + /healthmed/medical-appointment/{crm}/reject/{appointmentNumber}: + post: + tags: + - medical-appointment-by-doctor-controller + operationId: rejectAppointment + parameters: + - name: crm + in: path + required: true + schema: + type: string + - name: appointmentNumber + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: + '*/*': + schema: + $ref: "#/components/schemas/MedicalAppointment" + /healthmed/medical-appointment/{crm}/cancel/{appointmentNumber}: + post: + tags: + - medical-appointment-by-doctor-controller + operationId: acceptAppointment + parameters: + - name: crm + in: path + required: true + schema: + type: string + - name: appointmentNumber + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: + '*/*': + schema: + $ref: "#/components/schemas/MedicalAppointment" + /healthmed/doctor: + post: + tags: + - doctor-controller + summary: Cria um cadastro de um médico + operationId: create_1 + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/DoctorRequest" + required: true + responses: + "200": + description: Operação bem-sucedida + content: + '*/*': + schema: + $ref: "#/components/schemas/Doctor" + /healthmed/doctor/{crm}/update-availability: + patch: + tags: + - doctor-controller + summary: Atualizar os horarios disponiveis do medico + operationId: updateAvailableTimes + parameters: + - name: crm + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/AvailableTimesRequest" + required: true + responses: + "200": + description: Operação bem-sucedida + content: + '*/*': + schema: + $ref: "#/components/schemas/Doctor" + /healthmed/medical-appointment/{document}: + get: + tags: + - medical-appointment-by-patient-controller + operationId: getAllMyAppointments + parameters: + - name: document + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: + '*/*': + schema: + type: array + items: + $ref: "#/components/schemas/MedicalAppointment" + /healthmed/medical-appointment/{crm}: + get: + tags: + - medical-appointment-by-doctor-controller + operationId: getAllMyAppointments_1 + parameters: + - name: crm + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: + '*/*': + schema: + type: array + items: + $ref: "#/components/schemas/MedicalAppointment" + /healthmed/doctor/search: + get: + tags: + - doctor-controller + operationId: search + parameters: + - name: query + in: query + required: true + schema: + type: object + additionalProperties: + type: string + responses: + "200": + description: OK + content: + '*/*': + schema: + type: array + items: + $ref: "#/components/schemas/Doctor" +components: + schemas: + PatientRequest: + type: object + properties: + document: + type: string + description: CPF + example: "12345678900" + name: + type: string + description: Name + example: Foo Bar + email: + type: string + description: Email + example: foo_bar@gmail.com + phoneNumber: + type: string + description: Phone number + example: "+5511999999999" + zipCode: + type: string + description: ZIP code + example: "12345678" + address: + type: string + description: Address + example: "123, Main St." + Patient: + required: + - address + - document + - email + - name + - phoneNumber + - zipCode + type: object + properties: + document: + type: string + name: + type: string + email: + type: string + phoneNumber: + type: string + zipCode: + type: string + address: + type: string + AvailablePeriodsRequest: + type: object + properties: + start: + type: string + description: Start time + format: date-time + end: + type: string + description: End time + format: date-time + description: Available slots + example: + MONDAY: + - start: 2022-12-31T00:00:00 + end: 2022-12-31T23:59:59 + AvailableTimesRequest: + type: object + properties: + slots: + type: object + additionalProperties: + type: array + description: Available slots + example: + MONDAY: + - start: 2022-12-31T00:00:00 + end: 2022-12-31T23:59:59 + items: + $ref: "#/components/schemas/AvailablePeriodsRequest" + description: Available slots + example: + MONDAY: + - start: 2022-12-31T00:00:00 + end: 2022-12-31T23:59:59 + description: Available times + DoctorRequest: + type: object + properties: + crm: + type: string + description: CRM + example: "123456" + document: + type: string + description: CPF + example: "28685216907" + specialty: + type: string + description: Specialty + example: Cardiologist + name: + type: string + description: Name + example: John Doe + email: + type: string + description: Email + example: john_doe@gmail.com + phoneNumber: + type: string + description: Phone number + example: "+5511999999999" + serviceZipCode: + type: string + description: Service ZIP code + example: "12345678" + serviceAddress: + type: string + description: Service address + example: "123, Main St." + availableTimes: + $ref: "#/components/schemas/AvailableTimesRequest" + appointmentPrice: + type: number + description: Appointment price + example: 100.0 + AvailablePeriods: + required: + - end + - start + type: object + properties: + start: + type: string + format: date-time + end: + type: string + format: date-time + AvailableTimes: + required: + - slots + type: object + properties: + slots: + type: object + additionalProperties: + type: array + items: + $ref: "#/components/schemas/AvailablePeriods" + Doctor: + required: + - appointmentPrice + - availableTimes + - crm + - document + - email + - name + - phoneNumber + - serviceAddress + - serviceZipCode + - specialty + type: object + properties: + crm: + type: string + document: + type: string + specialty: + type: string + name: + type: string + email: + type: string + phoneNumber: + type: string + serviceZipCode: + type: string + serviceAddress: + type: string + availableTimes: + $ref: "#/components/schemas/AvailableTimes" + appointmentPrice: + type: number + MedicalRecordContentRequest: + type: object + properties: + content: + type: string + description: Medical record content + example: Patient has a fever + MedicalAppointment: + required: + - doctor + - estimatedTimeSpentInMinutes + - expectedStartTime + - patient + - status + type: object + properties: + number: + type: integer + format: int64 + doctor: + $ref: "#/components/schemas/Doctor" + patient: + $ref: "#/components/schemas/Patient" + expectedStartTime: + type: string + format: date-time + estimatedTimeSpentInMinutes: + type: integer + format: int32 + status: + type: string + enum: + - SCHEDULED + - CANCELED + - ACCEPTED + - REJECTED + - FINISHED + statusChangedAt: + type: string + format: date-time + justificationCancellationByPatient: + type: string + TimeAndDateToScheduleRequest: + type: object + properties: + scheduleAt: + type: string + description: scheduleAt + format: date-time + JustificationCancellationRequest: + type: object + properties: + text: + type: string + description: Justification text + example: I'm not feeling well + securitySchemes: + Bearer Authentication: + type: http + scheme: bearer + bearerFormat: JWT diff --git a/pom.xml b/pom.xml index a9f68f1..c6c576a 100644 --- a/pom.xml +++ b/pom.xml @@ -13,8 +13,8 @@ healthmed 0.0.1-SNAPSHOT - Health&Med - Health&Med API + Health&Med MVP + Demo API para sistema de consultas médicas online 17 diff --git a/src/main/kotlin/com/fiap/healthmed/HealthMedApp.kt b/src/main/kotlin/com/fiap/healthmed/HealthMedApp.kt index 2d75d3f..5e008f2 100644 --- a/src/main/kotlin/com/fiap/healthmed/HealthMedApp.kt +++ b/src/main/kotlin/com/fiap/healthmed/HealthMedApp.kt @@ -11,12 +11,12 @@ import org.springframework.boot.runApplication @OpenAPIDefinition( info = Info( - title = "HealthMed App", + title = "Health&Med MVP", version = "1.0.0", - description = "MVP telemedicine app", + description = "Demo API para sistema de consultas médicas online", contact = Contact( name = "Grupo 15", - url = "http://fiap-3soat-g15.s3-website-us-east-1.amazonaws.com", + url = "http://fiap-3soat-g15-healthmed.s3-website-us-east-1.amazonaws.com", ), ), servers = [ diff --git a/src/main/kotlin/com/fiap/healthmed/adapter/controller/config/ControllerExceptionHandler.kt b/src/main/kotlin/com/fiap/healthmed/adapter/controller/config/ControllerExceptionHandler.kt index 1acea2f..a55eccf 100644 --- a/src/main/kotlin/com/fiap/healthmed/adapter/controller/config/ControllerExceptionHandler.kt +++ b/src/main/kotlin/com/fiap/healthmed/adapter/controller/config/ControllerExceptionHandler.kt @@ -23,7 +23,7 @@ class ControllerExceptionHandler { ) ErrorType.PATIENT_ALREADY_EXISTS, - ErrorType.DOCKER_ALREADY_EXISTS, + ErrorType.DOCTOR_ALREADY_EXISTS, ErrorType.IMCOMPATIBLE_SCHEDULE, ErrorType.UNAVAILABLE_TIME, ErrorType.NO_MEDICAL_RECORDS, -> diff --git a/src/main/kotlin/com/fiap/healthmed/adapter/gateway/DoctorGateway.kt b/src/main/kotlin/com/fiap/healthmed/adapter/gateway/DoctorGateway.kt index 161a2e6..fbc733c 100644 --- a/src/main/kotlin/com/fiap/healthmed/adapter/gateway/DoctorGateway.kt +++ b/src/main/kotlin/com/fiap/healthmed/adapter/gateway/DoctorGateway.kt @@ -13,7 +13,7 @@ interface DoctorGateway { fun get(crm: String): Doctor - fun searchDoctorWithName(speciality: String): List + fun searchDoctorWithName(name: String): List fun searchDoctorWithSpeciality(speciality: String): List diff --git a/src/main/kotlin/com/fiap/healthmed/adapter/gateway/impl/DoctorGatewayImpl.kt b/src/main/kotlin/com/fiap/healthmed/adapter/gateway/impl/DoctorGatewayImpl.kt index 22b18a9..4e0b227 100644 --- a/src/main/kotlin/com/fiap/healthmed/adapter/gateway/impl/DoctorGatewayImpl.kt +++ b/src/main/kotlin/com/fiap/healthmed/adapter/gateway/impl/DoctorGatewayImpl.kt @@ -19,7 +19,7 @@ class DoctorGatewayImpl( override fun createDoctor(doctor: Doctor): Doctor { getByCrm(doctor.crm)?.let { throw HealthMedException( - errorType = ErrorType.DOCKER_ALREADY_EXISTS, + errorType = ErrorType.DOCTOR_ALREADY_EXISTS, message = "Doctor with CRM [${doctor.crm}] already exists", ) } diff --git a/src/main/kotlin/com/fiap/healthmed/adapter/gateway/impl/MedicalAppointmentGatewayImpl.kt b/src/main/kotlin/com/fiap/healthmed/adapter/gateway/impl/MedicalAppointmentGatewayImpl.kt index 5627ffb..79b1581 100644 --- a/src/main/kotlin/com/fiap/healthmed/adapter/gateway/impl/MedicalAppointmentGatewayImpl.kt +++ b/src/main/kotlin/com/fiap/healthmed/adapter/gateway/impl/MedicalAppointmentGatewayImpl.kt @@ -48,7 +48,7 @@ class MedicalAppointmentGatewayImpl(private val medicalAppointmentJpaRepository: return medicalAppointmentJpaRepository.save( MedicalAppointment( doctor = doctor, - patientDocument = patient, + patient = patient, status = MedicalAppointmentStatus.SCHEDULED, expectedStartTime = scheduleAt, estimatedTimeSpentInMinutes = ESTIMATED_MEDICAL_APPOINTMENT_DURATION_IN_MINUTES, diff --git a/src/main/kotlin/com/fiap/healthmed/domain/MedicalAppointment.kt b/src/main/kotlin/com/fiap/healthmed/domain/MedicalAppointment.kt index 5143eb6..8c63346 100644 --- a/src/main/kotlin/com/fiap/healthmed/domain/MedicalAppointment.kt +++ b/src/main/kotlin/com/fiap/healthmed/domain/MedicalAppointment.kt @@ -5,7 +5,7 @@ import java.time.LocalDateTime data class MedicalAppointment( val number: Long? = null, val doctor: Doctor, - val patientDocument: Patient, + val patient: Patient, val expectedStartTime: LocalDateTime, val estimatedTimeSpentInMinutes: Int = ESTIMATED_MEDICAL_APPOINTMENT_DURATION_IN_MINUTES, val status: MedicalAppointmentStatus, diff --git a/src/main/kotlin/com/fiap/healthmed/domain/errors/ErrorType.kt b/src/main/kotlin/com/fiap/healthmed/domain/errors/ErrorType.kt index e2e952e..d4cc328 100644 --- a/src/main/kotlin/com/fiap/healthmed/domain/errors/ErrorType.kt +++ b/src/main/kotlin/com/fiap/healthmed/domain/errors/ErrorType.kt @@ -1,14 +1,17 @@ package com.fiap.healthmed.domain.errors enum class ErrorType { - APPOINTMENT_NOT_FOUND, + DOCTOR_ALREADY_EXISTS, DOCTOR_NOT_FOUND, - DOCKER_ALREADY_EXISTS, + PATIENT_ALREADY_EXISTS, PATIENT_NOT_FOUND, - UNEXPECTED_ERROR, IMCOMPATIBLE_SCHEDULE, UNAVAILABLE_TIME, + APPOINTMENT_NOT_FOUND, + NO_MEDICAL_RECORDS, + + UNEXPECTED_ERROR, } diff --git a/src/main/kotlin/com/fiap/healthmed/driver/database/persistence/mapper/MedicalAppointmentMapper.kt b/src/main/kotlin/com/fiap/healthmed/driver/database/persistence/mapper/MedicalAppointmentMapper.kt index 1a4aa78..5727d87 100644 --- a/src/main/kotlin/com/fiap/healthmed/driver/database/persistence/mapper/MedicalAppointmentMapper.kt +++ b/src/main/kotlin/com/fiap/healthmed/driver/database/persistence/mapper/MedicalAppointmentMapper.kt @@ -6,7 +6,6 @@ import org.mapstruct.Mapper @Mapper interface MedicalAppointmentMapper { - fun toDomain(entity: MedicalAppointmentEntity): MedicalAppointment fun fromDomain(domain: MedicalAppointment): MedicalAppointmentEntity diff --git a/src/main/kotlin/com/fiap/healthmed/driver/database/persistence/mapper/MedicalRecordMapper.kt b/src/main/kotlin/com/fiap/healthmed/driver/database/persistence/mapper/MedicalRecordMapper.kt index eb33c53..54d2596 100644 --- a/src/main/kotlin/com/fiap/healthmed/driver/database/persistence/mapper/MedicalRecordMapper.kt +++ b/src/main/kotlin/com/fiap/healthmed/driver/database/persistence/mapper/MedicalRecordMapper.kt @@ -6,7 +6,6 @@ import org.mapstruct.Mapper @Mapper interface MedicalRecordMapper { - fun toDomain(entity: MedicalRecordEntity): MedicalRecord fun fromDomain(domain: MedicalRecord): MedicalRecordEntity diff --git a/src/main/kotlin/com/fiap/healthmed/driver/web/DoctorApi.kt b/src/main/kotlin/com/fiap/healthmed/driver/web/DoctorApi.kt index 7e63058..5737aa4 100644 --- a/src/main/kotlin/com/fiap/healthmed/driver/web/DoctorApi.kt +++ b/src/main/kotlin/com/fiap/healthmed/driver/web/DoctorApi.kt @@ -18,7 +18,7 @@ interface DoctorApi { ApiResponse(responseCode = "200", description = "Operação bem-sucedida"), ], ) - @PostMapping("/create") + @PostMapping fun create(@RequestBody request: DoctorRequest): ResponseEntity @Operation(summary = "Atualizar um cadastro de um médico") @@ -27,7 +27,7 @@ interface DoctorApi { ApiResponse(responseCode = "200", description = "Operação bem-sucedida"), ], ) - @PutMapping("/update/{crm}") + @PutMapping("{crm}") fun update( @PathVariable crm: String, @RequestBody request: DoctorRequest @@ -39,13 +39,12 @@ interface DoctorApi { ApiResponse(responseCode = "200", description = "Operação bem-sucedida"), ], ) - @PatchMapping("/update/available/{crm}") + @PatchMapping("/{crm}/update-availability") fun updateAvailableTimes( @PathVariable crm: String, @RequestBody request: AvailableTimesRequest ): ResponseEntity - @GetMapping("/search") fun search(@RequestParam query: Map): ResponseEntity> diff --git a/src/main/kotlin/com/fiap/healthmed/driver/web/PatientApi.kt b/src/main/kotlin/com/fiap/healthmed/driver/web/PatientApi.kt index 2ed454a..8ba41cc 100644 --- a/src/main/kotlin/com/fiap/healthmed/driver/web/PatientApi.kt +++ b/src/main/kotlin/com/fiap/healthmed/driver/web/PatientApi.kt @@ -17,7 +17,7 @@ interface PatientApi { ApiResponse(responseCode = "200", description = "Operação bem-sucedida"), ], ) - @PostMapping("/create") + @PostMapping fun create(@RequestBody request: PatientRequest): ResponseEntity @Operation(summary = "Atualizar um cadastro de um paciente") @@ -26,7 +26,7 @@ interface PatientApi { ApiResponse(responseCode = "200", description = "Operação bem-sucedida"), ], ) - @PutMapping("/update/{document}") + @PutMapping("{document}") fun update( @PathVariable document: String, @RequestBody request: PatientRequest diff --git a/src/main/kotlin/com/fiap/healthmed/usecases/service/MedicalAppointmentService.kt b/src/main/kotlin/com/fiap/healthmed/usecases/service/MedicalAppointmentService.kt index 96c3211..afd34a8 100644 --- a/src/main/kotlin/com/fiap/healthmed/usecases/service/MedicalAppointmentService.kt +++ b/src/main/kotlin/com/fiap/healthmed/usecases/service/MedicalAppointmentService.kt @@ -20,6 +20,7 @@ class MedicalAppointmentService( ListMedicalAppointmentByDoctorUseCase, RejectAppointmentUseCase, ScheduleAppointmentUseCase { + override fun accept(crm: String, appointmentNumber: String): MedicalAppointment { return medicalAppointmentGateway.acceptAppointment(appointmentNumber) } diff --git a/src/main/kotlin/com/fiap/healthmed/usecases/service/MedicalRecordService.kt b/src/main/kotlin/com/fiap/healthmed/usecases/service/MedicalRecordService.kt index 4bb27e8..3bbf07c 100644 --- a/src/main/kotlin/com/fiap/healthmed/usecases/service/MedicalRecordService.kt +++ b/src/main/kotlin/com/fiap/healthmed/usecases/service/MedicalRecordService.kt @@ -13,8 +13,6 @@ class MedicalRecordService( private val medicalRecordGateway: MedicalRecordGateway, private val medicalAppointmentGateway: MedicalAppointmentGateway) : AppendInMedicalRecordUseCase { - - override fun append(content: String, appointmentNumber: String): MedicalAppointment { val appointment = medicalAppointmentGateway.findAppointment(appointmentNumber) @@ -22,7 +20,7 @@ class MedicalRecordService( MedicalRecord( medicalAppointment = appointment, doctorCrm = appointment.doctor.crm, - patientDocument = appointment.patientDocument.document, + patientDocument = appointment.patient.document, fileLocation = LOCATION_FILES, content = content, fileName = String.format("%s-%s.txt", appointmentNumber, appointment.doctor.crm) diff --git a/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 66ef5cd..0002c5e 100644 --- a/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -4,36 +4,6 @@ "name": "admin.access-token", "type": "java.lang.String", "description": "Description for admin-access-token." - }, - { - "name": "payment-provider.mock", - "type": "java.lang.String", - "description": "Description for payment-provider.mock." - }, - { - "name": "mercadopago.api.url", - "type": "java.lang.String", - "description": "Description for mercadopago.api.url." - }, - { - "name": "mercadopago.api.token", - "type": "java.lang.String", - "description": "Description for mercadopago.api.token." - }, - { - "name": "mercadopago.api.userId", - "type": "java.lang.String", - "description": "Description for mercadopago.api.userId." - }, - { - "name": "mercadopago.integration.posId", - "type": "java.lang.String", - "description": "Description for mercadopago.integration.posId." - }, - { - "name": "mercadopago.integration.webhookBaseUrl", - "type": "java.lang.String", - "description": "Description for mercadopago.integration.webhookBaseUrl." } ] } diff --git a/src/main/resources/application-openapi.yml b/src/main/resources/application-openapi.yml index d15a738..423c178 100644 --- a/src/main/resources/application-openapi.yml +++ b/src/main/resources/application-openapi.yml @@ -10,6 +10,3 @@ spring: properties: hibernate: dialect: org.hibernate.dialect.H2Dialect - -payment-provider: - mock: true diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml index bc69c31..cb47432 100644 --- a/src/main/resources/application-test.yml +++ b/src/main/resources/application-test.yml @@ -1,5 +1,2 @@ admin: access-token: token - -payment-provider: - mock: true diff --git a/src/test/kotlin/com/fiap/healthmed/TestAnnotations.kt b/src/test/kotlin/com/fiap/healthmed/TestAnnotations.kt new file mode 100644 index 0000000..a55ada0 --- /dev/null +++ b/src/test/kotlin/com/fiap/healthmed/TestAnnotations.kt @@ -0,0 +1,10 @@ +package com.fiap.healthmed + +import com.fiap.healthmed.it.PostgreSQLContainerInitializer +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase +import org.springframework.test.context.ContextConfiguration + +@ContextConfiguration(initializers = [PostgreSQLContainerInitializer::class]) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@Target(AnnotationTarget.CLASS, AnnotationTarget.FILE) +annotation class WithPostgreSQL diff --git a/src/test/kotlin/com/fiap/healthmed/TestFixtures.kt b/src/test/kotlin/com/fiap/healthmed/TestFixtures.kt new file mode 100644 index 0000000..0436671 --- /dev/null +++ b/src/test/kotlin/com/fiap/healthmed/TestFixtures.kt @@ -0,0 +1,84 @@ +package com.fiap.healthmed + +import com.fiap.healthmed.domain.* +import com.fiap.healthmed.domain.AvailableTimes.AvailablePeriods +import java.math.BigDecimal +import java.time.DayOfWeek +import java.time.LocalDateTime + +fun createAvailableTimes(): AvailableTimes { + val availablePeriods = AvailablePeriods( + start = LocalDateTime.of(2024, 7, 26, 9, 0), + end = LocalDateTime.of(2024, 7, 26, 17, 0) + ) + + return AvailableTimes( + mapOf( + DayOfWeek.MONDAY to listOf(availablePeriods), + DayOfWeek.TUESDAY to listOf(availablePeriods), + DayOfWeek.WEDNESDAY to listOf(availablePeriods), + DayOfWeek.THURSDAY to listOf(availablePeriods), + DayOfWeek.FRIDAY to listOf(availablePeriods), + ) + ) +} + +fun createDoctor( + crm: String = "111.222", + document: String = "111.222.333-44", + specialty: String = "Cardiologia", + name: String = "Dr. Fulano de Tal", + email: String = "fulanodetal@example.com", + phoneNumber: String = "999999999", + serviceZipCode: String = "01538-001", + serviceAddress: String = "Av. Paulista, 1106", + availableTimes: AvailableTimes = createAvailableTimes(), + appointmentPrice: BigDecimal = BigDecimal("150.00") +) = Doctor( + crm = crm, + document = document, + specialty = specialty, + name = name, + email = email, + phoneNumber = phoneNumber, + serviceZipCode = serviceZipCode, + serviceAddress = serviceAddress, + availableTimes = availableTimes, + appointmentPrice = appointmentPrice +) + +fun createPatient( + document: String = "999.888.777-66", + name: String = "Silvio Santos", + email: String = "silviosantos@exemplo.com", + phoneNumber: String = "988888888", + address: String = "Av. Lins de Vasconcelos, 1222", + zipCode: String = "01538-001" +) = Patient( + document = document, + name = name, + email = email, + phoneNumber = phoneNumber, + address = address, + zipCode = zipCode +) + +fun createMedicalAppointment( + number: Long? = null, + doctor: Doctor = createDoctor(), + patient: Patient = createPatient(), + expectedStartTime: LocalDateTime = LocalDateTime.of(2024, 7, 24, 9, 0), + estimatedTimeSpentInMinutes: Int = ESTIMATED_MEDICAL_APPOINTMENT_DURATION_IN_MINUTES, + status: MedicalAppointmentStatus = MedicalAppointmentStatus.SCHEDULED, + statusChangedAt: LocalDateTime = LocalDateTime.now(), + justificationCancellationByPatient: String? = null +) = MedicalAppointment( + number = number, + doctor = doctor, + patient = patient, + expectedStartTime = expectedStartTime, + estimatedTimeSpentInMinutes = estimatedTimeSpentInMinutes, + status = status, + statusChangedAt = statusChangedAt, + justificationCancellationByPatient = justificationCancellationByPatient +) diff --git a/src/test/kotlin/com/fiap/healthmed/it/CommonSteps.kt b/src/test/kotlin/com/fiap/healthmed/it/CommonSteps.kt new file mode 100644 index 0000000..ea593e0 --- /dev/null +++ b/src/test/kotlin/com/fiap/healthmed/it/CommonSteps.kt @@ -0,0 +1,33 @@ +package com.fiap.healthmed.it + +import io.cucumber.java.Before +import io.restassured.RestAssured +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.web.server.LocalServerPort +import org.springframework.jdbc.core.JdbcTemplate + +class CommonSteps: IntegrationTest() { + + @Autowired + lateinit var jdbcTemplate: JdbcTemplate + + @LocalServerPort + private val port: Int? = null + + @Before + fun setup() { + RestAssured.baseURI = "http://localhost:$port" + RestAssured.enableLoggingOfRequestAndResponseIfValidationFails() + } + + @Before("@database") + fun setupDatabase() { + jdbcTemplate.execute(""" + TRUNCATE TABLE medical_appointment RESTART IDENTITY CASCADE; + TRUNCATE TABLE medical_record RESTART IDENTITY CASCADE; + TRUNCATE TABLE doctor RESTART IDENTITY CASCADE; + TRUNCATE TABLE patient RESTART IDENTITY CASCADE; + """.trimIndent() + ) + } +} diff --git a/src/test/kotlin/com/fiap/healthmed/it/IntegrationTest.kt b/src/test/kotlin/com/fiap/healthmed/it/IntegrationTest.kt new file mode 100644 index 0000000..be1b7b9 --- /dev/null +++ b/src/test/kotlin/com/fiap/healthmed/it/IntegrationTest.kt @@ -0,0 +1,12 @@ +package com.fiap.healthmed.it + +import com.fiap.healthmed.WithPostgreSQL +import io.cucumber.spring.CucumberContextConfiguration +import org.junit.jupiter.api.Tag +import org.springframework.boot.test.context.SpringBootTest + +@CucumberContextConfiguration +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@WithPostgreSQL +@Tag("IntegrationTest") +class IntegrationTest diff --git a/src/test/kotlin/com/fiap/healthmed/it/IntegrationTestFixtures.kt b/src/test/kotlin/com/fiap/healthmed/it/IntegrationTestFixtures.kt new file mode 100644 index 0000000..46633e2 --- /dev/null +++ b/src/test/kotlin/com/fiap/healthmed/it/IntegrationTestFixtures.kt @@ -0,0 +1,65 @@ +package com.fiap.healthmed.it + +import com.fiap.healthmed.driver.web.request.AvailableTimesRequest +import com.fiap.healthmed.driver.web.request.DoctorRequest +import com.fiap.healthmed.driver.web.request.PatientRequest +import java.math.BigDecimal +import java.time.DayOfWeek +import java.time.LocalDateTime + +fun createAvailableTimesRequest(): AvailableTimesRequest { + val availablePeriods = AvailableTimesRequest.AvailablePeriodsRequest( + start = LocalDateTime.of(2024, 7, 26, 9, 0), + end = LocalDateTime.of(2024, 7, 26, 17, 0) + ) + + return AvailableTimesRequest( + mapOf( + DayOfWeek.MONDAY to listOf(availablePeriods), + DayOfWeek.TUESDAY to listOf(availablePeriods), + DayOfWeek.WEDNESDAY to listOf(availablePeriods), + DayOfWeek.THURSDAY to listOf(availablePeriods), + DayOfWeek.FRIDAY to listOf(availablePeriods), + ) + ) +} + +fun createDoctorRequest( + crm: String = "111.222", + document: String = "111.222.333-44", + specialty: String = "Cardiologia", + name: String = "Dr. Fulano de Tal", + email: String = "fulanodetal@example.com", + phoneNumber: String = "999999999", + serviceZipCode: String = "01538-001", + serviceAddress: String = "Av. Paulista, 1106", + availableTimes: AvailableTimesRequest = createAvailableTimesRequest(), + appointmentPrice: BigDecimal = BigDecimal("150.00") +) = DoctorRequest( + crm = crm, + document = document, + specialty = specialty, + name = name, + email = email, + phoneNumber = phoneNumber, + serviceZipCode = serviceZipCode, + serviceAddress = serviceAddress, + availableTimes = availableTimes, + appointmentPrice = appointmentPrice, +) + +fun createPatientRequest( + document: String = "999.888.777-66", + name: String = "Silvio Santos", + email: String = "silviosantos@exemplo.com", + phoneNumber: String = "988888888", + address: String = "Av. Lins de Vasconcelos, 1222", + zipCode: String = "01538-001", +) = PatientRequest( + document = document, + name = name, + email = email, + phoneNumber = phoneNumber, + zipCode = zipCode, + address = address, +) diff --git a/src/test/kotlin/com/fiap/healthmed/it/PostgresContainerInitiliazer.kt b/src/test/kotlin/com/fiap/healthmed/it/PostgresContainerInitiliazer.kt new file mode 100644 index 0000000..91b2ffa --- /dev/null +++ b/src/test/kotlin/com/fiap/healthmed/it/PostgresContainerInitiliazer.kt @@ -0,0 +1,29 @@ +package com.fiap.healthmed.it + +import org.springframework.boot.test.util.TestPropertyValues +import org.springframework.context.ApplicationContextInitializer +import org.springframework.context.ConfigurableApplicationContext +import org.testcontainers.containers.PostgreSQLContainer +import org.testcontainers.containers.wait.strategy.Wait.forListeningPort + +class PostgreSQLContainerInitializer : + ApplicationContextInitializer, + PostgreSQLContainer("postgres:15.4") { + companion object { + private val instance: PostgreSQLContainerInitializer = + PostgreSQLContainerInitializer() + .withDatabaseName("database") + .withUsername("database") + .withPassword("database") + .waitingFor(forListeningPort()) + } + + override fun initialize(configurableApplicationContext: ConfigurableApplicationContext) { + instance.start() + TestPropertyValues.of( + "spring.datasource.url=${instance.jdbcUrl}", + "spring.datasource.username=${instance.username}", + "spring.datasource.password=${instance.password}", + ).applyTo(configurableApplicationContext) + } +} diff --git a/src/test/kotlin/com/fiap/healthmed/it/RegisterDoctorIntegrationTest.kt b/src/test/kotlin/com/fiap/healthmed/it/RegisterDoctorIntegrationTest.kt new file mode 100644 index 0000000..12a2e23 --- /dev/null +++ b/src/test/kotlin/com/fiap/healthmed/it/RegisterDoctorIntegrationTest.kt @@ -0,0 +1,75 @@ +package com.fiap.healthmed.it + +import com.fiap.healthmed.adapter.gateway.DoctorGateway +import com.fiap.healthmed.domain.Doctor +import com.fiap.healthmed.driver.web.request.DoctorRequest +import com.fiap.healthmed.driver.web.request.toDomain +import io.cucumber.java.en.Given +import io.cucumber.java.en.Then +import io.cucumber.java.en.When +import io.restassured.RestAssured.given +import io.restassured.http.ContentType +import io.restassured.response.Response +import org.assertj.core.api.Assertions.assertThat +import org.hamcrest.CoreMatchers.equalTo +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.HttpStatus + +class RegisterDoctorIntegrationTest: IntegrationTest() { + + @Autowired + lateinit var doctorRepository: DoctorGateway + + private lateinit var doctorRequest: DoctorRequest + private lateinit var response: Response + + @Given("valid data for doctor") + fun validDataForDoctor() { + doctorRequest = createDoctorRequest() + } + + @When("request to register doctor") + fun requestToRegisterDoctor() { + response = given() + .contentType(ContentType.JSON) + .body(doctorRequest) + .`when`() + .post("/healthmed/doctor") + } + + @Then("doctor should be registered") + fun doctorShouldBeRegistered() { + val persistedDoctor = doctorRepository.get(doctorRequest.crm) + + assertThat(persistedDoctor).isEqualTo( + Doctor( + crm = doctorRequest.crm, + document = doctorRequest.document, + specialty = doctorRequest.specialty, + name = doctorRequest.name, + email = doctorRequest.email, + phoneNumber = doctorRequest.phoneNumber, + serviceZipCode = doctorRequest.serviceZipCode, + serviceAddress = doctorRequest.serviceAddress, + availableTimes = doctorRequest.availableTimes.toDomain(), + appointmentPrice = doctorRequest.appointmentPrice + ) + ) + + response + .then() + .statusCode(HttpStatus.OK.value()) + .body( + "crm", equalTo(doctorRequest.crm), + "document", equalTo(doctorRequest.document), + "specialty", equalTo(doctorRequest.specialty), + "name", equalTo(doctorRequest.name), + "email", equalTo(doctorRequest.email), + "phoneNumber", equalTo(doctorRequest.phoneNumber), + "serviceZipCode", equalTo(doctorRequest.serviceZipCode), + "serviceAddress", equalTo(doctorRequest.serviceAddress), + "availableTimes", equalTo(doctorRequest.availableTimes.toDomain()), + "appointmentPrice", equalTo(doctorRequest.appointmentPrice), + ) + } +} diff --git a/src/test/kotlin/com/fiap/healthmed/it/RegisterPatientIntegrationTest.kt b/src/test/kotlin/com/fiap/healthmed/it/RegisterPatientIntegrationTest.kt new file mode 100644 index 0000000..108d7fa --- /dev/null +++ b/src/test/kotlin/com/fiap/healthmed/it/RegisterPatientIntegrationTest.kt @@ -0,0 +1,66 @@ +package com.fiap.healthmed.it + +import com.fiap.healthmed.adapter.gateway.PatientGateway +import com.fiap.healthmed.domain.Patient +import com.fiap.healthmed.driver.web.request.PatientRequest +import io.cucumber.java.en.Given +import io.cucumber.java.en.Then +import io.cucumber.java.en.When +import io.restassured.RestAssured.given +import io.restassured.http.ContentType +import io.restassured.response.Response +import org.assertj.core.api.Assertions.assertThat +import org.hamcrest.CoreMatchers.equalTo +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.HttpStatus + +class RegisterPatientIntegrationTest: IntegrationTest() { + + @Autowired + lateinit var patientRepository: PatientGateway + + private lateinit var patientRequest: PatientRequest + private lateinit var response: Response + + @Given("valid data for patient") + fun validDataForPatient() { + patientRequest = createPatientRequest() + } + + @When("request to register patient") + fun requestToRegisterPatient() { + response = given() + .contentType(ContentType.JSON) + .body(patientRequest) + .`when`() + .post("/healthmed/patient") + } + + @Then("patient should be registered") + fun patientShouldBeRegistered() { + val persistedPatient = patientRepository.get(patientRequest.document) + + assertThat(persistedPatient).isEqualTo( + Patient( + document = patientRequest.document, + name = patientRequest.name, + email = patientRequest.email, + phoneNumber = patientRequest.phoneNumber, + zipCode = patientRequest.zipCode, + address = patientRequest.address, + ) + ) + + response + .then() + .statusCode(HttpStatus.OK.value()) + .body( + "document", equalTo(patientRequest.document), + "name", equalTo(patientRequest.name), + "email", equalTo(patientRequest.email), + "phoneNumber", equalTo(patientRequest.phoneNumber), + "zipCode", equalTo(patientRequest.zipCode), + "address", equalTo(patientRequest.address), + ) + } +} diff --git a/src/test/kotlin/com/fiap/healthmed/it/RunCucumberIT.kt b/src/test/kotlin/com/fiap/healthmed/it/RunCucumberIT.kt new file mode 100644 index 0000000..665556c --- /dev/null +++ b/src/test/kotlin/com/fiap/healthmed/it/RunCucumberIT.kt @@ -0,0 +1,16 @@ +package com.fiap.healthmed.it + +import io.cucumber.junit.platform.engine.Constants +import org.junit.platform.suite.api.ConfigurationParameter +import org.junit.platform.suite.api.IncludeEngines +import org.junit.platform.suite.api.SelectClasspathResource +import org.junit.platform.suite.api.Suite + +@Suite +@IncludeEngines("cucumber") +@SelectClasspathResource("features") +@ConfigurationParameter(key = Constants.GLUE_PROPERTY_NAME, value = "com.fiap.healthmed") +@ConfigurationParameter(key = Constants.PLUGIN_PROPERTY_NAME, value = "pretty") +@ConfigurationParameter(key = Constants.PLUGIN_PUBLISH_QUIET_PROPERTY_NAME, value = "true") +@ConfigurationParameter(key = Constants.JUNIT_PLATFORM_NAMING_STRATEGY_PROPERTY_NAME, value = "long") +class RunCucumberIT diff --git a/src/test/kotlin/com/fiap/healthmed/usecases/service/DoctorServiceTest.kt b/src/test/kotlin/com/fiap/healthmed/usecases/service/DoctorServiceTest.kt new file mode 100644 index 0000000..0851000 --- /dev/null +++ b/src/test/kotlin/com/fiap/healthmed/usecases/service/DoctorServiceTest.kt @@ -0,0 +1,131 @@ +package com.fiap.healthmed.usecases.service + +import com.fiap.healthmed.adapter.gateway.DoctorGateway +import com.fiap.healthmed.createDoctor +import com.fiap.healthmed.domain.AvailableTimes +import com.fiap.healthmed.domain.AvailableTimes.AvailablePeriods +import com.fiap.healthmed.domain.Doctor +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.time.DayOfWeek +import java.time.LocalDateTime + +class DoctorServiceTest { + + private lateinit var doctorGateway: DoctorGateway + private lateinit var doctorService: DoctorService + + @BeforeEach + fun setUp() { + doctorGateway = mockk() + doctorService = DoctorService(doctorGateway) + } + + private val doctor = createDoctor() + + @Test + fun `should create a doctor`() { + every { doctorGateway.createDoctor(doctor) } returns doctor + + val result = doctorService.create(doctor) + + assertEquals(doctor, result) + verify { doctorGateway.createDoctor(doctor) } + } + + @Test + fun `should update a doctor`() { + every { doctorGateway.updateDoctor(doctor) } returns doctor + + val result = doctorService.update(doctor) + + assertEquals(doctor, result) + verify { doctorGateway.updateDoctor(doctor) } + } + + @Test + fun `should update available times for a doctor`() { + val crm = "111.222" + val newAvailableTimes = AvailableTimes( + mapOf( + DayOfWeek.TUESDAY to listOf( + AvailablePeriods( + start = LocalDateTime.of(2024, 7, 26, 9, 0), + end = LocalDateTime.of(2024, 7, 26, 17, 0) + ) + ) + ) + ) + val updatedDoctor = doctor.copy(availableTimes = newAvailableTimes) + every { doctorGateway.updateDoctorAvailableTimes(crm, newAvailableTimes) } returns updatedDoctor + + val result = doctorService.updateAvailableTime(crm, newAvailableTimes) + + assertEquals(updatedDoctor, result) + verify { doctorGateway.updateDoctorAvailableTimes(crm, newAvailableTimes) } + } + + @Test + fun `should search doctor by name and speciality`() { + val query = mapOf("speciality" to "Cardiologia", "name" to "Fulano de Tal") + val doctors = listOf(doctor) + every { doctorGateway.searchDoctorWithNameAndSpeciality("Cardiologia", "Fulano de Tal") } returns doctors + + val result = doctorService.search(query) + + assertEquals(doctors, result) + verify { doctorGateway.searchDoctorWithNameAndSpeciality("Cardiologia", "Fulano de Tal") } + } + + @Test + fun `should search doctor by speciality`() { + val query = mapOf("speciality" to "Cardiologia") + val doctors = listOf(doctor) + every { doctorGateway.searchDoctorWithSpeciality("Cardiologia") } returns doctors + + val result = doctorService.search(query) + + assertEquals(doctors, result) + verify { doctorGateway.searchDoctorWithSpeciality("Cardiologia") } + } + + @Test + fun `should search doctor by name`() { + val query = mapOf("name" to "Fulano de Tal") + val doctors = listOf(doctor) + every { doctorGateway.searchDoctorWithName("Fulano de Tal") } returns doctors + + val result = doctorService.search(query) + + assertEquals(doctors, result) + verify { doctorGateway.searchDoctorWithName("Fulano de Tal") } + } + + @Test + fun `should return empty list when search query is empty`() { + val query = emptyMap() + every { doctorGateway.searchDoctorWithName(any()) } returns emptyList() + every { doctorGateway.searchDoctorWithSpeciality(any()) } returns emptyList() + every { doctorGateway.searchDoctorWithNameAndSpeciality(any(), any()) } returns emptyList() + + val result = doctorService.search(query) + + assertEquals(emptyList(), result) + } + + @Test + fun `should get doctor by crm`() { + val crm = "111.222" + every { doctorGateway.get(crm) } returns doctor + + val result = doctorService.get(crm) + + assertEquals(doctor, result) + verify { doctorGateway.get(crm) } + } +} + diff --git a/src/test/kotlin/com/fiap/healthmed/usecases/service/MedicalAppointmentServiceTest.kt b/src/test/kotlin/com/fiap/healthmed/usecases/service/MedicalAppointmentServiceTest.kt new file mode 100644 index 0000000..566a625 --- /dev/null +++ b/src/test/kotlin/com/fiap/healthmed/usecases/service/MedicalAppointmentServiceTest.kt @@ -0,0 +1,143 @@ +package com.fiap.healthmed.usecases.service + +import com.fiap.healthmed.adapter.gateway.DoctorGateway +import com.fiap.healthmed.adapter.gateway.MedicalAppointmentGateway +import com.fiap.healthmed.adapter.gateway.PatientGateway +import com.fiap.healthmed.createDoctor +import com.fiap.healthmed.createMedicalAppointment +import com.fiap.healthmed.createPatient +import com.fiap.healthmed.domain.ESTIMATED_MEDICAL_APPOINTMENT_DURATION_IN_MINUTES +import com.fiap.healthmed.domain.errors.ErrorType +import com.fiap.healthmed.domain.errors.HealthMedException +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.time.LocalDateTime + +class MedicalAppointmentServiceTest { + + private lateinit var medicalAppointmentGateway: MedicalAppointmentGateway + private lateinit var doctorGateway: DoctorGateway + private lateinit var patientGateway: PatientGateway + private lateinit var medicalAppointmentService: MedicalAppointmentService + + @BeforeEach + fun setUp() { + medicalAppointmentGateway = mockk() + doctorGateway = mockk() + patientGateway = mockk() + medicalAppointmentService = MedicalAppointmentService(medicalAppointmentGateway, doctorGateway, patientGateway) + } + + private val doctor = createDoctor() + private val patient = createPatient() + private val medicalAppointment = createMedicalAppointment( + number = 1L, + expectedStartTime = LocalDateTime.of(2024, 7, 26, 10, 0), + ) + + @Test + fun `should accept an appointment`() { + every { medicalAppointmentGateway.acceptAppointment(medicalAppointment.number.toString()) } returns medicalAppointment + + val result = medicalAppointmentService.accept(doctor.crm, medicalAppointment.number.toString()) + + assertEquals(medicalAppointment, result) + verify { medicalAppointmentGateway.acceptAppointment(medicalAppointment.number.toString()) } + } + + @Test + fun `should cancel an appointment`() { + val justification = "No longer available" + every { medicalAppointmentGateway.cancelAppointment(medicalAppointment.number.toString(), justification) } returns medicalAppointment.copy( + justificationCancellationByPatient = justification + ) + + val result = medicalAppointmentService.cancel(patient.document, medicalAppointment.number.toString(), justification) + + assertEquals(medicalAppointment.copy(justificationCancellationByPatient = justification), result) + verify { medicalAppointmentGateway.cancelAppointment(medicalAppointment.number.toString(), justification) } + } + + @Test + fun `should list appointments by patient`() { + every { medicalAppointmentGateway.findAppointmentsByPatient(patient.document) } returns listOf(medicalAppointment) + + val result = medicalAppointmentService.listByPatient(patient.document) + + assertEquals(listOf(medicalAppointment), result) + verify { medicalAppointmentGateway.findAppointmentsByPatient(patient.document) } + } + + @Test + fun `should list appointments by doctor`() { + every { medicalAppointmentGateway.findAppointmentsByDoctor(doctor.crm) } returns listOf(medicalAppointment) + + val result = medicalAppointmentService.listByDoctor(doctor.crm) + + assertEquals(listOf(medicalAppointment), result) + verify { medicalAppointmentGateway.findAppointmentsByDoctor(doctor.crm) } + } + + @Test + fun `should reject an appointment`() { + every { medicalAppointmentGateway.rejectAppointment(medicalAppointment.number.toString()) } returns medicalAppointment + + val result = medicalAppointmentService.reject(doctor.crm, medicalAppointment.number.toString()) + + assertEquals(medicalAppointment, result) + verify { medicalAppointmentGateway.rejectAppointment(medicalAppointment.number.toString()) } + } + + @Test + fun `should schedule an appointment if time is available`() { + val scheduleAt = LocalDateTime.of(2024, 7, 26, 10, 0) + val endTime = scheduleAt.plusMinutes(ESTIMATED_MEDICAL_APPOINTMENT_DURATION_IN_MINUTES.toLong()) + every { doctorGateway.get(doctor.crm) } returns doctor + every { patientGateway.get(patient.document) } returns patient + every { medicalAppointmentGateway.findAppointmentsByTimeAndDoctor(doctor.crm, scheduleAt, endTime) } returns emptyList() + every { medicalAppointmentGateway.createAppointment(doctor, patient, scheduleAt) } returns medicalAppointment + + val result = medicalAppointmentService.schedule(doctor.crm, patient.document, scheduleAt) + + assertEquals(medicalAppointment, result) + verify { doctorGateway.get(doctor.crm) } + verify { patientGateway.get(patient.document) } + verify { medicalAppointmentGateway.findAppointmentsByTimeAndDoctor(doctor.crm, scheduleAt, endTime) } + verify { medicalAppointmentGateway.createAppointment(doctor, patient, scheduleAt) } + } + + @Test + fun `should throw HealthMedException if time is incompatible with doctor's schedule`() { + val scheduleAt = LocalDateTime.of(2024, 7, 26, 18, 0) // Outside available time + every { doctorGateway.get(doctor.crm) } returns doctor + every { patientGateway.get(patient.document) } returns patient + + val exception = assertThrows { + medicalAppointmentService.schedule(doctor.crm, patient.document, scheduleAt) + } + + assertEquals(ErrorType.IMCOMPATIBLE_SCHEDULE, exception.errorType) + assertEquals("Time incompatible with the doctor's schedule", exception.message) + } + + @Test + fun `should throw HealthMedException if doctor has another appointment at the same time`() { + val scheduleAt = LocalDateTime.of(2024, 7, 26, 10, 0) + val endTime = scheduleAt.plusMinutes(ESTIMATED_MEDICAL_APPOINTMENT_DURATION_IN_MINUTES.toLong()) + every { doctorGateway.get(doctor.crm) } returns doctor + every { patientGateway.get(patient.document) } returns patient + every { medicalAppointmentGateway.findAppointmentsByTimeAndDoctor(doctor.crm, scheduleAt, endTime) } returns listOf(medicalAppointment) + + val exception = assertThrows { + medicalAppointmentService.schedule(doctor.crm, patient.document, scheduleAt) + } + + assertEquals(ErrorType.UNAVAILABLE_TIME, exception.errorType) + assertEquals("Time unavailable because the doctor already has another appointment", exception.message) + } +} diff --git a/src/test/kotlin/com/fiap/healthmed/usecases/service/MedicalRecordServiceTest.kt b/src/test/kotlin/com/fiap/healthmed/usecases/service/MedicalRecordServiceTest.kt new file mode 100644 index 0000000..be0e22d --- /dev/null +++ b/src/test/kotlin/com/fiap/healthmed/usecases/service/MedicalRecordServiceTest.kt @@ -0,0 +1,62 @@ +package com.fiap.healthmed.usecases.service + +import com.fiap.healthmed.adapter.gateway.MedicalAppointmentGateway +import com.fiap.healthmed.adapter.gateway.MedicalRecordGateway +import com.fiap.healthmed.createMedicalAppointment +import com.fiap.healthmed.createDoctor +import com.fiap.healthmed.createPatient +import com.fiap.healthmed.domain.* +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class MedicalRecordServiceTest { + + private lateinit var medicalRecordGateway: MedicalRecordGateway + private lateinit var medicalAppointmentGateway: MedicalAppointmentGateway + private lateinit var medicalRecordService: MedicalRecordService + + @BeforeEach + fun setUp() { + medicalRecordGateway = mockk() + medicalAppointmentGateway = mockk() + medicalRecordService = MedicalRecordService(medicalRecordGateway, medicalAppointmentGateway) + } + + private val doctor = createDoctor() + private val patient = createPatient() + private val medicalAppointment = createMedicalAppointment() + + @Test + fun `should append to medical record and update appointment status`() { + val content = "Bla bla bla" + val appointmentNumber = "123" + val updatedAppointment = medicalAppointment.copy(status = MedicalAppointmentStatus.FINISHED) + + val expectedRecord = MedicalRecord( + medicalAppointment = medicalAppointment, + doctorCrm = doctor.crm, + patientDocument = patient.document, + fileLocation = "/fake/path/to/medicalrecord", + content = content, + fileName = "123-111.222.txt" + ) + + every { medicalAppointmentGateway.findAppointment(appointmentNumber) } returns medicalAppointment + every { medicalRecordGateway.sendFileProvider(expectedRecord) } returns expectedRecord + every { medicalRecordGateway.persistMetadata(expectedRecord) } returns mockk() + every { medicalAppointmentGateway.updateAppointmentStatus(appointmentNumber, MedicalAppointmentStatus.FINISHED) } returns updatedAppointment + + val result = medicalRecordService.append(content, appointmentNumber) + + assertEquals(updatedAppointment, result) + verify { medicalAppointmentGateway.findAppointment(appointmentNumber) } + verify { medicalRecordGateway.sendFileProvider(expectedRecord) } + verify { medicalRecordGateway.persistMetadata(expectedRecord) } + verify { medicalAppointmentGateway.updateAppointmentStatus(appointmentNumber, MedicalAppointmentStatus.FINISHED) } + } +} + diff --git a/src/test/kotlin/com/fiap/healthmed/usecases/service/PatientServiceTest.kt b/src/test/kotlin/com/fiap/healthmed/usecases/service/PatientServiceTest.kt new file mode 100644 index 0000000..4319282 --- /dev/null +++ b/src/test/kotlin/com/fiap/healthmed/usecases/service/PatientServiceTest.kt @@ -0,0 +1,45 @@ +package com.fiap.healthmed.usecases.service + +import com.fiap.healthmed.adapter.gateway.PatientGateway +import com.fiap.healthmed.createPatient +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class PatientServiceTest { + + private lateinit var patientGateway: PatientGateway + private lateinit var patientService: PatientService + + @BeforeEach + fun setUp() { + patientGateway = mockk() + patientService = PatientService(patientGateway) + } + + private val patient = createPatient() + + @Test + fun `should create a patient`() { + every { patientGateway.createPatient(patient) } returns patient + + val result = patientService.create(patient) + + assertEquals(patient, result) + verify { patientGateway.createPatient(patient) } + } + + @Test + fun `should update a patient`() { + every { patientGateway.updatePatient(patient) } returns patient + + val result = patientService.update(patient) + + assertEquals(patient, result) + verify { patientGateway.updatePatient(patient) } + } +} + diff --git a/src/test/resources/features/Doctor.feature b/src/test/resources/features/Doctor.feature new file mode 100644 index 0000000..4209280 --- /dev/null +++ b/src/test/resources/features/Doctor.feature @@ -0,0 +1,7 @@ +#Feature: Doctor +# +# @database +# Scenario: Registering doctor +# Given valid data for doctor +# When request to register doctor +# Then doctor should be registered diff --git a/src/test/resources/features/Patient.feature b/src/test/resources/features/Patient.feature new file mode 100644 index 0000000..29c8c3a --- /dev/null +++ b/src/test/resources/features/Patient.feature @@ -0,0 +1,7 @@ +Feature: Patient + + @database + Scenario: Registering patient + Given valid data for patient + When request to register patient + Then patient should be registered