From 96d81a36286e41035ff4068859a3b0f9da924fbc Mon Sep 17 00:00:00 2001 From: vlad Date: Fri, 30 Jun 2017 19:38:37 -0700 Subject: Latest PDS UI utils including all the domain stuff --- .../concurrent/SafeBridgeUploadQueue.scala | 25 ++- .../scala/xyz/driver/pdsuicommon/db/FakeDbIo.scala | 4 +- .../xyz/driver/pdsuicommon/db/MySqlContext.scala | 3 +- .../driver/pdsuicommon/json/Serialization.scala | 5 + .../xyz/driver/pdsuicommon/utils/CharOps.scala | 2 +- .../xyz/driver/pdsuicommon/utils/StringOps.scala | 2 +- .../pdsuicommon/utils/WriteableImplicits.scala | 4 +- .../xyz/driver/pdsuicommon/utils/WritesUtils.scala | 2 +- .../xyz/driver/pdsuidomain/entities/Document.scala | 167 ++++++++++++++++++++- .../pdsuidomain/entities/ExtractedData.scala | 1 - .../driver/pdsuidomain/entities/Intervention.scala | 1 + .../pdsuidomain/entities/LinkedPatient.scala | 16 ++ .../pdsuidomain/entities/MedicalRecord.scala | 4 +- .../xyz/driver/pdsuidomain/entities/Patient.scala | 3 +- .../entities/PatientEligibleTrial.scala | 39 +---- .../entities/PatientLabelEvidenceView.scala | 28 ++++ .../pdsuidomain/entities/RawTrialLabel.scala | 3 +- .../xyz/driver/pdsuidomain/entities/Trial.scala | 5 +- .../driver/pdsuidomain/entities/UserHistory.scala | 156 +++++++++++++++++++ .../export/patient/ExportPatientLabel.scala | 30 ++++ .../patient/ExportPatientLabelEvidence.scala | 32 ++++ .../ExportPatientLabelEvidenceDocument.scala | 30 ++++ .../export/patient/ExportPatientWithLabels.scala | 26 ++++ .../entities/export/trial/ExportTrial.scala | 27 ++++ .../entities/export/trial/ExportTrialArm.scala | 15 ++ .../export/trial/ExportTrialLabelCriterion.scala | 32 ++++ .../export/trial/ExportTrialWithLabels.scala | 52 +++++++ .../formats/json/criterion/ApiNewCriterion.scala | 4 +- .../formats/json/document/ApiDocument.scala | 14 +- .../formats/json/document/ApiPartialDocument.scala | 8 +- .../formats/json/document/DocumentUtils.scala | 24 --- .../json/evidence/ApiPatientLabelEvidence.scala | 28 ++-- .../json/linkedpatient/ApiLinkedPatient.scala | 33 ++++ .../formats/json/message/ApiPartialMessage.scala | 2 - .../formats/json/patient/ApiPatient.scala | 3 + .../patient/eligible/ApiPatientEligibleTrial.scala | 6 +- .../patient/hypothesis/ApiPatientHypothesis.scala | 11 +- .../patient/label/ApiPartialPatientLabel.scala | 2 - .../json/patient/trial/ApiPatientCriterion.scala | 3 + .../formats/json/record/ApiCreateRecord.scala | 1 + .../formats/json/record/ApiRecord.scala | 3 + .../pdsuidomain/formats/json/trial/ApiTrial.scala | 3 + .../formats/json/user/ApiPartialUser.scala | 20 +-- .../formats/json/userhistory/ApiUserHistory.scala | 43 ++++++ .../driver/pdsuidomain/services/ArmService.scala | 12 +- .../pdsuidomain/services/CategoryService.scala | 3 +- .../pdsuidomain/services/DocumentService.scala | 2 - .../pdsuidomain/services/ExportService.scala | 64 ++++++++ .../driver/pdsuidomain/services/LabelService.scala | 3 +- .../services/LinkedPatientService.scala | 62 ++++++++ .../driver/pdsuidomain/services/MailService.scala | 39 +++++ .../pdsuidomain/services/MessageService.scala | 4 +- .../services/PatientCriterionService.scala | 7 +- .../services/PatientHypothesisService.scala | 8 +- .../services/PatientLabelEvidenceService.scala | 19 ++- .../pdsuidomain/services/QueueUploadService.scala | 78 ++++++++++ .../pdsuidomain/services/UserHistoryService.scala | 31 ++++ .../driver/pdsuidomain/services/UserService.scala | 4 +- .../services/fake/StubMailService.scala | 13 ++ .../services/rest/SendGridMailService.scala | 45 ++++++ 60 files changed, 1149 insertions(+), 167 deletions(-) create mode 100644 src/main/scala/xyz/driver/pdsuidomain/entities/LinkedPatient.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/entities/PatientLabelEvidenceView.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/entities/UserHistory.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/entities/export/patient/ExportPatientLabel.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/entities/export/patient/ExportPatientLabelEvidence.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/entities/export/patient/ExportPatientLabelEvidenceDocument.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/entities/export/patient/ExportPatientWithLabels.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/entities/export/trial/ExportTrial.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/entities/export/trial/ExportTrialArm.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/entities/export/trial/ExportTrialLabelCriterion.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/entities/export/trial/ExportTrialWithLabels.scala delete mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/document/DocumentUtils.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/linkedpatient/ApiLinkedPatient.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/userhistory/ApiUserHistory.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/services/ExportService.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/services/LinkedPatientService.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/services/MailService.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/services/QueueUploadService.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/services/UserHistoryService.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/services/fake/StubMailService.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/services/rest/SendGridMailService.scala (limited to 'src/main') diff --git a/src/main/scala/xyz/driver/pdsuicommon/concurrent/SafeBridgeUploadQueue.scala b/src/main/scala/xyz/driver/pdsuicommon/concurrent/SafeBridgeUploadQueue.scala index bab29d5..0bc8220 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/concurrent/SafeBridgeUploadQueue.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/concurrent/SafeBridgeUploadQueue.scala @@ -11,8 +11,7 @@ object SafeBridgeUploadQueue { trait Tag extends Product with Serializable - case class SafeTask[T <: Tag](tag: T, - private[SafeBridgeUploadQueue] val queueItem: BridgeUploadQueue.Item) + case class SafeTask[T <: Tag](tag: T, private[SafeBridgeUploadQueue] val queueItem: BridgeUploadQueue.Item) object SafeTask { implicit def toPhiString[T <: Tag](x: SafeTask[T]): PhiString = { @@ -27,20 +26,20 @@ object SafeBridgeUploadQueue { } -class SafeBridgeUploadQueue[T <: Tag](kind: String, - origQueue: BridgeUploadQueue) - (implicit - tagMarshaller: Marshaller[T, String], - dependencyResolver: DependencyResolver[T], - executionContext: ExecutionContext) { +class SafeBridgeUploadQueue[T <: Tag](kind: String, origQueue: BridgeUploadQueue)( + implicit tagMarshaller: Marshaller[T, String], + dependencyResolver: DependencyResolver[T], + executionContext: ExecutionContext) { type Task = SafeTask[T] - def add(tag: T): Future[BridgeUploadQueue.Item] = origQueue.add(BridgeUploadQueue.Item( - kind = kind, - tag = tagMarshaller.write(tag), - dependency = dependencyResolver.getDependency(tag) - )) + def add(tag: T): Future[BridgeUploadQueue.Item] = + origQueue.add( + BridgeUploadQueue.Item( + kind = kind, + tag = tagMarshaller.write(tag), + dependency = dependencyResolver.getDependency(tag) + )) def tryRetry(task: Task): Future[Option[Task]] = wrap(origQueue.tryRetry(task.queueItem)) diff --git a/src/main/scala/xyz/driver/pdsuicommon/db/FakeDbIo.scala b/src/main/scala/xyz/driver/pdsuicommon/db/FakeDbIo.scala index e5a628c..ac42a34 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/db/FakeDbIo.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/db/FakeDbIo.scala @@ -3,7 +3,7 @@ package xyz.driver.pdsuicommon.db import scala.concurrent.Future object FakeDbIo extends DbIo { - override def runAsync[T](f: => T): Future[T] = Future.successful(f) + override def runAsync[T](f: => T): Future[T] = Future.successful(f) override def runAsyncTx[T](f: => T): Future[T] = Future.successful(f) - override def runSyncTx[T](f: => T): Unit = f + override def runSyncTx[T](f: => T): Unit = f } diff --git a/src/main/scala/xyz/driver/pdsuicommon/db/MySqlContext.scala b/src/main/scala/xyz/driver/pdsuicommon/db/MySqlContext.scala index 768d1e3..f804e87 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/db/MySqlContext.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/db/MySqlContext.scala @@ -45,8 +45,7 @@ object MySqlContext extends PhiLogging { } class MySqlContext(dataSource: DataSource with Closeable, settings: Settings) - extends MysqlJdbcContext[MysqlEscape](dataSource) - with TransactionalContext + extends MysqlJdbcContext[MysqlEscape](dataSource) with TransactionalContext with EntityExtractorDerivation[Literal] { private val tpe = Executors.newFixedThreadPool(settings.threadPoolSize) diff --git a/src/main/scala/xyz/driver/pdsuicommon/json/Serialization.scala b/src/main/scala/xyz/driver/pdsuicommon/json/Serialization.scala index 223c66e..9800903 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/json/Serialization.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/json/Serialization.scala @@ -5,6 +5,7 @@ import java.net.URI import play.api.libs.functional.syntax._ import play.api.libs.json._ import xyz.driver.pdsuicommon.domain._ +import xyz.driver.pdsuidomain.entities.CaseId object Serialization { @@ -44,4 +45,8 @@ object Serialization { private val passwordHashJsonWrites: Writes[PasswordHash] = Writes( passwordHash => JsString(passwordHash.value.toString)) implicit val passwordHashJsonFormat: Format[PasswordHash] = Format(passwordHashJsonReads, passwordHashJsonWrites) + + private val caseIdJsonReads: Reads[CaseId] = Reads.StringReads.map(CaseId(_)) + private val caseIdJsonWrites: Writes[CaseId] = Writes(caseId => JsString(caseId.id)) + implicit val caseIdJsonFormat: Format[CaseId] = Format(caseIdJsonReads, caseIdJsonWrites) } diff --git a/src/main/scala/xyz/driver/pdsuicommon/utils/CharOps.scala b/src/main/scala/xyz/driver/pdsuicommon/utils/CharOps.scala index 42bf92d..cee9c73 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/utils/CharOps.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/utils/CharOps.scala @@ -20,7 +20,7 @@ private object CharOps { "\u3000\u2004\u3000\u3000\u2028\n\u2007\u3000" private val Multiplier: Int = 1682554634 - private val Shift: Int = Integer.numberOfLeadingZeros(Table.length - 1) + private val Shift: Int = Integer.numberOfLeadingZeros(Table.length - 1) def matches(c: Char): Boolean = Table.charAt((Multiplier * c) >>> Shift) == c } diff --git a/src/main/scala/xyz/driver/pdsuicommon/utils/StringOps.scala b/src/main/scala/xyz/driver/pdsuicommon/utils/StringOps.scala index b38721e..eaac761 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/utils/StringOps.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/utils/StringOps.scala @@ -11,7 +11,7 @@ final class StringOps(val self: String) extends AnyVal { "" } else { val start = self.indexWhere(shouldKeep) - val end = self.lastIndexWhere(shouldKeep) + val end = self.lastIndexWhere(shouldKeep) if (start >= 0 && end >= 0) { self.substring(start, end + 1) diff --git a/src/main/scala/xyz/driver/pdsuicommon/utils/WriteableImplicits.scala b/src/main/scala/xyz/driver/pdsuicommon/utils/WriteableImplicits.scala index 2c66a23..6c04dfa 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/utils/WriteableImplicits.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/utils/WriteableImplicits.scala @@ -8,7 +8,9 @@ trait WriteableImplicits { // Write JSON by default at now implicit def defaultWriteable[T](implicit inner: Writes[T]) = Writeable[T]( - { x: T => Writeable.writeableOf_JsValue.transform(Json.toJson(x)) }, + { x: T => + Writeable.writeableOf_JsValue.transform(Json.toJson(x)) + }, Option(ContentTypes.JSON) ) diff --git a/src/main/scala/xyz/driver/pdsuicommon/utils/WritesUtils.scala b/src/main/scala/xyz/driver/pdsuicommon/utils/WritesUtils.scala index fa05e96..3476a1e 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/utils/WritesUtils.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/utils/WritesUtils.scala @@ -14,7 +14,7 @@ object WritesUtils { w.transform { input: JsValue => input match { case JsObject(map) => JsObject(map.filter(Function.tupled(p))) - case x => x + case x => x } } } diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/Document.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/Document.scala index 5af00bc..5c957fb 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/entities/Document.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/entities/Document.scala @@ -1,6 +1,6 @@ package xyz.driver.pdsuidomain.entities -import java.time.{LocalDate, LocalDateTime} +import java.time.{LocalDate, LocalDateTime, ZoneId} import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.core.{JsonGenerator, JsonParser} @@ -17,6 +17,72 @@ import xyz.driver.pdsuicommon.compat.Implicits._ final case class ProviderType(id: LongId[ProviderType], name: String) object ProviderType { + sealed trait ProviderTypeName + case object MedicalOncology extends ProviderTypeName + case object Surgery extends ProviderTypeName + case object Pathology extends ProviderTypeName + case object MolecularPathology extends ProviderTypeName + case object LaboratoryMedicine extends ProviderTypeName + case object Radiology extends ProviderTypeName + case object InterventionalRadiology extends ProviderTypeName + case object RadiationOncology extends ProviderTypeName + case object PrimaryCare extends ProviderTypeName + case object Cardiology extends ProviderTypeName + case object Dermatology extends ProviderTypeName + case object Ophthalmology extends ProviderTypeName + case object Gastroenterology extends ProviderTypeName + case object Neurology extends ProviderTypeName + case object Psychiatry extends ProviderTypeName + case object Gynecology extends ProviderTypeName + case object InfectiousDisease extends ProviderTypeName + case object Immunology extends ProviderTypeName + case object Nephrology extends ProviderTypeName + case object Rheumatology extends ProviderTypeName + case object Cytology extends ProviderTypeName + case object Otolaryngology extends ProviderTypeName + case object Anesthesiology extends ProviderTypeName + case object Urology extends ProviderTypeName + case object PalliativeCare extends ProviderTypeName + case object EmergencyMedicine extends ProviderTypeName + case object SocialWork extends ProviderTypeName + case object NA extends ProviderTypeName + case object Other extends ProviderTypeName + + def fromString(txt: String): Option[ProviderTypeName] = { + txt match { + case "Medical Oncology" => Some(MedicalOncology) + case "Surgery" => Some(Surgery) + case "Pathology" => Some(Pathology) + case "Molecular Pathology" => Some(MolecularPathology) + case "LaboratoryMedicine" => Some(LaboratoryMedicine) + case "Radiology" => Some(Radiology) + case "Interventional Radiology" => Some(InterventionalRadiology) + case "Radiation Oncology" => Some(RadiationOncology) + case "Primary Care" => Some(PrimaryCare) + case "Cardiology" => Some(Cardiology) + case "Dermatology" => Some(Dermatology) + case "Ophthalmology" => Some(Ophthalmology) + case "Gastroenterology" => Some(Gastroenterology) + case "Neurology" => Some(Neurology) + case "Psychiatry" => Some(Psychiatry) + case "Gynecology" => Some(Gynecology) + case "Infectious Disease" => Some(InfectiousDisease) + case "Immunology" => Some(Immunology) + case "Nephrology" => Some(Nephrology) + case "Rheumatology" => Some(Rheumatology) + case "Cytology" => Some(Cytology) + case "Otolaryngology" => Some(Otolaryngology) + case "Anesthesiology" => Some(Anesthesiology) + case "Urology" => Some(Urology) + case "Palliative Care" => Some(PalliativeCare) + case "Emergency Medicine" => Some(EmergencyMedicine) + case "Social Work" => Some(SocialWork) + case "N/A" => Some(NA) + case "Other" => Some(Other) + case _ => None + } + } + implicit def toPhiString(x: ProviderType): PhiString = { import x._ phi"ProviderType(id=$id, category=${Unsafe(name)})" @@ -26,6 +92,38 @@ object ProviderType { final case class DocumentType(id: LongId[DocumentType], name: String) object DocumentType { + sealed trait DocumentTypeName + case object OutpatientPhysicianNote extends DocumentTypeName + case object DischargeNote extends DocumentTypeName + case object LaboratoryReport extends DocumentTypeName + case object MedicationList extends DocumentTypeName + case object HospitalizationNote extends DocumentTypeName + case object PathologyReport extends DocumentTypeName + case object RadiologyReport extends DocumentTypeName + case object OperativeProcedureReport extends DocumentTypeName + case object MedicationAdministration extends DocumentTypeName + case object SocialWorkCaseManagementNote extends DocumentTypeName + case object NonPhysicianProviderNote extends DocumentTypeName + case object Administrative extends DocumentTypeName + + def fromString(txt: String): Option[DocumentTypeName] = { + txt match { + case "Outpatient Physician Note" => Some(OutpatientPhysicianNote) + case "Discharge Note" => Some(DischargeNote) + case "Laboratory Report" => Some(LaboratoryReport) + case "Medication List" => Some(MedicationList) + case "Hospitalization Note" => Some(HospitalizationNote) + case "Pathology Report" => Some(PathologyReport) + case "Radiology Report" => Some(RadiologyReport) + case "Operative/Procedure Report" => Some(OperativeProcedureReport) + case "Medication Administration" => Some(MedicationAdministration) + case "Social Work/Case Management Note" => Some(SocialWorkCaseManagementNote) + case "Non-physician Provider Note" => Some(NonPhysicianProviderNote) + case "Administrative" => Some(Administrative) + case _ => None + } + } + implicit def toPhiString(x: DocumentType): PhiString = { import x._ phi"DocumentType(id=$id, name=${Unsafe(name)})" @@ -71,6 +169,7 @@ object Document { def oneOf(xs: Set[Status]): Boolean = xs.contains(this) } + object Status { case object New extends Status case object Organized extends Status @@ -82,12 +181,58 @@ object Document { val All = Set[Status](New, Organized, Extracted, Done, Flagged, Archived) val AllPrevious = Set[Status](Organized, Extracted) + val fromString: PartialFunction[String, Status] = { + case "New" => Status.New + case "Organized" => Status.Organized + case "Extracted" => Status.Extracted + case "Done" => Status.Done + case "Flagged" => Status.Flagged + case "Archived" => Status.Archived + } + + def statusToString(x: Status): String = x match { + case Status.New => "New" + case Status.Organized => "Organized" + case Status.Extracted => "Extracted" + case Status.Done => "Done" + case Status.Flagged => "Flagged" + case Status.Archived => "Archived" + } + implicit def toPhiString(x: Status): PhiString = Unsafe(Utils.getClassSimpleName(x.getClass)) } + sealed trait RequiredType extends Product with Serializable { + + def oneOf(xs: RequiredType*): Boolean = xs.contains(this) + + def oneOf(xs: Set[RequiredType]): Boolean = xs.contains(this) + + } + + object RequiredType { + case object OPN extends RequiredType + case object PN extends RequiredType + + val All = Set[RequiredType](OPN, PN) + + val fromString: PartialFunction[String, RequiredType] = { + case "OPN" => RequiredType.OPN + case "PN" => RequiredType.PN + } + + def requiredTypeToString(x: RequiredType): String = x match { + case RequiredType.OPN => "OPN" + case RequiredType.PN => "PN" + } + + implicit def toPhiString(x: RequiredType): PhiString = Unsafe(Utils.getClassSimpleName(x.getClass)) + } + implicit def toPhiString(x: Document): PhiString = { import x._ - phi"Document(id=$id, status=$status, assignee=$assignee, previousAssignee=$previousAssignee, recordId=$recordId)" + phi"Document(id=$id, status=$status, assignee=$assignee, " + + phi"previousAssignee=$previousAssignee, lastActiveUserId=$lastActiveUserId, recordId=$recordId)" } val validator: Validator[Document, Document] = { input => @@ -126,11 +271,13 @@ final case class Document(id: LongId[Document] = LongId(0L), previousStatus: Option[Document.Status], assignee: Option[LongId[User]], previousAssignee: Option[LongId[User]], + lastActiveUserId: Option[LongId[User]], recordId: LongId[MedicalRecord], physician: Option[String], typeId: Option[LongId[DocumentType]], // not null providerName: Option[String], // not null providerTypeId: Option[LongId[ProviderType]], // not null + requiredType: Option[Document.RequiredType], meta: Option[TextJson[Meta]], // not null startDate: Option[LocalDate], // not null endDate: Option[LocalDate], @@ -142,6 +289,22 @@ final case class Document(id: LongId[Document] = LongId(0L), assert(AllPrevious.contains(previousStatus.get), s"Previous status has invalid value: ${previousStatus.get}") } + def getRequiredType(documentTypeName: String, providerTypeName: String): Option[Document.RequiredType] = { + import DocumentType.{OutpatientPhysicianNote, PathologyReport} + import ProviderType.MedicalOncology + + (DocumentType.fromString(documentTypeName), ProviderType.fromString(providerTypeName), startDate) match { + case (Some(OutpatientPhysicianNote), Some(MedicalOncology), Some(date)) + if !(date.isAfter(LocalDate.now(ZoneId.of("Z"))) || date.isBefore( + LocalDate.now(ZoneId.of("Z")).minusMonths(6))) => + Some(Document.RequiredType.OPN) + + case (Some(PathologyReport), _, _) => Some(Document.RequiredType.PN) + + case _ => None + } + } + // TODO: with the current business logic code this constraint sometimes harmful // require(status match { // case Document.Status.New if assignee.isDefined => false diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/ExtractedData.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/ExtractedData.scala index 9972142..32258dc 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/entities/ExtractedData.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/entities/ExtractedData.scala @@ -19,7 +19,6 @@ object ExtractedData { final case class Meta(keyword: Meta.Keyword, evidence: Meta.Evidence) object Meta { - final case class Evidence(pageRatio: Double, start: TextLayerPosition, end: TextLayerPosition) final case class TextLayerPosition(page: Integer, index: Integer, offset: Integer) diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/Intervention.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/Intervention.scala index dc82050..e691547 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/entities/Intervention.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/entities/Intervention.scala @@ -28,6 +28,7 @@ final case class Intervention(id: LongId[Intervention], typeId: Option[LongId[InterventionType]], originalType: Option[String], description: String, + originalDescription: String, isActive: Boolean) object Intervention { diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/LinkedPatient.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/LinkedPatient.scala new file mode 100644 index 0000000..e82bcc3 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/entities/LinkedPatient.scala @@ -0,0 +1,16 @@ +package xyz.driver.pdsuidomain.entities + +import xyz.driver.pdsuicommon.domain._ +import xyz.driver.pdsuicommon.logging._ + +object LinkedPatient { + + implicit def toPhiString(x: LinkedPatient): PhiString = { + import x._ + phi"LinkedPatient(userId=$userId, patientId=$patientId, trialId=$trialId)" + } +} + +case class LinkedPatient(userId: LongId[User], + patientId: UuidId[Patient], + trialId: StringId[Trial]) diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/MedicalRecord.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/MedicalRecord.scala index 17de7de..0e30fa0 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/entities/MedicalRecord.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/entities/MedicalRecord.scala @@ -134,7 +134,8 @@ object MedicalRecord { implicit def toPhiString(x: MedicalRecord): PhiString = { import x._ - phi"MedicalRecord(id=$id, status=$status, assignee=$assignee, previousAssignee=$previousAssignee)" + phi"MedicalRecord(id=$id, status=$status, assignee=$assignee, " + + phi"previousAssignee=$previousAssignee, lastActiveUserId=$lastActiveUserId)" } } @@ -143,6 +144,7 @@ case class MedicalRecord(id: LongId[MedicalRecord], previousStatus: Option[MedicalRecord.Status], assignee: Option[LongId[User]], previousAssignee: Option[LongId[User]], + lastActiveUserId: Option[LongId[User]], patientId: UuidId[Patient], requestId: RecordRequestId, disease: String, diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/Patient.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/Patient.scala index 93262a4..695245c 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/entities/Patient.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/entities/Patient.scala @@ -45,7 +45,7 @@ object Patient { implicit def toPhiString(x: Patient): PhiString = { import x._ - phi"Patient(id=$id, status=$status, previousStatus=$previousStatus, " + + phi"Patient(id=$id, status=$status, previousStatus=$previousStatus, lastActiveUserId=$lastActiveUserId" + phi"assignee=$assignee, previousAssignee=$previousAssignee)" } } @@ -57,6 +57,7 @@ final case class Patient(id: UuidId[Patient], assignee: Option[LongId[User]], previousStatus: Option[Patient.Status], previousAssignee: Option[LongId[User]], + lastActiveUserId: Option[LongId[User]], isUpdateRequired: Boolean, condition: String, orderId: PatientOrderId, diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/PatientEligibleTrial.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/PatientEligibleTrial.scala index 8084884..e16c324 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/entities/PatientEligibleTrial.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/entities/PatientEligibleTrial.scala @@ -82,35 +82,8 @@ object PatientTrialArmGroup { implicit def toPhiString(x: PatientTrialArmGroup): PhiString = { import x._ phi"PatientTrialArmGroup(id=$id, eligibleTrialId=$eligibleTrialId, " + - phi"eligibilityStatus=${Unsafe(eligibilityStatus)}, isVerified=$isVerified)" - } - - /** - * @see https://driverinc.atlassian.net/wiki/display/DMPD/EV+Business+Process - */ - def getEligibilityStatusForEvToRc(criterionList: List[(Option[Boolean], Option[FuzzyValue])]): Option[FuzzyValue] = { - def isEligible = { - // Eligible, if for all (verified for EV) label-criteria eligibilityStatus=NULL or YES and at least one has status=YES - // If method executes from PatientService (when EV submit patient) need to check value PatientCriterion.isVerified - val verifiedList = criterionList.filter { case (isVerifiedOpt, _) => isVerifiedOpt.isEmpty || isVerifiedOpt.get } - verifiedList.forall { - case (_, eligibilityStatus) => - eligibilityStatus.isEmpty || eligibilityStatus.contains(FuzzyValue.Yes) - } && verifiedList.exists { case (_, eligibilityStatus) => eligibilityStatus.contains(FuzzyValue.Yes) } - } - - if (criterionList.exists { - case (isVerified, eligibilityStatus) => - eligibilityStatus.contains(FuzzyValue.No) && (isVerified.isEmpty || isVerified.get) - }) { Some(FuzzyValue.No) } else if (criterionList.forall { - case (_, eligibilityStatus) => eligibilityStatus.isEmpty - }) { - None - } else if (isEligible) { - Some(FuzzyValue.Yes) - } else { - Some(FuzzyValue.Maybe) - } + phi"eligibilityStatus=${Unsafe(eligibilityStatus)}, " + + phi"verifiedEligibilityStatus=${Unsafe(verifiedEligibilityStatus)}, isVerified=$isVerified)" } /** @@ -131,14 +104,16 @@ object PatientTrialArmGroup { final case class PatientTrialArmGroup(id: LongId[PatientTrialArmGroup], eligibleTrialId: UuidId[PatientEligibleTrial], eligibilityStatus: Option[FuzzyValue], + verifiedEligibilityStatus: Option[FuzzyValue], isVerified: Boolean) object PatientTrialArmGroupView { implicit def toPhiString(x: PatientTrialArmGroupView): PhiString = { import x._ - phi"PatientTrialArmGroupView(id=$id, patientId=$patientId, trialId=$trialId, hypothesisId=$hypothesisId, " + - phi"eligibilityStatus=${Unsafe(eligibilityStatus)}, isVerified=$isVerified)" + phi"PatientTrialArmGroupView(id=$id, patientId=$patientId, trialId=$trialId, " + + phi"hypothesisId=$hypothesisId, eligibilityStatus=${Unsafe(eligibilityStatus)}, " + + phi"verifiedEligibilityStatus=${Unsafe(verifiedEligibilityStatus)}, isVerified=$isVerified)" } } @@ -147,11 +122,13 @@ final case class PatientTrialArmGroupView(id: LongId[PatientTrialArmGroup], trialId: StringId[Trial], hypothesisId: UuidId[Hypothesis], eligibilityStatus: Option[FuzzyValue], + verifiedEligibilityStatus: Option[FuzzyValue], isVerified: Boolean) { def applyTo(trialArmGroup: PatientTrialArmGroup) = { trialArmGroup.copy( eligibilityStatus = this.eligibilityStatus, + verifiedEligibilityStatus = this.verifiedEligibilityStatus, isVerified = this.isVerified ) } diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/PatientLabelEvidenceView.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/PatientLabelEvidenceView.scala new file mode 100644 index 0000000..60298b9 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/entities/PatientLabelEvidenceView.scala @@ -0,0 +1,28 @@ +package xyz.driver.pdsuidomain.entities + +import java.time.LocalDate + +import xyz.driver.pdsuicommon.domain._ +import xyz.driver.pdsuicommon.logging._ + +object PatientLabelEvidenceView { + implicit def toPhiString(x: PatientLabelEvidenceView): PhiString = { + import x._ + phi"PatientLabelEvidenceViewRepository.Row(id=$id, value=$value, documentId=$documentId, " + + phi"evidenceId=$evidenceId, reportId=$reportId, patientId=$patientId, labelId=$labelId, " + + phi"isImplicitMatch=$isImplicitMatch)" + } +} + +final case class PatientLabelEvidenceView(id: LongId[PatientLabelEvidence], + value: FuzzyValue, + evidenceText: String, + documentId: Option[LongId[Document]], + evidenceId: Option[LongId[ExtractedData]], + reportId: Option[UuidId[DirectReport]], + documentType: String, + date: Option[LocalDate], // Document.startDate is optional + providerType: String, + patientId: UuidId[Patient], + labelId: LongId[Label], + isImplicitMatch: Boolean) diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/RawTrialLabel.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/RawTrialLabel.scala index e3b323f..82204c2 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/entities/RawTrialLabel.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/entities/RawTrialLabel.scala @@ -12,7 +12,6 @@ case class RawTrialLabel(nctId: StringId[Trial], armName: String, armId: LongId[Arm], labelId: LongId[Label], - label: String, value: Option[Boolean], criterionId: LongId[Criterion], criteria: String, @@ -25,7 +24,7 @@ object RawTrialLabel { implicit def toPhiString(x: RawTrialLabel): PhiString = { import x._ phi"RawTrialLabel(nctId=$nctId, trialId=$trialId, condition=${Unsafe(condition)}, lastReviewed=$lastReviewed, " + - phi"armId=$armId, armName=${Unsafe(armName)}, labelId=$labelId, label=${Unsafe(label)}, value=$value, " + + phi"armId=$armId, armName=${Unsafe(armName)}, labelId=$labelId, value=$value, " + phi"criterionId=$criterionId, criteria=${Unsafe(criteria)}, criterionArmId=$criterionArmId, " + phi"isCompound=$isCompound, isDefining=$isDefining)" } diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/Trial.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/Trial.scala index f5ab3cb..0f478ca 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/entities/Trial.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/entities/Trial.scala @@ -57,8 +57,8 @@ object Trial { implicit def toPhiString(x: Trial): PhiString = { import x._ phi"Trial(id=$id, externalId=$externalId, status=$status, previousStatus=$previousStatus, " + - phi"assignee=$assignee, previousAssignee=$previousAssignee, isSummaryReviewed=$isSummaryReviewed, " + - phi"isCriteriaReviewed=$isCriteriaReviewed)" + phi"lastActiveUserId=$lastActiveUserId, assignee=$assignee, previousAssignee=$previousAssignee, " + + phi"isSummaryReviewed=$isSummaryReviewed, isCriteriaReviewed=$isCriteriaReviewed)" } case class Locations(locations: List[String]) @@ -81,6 +81,7 @@ final case class Trial(id: StringId[Trial], assignee: Option[LongId[User]], previousStatus: Option[Status], previousAssignee: Option[LongId[User]], + lastActiveUserId: Option[LongId[User]], lastUpdate: LocalDateTime, condition: Condition, phase: String, diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/UserHistory.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/UserHistory.scala new file mode 100644 index 0000000..b55369e --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/entities/UserHistory.scala @@ -0,0 +1,156 @@ +package xyz.driver.pdsuidomain.entities + +import java.time.{LocalDateTime, ZoneId} + +import xyz.driver.pdsuicommon.domain._ +import xyz.driver.pdsuicommon.logging._ +import xyz.driver.pdsuicommon.utils.Utils + +final case class UserHistory(id: LongId[UserHistory], + executor: LongId[User], + recordId: Option[LongId[MedicalRecord]] = None, + documentId: Option[LongId[Document]] = None, + trialId: Option[StringId[Trial]] = None, + patientId: Option[UuidId[Patient]] = None, + state: UserHistory.State, + action: UserHistory.Action, + created: LocalDateTime = LocalDateTime.now(ZoneId.of("Z"))) + +object UserHistory { + + def forDocument(executor: LongId[User], + documentId: LongId[Document], + state: UserHistory.State, + action: UserHistory.Action): UserHistory = UserHistory ( + id = LongId(0L), + executor = executor, + documentId = Some(documentId), + state = state, + action = action + ) + + def forRecord(executor: LongId[User], + recordId: LongId[MedicalRecord], + state: UserHistory.State, + action: UserHistory.Action): UserHistory = UserHistory ( + id = LongId(0L), + executor = executor, + recordId = Some(recordId), + state = state, + action = action + ) + + def forTrial(executor: LongId[User], + trialId: StringId[Trial], + state: UserHistory.State, + action: UserHistory.Action): UserHistory = UserHistory ( + id = LongId(0L), + executor = executor, + trialId = Some(trialId), + state = state, + action = action + ) + + def forPatient(executor: LongId[User], + patientId: UuidId[Patient], + state: UserHistory.State, + action: UserHistory.Action): UserHistory = UserHistory ( + id = LongId(0L), + executor = executor, + patientId = Some(patientId), + state = state, + action = action + ) + + sealed trait State extends Product with Serializable { + + def oneOf(xs: State*): Boolean = xs.contains(this) + + def oneOf(xs: Set[State]): Boolean = xs.contains(this) + } + + object State { + case object Clean extends State + case object Organize extends State + case object Extract extends State + case object Summarize extends State + case object Criteriarize extends State + case object Verify extends State + case object Curate extends State + case object Review extends State + case object Flag extends State + + val All: Set[State] = Set[State](Clean, Organize, Extract, Summarize, Criteriarize, Verify, Curate, Review, Flag) + + val fromString: PartialFunction[String, State] = { + case "Clean" => State.Clean + case "Organize" => State.Organize + case "Extract" => State.Extract + case "Summarize" => State.Summarize + case "Criteriarize" => State.Criteriarize + case "Verify" => State.Verify + case "Curate" => State.Curate + case "Review" => State.Review + case "Flag" => State.Flag + } + + def stateToString(x: State): String = x match { + case State.Clean => "Clean" + case State.Organize => "Organize" + case State.Extract => "Extract" + case State.Summarize => "Summarize" + case State.Criteriarize => "Criteriarize" + case State.Verify => "Verify" + case State.Curate => "Curate" + case State.Review => "Review" + case State.Flag => "Flag" + } + + implicit def toPhiString(x: State): PhiString = Unsafe(Utils.getClassSimpleName(x.getClass)) + } + + sealed trait Action extends Product with Serializable { + + def oneOf(xs: Action*): Boolean = xs.contains(this) + + def oneOf(xs: Set[Action]): Boolean = xs.contains(this) + } + + object Action { + case object Start extends Action + case object Submit extends Action + case object Unassign extends Action + case object Resolve extends Action + case object Flag extends Action + case object Archive extends Action + + val All: Set[Action] = Set[Action](Start, Submit, Unassign, Resolve, Flag, Archive) + + val fromString: PartialFunction[String, Action] = { + case "Start" => Action.Start + case "Submit" => Action.Submit + case "Unassign" => Action.Unassign + case "Resolve" => Action.Resolve + case "Flag" => Action.Flag + case "Archive" => Action.Archive + } + + def actionToString(x: Action): String = x match { + case Action.Start => "Start" + case Action.Submit => "Submit" + case Action.Unassign => "Unassign" + case Action.Resolve => "Resolve" + case Action.Flag => "Flag" + case Action.Archive => "Archive" + } + + implicit def toPhiString(x: Action): PhiString = Unsafe(Utils.getClassSimpleName(x.getClass)) + } + + implicit def toPhiString(x: UserHistory): PhiString = { + import x._ + phi"UserHistory(id=$id, executor=$executor, recordId=$recordId, " + + phi"documentId=$documentId, trialId=$trialId, patientId=$patientId, " + + phi"state=$state, action=$action, created=$created)" + } +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/export/patient/ExportPatientLabel.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/export/patient/ExportPatientLabel.scala new file mode 100644 index 0000000..5474413 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/entities/export/patient/ExportPatientLabel.scala @@ -0,0 +1,30 @@ +package xyz.driver.pdsuidomain.entities.export.patient + +import xyz.driver.pdsuicommon.domain.LongId +import xyz.driver.pdsuicommon.logging._ +import xyz.driver.pdsuidomain.entities.{Label, RawPatientLabel} + +final case class ExportPatientLabel(id: LongId[Label], + name: String, + evidences: List[ExportPatientLabelEvidence]) + +object ExportPatientLabel extends PhiLogging { + + implicit def toPhiString(x: ExportPatientLabel): PhiString = { + import x._ + phi"ExportPatientLabel(id=$id, evidences=$evidences)" + } + + def fromRaw(labelId: LongId[Label], rawPatientLabels: List[RawPatientLabel]): ExportPatientLabel = { + val firstLabel = rawPatientLabels.headOption + if (firstLabel.isEmpty) { + logger.warn(phi"rawPatientLabels is empty, labelId: $labelId") + } + + ExportPatientLabel( + id = labelId, + name = firstLabel.map(_.label).getOrElse(""), + evidences = rawPatientLabels.map(ExportPatientLabelEvidence.fromRaw) + ) + } +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/export/patient/ExportPatientLabelEvidence.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/export/patient/ExportPatientLabelEvidence.scala new file mode 100644 index 0000000..fb40339 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/entities/export/patient/ExportPatientLabelEvidence.scala @@ -0,0 +1,32 @@ +package xyz.driver.pdsuidomain.entities.export.patient + +import xyz.driver.pdsuicommon.domain._ +import xyz.driver.pdsuicommon.logging._ +import xyz.driver.pdsuidomain.entities.{ExtractedData, RawPatientLabel} + +final case class ExportPatientLabelEvidence(id: LongId[ExtractedData], + value: FuzzyValue, + evidenceText: String, + document: ExportPatientLabelEvidenceDocument) + +object ExportPatientLabelEvidence { + + implicit def toPhiString(x: ExportPatientLabelEvidence): PhiString = { + import x._ + phi"ExportPatientLabelEvidence(id=${Unsafe(id)}, value=$value, " + + phi"evidenceText=${Unsafe(evidenceText)}, document=$document)" + } + + def fromRaw(x: RawPatientLabel) = ExportPatientLabelEvidence( + id = x.evidenceId, + value = x.value, + evidenceText = x.evidenceText, + document = ExportPatientLabelEvidenceDocument( + x.documentId, + x.requestId, + x.documentType, + x.providerType, + x.startDate + ) + ) +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/export/patient/ExportPatientLabelEvidenceDocument.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/export/patient/ExportPatientLabelEvidenceDocument.scala new file mode 100644 index 0000000..99912bc --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/entities/export/patient/ExportPatientLabelEvidenceDocument.scala @@ -0,0 +1,30 @@ +package xyz.driver.pdsuidomain.entities.export.patient + +import java.time.LocalDate + +import xyz.driver.pdsuicommon.domain._ +import xyz.driver.pdsuicommon.logging._ +import xyz.driver.pdsuidomain.entities.{Document, RawPatientLabel, RecordRequestId} + +final case class ExportPatientLabelEvidenceDocument(documentId: LongId[Document], + requestId: RecordRequestId, + documentType: String, + providerType: String, + date: LocalDate) + +object ExportPatientLabelEvidenceDocument extends PhiLogging { + + implicit def toPhiString(x: ExportPatientLabelEvidenceDocument): PhiString = { + import x._ + phi"ExportPatientLabelEvidenceDocument(documentId=$documentId, requestId=$requestId, " + + phi"documentType=${Unsafe(documentType)}, providerType=${Unsafe(providerType)}, date=$date)" + } + + def fromRaw(x: RawPatientLabel) = ExportPatientLabelEvidenceDocument( + documentId = x.documentId, + requestId = x.requestId, + documentType = x.documentType, + providerType = x.providerType, + date = x.startDate + ) +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/export/patient/ExportPatientWithLabels.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/export/patient/ExportPatientWithLabels.scala new file mode 100644 index 0000000..e6262ed --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/entities/export/patient/ExportPatientWithLabels.scala @@ -0,0 +1,26 @@ +package xyz.driver.pdsuidomain.entities.export.patient + +import xyz.driver.pdsuicommon.domain._ +import xyz.driver.pdsuicommon.logging._ +import xyz.driver.pdsuidomain.entities.{Patient, RawPatientLabel} + +import scala.collection.breakOut + +final case class ExportPatientWithLabels(patientId: UuidId[Patient], labelVersion: Long, labels: List[ExportPatientLabel]) + +object ExportPatientWithLabels { + + implicit def toPhiString(x: ExportPatientWithLabels): PhiString = { + import x._ + phi"ExportPatientWithLabels(patientId=$patientId, version=${Unsafe(labelVersion)}, labels=$labels)" + } + + def fromRaw(patientId: UuidId[Patient], + rawPatientRefs: List[RawPatientLabel]) = ExportPatientWithLabels( + patientId = patientId, + labelVersion = 1L, // TODO It is needed to replace this mock label version. + labels = rawPatientRefs + .groupBy(_.labelId) + .map(Function.tupled(ExportPatientLabel.fromRaw))(breakOut) + ) +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/export/trial/ExportTrial.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/export/trial/ExportTrial.scala new file mode 100644 index 0000000..1aed121 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/entities/export/trial/ExportTrial.scala @@ -0,0 +1,27 @@ +package xyz.driver.pdsuidomain.entities.export.trial + +import java.time.LocalDateTime + +import xyz.driver.pdsuicommon.domain._ +import xyz.driver.pdsuicommon.logging._ +import xyz.driver.pdsuidomain.entities.Trial + +final case class ExportTrial(nctId: StringId[Trial], + trialId: UuidId[Trial], + condition: Trial.Condition, + lastReviewed: LocalDateTime) + +object ExportTrial { + + implicit def toPhiString(x: ExportTrial): PhiString = { + import x._ + phi"ExportTrial(nctId=$nctId, trialId=$trialId, condition=${Unsafe(condition)}, lastReviewed=$lastReviewed)" + } + + def fromDomain(x: Trial) = ExportTrial( + nctId = x.id, + trialId = x.externalId, + condition = x.condition, + lastReviewed = x.lastUpdate + ) +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/export/trial/ExportTrialArm.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/export/trial/ExportTrialArm.scala new file mode 100644 index 0000000..5a9a406 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/entities/export/trial/ExportTrialArm.scala @@ -0,0 +1,15 @@ +package xyz.driver.pdsuidomain.entities.export.trial + +import xyz.driver.pdsuicommon.domain._ +import xyz.driver.pdsuicommon.logging._ +import xyz.driver.pdsuidomain.entities.Arm + +final case class ExportTrialArm(armId: LongId[Arm], armName: String) + +object ExportTrialArm { + + implicit def toPhiString(x: ExportTrialArm): PhiString = { + import x._ + phi"ExportTrialArm(armId=$armId, armName=${Unsafe(armName)})" + } +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/export/trial/ExportTrialLabelCriterion.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/export/trial/ExportTrialLabelCriterion.scala new file mode 100644 index 0000000..7bff22c --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/entities/export/trial/ExportTrialLabelCriterion.scala @@ -0,0 +1,32 @@ +package xyz.driver.pdsuidomain.entities.export.trial + +import xyz.driver.pdsuicommon.domain._ +import xyz.driver.pdsuicommon.logging._ +import xyz.driver.pdsuidomain.entities.{Arm, Criterion, Label, RawTrialLabel} + +final case class ExportTrialLabelCriterion(criterionId: LongId[Criterion], + value: Option[Boolean], + labelId: LongId[Label], + armIds: Set[LongId[Arm]], + criteria: String, + isCompound: Boolean, + isDefining: Boolean) + +object ExportTrialLabelCriterion { + + implicit def toPhiString(x: ExportTrialLabelCriterion): PhiString = { + import x._ + phi"TrialLabelCriterion(criterionId=$criterionId, value=$value, labelId=$labelId, " + + phi"criteria=${Unsafe(criteria)}, isCompound=$isCompound, isDefining=$isDefining)" + } + + def fromRaw(x: RawTrialLabel, armIds: Set[LongId[Arm]]) = ExportTrialLabelCriterion( + criterionId = x.criterionId, + value = x.value, + labelId = x.labelId, + armIds = armIds, + criteria = x.criteria, + isCompound = x.isCompound, + isDefining = x.isDefining + ) +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/export/trial/ExportTrialWithLabels.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/export/trial/ExportTrialWithLabels.scala new file mode 100644 index 0000000..1655a88 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/entities/export/trial/ExportTrialWithLabels.scala @@ -0,0 +1,52 @@ +package xyz.driver.pdsuidomain.entities.export.trial + +import java.time.LocalDateTime + +import xyz.driver.pdsuicommon.domain._ +import xyz.driver.pdsuicommon.logging._ +import xyz.driver.pdsuidomain.entities.{RawTrialLabel, Trial} + +import scala.collection.breakOut + +final case class ExportTrialWithLabels(nctId: StringId[Trial], + trialId: UuidId[Trial], + condition: String, + lastReviewed: LocalDateTime, + labelVersion: Long, + arms: List[ExportTrialArm], + criteria: List[ExportTrialLabelCriterion]) + +object ExportTrialWithLabels { + + implicit def toPhiString(x: ExportTrialWithLabels): PhiString = { + import x._ + phi"TrialWithLabels(nctId=$nctId, trialId=$trialId, condition=${Unsafe(condition)}, " + + phi"lastReviewed=$lastReviewed, labelVersion=${Unsafe(labelVersion)}, arms=$arms, criteria=$criteria)" + } + + def fromRaw(rawData: List[RawTrialLabel]): ExportTrialWithLabels = { + val trials: Set[StringId[Trial]] = rawData.map(_.nctId)(breakOut) + + assert(trials.size == 1, "There are more than one trials in the rawData") + val trial = rawData.head + + ExportTrialWithLabels( + nctId = trial.nctId, + trialId = trial.trialId, + condition = trial.condition, + lastReviewed = trial.lastReviewed, + labelVersion = 1, // TODO It is needed to replace this mock label version. + arms = rawData.groupBy(_.armId).map { case (armId, rawTrials) => + ExportTrialArm(armId, rawTrials.head.armName) + }(breakOut), + criteria = rawData.groupBy { x => + (x.criterionId, x.labelId) + }.map { + case (_, rawTrialLabels) => + val armIds = rawTrialLabels.map(_.criterionArmId).toSet + ExportTrialLabelCriterion.fromRaw(rawTrialLabels.head, armIds) + }(breakOut) + ) + } + +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/criterion/ApiNewCriterion.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/criterion/ApiNewCriterion.scala index 604a98b..ab7641f 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/criterion/ApiNewCriterion.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/criterion/ApiNewCriterion.scala @@ -11,6 +11,7 @@ import xyz.driver.pdsuidomain.services.CriterionService.RichCriterion final case class ApiNewCriterion(meta: Option[String], arms: Option[Seq[Long]], text: Option[String], + isCompound: Option[Boolean], labels: Seq[ApiCriterionLabel], trialId: String) { @@ -19,7 +20,7 @@ final case class ApiNewCriterion(meta: Option[String], id = LongId(0L), meta = meta.getOrElse(""), trialId = StringId(trialId), - isCompound = false, + isCompound = isCompound.getOrElse(false), text = text ), armIds = arms.getOrElse(Seq.empty).map(LongId[Arm]), @@ -35,6 +36,7 @@ object ApiNewCriterion { }, Writes[String](Json.parse))) and (JsPath \ "arms").formatNullable(seqJsonFormat[Long]) and (JsPath \ "text").formatNullable[String] and + (JsPath \ "isCompound").formatNullable[Boolean] and (JsPath \ "labels").format(seqJsonFormat[ApiCriterionLabel]) and (JsPath \ "trialId").format[String] )(ApiNewCriterion.apply, unlift(ApiNewCriterion.unapply)) diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiDocument.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiDocument.scala index 0fd96b3..73b6166 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiDocument.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiDocument.scala @@ -17,18 +17,20 @@ final case class ApiDocument(id: Long, endDate: Option[LocalDate], provider: Option[String], providerTypeId: Option[Long], + requiredType: Option[String], status: Option[String], previousStatus: Option[String], assignee: Option[Long], previousAssignee: Option[Long], + lastActiveUser: Option[Long], meta: Option[String]) object ApiDocument { private val statusFormat = Format( Reads.StringReads.filter(ValidationError("unknown status")) { - case x if DocumentUtils.statusFromString.isDefinedAt(x) => true - case _ => false + case x if Document.RequiredType.fromString.isDefinedAt(x) => true + case _ => false }, Writes.StringWrites ) @@ -43,10 +45,12 @@ object ApiDocument { (JsPath \ "endDate").formatNullable[LocalDate] and (JsPath \ "provider").formatNullable[String] and (JsPath \ "providerTypeId").formatNullable[Long] and + (JsPath \ "requiredType").formatNullable[String] and (JsPath \ "status").formatNullable(statusFormat) and (JsPath \ "previousStatus").formatNullable(statusFormat) and (JsPath \ "assignee").formatNullable[Long] and (JsPath \ "previousAssignee").formatNullable[Long] and + (JsPath \ "lastActiveUser").formatNullable[Long] and (JsPath \ "meta").formatNullable(Format(Reads { x => JsSuccess(Json.stringify(x)) }, Writes[String](Json.parse))) @@ -63,10 +67,12 @@ object ApiDocument { endDate = document.endDate, provider = document.providerName, providerTypeId = document.providerTypeId.map(_.id), - status = Option(DocumentUtils.statusToString(document.status)), - previousStatus = document.previousStatus.map(DocumentUtils.statusToString), + requiredType = document.requiredType.map(Document.RequiredType.requiredTypeToString), + status = Option(Document.Status.statusToString(document.status)), + previousStatus = document.previousStatus.map(Document.Status.statusToString), assignee = document.assignee.map(_.id), previousAssignee = document.previousAssignee.map(_.id), + lastActiveUser = document.lastActiveUserId.map(_.id), meta = document.meta.map(meta => JsonSerializer.serialize(meta.content)) ) } diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiPartialDocument.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiPartialDocument.scala index 7682bb5..84e9e09 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiPartialDocument.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiPartialDocument.scala @@ -31,15 +31,17 @@ final case class ApiPartialDocument(recordId: Option[Long], def applyTo(orig: Document): Document = Document( id = orig.id, - status = status.map(DocumentUtils.statusFromString).getOrElse(orig.status), + status = status.map(Document.Status.fromString).getOrElse(orig.status), previousStatus = orig.previousStatus, assignee = assignee.map(LongId[User]).cata(Some(_), None, orig.assignee), previousAssignee = orig.previousAssignee, + lastActiveUserId = orig.lastActiveUserId, recordId = recordId.map(LongId[MedicalRecord]).getOrElse(orig.recordId), physician = physician.orElse(orig.physician), typeId = typeId.map(LongId[DocumentType]).cata(Some(_), None, orig.typeId), providerName = provider.cata(Some(_), None, orig.providerName), providerTypeId = providerTypeId.map(LongId[ProviderType]).cata(Some(_), None, orig.providerTypeId), + requiredType = orig.requiredType, meta = meta.cata(x => Some(TextJson(JsonSerializer.deserialize[Meta](x))), None, orig.meta), startDate = startDate.cata(Some(_), None, orig.startDate), endDate = endDate.cata(Some(_), None, orig.endDate), @@ -64,10 +66,12 @@ final case class ApiPartialDocument(recordId: Option[Long], endDate = endDate.toOption, providerName = provider.toOption, providerTypeId = providerTypeId.map(LongId[ProviderType]).toOption, + requiredType = None, meta = meta.map(x => TextJson(JsonSerializer.deserialize[Meta](x))).toOption, previousStatus = None, assignee = None, previousAssignee = None, + lastActiveUserId = None, lastUpdate = LocalDateTime.MIN ) } else { @@ -87,7 +91,7 @@ object ApiPartialDocument { (JsPath \ "provider").readTristate[String] and (JsPath \ "providerTypeId").readTristate[Long] and (JsPath \ "status").readNullable[String](Reads.of[String].filter(ValidationError("unknown status"))({ - case x if DocumentUtils.statusFromString.isDefinedAt(x) => true + case x if Document.Status.fromString.isDefinedAt(x) => true case _ => false })) and (JsPath \ "assignee").readTristate[Long] and diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/DocumentUtils.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/DocumentUtils.scala deleted file mode 100644 index 24e388e..0000000 --- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/DocumentUtils.scala +++ /dev/null @@ -1,24 +0,0 @@ -package xyz.driver.pdsuidomain.formats.json.document - -import xyz.driver.pdsuidomain.entities.Document.Status - -object DocumentUtils { - - val statusFromString: PartialFunction[String, Status] = { - case "New" => Status.New - case "Organized" => Status.Organized - case "Extracted" => Status.Extracted - case "Done" => Status.Done - case "Flagged" => Status.Flagged - case "Archived" => Status.Archived - } - - def statusToString(x: Status): String = x match { - case Status.New => "New" - case Status.Organized => "Organized" - case Status.Extracted => "Extracted" - case Status.Done => "Done" - case Status.Flagged => "Flagged" - case Status.Archived => "Archived" - } -} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/evidence/ApiPatientLabelEvidence.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/evidence/ApiPatientLabelEvidence.scala index 2bb4945..e0f23e3 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/evidence/ApiPatientLabelEvidence.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/evidence/ApiPatientLabelEvidence.scala @@ -2,9 +2,9 @@ package xyz.driver.pdsuidomain.formats.json.evidence import java.time.LocalDate -import xyz.driver.pdsuidomain.services.PatientLabelEvidenceService import play.api.libs.json._ import xyz.driver.pdsuicommon.domain.FuzzyValue +import xyz.driver.pdsuidomain.entities.PatientLabelEvidenceView final case class ApiPatientLabelEvidence(id: Long, value: String, @@ -20,19 +20,15 @@ object ApiPatientLabelEvidence { implicit val format: Format[ApiPatientLabelEvidence] = Json.format - def fromDomain(x: PatientLabelEvidenceService.Aggregated): ApiPatientLabelEvidence = { - import x._ - - ApiPatientLabelEvidence( - id = evidence.id.id, - value = FuzzyValue.valueToString(evidence.value), - evidenceText = evidence.evidenceText, - documentId = evidence.documentId.map(_.id), - evidenceId = evidence.evidenceId.map(_.id), - reportId = evidence.reportId.map(_.toString), - documentType = documentType, - date = date, - providerType = providerType - ) - } + def fromDomain(x: PatientLabelEvidenceView) = ApiPatientLabelEvidence( + id = x.id.id, + value = FuzzyValue.valueToString(x.value), + evidenceText = x.evidenceText, + documentId = x.documentId.map(_.id), + evidenceId = x.evidenceId.map(_.id), + reportId = x.reportId.map(_.toString), + documentType = x.documentType, + date = x.date.get, + providerType = x.providerType + ) } diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/linkedpatient/ApiLinkedPatient.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/linkedpatient/ApiLinkedPatient.scala new file mode 100644 index 0000000..20d4a74 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/linkedpatient/ApiLinkedPatient.scala @@ -0,0 +1,33 @@ + +package xyz.driver.pdsuidomain.formats.json.linkedpatient + +import java.util.UUID + +import play.api.libs.json.{Format, Json} +import xyz.driver.pdsuicommon.domain._ +import xyz.driver.pdsuidomain.services.LinkedPatientService.RichLinkedPatient + +case class ApiLinkedPatient(email: String, + name: String, + patientId: UUID, + trialId: String) { + + def toDomain = RichLinkedPatient( + email = Email(email), + name = name, + patientId = UuidId(patientId), + trialId = StringId(trialId) + ) +} + +object ApiLinkedPatient { + + implicit val format: Format[ApiLinkedPatient] = Json.format[ApiLinkedPatient] + + def fromDomain(entity: RichLinkedPatient) = ApiLinkedPatient( + email = entity.email.value, + name = entity.name, + patientId = entity.patientId.id, + trialId = entity.trialId.id + ) +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/message/ApiPartialMessage.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/message/ApiPartialMessage.scala index 8c390e7..88dd5a3 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/message/ApiPartialMessage.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/message/ApiPartialMessage.scala @@ -50,7 +50,6 @@ final case class ApiPartialMessage(text: Option[String], lastUpdate = LocalDateTime.MIN ) } - } object ApiPartialMessage { @@ -80,5 +79,4 @@ object ApiPartialMessage { archiveRequired = domain.archiveRequired, meta = domain.meta ) - } diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/ApiPatient.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/ApiPatient.scala index caa556b..68e965c 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/ApiPatient.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/ApiPatient.scala @@ -13,6 +13,7 @@ final case class ApiPatient(id: String, assignee: Option[Long], previousStatus: Option[String], previousAssignee: Option[Long], + lastActiveUser: Option[Long], lastUpdate: ZonedDateTime, condition: String) @@ -26,6 +27,7 @@ object ApiPatient { (JsPath \ "assignee").formatNullable[Long] and (JsPath \ "previousStatus").formatNullable[String] and (JsPath \ "previousAssignee").formatNullable[Long] and + (JsPath \ "lastActiveUser").formatNullable[Long] and (JsPath \ "lastUpdate").format[ZonedDateTime] and (JsPath \ "condition").format[String] )(ApiPatient.apply, unlift(ApiPatient.unapply)) @@ -38,6 +40,7 @@ object ApiPatient { assignee = patient.assignee.map(_.id), previousStatus = patient.previousStatus.map(PatientStatus.statusToString), previousAssignee = patient.previousAssignee.map(_.id), + lastActiveUser = patient.lastActiveUserId.map(_.id), lastUpdate = ZonedDateTime.of(patient.lastUpdate, ZoneId.of("Z")), condition = patient.condition ) diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/eligible/ApiPatientEligibleTrial.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/eligible/ApiPatientEligibleTrial.scala index 42c3259..033d73e 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/eligible/ApiPatientEligibleTrial.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/eligible/ApiPatientEligibleTrial.scala @@ -14,7 +14,7 @@ final case class ApiPatientEligibleTrial(id: Long, trialTitle: String, arms: List[String], hypothesisId: UUID, - eligibilityStatus: Option[String], + verifiedEligibilityStatus: Option[String], isVerified: Boolean) object ApiPatientEligibleTrial { @@ -26,7 +26,7 @@ object ApiPatientEligibleTrial { (JsPath \ "trialTitle").format[String] and (JsPath \ "arms").format[List[String]] and (JsPath \ "hypothesisId").format[UUID] and - (JsPath \ "eligibilityStatus").formatNullable[String](Format( + (JsPath \ "verifiedEligibilityStatus").formatNullable[String](Format( Reads .of[String] .filter(ValidationError("unknown eligibility status"))({ @@ -45,7 +45,7 @@ object ApiPatientEligibleTrial { trialTitle = eligibleTrialWithTrial.trial.title, arms = eligibleTrialWithTrial.arms.map(_.name), hypothesisId = eligibleTrialWithTrial.group.hypothesisId.id, - eligibleTrialWithTrial.group.eligibilityStatus.map(FuzzyValue.valueToString), + eligibleTrialWithTrial.group.verifiedEligibilityStatus.map(FuzzyValue.valueToString), eligibleTrialWithTrial.group.isVerified ) } diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/hypothesis/ApiPatientHypothesis.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/hypothesis/ApiPatientHypothesis.scala index 374370e..584ff72 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/hypothesis/ApiPatientHypothesis.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/hypothesis/ApiPatientHypothesis.scala @@ -10,7 +10,8 @@ final case class ApiPatientHypothesis(id: UUID, patientId: String, hypothesisId: UUID, matchedTrials: Long, - rationale: Option[String]) + rationale: Option[String], + isRationaleRequired: Boolean) object ApiPatientHypothesis { @@ -19,14 +20,16 @@ object ApiPatientHypothesis { (JsPath \ "patientId").format[String] and (JsPath \ "hypothesisId").format[UUID] and (JsPath \ "matchedTrials").format[Long] and - (JsPath \ "rationale").formatNullable[String] + (JsPath \ "rationale").formatNullable[String] and + (JsPath \ "isRationaleRequired").format[Boolean] )(ApiPatientHypothesis.apply, unlift(ApiPatientHypothesis.unapply)) - def fromDomain(patientHypothesis: PatientHypothesis) = ApiPatientHypothesis( + def fromDomain(patientHypothesis: PatientHypothesis, isRationaleRequired: Boolean) = ApiPatientHypothesis( id = patientHypothesis.id.id, patientId = patientHypothesis.patientId.toString, hypothesisId = patientHypothesis.hypothesisId.id, matchedTrials = patientHypothesis.matchedTrials, - rationale = patientHypothesis.rationale + rationale = patientHypothesis.rationale, + isRationaleRequired = isRationaleRequired ) } diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/label/ApiPartialPatientLabel.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/label/ApiPartialPatientLabel.scala index 40f7de4..91d0a0e 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/label/ApiPartialPatientLabel.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/label/ApiPartialPatientLabel.scala @@ -17,7 +17,6 @@ final case class ApiPartialPatientLabel(primaryValue: Option[String], verifiedPr verifiedPrimaryValue.cata(x => Some(FuzzyValue.fromString(x)), None, orig.verifiedPrimaryValue) ) } - } object ApiPartialPatientLabel { @@ -42,5 +41,4 @@ object ApiPartialPatientLabel { Writes.of[String] )) )(ApiPartialPatientLabel.apply, unlift(ApiPartialPatientLabel.unapply)) - } diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/trial/ApiPatientCriterion.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/trial/ApiPatientCriterion.scala index 3e2de99..7ac55f6 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/trial/ApiPatientCriterion.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/trial/ApiPatientCriterion.scala @@ -11,6 +11,7 @@ import play.api.libs.json.{Format, JsPath, Reads, Writes} final case class ApiPatientCriterion(id: Long, labelId: Long, nctId: String, + criterionId: Long, criterionText: String, criterionValue: Option[String], criterionIsDefining: Boolean, @@ -28,6 +29,7 @@ object ApiPatientCriterion { (JsPath \ "id").format[Long] and (JsPath \ "labelId").format[Long] and (JsPath \ "nctId").format[String] and + (JsPath \ "criterionId").format[Long] and (JsPath \ "criterionText").format[String] and (JsPath \ "criterionValue").formatNullable[String](Format(Reads.of[String].filter(ValidationError("unknown value"))({ x => x == "Yes" || x == "No" @@ -56,6 +58,7 @@ object ApiPatientCriterion { id = patientCriterion.id.id, labelId = labelId.id, nctId = patientCriterion.nctId.id, + criterionId = patientCriterion.criterionId.id, criterionText = patientCriterion.criterionText, criterionValue = patientCriterion.criterionValue.map { x => FuzzyValue.valueToString(FuzzyValue.fromBoolean(x)) diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/record/ApiCreateRecord.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/record/ApiCreateRecord.scala index cee67b2..e96bc81 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/record/ApiCreateRecord.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/record/ApiCreateRecord.scala @@ -15,6 +15,7 @@ final case class ApiCreateRecord(disease: String, patientId: String, requestId: previousStatus = None, assignee = None, previousAssignee = None, + lastActiveUserId = None, patientId = UuidId(patientId), requestId = RecordRequestId(requestId), disease = disease, diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/record/ApiRecord.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/record/ApiRecord.scala index de54a9e..dca441b 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/record/ApiRecord.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/record/ApiRecord.scala @@ -17,6 +17,7 @@ final case class ApiRecord(id: Long, previousStatus: Option[String], assignee: Option[Long], previousAssignee: Option[Long], + lastActiveUser: Option[Long], meta: String) object ApiRecord { @@ -39,6 +40,7 @@ object ApiRecord { (JsPath \ "previousStatus").formatNullable(statusFormat) and (JsPath \ "assignee").formatNullable[Long] and (JsPath \ "previousAssignee").formatNullable[Long] and + (JsPath \ "lastActiveUser").formatNullable[Long] and (JsPath \ "meta").format(Format(Reads { x => JsSuccess(Json.stringify(x)) }, Writes[String](Json.parse))) @@ -54,6 +56,7 @@ object ApiRecord { previousStatus = record.previousStatus.map(MedicalRecordStatus.statusToString), assignee = record.assignee.map(_.id), previousAssignee = record.previousAssignee.map(_.id), + lastActiveUser = record.lastActiveUserId.map(_.id), meta = record.meta.map(x => JsonSerializer.serialize(x.content)).getOrElse("[]") ) } diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/trial/ApiTrial.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/trial/ApiTrial.scala index db980ee..97bab5e 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/trial/ApiTrial.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/trial/ApiTrial.scala @@ -13,6 +13,7 @@ final case class ApiTrial(id: String, assignee: Option[Long], previousStatus: Option[String], previousAssignee: Option[Long], + lastActiveUser: Option[Long], condition: Option[String], phase: Option[String], hypothesisId: Option[UUID], @@ -32,6 +33,7 @@ object ApiTrial { (JsPath \ "assignee").formatNullable[Long] and (JsPath \ "previousStatus").formatNullable[String] and (JsPath \ "previousAssignee").formatNullable[Long] and + (JsPath \ "lastActiveUser").formatNullable[Long] and (JsPath \ "condition").formatNullable[String] and (JsPath \ "phase").formatNullable[String] and (JsPath \ "hypothesisId").formatNullable[UUID] and @@ -49,6 +51,7 @@ object ApiTrial { assignee = trial.assignee.map(_.id), previousStatus = trial.previousStatus.map(TrialStatus.statusToString), previousAssignee = trial.previousAssignee.map(_.id), + lastActiveUser = trial.lastActiveUserId.map(_.id), lastUpdate = Option(ZonedDateTime.of(trial.lastUpdate, ZoneId.of("Z"))), condition = Option(trial.condition.toString), phase = Option(trial.phase), diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/user/ApiPartialUser.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/user/ApiPartialUser.scala index 654508c..a37fea8 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/user/ApiPartialUser.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/user/ApiPartialUser.scala @@ -1,8 +1,5 @@ package xyz.driver.pdsuidomain.formats.json.user -import java.math.BigInteger -import java.security.SecureRandom - import xyz.driver.pdsuicommon.domain.{Email, LongId, PasswordHash, User} import play.api.data.validation._ import play.api.libs.functional.syntax._ @@ -10,7 +7,7 @@ import play.api.libs.json._ import scala.collection._ import scala.util.Try -import ApiPartialUser._ +import User._ import xyz.driver.pdsuicommon.json.JsonValidationException import xyz.driver.pdsuicommon.validation.{AdditionalConstraints, JsonValidationErrors} @@ -62,19 +59,12 @@ final case class ApiPartialUser(email: Option[String], name: Option[String], rol object ApiPartialUser { - // SecureRandom is thread-safe, see the implementation - private val random = new SecureRandom() - - def createPassword: String = new BigInteger(240, random).toString(32) - implicit val format: Format[ApiPartialUser] = ( (JsPath \ "email").formatNullable[String](Format(Reads.email, Writes.StringWrites)) and - (JsPath \ "name").formatNullable[String]( - Format( - Reads.filterNot[String](ValidationError("Username is too long (max length is 255 chars)", 255))( - _.length > 255), - Writes.StringWrites - )) and + (JsPath \ "name").formatNullable[String](Format( + Reads.filterNot[String](ValidationError("Username is too long (max length is 255 chars)", 255))(_.size > 255), + Writes.StringWrites + )) and (JsPath \ "roleId").formatNullable[String]( Format(Reads .of[String] diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/userhistory/ApiUserHistory.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/userhistory/ApiUserHistory.scala new file mode 100644 index 0000000..befff05 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/userhistory/ApiUserHistory.scala @@ -0,0 +1,43 @@ +package xyz.driver.pdsuidomain.formats.json.userhistory + +import java.time.{ZoneId, ZonedDateTime} + +import play.api.libs.functional.syntax._ +import play.api.libs.json.{Format, JsPath} +import xyz.driver.pdsuidomain.entities.UserHistory + +final case class ApiUserHistory(id: Long, + executor: Long, + recordId: Option[Long], + documentId: Option[Long], + trialId: Option[String], + patientId: Option[String], + state: String, + action: String, + created: ZonedDateTime) + +object ApiUserHistory { + implicit val format: Format[ApiUserHistory] = ( + (JsPath \ "id").format[Long] and + (JsPath \ "executor").format[Long] and + (JsPath \ "recordId").formatNullable[Long] and + (JsPath \ "documentId").formatNullable[Long] and + (JsPath \ "trialId").formatNullable[String] and + (JsPath \ "patientId").formatNullable[String] and + (JsPath \ "state").format[String] and + (JsPath \ "action").format[String] and + (JsPath \ "created").format[ZonedDateTime] + ) (ApiUserHistory.apply, unlift(ApiUserHistory.unapply)) + + def fromDomain(x: UserHistory) = ApiUserHistory( + id = x.id.id, + executor = x.executor.id, + recordId = x.recordId.map(_.id), + documentId = x.documentId.map(_.id), + trialId = x.trialId.map(_.id), + patientId = x.patientId.map(_.id.toString), + state = UserHistory.State.stateToString(x.state), + action = UserHistory.Action.actionToString(x.action), + created = ZonedDateTime.of(x.created, ZoneId.of("Z")) + ) +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/ArmService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/ArmService.scala index 43e46ed..8c89505 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/services/ArmService.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/services/ArmService.scala @@ -33,7 +33,6 @@ object ArmService { case class CommonError(userMessage: String)(implicit requestContext: AuthenticatedRequestContext) extends GetByIdReply with DomainError - } sealed trait GetListReply @@ -44,13 +43,12 @@ object ArmService { case object AuthorizationError extends GetListReply with DomainError.AuthorizationError with DefaultAccessDeniedError - } sealed trait UpdateReply object UpdateReply { - case class Updated(updated: Arm) extends UpdateReply + final case class Updated(updated: Arm) extends UpdateReply type Error = UpdateReply with DomainError @@ -59,9 +57,9 @@ object ArmService { case object AuthorizationError extends UpdateReply with DefaultAccessDeniedError with DomainError.AuthorizationError - case class CommonError(userMessage: String) extends UpdateReply with DomainError + final case class CommonError(userMessage: String) extends UpdateReply with DomainError - case class AlreadyExistsError(x: Arm) extends UpdateReply with DomainError { + final case class AlreadyExistsError(x: Arm) extends UpdateReply with DomainError { val userMessage = s"The arm with such name of trial already exists." } @@ -80,9 +78,9 @@ object ArmService { case object AuthorizationError extends CreateReply with DefaultAccessDeniedError with DomainError.AuthorizationError - case class CommonError(userMessage: String) extends CreateReply with DomainError + final case class CommonError(userMessage: String) extends CreateReply with DomainError - case class AlreadyExistsError(x: Arm) extends CreateReply with DomainError { + final case class AlreadyExistsError(x: Arm) extends CreateReply with DomainError { val userMessage = s"The arm with this name of trial already exists." } diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/CategoryService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/CategoryService.scala index a735ade..f8d820b 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/services/CategoryService.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/services/CategoryService.scala @@ -3,7 +3,6 @@ package xyz.driver.pdsuidomain.services import xyz.driver.pdsuicommon.auth.AuthenticatedRequestContext import xyz.driver.pdsuicommon.db.Sorting import xyz.driver.pdsuicommon.error.DomainError -import xyz.driver.pdsuicommon.logging._ import xyz.driver.pdsuidomain.entities.CategoryWithLabels import scala.concurrent.Future @@ -19,7 +18,7 @@ object CategoryService { } } -trait CategoryService extends PhiLogging { +trait CategoryService { import CategoryService._ diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/DocumentService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/DocumentService.scala index ffa252c..2c01a7e 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/services/DocumentService.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/services/DocumentService.scala @@ -99,7 +99,6 @@ object DocumentService { case class CommonError(userMessage: String) extends DeleteReply with DomainError } - } trait DocumentService { @@ -115,7 +114,6 @@ trait DocumentService { def create(draftDocument: Document)(implicit requestContext: AuthenticatedRequestContext): Future[CreateReply] - // Update operations are validated in internal.*Command def update(orig: Document, draft: Document)( implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/ExportService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/ExportService.scala new file mode 100644 index 0000000..8fb399a --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/services/ExportService.scala @@ -0,0 +1,64 @@ +package xyz.driver.pdsuidomain.services + +import java.time.LocalDateTime + +import xyz.driver.pdsuicommon.auth.AnonymousRequestContext +import xyz.driver.pdsuicommon.db.SearchFilterExpr +import xyz.driver.pdsuicommon.domain._ +import xyz.driver.pdsuicommon.error.DomainError +import xyz.driver.pdsuidomain.entities.export.patient.ExportPatientWithLabels +import xyz.driver.pdsuidomain.entities.export.trial.{ExportTrial, ExportTrialWithLabels} +import xyz.driver.pdsuidomain.entities.{MedicalRecord, Patient, Trial} + +import scala.concurrent.Future + +object ExportService { + + sealed trait GetPatientReply + object GetPatientReply { + type Error = GetPatientReply with DomainError + + case class Entity(x: ExportPatientWithLabels) extends GetPatientReply + + case object NotFoundError extends GetPatientReply with DomainError.NotFoundError { + def userMessage: String = "Patient not found" + } + } + + sealed trait GetTrialListReply + object GetTrialListReply { + case class EntityList(xs: Seq[ExportTrial], totalFound: Int, lastUpdate: Option[LocalDateTime]) + extends GetTrialListReply + } + + sealed trait GetTrialReply + object GetTrialReply { + type Error = GetTrialReply with DomainError + + case class Entity(x: ExportTrialWithLabels) extends GetTrialReply + + case object NotFoundError extends GetTrialReply with DomainError.NotFoundError { + def userMessage: String = "Trial not found" + } + } +} + +trait ExportService { + + import ExportService._ + + def getPatient(id: UuidId[Patient]) + (implicit requestContext: AnonymousRequestContext): Future[GetPatientReply] + + def getTrialList(filter: SearchFilterExpr = SearchFilterExpr.Empty) + (implicit requestContext: AnonymousRequestContext): Future[GetTrialListReply] + + def getTrial(trialId: StringId[Trial], condition: String) + (implicit requestContext: AnonymousRequestContext): Future[GetTrialReply] + + def getRecords(patientId: UuidId[Patient]) + (implicit requestContext: AnonymousRequestContext): Future[MedicalRecordService.GetListReply] + + def getRecordPdf(recordId: LongId[MedicalRecord]) + (implicit requestContext: AnonymousRequestContext): Future[MedicalRecordService.GetPdfSourceReply] +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/LabelService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/LabelService.scala index 0677584..aadc5fb 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/services/LabelService.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/services/LabelService.scala @@ -3,7 +3,6 @@ package xyz.driver.pdsuidomain.services import xyz.driver.pdsuicommon.auth.AuthenticatedRequestContext import xyz.driver.pdsuicommon.db.Sorting import xyz.driver.pdsuicommon.error.DomainError -import xyz.driver.pdsuicommon.logging.PhiLogging import xyz.driver.pdsuidomain.entities.Label import scala.concurrent.Future @@ -20,7 +19,7 @@ object LabelService { } } -trait LabelService extends PhiLogging { +trait LabelService { import LabelService._ def getAll(sorting: Option[Sorting] = None)( diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/LinkedPatientService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/LinkedPatientService.scala new file mode 100644 index 0000000..8fc1662 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/services/LinkedPatientService.scala @@ -0,0 +1,62 @@ +package xyz.driver.pdsuidomain.services + +import xyz.driver.pdsuicommon.domain._ +import xyz.driver.pdsuicommon.error.DomainError +import xyz.driver.pdsuicommon.logging._ +import xyz.driver.pdsuidomain.entities.{LinkedPatient, Patient, Trial} + +import scala.concurrent.Future + +object LinkedPatientService { + + trait DefaultTrialNotFoundError { + def userMessage: String = "Trial not found" + } + + trait DefaultPatientNotFoundError { + def userMessage: String = "Patient not found" + } + + final case class RichLinkedPatient(email: Email, + name: String, + patientId: UuidId[Patient], + trialId: StringId[Trial]) { + def toLinkedPatient(user: User) = LinkedPatient( + userId = user.id, + patientId = patientId, + trialId = trialId + ) + } + + object RichLinkedPatient { + implicit def toPhiString(x: RichLinkedPatient): PhiString = { + import x._ + phi"RichLinkedPatient(email=${Unsafe(email)}, patientId=$patientId, trialId=$trialId)" + } + } + + sealed trait CreateReply + object CreateReply { + type Error = CreateReply with DomainError + + /** + * @param createdUser None if a user was created before + */ + final case class Created(x: RichLinkedPatient, createdUser: Option[User]) extends CreateReply + + case object PatientNotFoundError + extends CreateReply with DefaultPatientNotFoundError with DomainError.NotFoundError + + case object TrialNotFoundError + extends CreateReply with DefaultPatientNotFoundError with DomainError.NotFoundError + + final case class CommonError(userMessage: String) extends CreateReply with DomainError + } +} + +trait LinkedPatientService { + + import LinkedPatientService._ + + def create(entity: RichLinkedPatient): Future[CreateReply] +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/MailService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/MailService.scala new file mode 100644 index 0000000..3022716 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/services/MailService.scala @@ -0,0 +1,39 @@ +package xyz.driver.pdsuidomain.services + +import java.io.{InputStream, StringReader, StringWriter} + +import xyz.driver.pdsuidomain.services.MailService.Template +import com.github.mustachejava.DefaultMustacheFactory +import com.twitter.mustache.ScalaObjectHandler + +import scala.io.Source + +object MailService { + + trait Template { + val subject: String + def parameters: Map[String, Any] + def filename: String + val contentType: String = "text/html" + + protected val factory = new DefaultMustacheFactory() + factory.setObjectHandler(new ScalaObjectHandler) + + protected def inputStream: InputStream = getClass.getClassLoader.getResourceAsStream(filename) + protected def templateContent: String = Source.fromInputStream(inputStream).getLines().mkString + + def content: String = { + val template = factory.compile(new StringReader(templateContent), filename) + val writer = new StringWriter + template + .execute(writer, parameters) + .close() + writer.toString + } + } +} + +trait MailService { + + def sendTo(email: String, template: Template): Boolean +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/MessageService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/MessageService.scala index 8ce06f2..df57e17 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/services/MessageService.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/services/MessageService.scala @@ -6,7 +6,6 @@ import xyz.driver.pdsuicommon.auth.AuthenticatedRequestContext import xyz.driver.pdsuicommon.db.{Pagination, SearchFilterExpr, Sorting} import xyz.driver.pdsuicommon.domain.LongId import xyz.driver.pdsuicommon.error.DomainError -import xyz.driver.pdsuicommon.logging._ import xyz.driver.pdsuidomain.entities.Message import scala.concurrent.Future @@ -66,10 +65,9 @@ object MessageService { case object NotFoundError extends DeleteReply with DomainError.NotFoundError with DefaultNotFoundError case class CommonError(userMessage: String) extends DeleteReply with DomainError } - } -trait MessageService extends PhiLogging { +trait MessageService { import MessageService._ diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/PatientCriterionService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/PatientCriterionService.scala index 593e2ce..e23dfc5 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/services/PatientCriterionService.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/services/PatientCriterionService.scala @@ -84,7 +84,10 @@ object PatientCriterionService { object UpdateReply { type Error = UpdateReply with DomainError - case object Updated extends UpdateReply + case class Updated(x: PatientCriterion, labelId: LongId[Label], armList: List[Arm], criterionIsCompound: Boolean) + extends UpdateReply + + case object UpdatedList extends UpdateReply case object AuthorizationError extends UpdateReply with DomainError.AuthorizationError with DefaultAccessDeniedError @@ -101,7 +104,7 @@ trait PatientCriterionService { import PatientCriterionService._ def getAll(patientId: UuidId[Patient], - filter: SearchFilterExpr = SearchFilterExpr.Empty, + origFilter: SearchFilterExpr = SearchFilterExpr.Empty, sorting: Option[Sorting] = None, pagination: Option[Pagination] = None)( implicit requestContext: AuthenticatedRequestContext): Future[GetListReply] diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/PatientHypothesisService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/PatientHypothesisService.scala index 39ee2b1..a15e11f 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/services/PatientHypothesisService.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/services/PatientHypothesisService.scala @@ -25,7 +25,7 @@ object PatientHypothesisService { sealed trait GetListReply object GetListReply { - case class EntityList(xs: Seq[PatientHypothesis], totalFound: Int) extends GetListReply + case class EntityList(xs: Seq[(PatientHypothesis, Boolean)], totalFound: Int) extends GetListReply case object AuthorizationError extends GetListReply with DomainError.AuthorizationError with DefaultAccessDeniedError @@ -38,7 +38,7 @@ object PatientHypothesisService { sealed trait GetByIdReply object GetByIdReply { - case class Entity(x: PatientHypothesis) extends GetByIdReply + case class Entity(x: PatientHypothesis, isRequired: Boolean) extends GetByIdReply type Error = GetByIdReply with DomainError @@ -53,8 +53,8 @@ object PatientHypothesisService { case class CommonError(userMessage: String) extends GetByIdReply with DomainError implicit def toPhiString(reply: GetByIdReply): PhiString = reply match { - case x: DomainError => phi"GetByIdReply.Error($x)" - case Entity(x) => phi"GetByIdReply.Entity($x)" + case x: DomainError => phi"GetByIdReply.Error($x)" + case Entity(x, isRequired) => phi"GetByIdReply.Entity($x, $isRequired)" } } diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/PatientLabelEvidenceService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/PatientLabelEvidenceService.scala index f6039f0..2586798 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/services/PatientLabelEvidenceService.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/services/PatientLabelEvidenceService.scala @@ -1,7 +1,5 @@ package xyz.driver.pdsuidomain.services -import java.time.LocalDate - import xyz.driver.pdsuicommon.auth.AuthenticatedRequestContext import xyz.driver.pdsuicommon.db._ import xyz.driver.pdsuicommon.domain.{LongId, UuidId} @@ -12,21 +10,23 @@ import scala.concurrent.Future object PatientLabelEvidenceService { - case class Aggregated(evidence: PatientLabelEvidence, date: LocalDate, documentType: String, providerType: String) - trait DefaultAccessDeniedError { def userMessage: String = "Access denied" } + trait DefaultPatientNotFoundError { + def userMessage: String = "Patient not found" + } + sealed trait GetByIdReply object GetByIdReply { - case class Entity(x: Aggregated) extends GetByIdReply + case class Entity(x: PatientLabelEvidenceView) extends GetByIdReply type Error = GetByIdReply with DomainError - case class NotFoundError(userMessage: String) extends GetByIdReply with DomainError.NotFoundError + final case class NotFoundError(userMessage: String) extends GetByIdReply with DomainError.NotFoundError - case class CommonError(userMessage: String) extends GetByIdReply with DomainError + final case class CommonError(userMessage: String) extends GetByIdReply with DomainError case object AuthorizationError extends GetByIdReply with DomainError.AuthorizationError with DefaultAccessDeniedError @@ -34,10 +34,13 @@ object PatientLabelEvidenceService { sealed trait GetListReply object GetListReply { - case class EntityList(xs: Seq[Aggregated], totalFound: Int) extends GetListReply + case class EntityList(xs: Seq[PatientLabelEvidenceView], totalFound: Int) extends GetListReply type Error = GetListReply with DomainError + case object PatientNotFoundError + extends GetListReply with DefaultPatientNotFoundError with DomainError.NotFoundError + case object AuthorizationError extends GetListReply with DomainError.AuthorizationError with DefaultAccessDeniedError diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/QueueUploadService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/QueueUploadService.scala new file mode 100644 index 0000000..55a408f --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/services/QueueUploadService.scala @@ -0,0 +1,78 @@ +package xyz.driver.pdsuidomain.services + +import xyz.driver.pdsuicommon.auth.AuthenticatedRequestContext +import xyz.driver.pdsuicommon.concurrent.BridgeUploadQueue +import xyz.driver.pdsuicommon.db.{Pagination, SearchFilterExpr, Sorting} +import xyz.driver.pdsuicommon.error.DomainError + +import scala.concurrent.Future + +object QueueUploadService { + trait DefaultNotFoundError { + def userMessage: String = "Message not found" + } + + trait DefaultAccessDeniedError { + def userMessage: String = "Access denied" + } + + sealed trait CreateReply + object CreateReply { + type Error = CreateReply with DomainError + + case class Created(x: BridgeUploadQueue.Item) extends CreateReply + case object AuthorizationError extends CreateReply with DomainError.AuthorizationError with DefaultAccessDeniedError + case class CommonError(userMessage: String) extends CreateReply with DomainError + } + + sealed trait GetByIdReply + object GetByIdReply { + type Error = GetByIdReply with DomainError + + case class Entity(x: BridgeUploadQueue.Item) extends GetByIdReply + case object AuthorizationError extends GetByIdReply with DomainError.AuthorizationError with DefaultAccessDeniedError + case object NotFoundError extends GetByIdReply with DomainError.NotFoundError with DefaultNotFoundError + case class CommonError(userMessage: String) extends GetByIdReply with DomainError + } + + sealed trait GetListReply + object GetListReply { + type Error = GetListReply with DomainError + + case class EntityList(xs: Seq[BridgeUploadQueue.Item], + totalFound: Int) extends GetListReply + + case object AuthorizationError + extends GetListReply with DomainError.AuthorizationError with DefaultAccessDeniedError + } + + sealed trait ResetReply + object ResetReply { + type Error = ResetReply with DomainError + + case class Updated(updated: BridgeUploadQueue.Item) extends ResetReply + case object AuthorizationError extends ResetReply with DomainError.AuthorizationError with DefaultAccessDeniedError + case object NotFoundError extends ResetReply with DefaultNotFoundError with DomainError.NotFoundError + case class CommonError(userMessage: String) extends ResetReply with DomainError + } +} + +trait QueueUploadService { + + import QueueUploadService._ + + def create(kind: String, tag: String) + (implicit requestContext: AuthenticatedRequestContext): Future[CreateReply] + + def getById(kind: String, tag: String) + (implicit requestContext: AuthenticatedRequestContext): Future[GetByIdReply] + + def getAll(filter: SearchFilterExpr = SearchFilterExpr.Empty, + sorting: Option[Sorting] = None, + pagination: Option[Pagination] = None) + (implicit requestContext: AuthenticatedRequestContext): Future[GetListReply] + + def reset(kind: String, tag: String) + (implicit requestContext: AuthenticatedRequestContext): Future[ResetReply] + +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/UserHistoryService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/UserHistoryService.scala new file mode 100644 index 0000000..3034027 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/services/UserHistoryService.scala @@ -0,0 +1,31 @@ +package xyz.driver.pdsuidomain.services + +import xyz.driver.pdsuicommon.auth.AuthenticatedRequestContext +import xyz.driver.pdsuicommon.db.{Pagination, SearchFilterExpr, Sorting} +import xyz.driver.pdsuicommon.error.DomainError +import xyz.driver.pdsuidomain.entities.UserHistory + +import scala.concurrent.Future + +object UserHistoryService { + + sealed trait GetListReply + object GetListReply { + case class EntityList(xs: Seq[UserHistory], totalFound: Int) extends GetListReply + + case object AuthorizationError + extends GetListReply with DomainError.AuthorizationError { + def userMessage: String = "Access denied" + } + } +} + +trait UserHistoryService { + + import UserHistoryService._ + + def getAll(filter: SearchFilterExpr = SearchFilterExpr.Empty, + sorting: Option[Sorting] = None, + pagination: Option[Pagination] = None) + (implicit requestContext: AuthenticatedRequestContext): Future[GetListReply] +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/UserService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/UserService.scala index 79e9835..a1d4800 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/services/UserService.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/services/UserService.scala @@ -4,7 +4,6 @@ import xyz.driver.pdsuicommon.auth.{AnonymousRequestContext, AuthenticatedReques import xyz.driver.pdsuicommon.db.{Pagination, SearchFilterExpr, Sorting} import xyz.driver.pdsuicommon.domain.{Email, LongId, User} import xyz.driver.pdsuicommon.error.DomainError -import xyz.driver.pdsuicommon.logging._ import scala.concurrent.Future @@ -95,10 +94,9 @@ object UserService { case object NotFoundError extends DeleteReply with DefaultNotFoundError with DomainError.NotFoundError case class CommonError(userMessage: String) extends DeleteReply with DomainError } - } -trait UserService extends PhiLogging { +trait UserService { import UserService._ diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/fake/StubMailService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/fake/StubMailService.scala new file mode 100644 index 0000000..932da67 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/services/fake/StubMailService.scala @@ -0,0 +1,13 @@ +package xyz.driver.pdsuidomain.services.fake + +import xyz.driver.pdsuicommon.logging._ +import xyz.driver.pdsuidomain.services.MailService +import xyz.driver.pdsuidomain.services.MailService.Template + +object StubMailService extends MailService with PhiLogging { + + override def sendTo(email: String, template: Template): Boolean = { + logger.debug(phi"sendTo(email=${Unsafe(email)}") + true + } +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/rest/SendGridMailService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/rest/SendGridMailService.scala new file mode 100644 index 0000000..37dc758 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/services/rest/SendGridMailService.scala @@ -0,0 +1,45 @@ +package xyz.driver.pdsuidomain.services.rest + +import com.sendgrid._ +import xyz.driver.pdsuicommon.logging._ +import xyz.driver.pdsuidomain.services.MailService +import xyz.driver.pdsuidomain.services.MailService.Template +import xyz.driver.pdsuidomain.services.rest.SendGridMailService._ + +import scala.util.control.NonFatal + +object SendGridMailService { + + private val ExpectedHttpCode = 202 + + case class Settings(provider: String, frontEndUrl: String, apiKey: String, from: String) +} + +class SendGridMailService(settings: Settings) extends MailService with PhiLogging { + + def sendTo(email: String, template: Template): Boolean = { + val to = new Email(email) + val content = new Content(template.contentType, template.content) + val mail = new Mail(new Email(settings.from), template.subject, to, content) + + val request = new Request() + val sendGrid = new SendGrid(settings.apiKey) + + try { + request.method = Method.POST + request.endpoint = "mail/send" + request.body = mail.build() + val response = sendGrid.api(request) + if (response.statusCode != ExpectedHttpCode) { + logger.error(phi"Unexpected response: ${Unsafe(response.statusCode)}, ${Unsafe(response.body.take(100))}") + } + + response.statusCode == ExpectedHttpCode + } + catch { + case NonFatal(e) => + logger.error(phi"Can not send an email: $e") + false + } + } +} -- cgit v1.2.3