From feb7768b124e9ee8f2f69bda727bc6504413b63e Mon Sep 17 00:00:00 2001 From: ChristianOertlin Date: Mon, 19 Aug 2024 11:29:05 +0200 Subject: [PATCH] refactor(submitters) (#3574) (patch) # Description refactoring of order submitters. --- cg/meta/orders/api.py | 52 +- cg/meta/orders/balsamic_qc_submitter.py | 5 - cg/meta/orders/balsamic_submitter.py | 5 - cg/meta/orders/balsamic_umi_submitter.py | 5 - cg/meta/orders/fluffy_submitter.py | 5 - cg/meta/orders/lims.py | 32 - cg/meta/orders/microsalt_submitter.py | 5 - cg/meta/orders/mip_dna_submitter.py | 5 - cg/meta/orders/mip_rna_submitter.py | 5 - cg/meta/orders/rml_submitter.py | 5 - cg/meta/orders/rnafusion_submitter.py | 16 - cg/meta/orders/submitter.py | 51 -- cg/meta/orders/tomte_submitter.py | 5 - cg/server/endpoints/orders.py | 5 +- cg/server/ext.py | 7 + .../order_lims_service/order_lims_service.py | 39 + .../store_order_services/store_case_order.py} | 183 ++--- .../store_fastq_order_service.py} | 62 +- .../store_metagenome_order.py} | 72 +- .../store_microbial_order.py} | 87 +- .../store_order_services/store_pool_order.py} | 68 +- cg/services/orders/submitters/__init__.py | 0 .../orders/submitters/case_order_submitter.py | 34 + .../submitters/fastq_order_submitter.py | 24 + .../submitters/metagenome_order_submitter.py | 24 + .../submitters/microbial_order_submitter.py | 28 + .../orders/submitters/order_submitter.py | 56 ++ .../submitters/order_submitter_registry.py | 153 ++++ .../orders/submitters/pool_order_submitter.py | 26 + .../validate_case_order.py | 102 +++ .../validate_fastq_order.py | 12 + .../validate_metagenome_order.py | 30 + .../validate_microbial_order.py} | 21 +- .../validate_pool_order.py | 35 + tests/conftest.py | 2 + .../orders_fixtures/order_form_fixtures.py} | 124 +++ .../order_store_service_fixtures.py | 43 + tests/meta/orders/conftest.py | 144 +--- .../test_SarsCov2Submitter_order_to_status.py | 49 -- .../test_SarsCov2Submitter_store_order.py | 45 -- tests/meta/orders/test_meta_orders_api.py | 52 +- tests/meta/orders/test_meta_orders_status.py | 743 ------------------ .../test_order_lims_service.py} | 26 +- .../test_fastq_order_service.py | 194 +++++ .../test_generic_order_store_service.py | 299 +++++++ .../test_metagenome_store_service.py | 51 ++ .../test_microbial_store_order_service.py | 247 ++++++ .../test_pool_order_store_service.py | 85 ++ .../test_validate_generic_order.py} | 12 +- .../test_validate_microbial_order_service.py} | 11 +- .../test_validate_pool_order_service.py} | 10 +- tests/store/conftest.py | 4 +- 52 files changed, 1929 insertions(+), 1476 deletions(-) delete mode 100644 cg/meta/orders/balsamic_qc_submitter.py delete mode 100644 cg/meta/orders/balsamic_submitter.py delete mode 100644 cg/meta/orders/balsamic_umi_submitter.py delete mode 100644 cg/meta/orders/fluffy_submitter.py delete mode 100644 cg/meta/orders/lims.py delete mode 100644 cg/meta/orders/microsalt_submitter.py delete mode 100644 cg/meta/orders/mip_dna_submitter.py delete mode 100644 cg/meta/orders/mip_rna_submitter.py delete mode 100644 cg/meta/orders/rml_submitter.py delete mode 100644 cg/meta/orders/rnafusion_submitter.py delete mode 100644 cg/meta/orders/submitter.py delete mode 100644 cg/meta/orders/tomte_submitter.py create mode 100644 cg/services/orders/order_lims_service/order_lims_service.py rename cg/{meta/orders/case_submitter.py => services/orders/store_order_services/store_case_order.py} (62%) rename cg/{meta/orders/fastq_submitter.py => services/orders/store_order_services/store_fastq_order_service.py} (70%) rename cg/{meta/orders/metagenome_submitter.py => services/orders/store_order_services/store_metagenome_order.py} (63%) rename cg/{meta/orders/microbial_submitter.py => services/orders/store_order_services/store_microbial_order.py} (85%) rename cg/{meta/orders/pool_submitter.py => services/orders/store_order_services/store_pool_order.py} (76%) create mode 100644 cg/services/orders/submitters/__init__.py create mode 100644 cg/services/orders/submitters/case_order_submitter.py create mode 100644 cg/services/orders/submitters/fastq_order_submitter.py create mode 100644 cg/services/orders/submitters/metagenome_order_submitter.py create mode 100644 cg/services/orders/submitters/microbial_order_submitter.py create mode 100644 cg/services/orders/submitters/order_submitter.py create mode 100644 cg/services/orders/submitters/order_submitter_registry.py create mode 100644 cg/services/orders/submitters/pool_order_submitter.py create mode 100644 cg/services/orders/validate_order_services/validate_case_order.py create mode 100644 cg/services/orders/validate_order_services/validate_fastq_order.py create mode 100644 cg/services/orders/validate_order_services/validate_metagenome_order.py rename cg/{meta/orders/sars_cov_2_submitter.py => services/orders/validate_order_services/validate_microbial_order.py} (55%) create mode 100644 cg/services/orders/validate_order_services/validate_pool_order.py rename tests/{apps/orderform/conftest.py => fixture_plugins/orders_fixtures/order_form_fixtures.py} (70%) create mode 100644 tests/fixture_plugins/orders_fixtures/order_store_service_fixtures.py delete mode 100644 tests/meta/orders/test_SarsCov2Submitter_order_to_status.py delete mode 100644 tests/meta/orders/test_SarsCov2Submitter_store_order.py delete mode 100644 tests/meta/orders/test_meta_orders_status.py rename tests/{meta/orders/test_meta_orders_lims.py => services/orders/order_lims_service/test_order_lims_service.py} (88%) create mode 100644 tests/services/orders/order_store_service/test_fastq_order_service.py create mode 100644 tests/services/orders/order_store_service/test_generic_order_store_service.py create mode 100644 tests/services/orders/order_store_service/test_metagenome_store_service.py create mode 100644 tests/services/orders/order_store_service/test_microbial_store_order_service.py create mode 100644 tests/services/orders/order_store_service/test_pool_order_store_service.py rename tests/{meta/orders/test_rnafusion_submitter.py => services/orders/test_validate_order_service/test_validate_generic_order.py} (76%) rename tests/{meta/orders/test_SarsCov2Submitter_validate_order.py => services/orders/test_validate_order_service/test_validate_microbial_order_service.py} (83%) rename tests/{meta/orders/test_PoolSubmitter_validate_order.py => services/orders/test_validate_order_service/test_validate_pool_order_service.py} (78%) diff --git a/cg/meta/orders/api.py b/cg/meta/orders/api.py index 1522791c42..3d73ac47ed 100644 --- a/cg/meta/orders/api.py +++ b/cg/meta/orders/api.py @@ -11,66 +11,36 @@ from cg.apps.lims import LimsAPI from cg.apps.osticket import OsTicket -from cg.meta.orders.balsamic_qc_submitter import BalsamicQCSubmitter -from cg.meta.orders.balsamic_submitter import BalsamicSubmitter -from cg.meta.orders.balsamic_umi_submitter import BalsamicUmiSubmitter -from cg.meta.orders.fastq_submitter import FastqSubmitter -from cg.meta.orders.fluffy_submitter import FluffySubmitter -from cg.meta.orders.metagenome_submitter import MetagenomeSubmitter -from cg.meta.orders.microsalt_submitter import MicrosaltSubmitter -from cg.meta.orders.mip_dna_submitter import MipDnaSubmitter -from cg.meta.orders.mip_rna_submitter import MipRnaSubmitter -from cg.meta.orders.rml_submitter import RmlSubmitter -from cg.meta.orders.rnafusion_submitter import RnafusionSubmitter -from cg.meta.orders.sars_cov_2_submitter import SarsCov2Submitter -from cg.meta.orders.submitter import Submitter from cg.meta.orders.ticket_handler import TicketHandler -from cg.meta.orders.tomte_submitter import TomteSubmitter from cg.models.orders.order import OrderIn, OrderType +from cg.services.orders.submitters.order_submitter_registry import OrderSubmitterRegistry from cg.store.store import Store LOG = logging.getLogger(__name__) -def _get_submit_handler(project: OrderType, lims: LimsAPI, status: Store) -> Submitter: - """Factory Method""" - - submitters = { - OrderType.BALSAMIC: BalsamicSubmitter, - OrderType.BALSAMIC_QC: BalsamicQCSubmitter, - OrderType.BALSAMIC_UMI: BalsamicUmiSubmitter, - OrderType.FASTQ: FastqSubmitter, - OrderType.FLUFFY: FluffySubmitter, - OrderType.METAGENOME: MetagenomeSubmitter, - OrderType.MICROSALT: MicrosaltSubmitter, - OrderType.MIP_DNA: MipDnaSubmitter, - OrderType.MIP_RNA: MipRnaSubmitter, - OrderType.RML: RmlSubmitter, - OrderType.RNAFUSION: RnafusionSubmitter, - OrderType.SARS_COV_2: SarsCov2Submitter, - OrderType.TOMTE: TomteSubmitter, - } - if project in submitters: - return submitters[project](lims=lims, status=status) - - class OrdersAPI: """Orders API for accepting new samples into the system.""" - def __init__(self, lims: LimsAPI, status: Store, osticket: OsTicket): + def __init__( + self, + lims: LimsAPI, + status: Store, + osticket: OsTicket, + submitter_registry: OrderSubmitterRegistry, + ): super().__init__() self.lims = lims self.status = status self.ticket_handler: TicketHandler = TicketHandler(osticket_api=osticket, status_db=status) + self.submitter_registry = submitter_registry def submit(self, project: OrderType, order_in: OrderIn, user_name: str, user_mail: str) -> dict: """Submit a batch of samples. Main entry point for the class towards interfaces that implements it. """ - submit_handler: Submitter = _get_submit_handler(project, lims=self.lims, status=self.status) - submit_handler.validate_order(order=order_in) - + submit_handler = self.submitter_registry.get_order_submitter(project) # detect manual ticket assignment ticket_number: str | None = TicketHandler.parse_ticket_number(order_in.name) if not ticket_number: @@ -86,4 +56,4 @@ def submit(self, project: OrderType, order_in: OrderIn, user_name: str, user_mai ticket_number=ticket_number, ) order_in.ticket = ticket_number - return submit_handler.submit_order(order=order_in) + return submit_handler.submit_order(order_in=order_in) diff --git a/cg/meta/orders/balsamic_qc_submitter.py b/cg/meta/orders/balsamic_qc_submitter.py deleted file mode 100644 index 5a27399883..0000000000 --- a/cg/meta/orders/balsamic_qc_submitter.py +++ /dev/null @@ -1,5 +0,0 @@ -from cg.meta.orders.balsamic_submitter import BalsamicSubmitter - - -class BalsamicQCSubmitter(BalsamicSubmitter): - pass diff --git a/cg/meta/orders/balsamic_submitter.py b/cg/meta/orders/balsamic_submitter.py deleted file mode 100644 index a53caf0ca4..0000000000 --- a/cg/meta/orders/balsamic_submitter.py +++ /dev/null @@ -1,5 +0,0 @@ -from cg.meta.orders.case_submitter import CaseSubmitter - - -class BalsamicSubmitter(CaseSubmitter): - pass diff --git a/cg/meta/orders/balsamic_umi_submitter.py b/cg/meta/orders/balsamic_umi_submitter.py deleted file mode 100644 index 457630b02c..0000000000 --- a/cg/meta/orders/balsamic_umi_submitter.py +++ /dev/null @@ -1,5 +0,0 @@ -from cg.meta.orders.balsamic_submitter import BalsamicSubmitter - - -class BalsamicUmiSubmitter(BalsamicSubmitter): - pass diff --git a/cg/meta/orders/fluffy_submitter.py b/cg/meta/orders/fluffy_submitter.py deleted file mode 100644 index 2cdca5f9cb..0000000000 --- a/cg/meta/orders/fluffy_submitter.py +++ /dev/null @@ -1,5 +0,0 @@ -from cg.meta.orders.pool_submitter import PoolSubmitter - - -class FluffySubmitter(PoolSubmitter): - pass diff --git a/cg/meta/orders/lims.py b/cg/meta/orders/lims.py deleted file mode 100644 index b0328e1724..0000000000 --- a/cg/meta/orders/lims.py +++ /dev/null @@ -1,32 +0,0 @@ -import logging - -from cg.apps.lims import LimsAPI -from cg.models.lims.sample import LimsSample -from cg.models.orders.order import OrderIn -from cg.models.orders.samples import OrderInSample - -LOG = logging.getLogger(__name__) - - -def build_lims_sample(customer: str, samples: list[OrderInSample]) -> list[LimsSample]: - """Convert order input to lims interface input.""" - samples_lims = [] - for sample in samples: - dict_sample = sample.__dict__ - LOG.debug(f"{sample.name}: prepare LIMS input") - dict_sample["customer"] = customer - lims_sample: LimsSample = LimsSample.parse_obj(dict_sample) - samples_lims.append(lims_sample) - return samples_lims - - -def process_lims(lims_api: LimsAPI, lims_order: OrderIn, new_samples: list[OrderInSample]): - """Process samples to add them to LIMS.""" - samples_lims: list[LimsSample] = build_lims_sample(lims_order.customer, samples=new_samples) - project_name = lims_order.ticket or lims_order.name - # Create new lims project - project_data = lims_api.submit_project( - project_name, [lims_sample.dict() for lims_sample in samples_lims] - ) - lims_map = lims_api.get_samples(projectlimsid=project_data["id"], map_ids=True) - return project_data, lims_map diff --git a/cg/meta/orders/microsalt_submitter.py b/cg/meta/orders/microsalt_submitter.py deleted file mode 100644 index 6b7a972478..0000000000 --- a/cg/meta/orders/microsalt_submitter.py +++ /dev/null @@ -1,5 +0,0 @@ -from cg.meta.orders.microbial_submitter import MicrobialSubmitter - - -class MicrosaltSubmitter(MicrobialSubmitter): - pass diff --git a/cg/meta/orders/mip_dna_submitter.py b/cg/meta/orders/mip_dna_submitter.py deleted file mode 100644 index d094b111ef..0000000000 --- a/cg/meta/orders/mip_dna_submitter.py +++ /dev/null @@ -1,5 +0,0 @@ -from cg.meta.orders.case_submitter import CaseSubmitter - - -class MipDnaSubmitter(CaseSubmitter): - pass diff --git a/cg/meta/orders/mip_rna_submitter.py b/cg/meta/orders/mip_rna_submitter.py deleted file mode 100644 index af5cee23e0..0000000000 --- a/cg/meta/orders/mip_rna_submitter.py +++ /dev/null @@ -1,5 +0,0 @@ -from cg.meta.orders.case_submitter import CaseSubmitter - - -class MipRnaSubmitter(CaseSubmitter): - pass diff --git a/cg/meta/orders/rml_submitter.py b/cg/meta/orders/rml_submitter.py deleted file mode 100644 index a90f11c151..0000000000 --- a/cg/meta/orders/rml_submitter.py +++ /dev/null @@ -1,5 +0,0 @@ -from cg.meta.orders.pool_submitter import PoolSubmitter - - -class RmlSubmitter(PoolSubmitter): - pass diff --git a/cg/meta/orders/rnafusion_submitter.py b/cg/meta/orders/rnafusion_submitter.py deleted file mode 100644 index d2e7550883..0000000000 --- a/cg/meta/orders/rnafusion_submitter.py +++ /dev/null @@ -1,16 +0,0 @@ -from cg.exc import OrderError -from cg.meta.orders.case_submitter import CaseSubmitter -from cg.models.orders.order import OrderIn -from cg.models.orders.samples import Of1508Sample - - -class RnafusionSubmitter(CaseSubmitter): - def validate_order(self, order: OrderIn) -> None: - super().validate_order(order=order) - self._validate_only_one_sample_per_case(order.samples) - - @staticmethod - def _validate_only_one_sample_per_case(samples: list[Of1508Sample]) -> None: - """Validates that each case contains only one sample.""" - if len({sample.family_name for sample in samples}) != len(samples): - raise OrderError("Each case in an RNAFUSION order must have exactly one sample.") diff --git a/cg/meta/orders/submitter.py b/cg/meta/orders/submitter.py deleted file mode 100644 index 2eb7e33b51..0000000000 --- a/cg/meta/orders/submitter.py +++ /dev/null @@ -1,51 +0,0 @@ -import datetime as dt -import logging -from abc import ABC, abstractmethod - -from cg.apps.lims import LimsAPI -from cg.models.orders.order import OrderIn -from cg.store.models import Base, Sample -from cg.store.store import Store - -LOG = logging.getLogger(__name__) - - -class Submitter(ABC): - def __init__(self, lims: LimsAPI, status: Store): - self.lims = lims - self.status = status - - def validate_order(self, order: OrderIn) -> None: - """Part of Submitter interface, base implementation""" - - @abstractmethod - def submit_order(self, order: OrderIn) -> dict: - pass - - @staticmethod - @abstractmethod - def order_to_status(order: OrderIn) -> dict: - pass - - @abstractmethod - def store_items_in_status( - self, customer_id: str, order: str, ordered: dt.datetime, ticket_id: int, items: list[dict] - ) -> list[Base]: - pass - - @staticmethod - def _fill_in_sample_ids(samples: list[dict], lims_map: dict, id_key: str = "internal_id"): - """Fill in LIMS sample ids.""" - for sample in samples: - LOG.debug(f"{sample['name']}: link sample to LIMS") - if not sample.get(id_key): - internal_id = lims_map[sample["name"]] - LOG.info(f"{sample['name']} -> {internal_id}: connect sample to LIMS") - sample[id_key] = internal_id - - def _add_missing_reads(self, samples: list[Sample]): - """Add expected reads/reads missing.""" - for sample_obj in samples: - LOG.info(f"{sample_obj.internal_id}: add missing reads in LIMS") - target_reads = sample_obj.application_version.application.target_reads / 1000000 - self.lims.update_sample(sample_obj.internal_id, target_reads=target_reads) diff --git a/cg/meta/orders/tomte_submitter.py b/cg/meta/orders/tomte_submitter.py deleted file mode 100644 index cf563af3ae..0000000000 --- a/cg/meta/orders/tomte_submitter.py +++ /dev/null @@ -1,5 +0,0 @@ -from cg.meta.orders.case_submitter import CaseSubmitter - - -class TomteSubmitter(CaseSubmitter): - pass diff --git a/cg/server/endpoints/orders.py b/cg/server/endpoints/orders.py index 20f1f1b822..8261233f47 100644 --- a/cg/server/endpoints/orders.py +++ b/cg/server/endpoints/orders.py @@ -40,6 +40,7 @@ lims, order_service, osticket, + order_submitter_registry, ) from cg.store.models import ( Application, @@ -155,7 +156,9 @@ def create_order_from_form(): @ORDERS_BLUEPRINT.route("/submit_order/", methods=["POST"]) def submit_order(order_type): """Submit an order for samples.""" - api = OrdersAPI(lims=lims, status=db, osticket=osticket) + api = OrdersAPI( + lims=lims, status=db, osticket=osticket, submitter_registry=order_submitter_registry + ) error_message: str try: request_json = request.get_json() diff --git a/cg/server/ext.py b/cg/server/ext.py index 623e1d6894..94d3692b94 100644 --- a/cg/server/ext.py +++ b/cg/server/ext.py @@ -13,6 +13,10 @@ from cg.services.orders.order_summary_service.order_summary_service import ( OrderSummaryService, ) +from cg.services.orders.submitters.order_submitter_registry import ( + OrderSubmitterRegistry, + setup_order_submitter_registry, +) from cg.services.sample_service.sample_service import SampleService from cg.store.database import initialize_database from cg.store.store import Store @@ -83,3 +87,6 @@ def init_app(self, app): summary_service = OrderSummaryService(store=db, analysis_client=analysis_client) order_service = OrderService(store=db, status_service=summary_service) sample_service = SampleService(db) +order_submitter_registry: OrderSubmitterRegistry = setup_order_submitter_registry( + lims=lims, status_db=db +) diff --git a/cg/services/orders/order_lims_service/order_lims_service.py b/cg/services/orders/order_lims_service/order_lims_service.py new file mode 100644 index 0000000000..219ddf6d9b --- /dev/null +++ b/cg/services/orders/order_lims_service/order_lims_service.py @@ -0,0 +1,39 @@ +import logging + +from cg.apps.lims import LimsAPI +from cg.models.lims.sample import LimsSample +from cg.models.orders.order import OrderIn +from cg.models.orders.samples import OrderInSample + +LOG = logging.getLogger(__name__) + + +class OrderLimsService: + + def __init__(self, lims_api: LimsAPI): + self.lims_api = lims_api + + @staticmethod + def _build_lims_sample(customer: str, samples: list[OrderInSample]) -> list[LimsSample]: + """Convert order input to lims interface input.""" + samples_lims = [] + for sample in samples: + dict_sample = sample.__dict__ + LOG.debug(f"{sample.name}: prepare LIMS input") + dict_sample["customer"] = customer + lims_sample: LimsSample = LimsSample.parse_obj(dict_sample) + samples_lims.append(lims_sample) + return samples_lims + + def process_lims(self, lims_order: OrderIn, new_samples: list[OrderInSample]): + """Process samples to add them to LIMS.""" + samples_lims: list[LimsSample] = self._build_lims_sample( + lims_order.customer, samples=new_samples + ) + project_name = lims_order.ticket or lims_order.name + # Create new lims project + project_data = self.lims_api.submit_project( + project_name, [lims_sample.dict() for lims_sample in samples_lims] + ) + lims_map = self.lims_api.get_samples(projectlimsid=project_data["id"], map_ids=True) + return project_data, lims_map diff --git a/cg/meta/orders/case_submitter.py b/cg/services/orders/store_order_services/store_case_order.py similarity index 62% rename from cg/meta/orders/case_submitter.py rename to cg/services/orders/store_order_services/store_case_order.py index f604c610c4..9d52dcb213 100644 --- a/cg/meta/orders/case_submitter.py +++ b/cg/services/orders/store_order_services/store_case_order.py @@ -1,110 +1,42 @@ -import datetime as dt import logging +from datetime import datetime -from cg.constants import DataDelivery, Priority -from cg.constants.constants import CaseActions, Workflow +from cg.constants import Workflow, Priority +from cg.constants.constants import CaseActions, DataDelivery from cg.constants.pedigree import Pedigree -from cg.exc import OrderError -from cg.meta.orders.lims import process_lims -from cg.meta.orders.submitter import Submitter from cg.models.orders.order import OrderIn -from cg.models.orders.samples import Of1508Sample, OrderInSample -from cg.store.models import ApplicationVersion, Case, CaseSample, Customer, Sample +from cg.models.orders.samples import Of1508Sample +from cg.services.orders.order_lims_service.order_lims_service import OrderLimsService +from cg.services.orders.submitters.order_submitter import StoreOrderService +from cg.store.models import Case, Customer, Sample, CaseSample, ApplicationVersion +from cg.store.store import Store LOG = logging.getLogger(__name__) -class CaseSubmitter(Submitter): - def validate_order(self, order: OrderIn) -> None: - self._validate_samples_available_to_customer( - samples=order.samples, customer_id=order.customer - ) - self._validate_case_names_are_unique(samples=order.samples, customer_id=order.customer) - self._validate_subject_sex(samples=order.samples, customer_id=order.customer) - - def _validate_subject_sex(self, samples: [Of1508Sample], customer_id: str): - """Validate that sex is consistent with existing samples, skips samples of unknown sex - - Args: - samples (list[dict]): Samples to validate - customer_id (str): Customer that the samples belong to - Returns: - Nothing - """ - - sample: Of1508Sample - for sample in samples: - subject_id: str = sample.subject_id - if not subject_id: - continue - new_gender: str = sample.sex - if new_gender == "unknown": - continue - - existing_samples: list[Sample] = self.status.get_samples_by_customer_and_subject_id( - customer_internal_id=customer_id, subject_id=subject_id - ) - existing_sample: Sample - for existing_sample in existing_samples: - previous_gender = existing_sample.sex - if previous_gender == "unknown": - continue - - if previous_gender != new_gender: - raise OrderError( - f"Sample gender inconsistency for subject_id: {subject_id}: previous gender {previous_gender}, new gender {new_gender}" - ) - - def _validate_samples_available_to_customer( - self, samples: list[OrderInSample], customer_id: str - ) -> None: - """Validate that the customer have access to all samples""" - sample: Of1508Sample - for sample in samples: - if not sample.internal_id: - continue - - existing_sample: Sample = self.status.get_sample_by_internal_id( - internal_id=sample.internal_id - ) - - data_customer: Customer = self.status.get_customer_by_internal_id( - customer_internal_id=customer_id - ) - - if existing_sample.customer not in data_customer.collaborators: - raise OrderError(f"Sample not available: {sample.name}") - - def _validate_case_names_are_unique( - self, samples: list[OrderInSample], customer_id: str - ) -> None: - """Validate that the names of all cases are unused for all samples""" - - customer: Customer = self.status.get_customer_by_internal_id( - customer_internal_id=customer_id - ) - - sample: Of1508Sample - for sample in samples: - if self._is_rerun_of_existing_case(sample=sample): - continue - if self.status.get_case_by_name_and_customer( - customer=customer, case_name=sample.family_name - ): - raise OrderError(f"Case name {sample.family_name} already in use") - - def submit_order(self, order: OrderIn) -> dict: +class StoreCaseOrderService(StoreOrderService): + """ + Service for storing generic orders in StatusDB and Lims. + This class is used to store orders for the following workflows: + - Balsamic + - Balsamic QC + - Balsamic UMI + - MIP DNA + - MIP RNA + - Tomte + """ + + def __init__( + self, + status_db: Store, + lims_service: OrderLimsService, + ): + self.status_db = status_db + self.lims = lims_service + + def store_order(self, order: OrderIn) -> dict: """Submit a batch of samples for sequencing and analysis.""" - result = self._process_case_samples(order=order) - for case_obj in result["records"]: - LOG.info(f"{case_obj.name}: submit family samples") - status_samples = [ - link_obj.sample - for link_obj in case_obj.links - if link_obj.sample.original_ticket == order.ticket - ] - self._add_missing_reads(status_samples) - return result + return self._process_case_samples(order=order) def _process_case_samples(self, order: OrderIn) -> dict: """Process samples to be analyzed.""" @@ -113,8 +45,8 @@ def _process_case_samples(self, order: OrderIn) -> dict: # submit new samples to lims new_samples = [sample for sample in order.samples if sample.internal_id is None] if new_samples: - project_data, lims_map = process_lims( - lims_api=self.lims, lims_order=order, new_samples=new_samples + project_data, lims_map = self.lims.process_lims( + lims_order=order, new_samples=new_samples ) status_data = self.order_to_status(order=order) @@ -125,7 +57,7 @@ def _process_case_samples(self, order: OrderIn) -> dict: new_cases: list[Case] = self.store_items_in_status( customer_id=status_data["customer"], order=status_data["order"], - ordered=project_data["date"] if project_data else dt.datetime.now(), + ordered=project_data["date"] if project_data else datetime.now(), ticket_id=order.ticket, items=status_data["families"], ) @@ -150,25 +82,20 @@ def _get_single_value(case_name, case_samples, value_key, value_default=None): single_value = values.pop() return single_value - @staticmethod - def order_to_status(order: OrderIn) -> dict: + def order_to_status(self, order: OrderIn) -> dict: """Converts order input to status interface input for MIP-DNA, MIP-RNA and Balsamic.""" status_data = {"customer": order.customer, "order": order.name, "families": []} - cases = CaseSubmitter._group_cases(order.samples) + cases = self._group_cases(order.samples) for case_name, case_samples in cases.items(): - case_internal_id: str = CaseSubmitter._get_single_value( + case_internal_id: str = self._get_single_value( case_name, case_samples, "case_internal_id" ) cohorts: set[str] = { cohort for sample in case_samples for cohort in sample.cohorts if cohort } - data_analysis = CaseSubmitter._get_single_value( - case_name, case_samples, "data_analysis" - ) - data_delivery = CaseSubmitter._get_single_value( - case_name, case_samples, "data_delivery" - ) + data_analysis = self._get_single_value(case_name, case_samples, "data_analysis") + data_delivery = self._get_single_value(case_name, case_samples, "data_delivery") panels: set[str] = set() if data_analysis in [Workflow.MIP_DNA, Workflow.TOMTE]: @@ -176,10 +103,10 @@ def order_to_status(order: OrderIn) -> dict: panel for sample in case_samples for panel in sample.panels if panel } - priority = CaseSubmitter._get_single_value( + priority = self._get_single_value( case_name, case_samples, "priority", Priority.standard.name ) - synopsis: str = CaseSubmitter._get_single_value(case_name, case_samples, "synopsis") + synopsis: str = self._get_single_value(case_name, case_samples, "synopsis") case = { "cohorts": list(cohorts), @@ -219,16 +146,16 @@ def order_to_status(order: OrderIn) -> dict: return status_data def store_items_in_status( - self, customer_id: str, order: str, ordered: dt.datetime, ticket_id: str, items: list[dict] + self, customer_id: str, order: str, ordered: datetime, ticket_id: str, items: list[dict] ) -> list[Case]: """Store cases, samples and their relationship in the Status database.""" - customer: Customer = self.status.get_customer_by_internal_id( + customer: Customer = self.status_db.get_customer_by_internal_id( customer_internal_id=customer_id ) new_cases: list[Case] = [] for case in items: - status_db_case: Case = self.status.get_case_by_internal_id( + status_db_case: Case = self.status_db.get_case_by_internal_id( internal_id=case["internal_id"] ) if not status_db_case: @@ -245,7 +172,7 @@ def store_items_in_status( case_samples: dict[str, Sample] = {} for sample in case["samples"]: - existing_sample: Sample = self.status.get_sample_by_internal_id( + existing_sample: Sample = self.status_db.get_sample_by_internal_id( internal_id=sample["internal_id"] ) if not existing_sample: @@ -264,8 +191,8 @@ def store_items_in_status( for sample in case["samples"]: sample_mother: Sample = case_samples.get(sample.get(Pedigree.MOTHER)) sample_father: Sample = case_samples.get(sample.get(Pedigree.FATHER)) - with self.status.session.no_autoflush: - case_sample: CaseSample = self.status.get_case_sample_link( + with self.status_db.session.no_autoflush: + case_sample: CaseSample = self.status_db.get_case_sample_link( case_internal_id=status_db_case.internal_id, sample_internal_id=sample["internal_id"], ) @@ -284,8 +211,8 @@ def store_items_in_status( mother_obj=sample_mother, sample=sample, ) - self.status.session.add_all(new_cases) - self.status.session.commit() + self.status_db.session.add_all(new_cases) + self.status_db.session.commit() return new_cases @staticmethod @@ -310,18 +237,18 @@ def _update_relationship(father_obj, link_obj, mother_obj, sample): link_obj.father = father_obj or link_obj.father def _create_link(self, case_obj, family_samples, father_obj, mother_obj, sample): - link_obj = self.status.relate_sample( + link_obj = self.status_db.relate_sample( case=case_obj, sample=family_samples[sample["name"]], status=sample["status"], mother=mother_obj, father=father_obj, ) - self.status.session.add(link_obj) + self.status_db.session.add(link_obj) return link_obj def _create_sample(self, case, customer_obj, order, ordered, sample, ticket): - sample_obj = self.status.add_sample( + sample_obj = self.status_db.add_sample( name=sample["name"], comment=sample["comment"], control=sample["control"], @@ -340,16 +267,16 @@ def _create_sample(self, case, customer_obj, order, ordered, sample, ticket): subject_id=sample["subject_id"], ) sample_obj.customer = customer_obj - with self.status.session.no_autoflush: + with self.status_db.session.no_autoflush: application_tag = sample["application"] sample_obj.application_version: ApplicationVersion = ( - self.status.get_current_application_version_by_tag(tag=application_tag) + self.status_db.get_current_application_version_by_tag(tag=application_tag) ) - self.status.session.add(sample_obj) + self.status_db.session.add(sample_obj) return sample_obj def _create_case(self, case: dict, customer_obj: Customer, ticket: str): - case_obj = self.status.add_case( + case_obj = self.status_db.add_case( cohorts=case["cohorts"], data_analysis=Workflow(case["data_analysis"]), data_delivery=DataDelivery(case["data_delivery"]), diff --git a/cg/meta/orders/fastq_submitter.py b/cg/services/orders/store_order_services/store_fastq_order_service.py similarity index 70% rename from cg/meta/orders/fastq_submitter.py rename to cg/services/orders/store_order_services/store_fastq_order_service.py index cae089772f..3e802ff06a 100644 --- a/cg/meta/orders/fastq_submitter.py +++ b/cg/services/orders/store_order_services/store_fastq_order_service.py @@ -1,23 +1,30 @@ -import datetime as dt +import logging +from datetime import datetime -from cg.constants import DataDelivery, GenePanelMasterList -from cg.constants.constants import CustomerId, PrepCategory, Workflow -from cg.constants.priority import Priority +from cg.constants import Workflow, DataDelivery, GenePanelMasterList, Priority +from cg.constants.constants import CustomerId, PrepCategory from cg.exc import OrderError -from cg.meta.orders.lims import process_lims -from cg.meta.orders.submitter import Submitter from cg.models.orders.order import OrderIn from cg.models.orders.sample_base import StatusEnum -from cg.store.models import ApplicationVersion, Case, CaseSample, Customer, Sample +from cg.services.orders.order_lims_service.order_lims_service import OrderLimsService +from cg.services.orders.submitters.order_submitter import StoreOrderService +from cg.store.models import Case, CaseSample, Sample, Customer, ApplicationVersion +from cg.store.store import Store +LOG = logging.getLogger(__name__) -class FastqSubmitter(Submitter): - def submit_order(self, order: OrderIn) -> dict: + +class StoreFastqOrderService(StoreOrderService): + """Storing service for FASTQ orders.""" + + def __init__(self, status_db: Store, lims_service: OrderLimsService): + self.status_db = status_db + self.lims = lims_service + + def store_order(self, order: OrderIn) -> dict: """Submit a batch of samples for FASTQ delivery.""" - project_data, lims_map = process_lims( - lims_api=self.lims, lims_order=order, new_samples=order.samples - ) + project_data, lims_map = self.lims.process_lims(lims_order=order, new_samples=order.samples) status_data = self.order_to_status(order) self._fill_in_sample_ids(samples=status_data["samples"], lims_map=lims_map) new_samples = self.store_items_in_status( @@ -27,7 +34,6 @@ def submit_order(self, order: OrderIn) -> dict: ticket_id=order.ticket, items=status_data["samples"], ) - self._add_missing_reads(new_samples) return {"project": project_data, "records": new_samples} @staticmethod @@ -57,7 +63,7 @@ def order_to_status(order: OrderIn) -> dict: def create_maf_case(self, sample_obj: Sample) -> None: """Add a MAF case to the Status database.""" - case: Case = self.status.add_case( + case: Case = self.status_db.add_case( data_analysis=Workflow(Workflow.MIP_DNA), data_delivery=DataDelivery(DataDelivery.NO_DELIVERY), name="_".join([sample_obj.name, "MAF"]), @@ -65,31 +71,31 @@ def create_maf_case(self, sample_obj: Sample) -> None: priority=Priority.research, ticket=sample_obj.original_ticket, ) - case.customer = self.status.get_customer_by_internal_id( + case.customer = self.status_db.get_customer_by_internal_id( customer_internal_id=CustomerId.CG_INTERNAL_CUSTOMER ) - relationship: CaseSample = self.status.relate_sample( + relationship: CaseSample = self.status_db.relate_sample( case=case, sample=sample_obj, status=StatusEnum.unknown ) - self.status.session.add_all([case, relationship]) + self.status_db.session.add_all([case, relationship]) def store_items_in_status( - self, customer_id: str, order: str, ordered: dt.datetime, ticket_id: str, items: list[dict] + self, customer_id: str, order: str, ordered: datetime, ticket_id: str, items: list[dict] ) -> list[Sample]: """Store fastq samples in the status database including family connection and delivery""" - customer: Customer = self.status.get_customer_by_internal_id( + customer: Customer = self.status_db.get_customer_by_internal_id( customer_internal_id=customer_id ) if not customer: raise OrderError(f"Unknown customer: {customer_id}") new_samples = [] - case: Case = self.status.get_case_by_name_and_customer( + case: Case = self.status_db.get_case_by_name_and_customer( customer=customer, case_name=ticket_id ) submitted_case: dict = items[0] - with self.status.session.no_autoflush: + with self.status_db.session.no_autoflush: for sample in items: - new_sample = self.status.add_sample( + new_sample = self.status_db.add_sample( name=sample["name"], sex=sample["sex"] or "unknown", comment=sample["comment"], @@ -105,14 +111,14 @@ def store_items_in_status( new_sample.customer: Customer = customer application_tag: str = sample["application"] application_version: ApplicationVersion = ( - self.status.get_current_application_version_by_tag(tag=application_tag) + self.status_db.get_current_application_version_by_tag(tag=application_tag) ) if application_version is None: raise OrderError(f"Invalid application: {sample['application']}") new_sample.application_version: ApplicationVersion = application_version new_samples.append(new_sample) if not case: - case = self.status.add_case( + case = self.status_db.add_case( data_analysis=Workflow(submitted_case["data_analysis"]), data_delivery=DataDelivery(submitted_case["data_delivery"]), name=ticket_id, @@ -126,11 +132,11 @@ def store_items_in_status( ): self.create_maf_case(sample_obj=new_sample) case.customer = customer - new_relationship = self.status.relate_sample( + new_relationship = self.status_db.relate_sample( case=case, sample=new_sample, status=StatusEnum.unknown ) - self.status.session.add_all([case, new_relationship]) + self.status_db.session.add_all([case, new_relationship]) - self.status.session.add_all(new_samples) - self.status.session.commit() + self.status_db.session.add_all(new_samples) + self.status_db.session.commit() return new_samples diff --git a/cg/meta/orders/metagenome_submitter.py b/cg/services/orders/store_order_services/store_metagenome_order.py similarity index 63% rename from cg/meta/orders/metagenome_submitter.py rename to cg/services/orders/store_order_services/store_metagenome_order.py index 0d918b6fae..4db0d6f3af 100644 --- a/cg/meta/orders/metagenome_submitter.py +++ b/cg/services/orders/store_order_services/store_metagenome_order.py @@ -1,41 +1,28 @@ -import datetime as dt +import logging +from datetime import datetime -from cg.constants import DataDelivery -from cg.constants.constants import Workflow -from cg.constants.subject import Sex +from cg.constants import Workflow, DataDelivery, Sex from cg.exc import OrderError -from cg.meta.orders.lims import process_lims -from cg.meta.orders.submitter import Submitter from cg.models.orders.order import OrderIn from cg.models.orders.sample_base import StatusEnum -from cg.models.orders.samples import MetagenomeSample -from cg.store.models import ApplicationVersion, Case, CaseSample, Customer, Sample +from cg.services.orders.order_lims_service.order_lims_service import OrderLimsService +from cg.services.orders.submitters.order_submitter import StoreOrderService +from cg.store.models import Sample, Customer, ApplicationVersion, Case, CaseSample +from cg.store.store import Store +LOG = logging.getLogger(__name__) -class MetagenomeSubmitter(Submitter): - def validate_order(self, order: OrderIn) -> None: - self._validate_sample_names_are_unique(samples=order.samples, customer_id=order.customer) - def _validate_sample_names_are_unique( - self, samples: list[MetagenomeSample], customer_id: str - ) -> None: - """Validate that the names of all samples are unused.""" - customer: Customer = self.status.get_customer_by_internal_id( - customer_internal_id=customer_id - ) - for sample in samples: - if sample.control: - continue - if self.status.get_sample_by_customer_and_name( - customer_entry_id=[customer.id], sample_name=sample.name - ): - raise OrderError(f"Sample name {sample.name} already in use") +class StoreMetagenomeOrderService(StoreOrderService): + """Storing service for metagenome orders.""" - def submit_order(self, order: OrderIn) -> dict: + def __init__(self, status_db: Store, lims_service: OrderLimsService): + self.status_db = status_db + self.lims = lims_service + + def store_order(self, order: OrderIn) -> dict: """Submit a batch of metagenome samples.""" - project_data, lims_map = process_lims( - lims_api=self.lims, lims_order=order, new_samples=order.samples - ) + project_data, lims_map = self.lims.process_lims(lims_order=order, new_samples=order.samples) status_data = self.order_to_status(order) self._fill_in_sample_ids(samples=status_data["families"][0]["samples"], lims_map=lims_map) new_samples = self.store_items_in_status( @@ -45,7 +32,6 @@ def submit_order(self, order: OrderIn) -> dict: ticket_id=order.ticket, items=status_data["families"], ) - self._add_missing_reads(new_samples) return {"project": project_data, "records": new_samples} @staticmethod @@ -78,24 +64,24 @@ def store_items_in_status( self, customer_id: str, order: str, - ordered: dt.datetime, + ordered: datetime, ticket_id: str, items: list[dict], ) -> list[Sample]: """Store samples in the status database.""" - customer: Customer = self.status.get_customer_by_internal_id( + customer: Customer = self.status_db.get_customer_by_internal_id( customer_internal_id=customer_id ) if customer is None: raise OrderError(f"unknown customer: {customer_id}") new_samples = [] - case: Case = self.status.get_case_by_name_and_customer( + case: Case = self.status_db.get_case_by_name_and_customer( customer=customer, case_name=str(ticket_id) ) case_dict: dict = items[0] - with self.status.session.no_autoflush: + with self.status_db.session.no_autoflush: for sample in case_dict["samples"]: - new_sample = self.status.add_sample( + new_sample = self.status_db.add_sample( name=sample["name"], sex=Sex.UNKNOWN, comment=sample["comment"], @@ -109,7 +95,7 @@ def store_items_in_status( new_sample.customer: Customer = customer application_tag: str = sample["application"] application_version: ApplicationVersion = ( - self.status.get_current_application_version_by_tag(tag=application_tag) + self.status_db.get_current_application_version_by_tag(tag=application_tag) ) if application_version is None: raise OrderError(f"Invalid application: {sample['application']}") @@ -117,7 +103,7 @@ def store_items_in_status( new_samples.append(new_sample) if not case: - case = self.status.add_case( + case = self.status_db.add_case( data_analysis=Workflow(case_dict["data_analysis"]), data_delivery=DataDelivery(case_dict["data_delivery"]), name=str(ticket_id), @@ -126,14 +112,14 @@ def store_items_in_status( ticket=ticket_id, ) case.customer = customer - self.status.session.add(case) - self.status.session.commit() + self.status_db.session.add(case) + self.status_db.session.commit() - new_relationship: CaseSample = self.status.relate_sample( + new_relationship: CaseSample = self.status_db.relate_sample( case=case, sample=new_sample, status=StatusEnum.unknown ) - self.status.session.add(new_relationship) + self.status_db.session.add(new_relationship) - self.status.session.add_all(new_samples) - self.status.session.commit() + self.status_db.session.add_all(new_samples) + self.status_db.session.commit() return new_samples diff --git a/cg/meta/orders/microbial_submitter.py b/cg/services/orders/store_order_services/store_microbial_order.py similarity index 85% rename from cg/meta/orders/microbial_submitter.py rename to cg/services/orders/store_order_services/store_microbial_order.py index eb078e2899..fc5c0eb9bb 100644 --- a/cg/meta/orders/microbial_submitter.py +++ b/cg/services/orders/store_order_services/store_microbial_order.py @@ -1,23 +1,53 @@ -import datetime as dt +import logging +from datetime import datetime -from cg.constants import DataDelivery -from cg.constants.constants import Workflow -from cg.constants.subject import Sex -from cg.meta.orders.lims import process_lims -from cg.meta.orders.submitter import Submitter +from cg.constants import Workflow, DataDelivery, Sex from cg.models.orders.order import OrderIn from cg.models.orders.samples import MicrobialSample -from cg.store.models import ( - ApplicationVersion, - Case, - CaseSample, - Customer, - Organism, - Sample, -) +from cg.services.orders.order_lims_service.order_lims_service import OrderLimsService +from cg.services.orders.submitters.order_submitter import StoreOrderService +from cg.store.models import Sample, Customer, Case, ApplicationVersion, Organism, CaseSample +from cg.store.store import Store +LOG = logging.getLogger(__name__) + + +class StoreMicrobialOrderService(StoreOrderService): + """ + Storing service for microbial orders. + These include: + - Mutant samples + - Microsalt samples + - Sars-Cov-2 samples + """ + + def __init__(self, status_db: Store, lims_service: OrderLimsService): + self.status = status_db + self.lims = lims_service + + def store_order(self, order: OrderIn) -> dict: + self._fill_in_sample_verified_organism(order.samples) + # submit samples to LIMS + project_data, lims_map = self.lims.process_lims(lims_order=order, new_samples=order.samples) + # prepare order for status database + status_data = self.order_to_status(order) + self._fill_in_sample_ids( + samples=status_data["samples"], lims_map=lims_map, id_key="internal_id" + ) + + # submit samples to Status + samples = self.store_items_in_status( + customer_id=status_data["customer"], + order=status_data["order"], + ordered=project_data["date"] if project_data else dt.datetime.now(), + ticket_id=order.ticket, + items=status_data["samples"], + comment=status_data["comment"], + data_analysis=Workflow(status_data["data_analysis"]), + data_delivery=DataDelivery(status_data["data_delivery"]), + ) + return {"project": project_data, "records": samples} -class MicrobialSubmitter(Submitter): @staticmethod def order_to_status(order: OrderIn) -> dict: """Convert order input for microbial samples.""" @@ -44,31 +74,6 @@ def order_to_status(order: OrderIn) -> dict: } return status_data - def submit_order(self, order: OrderIn) -> dict: - self._fill_in_sample_verified_organism(order.samples) - # submit samples to LIMS - project_data, lims_map = process_lims( - lims_api=self.lims, lims_order=order, new_samples=order.samples - ) - # prepare order for status database - status_data = self.order_to_status(order) - self._fill_in_sample_ids( - samples=status_data["samples"], lims_map=lims_map, id_key="internal_id" - ) - - # submit samples to Status - samples = self.store_items_in_status( - customer_id=status_data["customer"], - order=status_data["order"], - ordered=project_data["date"] if project_data else dt.datetime.now(), - ticket_id=order.ticket, - items=status_data["samples"], - comment=status_data["comment"], - data_analysis=Workflow(status_data["data_analysis"]), - data_delivery=DataDelivery(status_data["data_delivery"]), - ) - return {"project": project_data, "records": samples} - def store_items_in_status( self, comment: str, @@ -76,7 +81,7 @@ def store_items_in_status( data_analysis: Workflow, data_delivery: DataDelivery, order: str, - ordered: dt.datetime, + ordered: datetime, items: list[dict], ticket_id: str, ) -> [Sample]: diff --git a/cg/meta/orders/pool_submitter.py b/cg/services/orders/store_order_services/store_pool_order.py similarity index 76% rename from cg/meta/orders/pool_submitter.py rename to cg/services/orders/store_order_services/store_pool_order.py index 1df24758d9..9d718578c6 100644 --- a/cg/meta/orders/pool_submitter.py +++ b/cg/services/orders/store_order_services/store_pool_order.py @@ -1,28 +1,34 @@ -import datetime as dt +import logging +from datetime import datetime -from cg.constants import DataDelivery -from cg.constants.constants import Workflow +from cg.constants import Workflow, DataDelivery from cg.exc import OrderError -from cg.meta.orders.lims import process_lims -from cg.meta.orders.submitter import Submitter from cg.models.orders.order import OrderIn from cg.models.orders.sample_base import SexEnum from cg.models.orders.samples import RmlSample -from cg.store.models import ApplicationVersion, Case, CaseSample, Customer, Pool, Sample +from cg.services.orders.order_lims_service.order_lims_service import OrderLimsService +from cg.services.orders.submitters.order_submitter import StoreOrderService +from cg.store.models import ApplicationVersion, Customer, Pool, Sample, CaseSample, Case +from cg.store.store import Store +LOG = logging.getLogger(__name__) -class PoolSubmitter(Submitter): - def validate_order(self, order: OrderIn) -> None: - super().validate_order(order=order) - self._validate_case_names_are_available( - customer_id=order.customer, samples=order.samples, ticket=order.ticket - ) - def submit_order(self, order: OrderIn) -> dict: +class StorePoolOrderService(StoreOrderService): + """ + Storing service for pool orders. + These include: + - Fluffy / NIPT samples + - RML samples + """ + + def __init__(self, status_db: Store, lims_service: OrderLimsService): + self.status_db = status_db + self.lims = lims_service + + def store_order(self, order: OrderIn) -> dict: status_data = self.order_to_status(order) - project_data, lims_map = process_lims( - lims_api=self.lims, lims_order=order, new_samples=order.samples - ) + project_data, lims_map = self.lims.process_lims(lims_order=order, new_samples=order.samples) samples = [sample for pool in status_data["pools"] for sample in pool["samples"]] self._fill_in_sample_ids(samples=samples, lims_map=lims_map, id_key="internal_id") new_records = self.store_items_in_status( @@ -104,28 +110,28 @@ def order_to_status(order: OrderIn) -> dict: return status_data def store_items_in_status( - self, customer_id: str, order: str, ordered: dt.datetime, ticket_id: str, items: list[dict] + self, customer_id: str, order: str, ordered: datetime, ticket_id: str, items: list[dict] ) -> list[Pool]: """Store pools in the status database.""" - customer: Customer = self.status.get_customer_by_internal_id( + customer: Customer = self.status_db.get_customer_by_internal_id( customer_internal_id=customer_id ) new_pools: list[Pool] = [] new_samples: list[Sample] = [] for pool in items: - with self.status.session.no_autoflush: + with self.status_db.session.no_autoflush: application_version: ApplicationVersion = ( - self.status.get_current_application_version_by_tag(tag=pool["application"]) + self.status_db.get_current_application_version_by_tag(tag=pool["application"]) ) priority: str = pool["priority"] case_name: str = self.create_case_name(ticket=ticket_id, pool_name=pool["name"]) - case: Case = self.status.get_case_by_name_and_customer( + case: Case = self.status_db.get_case_by_name_and_customer( customer=customer, case_name=case_name ) if not case: data_analysis: Workflow = Workflow(pool["data_analysis"]) data_delivery: DataDelivery = DataDelivery(pool["data_delivery"]) - case = self.status.add_case( + case = self.status_db.add_case( data_analysis=data_analysis, data_delivery=data_delivery, name=case_name, @@ -134,9 +140,9 @@ def store_items_in_status( ticket=ticket_id, ) case.customer = customer - self.status.session.add(case) + self.status_db.session.add(case) - new_pool: Pool = self.status.add_pool( + new_pool: Pool = self.status_db.add_pool( application_version=application_version, customer=customer, name=pool["name"], @@ -146,7 +152,7 @@ def store_items_in_status( ) sex: SexEnum = SexEnum.unknown for sample in pool["samples"]: - new_sample = self.status.add_sample( + new_sample = self.status_db.add_sample( name=sample["name"], sex=sex, comment=sample["comment"], @@ -161,25 +167,25 @@ def store_items_in_status( no_invoice=True, ) new_samples.append(new_sample) - link: CaseSample = self.status.relate_sample( + link: CaseSample = self.status_db.relate_sample( case=case, sample=new_sample, status="unknown" ) - self.status.session.add(link) + self.status_db.session.add(link) new_pools.append(new_pool) - self.status.session.add_all(new_pools) - self.status.session.commit() + self.status_db.session.add_all(new_pools) + self.status_db.session.commit() return new_pools def _validate_case_names_are_available( self, customer_id: str, samples: list[RmlSample], ticket: str ): """Validate names of all samples are not already in use.""" - customer: Customer = self.status.get_customer_by_internal_id( + customer: Customer = self.status_db.get_customer_by_internal_id( customer_internal_id=customer_id ) for sample in samples: case_name: str = self.create_case_name(pool_name=sample.pool, ticket=ticket) - if self.status.get_case_by_name_and_customer(customer=customer, case_name=case_name): + if self.status_db.get_case_by_name_and_customer(customer=customer, case_name=case_name): raise OrderError( f"Case name {case_name} already in use for customer {customer.name}" ) diff --git a/cg/services/orders/submitters/__init__.py b/cg/services/orders/submitters/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cg/services/orders/submitters/case_order_submitter.py b/cg/services/orders/submitters/case_order_submitter.py new file mode 100644 index 0000000000..d493965a47 --- /dev/null +++ b/cg/services/orders/submitters/case_order_submitter.py @@ -0,0 +1,34 @@ +"""Module for a generic order submitter.""" + +from cg.models.orders.order import OrderIn +from cg.services.orders.store_order_services.store_case_order import StoreCaseOrderService +from cg.services.orders.submitters.order_submitter import OrderSubmitter +from cg.services.orders.validate_order_services.validate_case_order import ( + ValidateCaseOrderService, +) + + +class CaseOrderSubmitter(OrderSubmitter): + """ + Class for submitting generic orders. + This class is used to submit orders for the following workflows: + - Balsamic + - Balsamic QC + - Balsamic UMI + - MIP DNA + - MIP RNA + - Tomte + """ + + def __init__( + self, + order_validation_service: ValidateCaseOrderService, + order_store_service: StoreCaseOrderService, + ): + self.order_validation_service = order_validation_service + self.order_store_service = order_store_service + + def submit_order(self, order_in: OrderIn) -> dict: + """Submit a generic order.""" + self.order_validation_service.validate_order(order_in) + return self.order_store_service.store_order(order_in) diff --git a/cg/services/orders/submitters/fastq_order_submitter.py b/cg/services/orders/submitters/fastq_order_submitter.py new file mode 100644 index 0000000000..58025d939b --- /dev/null +++ b/cg/services/orders/submitters/fastq_order_submitter.py @@ -0,0 +1,24 @@ +from cg.models.orders.order import OrderIn +from cg.services.orders.store_order_services.store_fastq_order_service import StoreFastqOrderService +from cg.services.orders.submitters.order_submitter import OrderSubmitter +from cg.services.orders.validate_order_services.validate_fastq_order import ( + ValidateFastqOrderService, +) + + +class FastqOrderSubmitter(OrderSubmitter): + """Submitter for fastq orders.""" + + def __init__( + self, + order_validation_service: ValidateFastqOrderService, + order_store_service: StoreFastqOrderService, + ): + self.order_validation_service = order_validation_service + self.order_store_service = order_store_service + + def submit_order(self, order_in: OrderIn) -> dict: + """Submit a fastq order.""" + self.order_validation_service.validate_order(order_in) + result: dict = self.order_store_service.store_order(order_in) + return result diff --git a/cg/services/orders/submitters/metagenome_order_submitter.py b/cg/services/orders/submitters/metagenome_order_submitter.py new file mode 100644 index 0000000000..aaca5ca5a2 --- /dev/null +++ b/cg/services/orders/submitters/metagenome_order_submitter.py @@ -0,0 +1,24 @@ +from cg.services.orders.store_order_services.store_metagenome_order import ( + StoreMetagenomeOrderService, +) +from cg.services.orders.submitters.order_submitter import OrderSubmitter +from cg.services.orders.validate_order_services.validate_metagenome_order import ( + ValidateMetagenomeOrderService, +) + + +class MetagenomeOrderSubmitter(OrderSubmitter): + """Class for submitting metagenome orders.""" + + def __init__( + self, + order_validation_service: ValidateMetagenomeOrderService, + order_store_service: StoreMetagenomeOrderService, + ): + self.order_validation_service = order_validation_service + self.order_store_service = order_store_service + + def submit_order(self, order_in) -> dict: + """Submit a metagenome order.""" + self.order_validation_service.validate_order(order_in) + return self.order_store_service.store_order(order_in) diff --git a/cg/services/orders/submitters/microbial_order_submitter.py b/cg/services/orders/submitters/microbial_order_submitter.py new file mode 100644 index 0000000000..fe77862b13 --- /dev/null +++ b/cg/services/orders/submitters/microbial_order_submitter.py @@ -0,0 +1,28 @@ +from cg.services.orders.store_order_services.store_microbial_order import StoreMicrobialOrderService +from cg.services.orders.submitters.order_submitter import OrderSubmitter +from cg.services.orders.validate_order_services.validate_microbial_order import ( + ValidateMicrobialOrderService, +) + + +class MicrobialOrderSubmitter(OrderSubmitter): + """ + Class for submitting microbial orders. + This class is used to submit orders for the following workflows: + - Sars-Cov-2 + - Microsalt + - Mutant + """ + + def __init__( + self, + order_validation_service: ValidateMicrobialOrderService, + order_store_service: StoreMicrobialOrderService, + ): + self.order_validation_service = order_validation_service + self.order_store_service = order_store_service + + def submit_order(self, order_in) -> dict: + """Submit a microbial order.""" + self.order_validation_service.validate_order(order_in) + return self.order_store_service.store_order(order_in) diff --git a/cg/services/orders/submitters/order_submitter.py b/cg/services/orders/submitters/order_submitter.py new file mode 100644 index 0000000000..1b25cdf807 --- /dev/null +++ b/cg/services/orders/submitters/order_submitter.py @@ -0,0 +1,56 @@ +"""Abstract base classes for order submitters.""" + +import logging +from abc import ABC, abstractmethod + +from cg.models.orders.order import OrderIn +from cg.services.orders.order_lims_service.order_lims_service import OrderLimsService +from cg.store.store import Store + +LOG = logging.getLogger(__name__) + + +class ValidateOrderService(ABC): + @abstractmethod + def __init__(self, status_db: Store): + self.status_db = status_db + + @abstractmethod + def validate_order(self, order_in: OrderIn): + pass + + +class StoreOrderService(ABC): + @abstractmethod + def __init__(self, status_db: Store, lims_service: OrderLimsService): + self.status_db = status_db + self.lims = lims_service + + @abstractmethod + def store_order(self, order_in: OrderIn): + pass + + @staticmethod + def _fill_in_sample_ids(samples: list[dict], lims_map: dict, id_key: str = "internal_id"): + """Fill in LIMS sample ids.""" + for sample in samples: + LOG.debug(f"{sample['name']}: link sample to LIMS") + if not sample.get(id_key): + internal_id = lims_map[sample["name"]] + LOG.info(f"{sample['name']} -> {internal_id}: connect sample to LIMS") + sample[id_key] = internal_id + + +class OrderSubmitter(ABC): + @abstractmethod + def __init__( + self, + validate_order_service: ValidateOrderService, + store_order_service: StoreOrderService, + ): + self.order_validation_service = validate_order_service + self.order_store_service = store_order_service + + @abstractmethod + def submit_order(self, order_in: OrderIn) -> dict: + pass diff --git a/cg/services/orders/submitters/order_submitter_registry.py b/cg/services/orders/submitters/order_submitter_registry.py new file mode 100644 index 0000000000..3741d84632 --- /dev/null +++ b/cg/services/orders/submitters/order_submitter_registry.py @@ -0,0 +1,153 @@ +from cg.apps.lims import LimsAPI +from cg.models.orders.constants import OrderType +from cg.services.orders.order_lims_service.order_lims_service import OrderLimsService +from cg.services.orders.store_order_services.store_fastq_order_service import StoreFastqOrderService +from cg.services.orders.store_order_services.store_case_order import StoreCaseOrderService +from cg.services.orders.store_order_services.store_metagenome_order import ( + StoreMetagenomeOrderService, +) +from cg.services.orders.store_order_services.store_microbial_order import StoreMicrobialOrderService +from cg.services.orders.store_order_services.store_pool_order import StorePoolOrderService +from cg.services.orders.submitters.fastq_order_submitter import FastqOrderSubmitter +from cg.services.orders.submitters.case_order_submitter import CaseOrderSubmitter +from cg.services.orders.submitters.metagenome_order_submitter import MetagenomeOrderSubmitter +from cg.services.orders.submitters.microbial_order_submitter import MicrobialOrderSubmitter +from cg.services.orders.submitters.order_submitter import OrderSubmitter +from cg.services.orders.submitters.pool_order_submitter import PoolOrderSubmitter +from cg.services.orders.validate_order_services.validate_fastq_order import ( + ValidateFastqOrderService, +) +from cg.services.orders.validate_order_services.validate_case_order import ( + ValidateCaseOrderService, +) +from cg.services.orders.validate_order_services.validate_metagenome_order import ( + ValidateMetagenomeOrderService, +) +from cg.services.orders.validate_order_services.validate_microbial_order import ( + ValidateMicrobialOrderService, +) +from cg.services.orders.validate_order_services.validate_pool_order import ValidatePoolOrderService +from cg.store.store import Store + + +class OrderSubmitterRegistry: + """ + A registry for OrderSubmitter instances, keyed by OrderType. + """ + + def __init__(self): + self._registry = {} + + def register(self, order_type: OrderType, order_submitter: OrderSubmitter): + """Register an OrderSubmitter instance for a given OrderType.""" + self._registry[order_type] = order_submitter + + def get_order_submitter(self, order_type: OrderType) -> OrderSubmitter: + """Fetch the registered OrderSubmitter for the given OrderType.""" + if order_submitter := self._registry.get(order_type): + return order_submitter + raise ValueError(f"No OrderSubmitter registered for order type: {order_type}") + + +order_service_mapping = { + OrderType.BALSAMIC: ( + OrderLimsService, + ValidateCaseOrderService, + StoreCaseOrderService, + CaseOrderSubmitter, + ), + OrderType.BALSAMIC_QC: ( + OrderLimsService, + ValidateCaseOrderService, + StoreCaseOrderService, + CaseOrderSubmitter, + ), + OrderType.BALSAMIC_UMI: ( + OrderLimsService, + ValidateCaseOrderService, + StoreCaseOrderService, + CaseOrderSubmitter, + ), + OrderType.FASTQ: ( + OrderLimsService, + ValidateFastqOrderService, + StoreFastqOrderService, + FastqOrderSubmitter, + ), + OrderType.FLUFFY: ( + OrderLimsService, + ValidatePoolOrderService, + StorePoolOrderService, + PoolOrderSubmitter, + ), + OrderType.METAGENOME: ( + OrderLimsService, + ValidateMetagenomeOrderService, + StoreMetagenomeOrderService, + MetagenomeOrderSubmitter, + ), + OrderType.MICROSALT: ( + OrderLimsService, + ValidateMicrobialOrderService, + StoreMicrobialOrderService, + MicrobialOrderSubmitter, + ), + OrderType.MIP_DNA: ( + OrderLimsService, + ValidateCaseOrderService, + StoreCaseOrderService, + CaseOrderSubmitter, + ), + OrderType.MIP_RNA: ( + OrderLimsService, + ValidateCaseOrderService, + StoreCaseOrderService, + CaseOrderSubmitter, + ), + OrderType.RML: ( + OrderLimsService, + ValidatePoolOrderService, + StorePoolOrderService, + PoolOrderSubmitter, + ), + OrderType.RNAFUSION: ( + OrderLimsService, + ValidateCaseOrderService, + StoreCaseOrderService, + CaseOrderSubmitter, + ), + OrderType.SARS_COV_2: ( + OrderLimsService, + ValidateMicrobialOrderService, + StoreMicrobialOrderService, + MicrobialOrderSubmitter, + ), + OrderType.TOMTE: ( + OrderLimsService, + ValidateCaseOrderService, + StoreCaseOrderService, + CaseOrderSubmitter, + ), +} + + +def build_submitter(lims: LimsAPI, status_db: Store, order_type: OrderType) -> OrderSubmitter: + """Build an OrderSubmitter instance for the given OrderType.""" + lims_service, validation_service, store_service, submitter_class = order_service_mapping[ + order_type + ] + return submitter_class( + order_validation_service=validation_service(status_db), + order_store_service=store_service(status_db, lims_service(lims)), + ) + + +def setup_order_submitter_registry(lims: LimsAPI, status_db: Store) -> OrderSubmitterRegistry: + """Set up the OrderSubmitterRegistry with all OrderSubmitter instances.""" + registry = OrderSubmitterRegistry() + for order_type in order_service_mapping.keys(): + registry.register( + order_type=order_type, + order_submitter=build_submitter(lims=lims, status_db=status_db, order_type=order_type), + ) + return registry diff --git a/cg/services/orders/submitters/pool_order_submitter.py b/cg/services/orders/submitters/pool_order_submitter.py new file mode 100644 index 0000000000..6d9b3290ef --- /dev/null +++ b/cg/services/orders/submitters/pool_order_submitter.py @@ -0,0 +1,26 @@ +from cg.models.orders.order import OrderIn +from cg.services.orders.store_order_services.store_pool_order import StorePoolOrderService +from cg.services.orders.submitters.order_submitter import OrderSubmitter +from cg.services.orders.validate_order_services.validate_pool_order import ValidatePoolOrderService + + +class PoolOrderSubmitter(OrderSubmitter): + """ + Class for submitting pool orders. + This class is used to submit orders for the following workflows: + - Fluffy + - RML (Ready made libraries) + + """ + + def __init__( + self, + order_validation_service: ValidatePoolOrderService, + order_store_service: StorePoolOrderService, + ): + self.order_validation_service = order_validation_service + self.order_store_service = order_store_service + + def submit_order(self, order_in: OrderIn) -> dict: + self.order_validation_service.validate_order(order_in) + return self.order_store_service.store_order(order_in) diff --git a/cg/services/orders/validate_order_services/validate_case_order.py b/cg/services/orders/validate_order_services/validate_case_order.py new file mode 100644 index 0000000000..01dc68a184 --- /dev/null +++ b/cg/services/orders/validate_order_services/validate_case_order.py @@ -0,0 +1,102 @@ +from cg.exc import OrderError +from cg.models.orders.constants import OrderType +from cg.models.orders.order import OrderIn +from cg.models.orders.samples import Of1508Sample, OrderInSample +from cg.services.orders.submitters.order_submitter import ValidateOrderService +from cg.store.models import Sample, Customer +from cg.store.store import Store + + +class ValidateCaseOrderService(ValidateOrderService): + + def __init__(self, status_db: Store): + self.status_db = status_db + + def validate_order(self, order: OrderIn) -> None: + self._validate_subject_sex(samples=order.samples, customer_id=order.customer) + self._validate_samples_available_to_customer( + samples=order.samples, customer_id=order.customer + ) + self._validate_case_names_are_unique(samples=order.samples, customer_id=order.customer) + if order.order_type == OrderType.RNAFUSION: + self._validate_only_one_sample_per_case(samples=order.samples) + + def _validate_subject_sex(self, samples: [Of1508Sample], customer_id: str): + """Validate that sex is consistent with existing samples, skips samples of unknown sex + + Args: + samples (list[dict]): Samples to validate + customer_id (str): Customer that the samples belong to + Returns: + Nothing + """ + sample: Of1508Sample + for sample in samples: + subject_id: str = sample.subject_id + if not subject_id: + continue + new_gender: str = sample.sex + if new_gender == "unknown": + continue + + existing_samples: list[Sample] = self.status_db.get_samples_by_customer_and_subject_id( + customer_internal_id=customer_id, subject_id=subject_id + ) + existing_sample: Sample + for existing_sample in existing_samples: + previous_gender = existing_sample.sex + if previous_gender == "unknown": + continue + + if previous_gender != new_gender: + raise OrderError( + f"Sample gender inconsistency for subject_id: {subject_id}: previous gender {previous_gender}, new gender {new_gender}" + ) + + def _validate_samples_available_to_customer( + self, samples: list[OrderInSample], customer_id: str + ) -> None: + """Validate that the customer have access to all samples""" + sample: Of1508Sample + for sample in samples: + if not sample.internal_id: + continue + + existing_sample: Sample = self.status_db.get_sample_by_internal_id( + internal_id=sample.internal_id + ) + + data_customer: Customer = self.status_db.get_customer_by_internal_id( + customer_internal_id=customer_id + ) + + if existing_sample.customer not in data_customer.collaborators: + raise OrderError(f"Sample not available: {sample.name}") + + def _validate_case_names_are_unique( + self, samples: list[OrderInSample], customer_id: str + ) -> None: + """Validate that the names of all cases are unused for all samples""" + + customer: Customer = self.status_db.get_customer_by_internal_id( + customer_internal_id=customer_id + ) + + sample: Of1508Sample + for sample in samples: + if self._is_rerun_of_existing_case(sample=sample): + continue + if self.status_db.get_case_by_name_and_customer( + customer=customer, case_name=sample.family_name + ): + raise OrderError(f"Case name {sample.family_name} already in use") + + @staticmethod + def _is_rerun_of_existing_case(sample: Of1508Sample) -> bool: + return sample.case_internal_id is not None + + @staticmethod + def _validate_only_one_sample_per_case(samples: list[Of1508Sample]) -> None: + """Validates that each case contains only one sample.""" + if len({sample.family_name for sample in samples}) != len(samples): + raise OrderError("Each case in an RNAFUSION order must have exactly one sample.") diff --git a/cg/services/orders/validate_order_services/validate_fastq_order.py b/cg/services/orders/validate_order_services/validate_fastq_order.py new file mode 100644 index 0000000000..cbfe5728a7 --- /dev/null +++ b/cg/services/orders/validate_order_services/validate_fastq_order.py @@ -0,0 +1,12 @@ +from cg.models.orders.order import OrderIn +from cg.services.orders.submitters.order_submitter import ValidateOrderService +from cg.store.store import Store + + +class ValidateFastqOrderService(ValidateOrderService): + + def __init__(self, status_db: Store): + self.status_db = status_db + + def validate_order(self, order: OrderIn) -> None: + pass diff --git a/cg/services/orders/validate_order_services/validate_metagenome_order.py b/cg/services/orders/validate_order_services/validate_metagenome_order.py new file mode 100644 index 0000000000..330abc985e --- /dev/null +++ b/cg/services/orders/validate_order_services/validate_metagenome_order.py @@ -0,0 +1,30 @@ +from cg.exc import OrderError +from cg.models.orders.order import OrderIn +from cg.models.orders.samples import MetagenomeSample +from cg.services.orders.submitters.order_submitter import ValidateOrderService +from cg.store.models import Customer +from cg.store.store import Store + + +class ValidateMetagenomeOrderService(ValidateOrderService): + + def __init__(self, status_db: Store): + self.status_db = status_db + + def validate_order(self, order: OrderIn) -> None: + self._validate_sample_names_are_unique(samples=order.samples, customer_id=order.customer) + + def _validate_sample_names_are_unique( + self, samples: list[MetagenomeSample], customer_id: str + ) -> None: + """Validate that the names of all samples are unused.""" + customer: Customer = self.status_db.get_customer_by_internal_id( + customer_internal_id=customer_id + ) + for sample in samples: + if sample.control: + continue + if self.status_db.get_sample_by_customer_and_name( + customer_entry_id=[customer.id], sample_name=sample.name + ): + raise OrderError(f"Sample name {sample.name} already in use") diff --git a/cg/meta/orders/sars_cov_2_submitter.py b/cg/services/orders/validate_order_services/validate_microbial_order.py similarity index 55% rename from cg/meta/orders/sars_cov_2_submitter.py rename to cg/services/orders/validate_order_services/validate_microbial_order.py index 2abdea82a9..82b3acff24 100644 --- a/cg/meta/orders/sars_cov_2_submitter.py +++ b/cg/services/orders/validate_order_services/validate_microbial_order.py @@ -1,29 +1,34 @@ from cg.exc import OrderError -from cg.meta.orders.microbial_submitter import MicrobialSubmitter +from cg.models.orders.constants import OrderType from cg.models.orders.order import OrderIn from cg.models.orders.samples import SarsCov2Sample +from cg.services.orders.submitters.order_submitter import ValidateOrderService from cg.store.models import Customer +from cg.store.store import Store -class SarsCov2Submitter(MicrobialSubmitter): - """Class for validating Sars-Cov submissions.""" +class ValidateMicrobialOrderService(ValidateOrderService): + + def __init__(self, status_db: Store): + self.status_db = status_db def validate_order(self, order: OrderIn) -> None: - """Validate order sample names.""" - super().validate_order(order=order) - self._validate_sample_names_are_available(samples=order.samples, customer_id=order.customer) + if order.order_type == OrderType.SARS_COV_2: + self._validate_sample_names_are_available( + samples=order.samples, customer_id=order.customer + ) def _validate_sample_names_are_available( self, samples: list[SarsCov2Sample], customer_id: str ) -> None: """Validate names of all samples are not already in use.""" - customer: Customer = self.status.get_customer_by_internal_id( + customer: Customer = self.status_db.get_customer_by_internal_id( customer_internal_id=customer_id ) for sample in samples: if sample.control: continue - if self.status.get_sample_by_customer_and_name( + if self.status_db.get_sample_by_customer_and_name( customer_entry_id=[customer.id], sample_name=sample.name ): raise OrderError( diff --git a/cg/services/orders/validate_order_services/validate_pool_order.py b/cg/services/orders/validate_order_services/validate_pool_order.py new file mode 100644 index 0000000000..4206a16e29 --- /dev/null +++ b/cg/services/orders/validate_order_services/validate_pool_order.py @@ -0,0 +1,35 @@ +from cg.exc import OrderError +from cg.models.orders.order import OrderIn +from cg.models.orders.samples import RmlSample +from cg.services.orders.submitters.order_submitter import ValidateOrderService +from cg.store.models import Customer +from cg.store.store import Store + + +class ValidatePoolOrderService(ValidateOrderService): + + def __init__(self, status_db: Store): + self.status_db = status_db + + def validate_order(self, order: OrderIn) -> None: + self._validate_case_names_are_available( + customer_id=order.customer, samples=order.samples, ticket=order.ticket + ) + + def _validate_case_names_are_available( + self, customer_id: str, samples: list[RmlSample], ticket: str + ): + """Validate names of all samples are not already in use.""" + customer: Customer = self.status_db.get_customer_by_internal_id( + customer_internal_id=customer_id + ) + for sample in samples: + case_name: str = self.create_case_name(pool_name=sample.pool, ticket=ticket) + if self.status_db.get_case_by_name_and_customer(customer=customer, case_name=case_name): + raise OrderError( + f"Case name {case_name} already in use for customer {customer.name}" + ) + + @staticmethod + def create_case_name(ticket: str, pool_name: str) -> str: + return f"{ticket}-{pool_name}" diff --git a/tests/conftest.py b/tests/conftest.py index 377c6bdce9..b3b8453f1b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -118,6 +118,8 @@ "tests.fixture_plugins.quality_controller_fixtures.sequencing_qc_check_scenario", "tests.fixture_plugins.quality_controller_fixtures.sequencing_qc_fixtures", "tests.fixture_plugins.timestamp_fixtures", + "tests.fixture_plugins.orders_fixtures.order_form_fixtures", + "tests.fixture_plugins.orders_fixtures.order_store_service_fixtures", ] diff --git a/tests/apps/orderform/conftest.py b/tests/fixture_plugins/orders_fixtures/order_form_fixtures.py similarity index 70% rename from tests/apps/orderform/conftest.py rename to tests/fixture_plugins/orders_fixtures/order_form_fixtures.py index c683d6146f..b0359b9856 100644 --- a/tests/apps/orderform/conftest.py +++ b/tests/fixture_plugins/orders_fixtures/order_form_fixtures.py @@ -12,6 +12,14 @@ from cg.constants.orderforms import Orderform from cg.io.controller import ReadFile from cg.models.orders.constants import OrderType +from cg.models.orders.order import OrderIn +from cg.services.orders.store_order_services.store_fastq_order_service import StoreFastqOrderService +from cg.services.orders.store_order_services.store_case_order import StoreCaseOrderService +from cg.services.orders.store_order_services.store_metagenome_order import ( + StoreMetagenomeOrderService, +) +from cg.services.orders.store_order_services.store_microbial_order import StoreMicrobialOrderService +from cg.services.orders.store_order_services.store_pool_order import StorePoolOrderService def get_nr_samples_excel(orderform_path: str) -> int: @@ -360,3 +368,119 @@ def json_order_list( OrderType.FLUFFY: fluffy_uploaded_json_order, OrderType.BALSAMIC: balsamic_uploaded_json_order, } + + +@pytest.fixture(scope="session") +def all_orders_to_submit( + balsamic_order_to_submit: dict, + fastq_order_to_submit: dict, + metagenome_order_to_submit: dict, + microbial_order_to_submit: dict, + mip_order_to_submit: dict, + mip_rna_order_to_submit: dict, + rml_order_to_submit: dict, + rnafusion_order_to_submit: dict, + sarscov2_order_to_submit: dict, +) -> dict: + """Returns a dict of parsed order for each order type.""" + return { + OrderType.BALSAMIC: OrderIn.parse_obj(balsamic_order_to_submit, project=OrderType.BALSAMIC), + OrderType.FASTQ: OrderIn.parse_obj(fastq_order_to_submit, project=OrderType.FASTQ), + OrderType.FLUFFY: OrderIn.parse_obj(rml_order_to_submit, project=OrderType.FLUFFY), + OrderType.METAGENOME: OrderIn.parse_obj( + metagenome_order_to_submit, project=OrderType.METAGENOME + ), + OrderType.MICROSALT: OrderIn.parse_obj( + microbial_order_to_submit, project=OrderType.MICROSALT + ), + OrderType.MIP_DNA: OrderIn.parse_obj(mip_order_to_submit, project=OrderType.MIP_DNA), + OrderType.MIP_RNA: OrderIn.parse_obj(mip_rna_order_to_submit, project=OrderType.MIP_RNA), + OrderType.RML: OrderIn.parse_obj(rml_order_to_submit, project=OrderType.RML), + OrderType.RNAFUSION: OrderIn.parse_obj( + rnafusion_order_to_submit, project=OrderType.RNAFUSION + ), + OrderType.SARS_COV_2: OrderIn.parse_obj( + sarscov2_order_to_submit, project=OrderType.SARS_COV_2 + ), + } + + +@pytest.fixture +def balsamic_status_data( + balsamic_order_to_submit: dict, store_generic_order_service: StoreCaseOrderService +) -> dict: + """Parse balsamic order example.""" + project: OrderType = OrderType.BALSAMIC + order: OrderIn = OrderIn.parse_obj(obj=balsamic_order_to_submit, project=project) + return store_generic_order_service.order_to_status(order=order) + + +@pytest.fixture +def fastq_status_data( + fastq_order_to_submit, store_fastq_order_service: StoreFastqOrderService +) -> dict: + """Parse fastq order example.""" + project: OrderType = OrderType.FASTQ + order: OrderIn = OrderIn.parse_obj(obj=fastq_order_to_submit, project=project) + return store_fastq_order_service.order_to_status(order=order) + + +@pytest.fixture +def metagenome_status_data( + metagenome_order_to_submit: dict, store_metagenome_order_service: StoreMetagenomeOrderService +) -> dict: + """Parse metagenome order example.""" + project: OrderType = OrderType.METAGENOME + order: OrderIn = OrderIn.parse_obj(obj=metagenome_order_to_submit, project=project) + + return store_metagenome_order_service.order_to_status(order=order) + + +@pytest.fixture +def microbial_status_data( + microbial_order_to_submit: dict, store_microbial_order_service: StoreMicrobialOrderService +) -> dict: + """Parse microbial order example.""" + project: OrderType = OrderType.MICROSALT + order: OrderIn = OrderIn.parse_obj(obj=microbial_order_to_submit, project=project) + return store_microbial_order_service.order_to_status(order=order) + + +@pytest.fixture +def mip_rna_status_data( + mip_rna_order_to_submit: dict, store_generic_order_service: StoreCaseOrderService +) -> dict: + """Parse rna order example.""" + project: OrderType = OrderType.MIP_RNA + order: OrderIn = OrderIn.parse_obj(obj=mip_rna_order_to_submit, project=project) + return store_generic_order_service.order_to_status(order=order) + + +@pytest.fixture +def mip_status_data( + mip_order_to_submit: dict, store_generic_order_service: StoreCaseOrderService +) -> dict: + """Parse scout order example.""" + project: OrderType = OrderType.MIP_DNA + order: OrderIn = OrderIn.parse_obj(obj=mip_order_to_submit, project=project) + return store_generic_order_service.order_to_status(order=order) + + +@pytest.fixture +def rml_status_data( + rml_order_to_submit: dict, store_pool_order_service: StorePoolOrderService +) -> dict: + """Parse rml order example.""" + project: OrderType = OrderType.RML + order: OrderIn = OrderIn.parse_obj(obj=rml_order_to_submit, project=project) + return store_pool_order_service.order_to_status(order=order) + + +@pytest.fixture +def tomte_status_data( + tomte_order_to_submit: dict, store_generic_order_service: StoreCaseOrderService +) -> dict: + """Parse TOMTE order example.""" + project: OrderType = OrderType.TOMTE + order: OrderIn = OrderIn.parse_obj(obj=tomte_order_to_submit, project=project) + return store_generic_order_service.order_to_status(order=order) diff --git a/tests/fixture_plugins/orders_fixtures/order_store_service_fixtures.py b/tests/fixture_plugins/orders_fixtures/order_store_service_fixtures.py new file mode 100644 index 0000000000..b4e0460458 --- /dev/null +++ b/tests/fixture_plugins/orders_fixtures/order_store_service_fixtures.py @@ -0,0 +1,43 @@ +import pytest + +from cg.services.orders.order_lims_service.order_lims_service import OrderLimsService +from cg.services.orders.store_order_services.store_fastq_order_service import StoreFastqOrderService +from cg.services.orders.store_order_services.store_case_order import StoreCaseOrderService +from cg.services.orders.store_order_services.store_metagenome_order import ( + StoreMetagenomeOrderService, +) +from cg.services.orders.store_order_services.store_microbial_order import StoreMicrobialOrderService +from cg.services.orders.store_order_services.store_pool_order import StorePoolOrderService +from cg.store.store import Store +from tests.mocks.limsmock import MockLimsAPI + + +@pytest.fixture +def store_generic_order_service(base_store: Store, lims_api: MockLimsAPI) -> StoreCaseOrderService: + return StoreCaseOrderService(status_db=base_store, lims_service=OrderLimsService(lims_api)) + + +@pytest.fixture +def store_pool_order_service(base_store: Store, lims_api: MockLimsAPI) -> StorePoolOrderService: + return StorePoolOrderService(status_db=base_store, lims_service=OrderLimsService(lims_api)) + + +@pytest.fixture +def store_fastq_order_service(base_store: Store, lims_api: MockLimsAPI) -> StoreFastqOrderService: + return StoreFastqOrderService(status_db=base_store, lims_service=OrderLimsService(lims_api)) + + +@pytest.fixture +def store_metagenome_order_service( + base_store: Store, lims_api: MockLimsAPI +) -> StoreMetagenomeOrderService: + return StoreMetagenomeOrderService( + status_db=base_store, lims_service=OrderLimsService(lims_api) + ) + + +@pytest.fixture +def store_microbial_order_service( + base_store: Store, lims_api: MockLimsAPI +) -> StoreMicrobialOrderService: + return StoreMicrobialOrderService(status_db=base_store, lims_service=OrderLimsService(lims_api)) diff --git a/tests/meta/orders/conftest.py b/tests/meta/orders/conftest.py index e701814300..24add743eb 100644 --- a/tests/meta/orders/conftest.py +++ b/tests/meta/orders/conftest.py @@ -3,138 +3,36 @@ import pytest from cg.meta.orders import OrdersAPI -from cg.meta.orders.api import FastqSubmitter -from cg.meta.orders.balsamic_submitter import BalsamicSubmitter -from cg.meta.orders.metagenome_submitter import MetagenomeSubmitter -from cg.meta.orders.microbial_submitter import MicrobialSubmitter -from cg.meta.orders.mip_dna_submitter import MipDnaSubmitter -from cg.meta.orders.mip_rna_submitter import MipRnaSubmitter -from cg.meta.orders.rml_submitter import RmlSubmitter from cg.meta.orders.ticket_handler import TicketHandler -from cg.meta.orders.tomte_submitter import TomteSubmitter -from cg.models.orders.order import OrderIn, OrderType -from cg.store.store import Store -from tests.apps.orderform.conftest import ( - balsamic_order_to_submit, - fastq_order_to_submit, - metagenome_order_to_submit, - microbial_order_to_submit, - mip_order_to_submit, - mip_rna_order_to_submit, - rml_order_to_submit, - rnafusion_order_to_submit, - sarscov2_order_to_submit, - tomte_order_to_submit, +from cg.services.orders.submitters.order_submitter_registry import ( + OrderSubmitterRegistry, + setup_order_submitter_registry, ) +from cg.store.store import Store from tests.mocks.limsmock import MockLimsAPI from tests.mocks.osticket import MockOsTicket -@pytest.fixture(scope="session") -def all_orders_to_submit( - balsamic_order_to_submit: dict, - fastq_order_to_submit: dict, - metagenome_order_to_submit: dict, - microbial_order_to_submit: dict, - mip_order_to_submit: dict, - mip_rna_order_to_submit: dict, - rml_order_to_submit: dict, - rnafusion_order_to_submit: dict, - sarscov2_order_to_submit: dict, -) -> dict: - """Returns a dict of parsed order for each order type.""" - return { - OrderType.BALSAMIC: OrderIn.parse_obj(balsamic_order_to_submit, project=OrderType.BALSAMIC), - OrderType.FASTQ: OrderIn.parse_obj(fastq_order_to_submit, project=OrderType.FASTQ), - OrderType.FLUFFY: OrderIn.parse_obj(rml_order_to_submit, project=OrderType.FLUFFY), - OrderType.METAGENOME: OrderIn.parse_obj( - metagenome_order_to_submit, project=OrderType.METAGENOME - ), - OrderType.MICROSALT: OrderIn.parse_obj( - microbial_order_to_submit, project=OrderType.MICROSALT - ), - OrderType.MIP_DNA: OrderIn.parse_obj(mip_order_to_submit, project=OrderType.MIP_DNA), - OrderType.MIP_RNA: OrderIn.parse_obj(mip_rna_order_to_submit, project=OrderType.MIP_RNA), - OrderType.RML: OrderIn.parse_obj(rml_order_to_submit, project=OrderType.RML), - OrderType.RNAFUSION: OrderIn.parse_obj( - rnafusion_order_to_submit, project=OrderType.RNAFUSION - ), - OrderType.SARS_COV_2: OrderIn.parse_obj( - sarscov2_order_to_submit, project=OrderType.SARS_COV_2 - ), - } - - -@pytest.fixture -def balsamic_status_data(balsamic_order_to_submit: dict): - """Parse balsamic order example.""" - project: OrderType = OrderType.BALSAMIC - order: OrderIn = OrderIn.parse_obj(obj=balsamic_order_to_submit, project=project) - return BalsamicSubmitter.order_to_status(order=order) - - -@pytest.fixture -def fastq_status_data(fastq_order_to_submit): - """Parse fastq order example.""" - project: OrderType = OrderType.FASTQ - order: OrderIn = OrderIn.parse_obj(obj=fastq_order_to_submit, project=project) - return FastqSubmitter.order_to_status(order=order) - - -@pytest.fixture -def metagenome_status_data(metagenome_order_to_submit: dict): - """Parse metagenome order example.""" - project: OrderType = OrderType.METAGENOME - order: OrderIn = OrderIn.parse_obj(obj=metagenome_order_to_submit, project=project) - - return MetagenomeSubmitter.order_to_status(order=order) - - -@pytest.fixture -def microbial_status_data(microbial_order_to_submit: dict): - """Parse microbial order example.""" - project: OrderType = OrderType.MICROSALT - order: OrderIn = OrderIn.parse_obj(obj=microbial_order_to_submit, project=project) - return MicrobialSubmitter.order_to_status(order=order) - - -@pytest.fixture -def mip_rna_status_data(mip_rna_order_to_submit: dict): - """Parse rna order example.""" - project: OrderType = OrderType.MIP_RNA - order: OrderIn = OrderIn.parse_obj(obj=mip_rna_order_to_submit, project=project) - return MipRnaSubmitter.order_to_status(order=order) - - -@pytest.fixture -def mip_status_data(mip_order_to_submit: dict): - """Parse scout order example.""" - project: OrderType = OrderType.MIP_DNA - order: OrderIn = OrderIn.parse_obj(obj=mip_order_to_submit, project=project) - return MipDnaSubmitter.order_to_status(order=order) - - -@pytest.fixture -def rml_status_data(rml_order_to_submit): - """Parse rml order example.""" - project: OrderType = OrderType.RML - order: OrderIn = OrderIn.parse_obj(obj=rml_order_to_submit, project=project) - return RmlSubmitter.order_to_status(order=order) - - -@pytest.fixture -def tomte_status_data(tomte_order_to_submit: dict): - """Parse TOMTE order example.""" - project: OrderType = OrderType.TOMTE - order: OrderIn = OrderIn.parse_obj(obj=tomte_order_to_submit, project=project) - return TomteSubmitter.order_to_status(order=order) - - @pytest.fixture(scope="function") -def orders_api(base_store, osticket: MockOsTicket, lims_api: MockLimsAPI): - return OrdersAPI(lims=lims_api, status=base_store, osticket=osticket) +def orders_api( + base_store, + osticket: MockOsTicket, + lims_api: MockLimsAPI, + order_submitter_registry: OrderSubmitterRegistry, +) -> OrdersAPI: + return OrdersAPI( + lims=lims_api, + status=base_store, + osticket=osticket, + submitter_registry=order_submitter_registry, + ) @pytest.fixture def ticket_handler(store: Store, osticket: MockOsTicket) -> TicketHandler: return TicketHandler(status_db=store, osticket_api=osticket) + + +@pytest.fixture +def order_submitter_registry(base_store: Store, lims_api: MockLimsAPI) -> OrderSubmitterRegistry: + return setup_order_submitter_registry(lims=lims_api, status_db=base_store) diff --git a/tests/meta/orders/test_SarsCov2Submitter_order_to_status.py b/tests/meta/orders/test_SarsCov2Submitter_order_to_status.py deleted file mode 100644 index 646144818b..0000000000 --- a/tests/meta/orders/test_SarsCov2Submitter_order_to_status.py +++ /dev/null @@ -1,49 +0,0 @@ -from cg.meta.orders.sars_cov_2_submitter import SarsCov2Submitter -from cg.models.orders.constants import OrderType -from cg.models.orders.order import OrderIn -from cg.models.orders.sample_base import ControlEnum -from cg.models.orders.samples import SarsCov2Sample -from cg.store.store import Store - - -def test_order_to_status_control_exists(sarscov2_order_to_submit: dict, base_store: Store): - # GIVEN sarscov2 order with three samples - order: OrderIn = OrderIn.parse_obj(sarscov2_order_to_submit, OrderType.SARS_COV_2) - - # WHEN transforming order to status structure - result: dict = SarsCov2Submitter.order_to_status(order=order) - - # THEN check that control is in the result - sample: dict - for sample in result.get("samples"): - assert "control" in sample - - -def test_order_to_status_control_has_input_value(sarscov2_order_to_submit: dict, base_store: Store): - # GIVEN sarscov2 order with three samples with control value set - control_value = ControlEnum.positive - order: OrderIn = OrderIn.parse_obj(sarscov2_order_to_submit, OrderType.SARS_COV_2) - sample: SarsCov2Sample - for sample in order.samples: - sample.control: ControlEnum = control_value - - # WHEN transforming order to status structure - result: dict = SarsCov2Submitter.order_to_status(order=order) - - # THEN check that control is in the result - sample: dict - for sample in result.get("samples"): - assert control_value in sample.get("control") - - -def test_mutant_sample_generates_fields(sarscov2_order_to_submit: dict, base_store: Store): - """Tests that Mutant orders with region and original_lab set can generate region_code and original_lab_address.""" - # GIVEN sarscov2 order with six samples, one without region_code and original_lab_address - - # WHEN parsing the order - order: OrderIn = OrderIn.parse_obj(sarscov2_order_to_submit, OrderType.SARS_COV_2) - - # THEN all samples should have region_code and original_lab_address set - for sample in order.samples: - assert sample.region_code - assert sample.original_lab_address diff --git a/tests/meta/orders/test_SarsCov2Submitter_store_order.py b/tests/meta/orders/test_SarsCov2Submitter_store_order.py deleted file mode 100644 index 6905c29097..0000000000 --- a/tests/meta/orders/test_SarsCov2Submitter_store_order.py +++ /dev/null @@ -1,45 +0,0 @@ -import datetime as dt - -from cg.constants import DataDelivery -from cg.constants.constants import Workflow -from cg.meta.orders.sars_cov_2_submitter import SarsCov2Submitter -from cg.models.orders.constants import OrderType -from cg.models.orders.order import OrderIn -from cg.models.orders.sample_base import ControlEnum -from cg.models.orders.samples import SarsCov2Sample -from cg.store.models import Customer, Sample -from cg.store.store import Store - - -def test_store_items_in_status_control_has_stored_value( - sarscov2_order_to_submit: dict, base_store: Store -): - # GIVEN sarscov2 order with three samples with control value - order: OrderIn = OrderIn.parse_obj(sarscov2_order_to_submit, OrderType.SARS_COV_2) - control_value = ControlEnum.positive - sample: SarsCov2Sample - for sample in order.samples: - sample.control: ControlEnum = control_value - submitter: SarsCov2Submitter = SarsCov2Submitter(status=base_store, lims=None) - status_data = submitter.order_to_status(order=order) - - # WHEN storing the order - submitter.store_items_in_status( - comment="", - customer_id=order.customer, - data_analysis=Workflow.MUTANT, - data_delivery=DataDelivery.FASTQ, - order="", - ordered=dt.datetime.now(), - ticket_id=123456, - items=status_data.get("samples"), - ) - - # THEN control should exist on the sample in the store - customer: Customer = base_store.get_customer_by_internal_id(customer_internal_id=order.customer) - sample: SarsCov2Sample - for sample in order.samples: - stored_sample: Sample = base_store.get_sample_by_customer_and_name( - customer_entry_id=[customer.id], sample_name=sample.name - ) - assert stored_sample.control == control_value diff --git a/tests/meta/orders/test_meta_orders_api.py b/tests/meta/orders/test_meta_orders_api.py index 0e22eb3525..885753ef03 100644 --- a/tests/meta/orders/test_meta_orders_api.py +++ b/tests/meta/orders/test_meta_orders_api.py @@ -1,5 +1,5 @@ import datetime as dt -from unittest.mock import patch +from unittest.mock import patch, Mock import pytest @@ -8,20 +8,23 @@ from cg.constants.subject import Sex from cg.exc import OrderError, TicketCreationError from cg.meta.orders import OrdersAPI -from cg.meta.orders.mip_dna_submitter import MipDnaSubmitter from cg.models.orders.order import OrderIn, OrderType from cg.models.orders.samples import MipDnaSample +from cg.services.orders.validate_order_services.validate_case_order import ( + ValidateCaseOrderService, +) from cg.store.models import Case, Customer, Pool, Sample from cg.store.store import Store from tests.store_helpers import StoreHelpers -SUBMITTERS = [ - "fastq_submitter", - "metagenome_submitter", - "microbial_submitter", - "case_submitter", - "pool_submitter", -] + +def monkeypatch_process_lims(monkeypatch, order_data) -> None: + lims_project_data = {"id": "ADM1234", "date": dt.datetime.now()} + lims_map = {sample.name: f"ELH123A{index}" for index, sample in enumerate(order_data.samples)} + monkeypatch.setattr( + "cg.services.orders.order_lims_service.order_lims_service.OrderLimsService.process_lims", + lambda *args, **kwargs: (lims_project_data, lims_map), + ) def test_too_long_order_name(): @@ -85,16 +88,6 @@ def test_submit( assert link_obj.sample.original_ticket == ticket_id -def monkeypatch_process_lims(monkeypatch, order_data): - lims_project_data = {"id": "ADM1234", "date": dt.datetime.now()} - lims_map = {sample.name: f"ELH123A{index}" for index, sample in enumerate(order_data.samples)} - for submitter in SUBMITTERS: - monkeypatch.setattr( - f"cg.meta.orders.{submitter}.process_lims", - lambda **kwargs: (lims_project_data, lims_map), - ) - - @pytest.mark.parametrize( "order_type", [OrderType.MIP_DNA, OrderType.MIP_RNA, OrderType.BALSAMIC], @@ -139,7 +132,7 @@ def test_submit_illegal_sample_customer( ): order_data = OrderIn.parse_obj(obj=all_orders_to_submit[order_type], project=order_type) monkeypatch_process_lims(monkeypatch, order_data) - + orders_api.ticket_handler = Mock() # GIVEN we have an order with a customer that is not in the same customer group as customer # that the samples originate from new_customer = sample_store.add_customer( @@ -185,6 +178,7 @@ def test_submit_scout_legal_sample_customer( ): order_data = OrderIn.parse_obj(obj=all_orders_to_submit[order_type], project=order_type) monkeypatch_process_lims(monkeypatch, order_data) + orders_api.ticket_handler = Mock() # GIVEN we have an order with a customer that is in the same customer group as customer # that the samples originate from collaboration = sample_store.add_collaboration("customer999only", "customer 999 only group") @@ -240,7 +234,7 @@ def test_submit_duplicate_sample_case_name( order_data = OrderIn.parse_obj(obj=all_orders_to_submit[order_type], project=order_type) store = orders_api.status customer: Customer = store.get_customer_by_internal_id(customer_internal_id=order_data.customer) - + orders_api.ticket_handler = Mock() for sample in order_data.samples: case_id = sample.family_name if not store.get_case_by_name_and_customer(customer=customer, case_name=case_id): @@ -358,12 +352,12 @@ def test_validate_sex_inconsistent_sex( store.session.commit() assert sample_obj.sex != sample.sex - submitter: MipDnaSubmitter = MipDnaSubmitter(lims=orders_api.lims, status=orders_api.status) + validator = ValidateCaseOrderService(status_db=orders_api.status) # WHEN calling _validate_sex # THEN an OrderError should be raised on non-matching sex with pytest.raises(OrderError): - submitter._validate_subject_sex(samples=order_data.samples, customer_id=order_data.customer) + validator._validate_subject_sex(samples=order_data.samples, customer_id=order_data.customer) def test_validate_sex_consistent_sex( @@ -388,10 +382,10 @@ def test_validate_sex_consistent_sex( store.session.commit() assert sample_obj.sex == sample.sex - submitter: MipDnaSubmitter = MipDnaSubmitter(lims=orders_api.lims, status=orders_api.status) + validator = ValidateCaseOrderService(status_db=orders_api.status) # WHEN calling _validate_sex - submitter._validate_subject_sex(samples=order_data.samples, customer_id=order_data.customer) + validator._validate_subject_sex(samples=order_data.samples, customer_id=order_data.customer) # THEN no OrderError should be raised on non-matching sex @@ -419,10 +413,10 @@ def test_validate_sex_unknown_existing_sex( store.session.commit() assert sample_obj.sex != sample.sex - submitter: MipDnaSubmitter = MipDnaSubmitter(lims=orders_api.lims, status=orders_api.status) + validator = ValidateCaseOrderService(status_db=orders_api.status) # WHEN calling _validate_sex - submitter._validate_subject_sex(samples=order_data.samples, customer_id=order_data.customer) + validator._validate_subject_sex(samples=order_data.samples, customer_id=order_data.customer) # THEN no OrderError should be raised on non-matching sex @@ -452,10 +446,10 @@ def test_validate_sex_unknown_new_sex( for sample in order_data.samples: assert sample_obj.sex != sample.sex - submitter: MipDnaSubmitter = MipDnaSubmitter(lims=orders_api.lims, status=orders_api.status) + validator = ValidateCaseOrderService(status_db=orders_api.status) # WHEN calling _validate_sex - submitter._validate_subject_sex(samples=order_data.samples, customer_id=order_data.customer) + validator._validate_subject_sex(samples=order_data.samples, customer_id=order_data.customer) # THEN no OrderError should be raised on non-matching sex diff --git a/tests/meta/orders/test_meta_orders_status.py b/tests/meta/orders/test_meta_orders_status.py deleted file mode 100644 index fc62f135aa..0000000000 --- a/tests/meta/orders/test_meta_orders_status.py +++ /dev/null @@ -1,743 +0,0 @@ -import datetime as dt -from copy import deepcopy - -import pytest - -from cg.constants import DataDelivery, Priority, Workflow -from cg.constants.constants import CaseActions, PrepCategory -from cg.exc import OrderError -from cg.meta.orders import OrdersAPI -from cg.meta.orders.api import FastqSubmitter -from cg.meta.orders.balsamic_qc_submitter import BalsamicQCSubmitter -from cg.meta.orders.balsamic_submitter import BalsamicSubmitter -from cg.meta.orders.balsamic_umi_submitter import BalsamicUmiSubmitter -from cg.meta.orders.metagenome_submitter import MetagenomeSubmitter -from cg.meta.orders.microbial_submitter import MicrobialSubmitter -from cg.meta.orders.mip_dna_submitter import MipDnaSubmitter -from cg.meta.orders.mip_rna_submitter import MipRnaSubmitter -from cg.meta.orders.rml_submitter import RmlSubmitter -from cg.meta.orders.sars_cov_2_submitter import SarsCov2Submitter -from cg.meta.orders.submitter import Submitter -from cg.models.orders.order import OrderIn, OrderType -from cg.store.models import Application, Case, Pool, Sample -from cg.store.store import Store - - -def test_pools_to_status(rml_order_to_submit): - # GIVEN a rml order with three samples in one pool - order = OrderIn.parse_obj(rml_order_to_submit, OrderType.RML) - - # WHEN parsing for status - data = RmlSubmitter.order_to_status(order=order) - - # THEN it should pick out the general information - assert data["customer"] == "cust000" - assert data["order"] == "#123456" - assert data["comment"] == "order comment" - - # ... and information about the pool(s) - assert len(data["pools"]) == 2 - pool = data["pools"][0] - assert pool["name"] == "pool-1" - assert pool["application"] == "RMLP05R800" - assert pool["data_analysis"] == Workflow.FASTQ - assert pool["data_delivery"] == str(DataDelivery.FASTQ) - assert len(pool["samples"]) == 2 - sample = pool["samples"][0] - assert sample["name"] == "sample1" - assert sample["comment"] == "test comment" - assert pool["priority"] == "research" - assert sample["control"] == "negative" - - -def test_samples_to_status(fastq_order_to_submit): - # GIVEN fastq order with two samples - order = OrderIn.parse_obj(fastq_order_to_submit, OrderType.FASTQ) - - # WHEN parsing for status - data = FastqSubmitter.order_to_status(order=order) - - # THEN it should pick out samples and relevant information - assert len(data["samples"]) == 2 - first_sample = data["samples"][0] - assert first_sample["name"] == "prov1" - assert first_sample["application"] == "WGSPCFC060" - assert first_sample["priority"] == "priority" - assert first_sample["tumour"] is False - assert first_sample["volume"] == "1" - - # ... and the other sample is a tumour - assert data["samples"][1]["tumour"] is True - - -def test_metagenome_to_status(metagenome_order_to_submit): - # GIVEN metagenome order with two samples - order = OrderIn.parse_obj(metagenome_order_to_submit, OrderType.METAGENOME) - - # WHEN parsing for status - data = MetagenomeSubmitter.order_to_status(order=order) - case = data["families"][0] - # THEN it should pick out samples and relevant information - assert len(case["samples"]) == 2 - first_sample = case["samples"][0] - assert first_sample["name"] == "Bristol" - assert first_sample["application"] == "METLIFR020" - assert first_sample["priority"] == "standard" - assert first_sample["volume"] == "1.0" - - -def test_microbial_samples_to_status(microbial_order_to_submit): - # GIVEN microbial order with three samples - order = OrderIn.parse_obj(microbial_order_to_submit, OrderType.MICROSALT) - - # WHEN parsing for status - data = MicrobialSubmitter.order_to_status(order=order) - - # THEN it should pick out samples and relevant information - assert len(data["samples"]) == 5 - assert data["customer"] == "cust002" - assert data["order"] == "Microbial samples" - assert data["comment"] == "Order comment" - assert data["data_analysis"] == Workflow.MICROSALT - assert data["data_delivery"] == str(DataDelivery.FASTQ) - - # THEN first sample should contain all the relevant data from the microbial order - sample_data = data["samples"][0] - assert sample_data["priority"] == "research" - assert sample_data["name"] == "all-fields" - assert sample_data.get("internal_id") is None - assert sample_data["organism_id"] == "M.upium" - assert sample_data["reference_genome"] == "NC_111" - assert sample_data["application"] == "MWRNXTR003" - assert sample_data["comment"] == "plate comment" - assert sample_data["volume"] == "1" - - -def test_sarscov2_samples_to_status(sarscov2_order_to_submit): - # GIVEN sarscov2 order with three samples - order = OrderIn.parse_obj(sarscov2_order_to_submit, OrderType.SARS_COV_2) - - # WHEN parsing for status - data = SarsCov2Submitter.order_to_status(order=order) - - # THEN it should pick out samples and relevant information - assert len(data["samples"]) == 6 - assert data["customer"] == "cust002" - assert data["order"] == "Sars-CoV-2 samples" - assert data["comment"] == "Order comment" - assert data["data_analysis"] == Workflow.MUTANT - assert data["data_delivery"] == str(DataDelivery.FASTQ) - - # THEN first sample should contain all the relevant data from the microbial order - sample_data = data["samples"][0] - assert sample_data.get("internal_id") is None - assert sample_data["priority"] == "research" - assert sample_data["application"] == "VWGDPTR001" - assert sample_data["comment"] == "plate comment" - assert sample_data["name"] == "all-fields" - assert sample_data["organism_id"] == "SARS CoV-2" - assert sample_data["reference_genome"] == "NC_111" - assert sample_data["volume"] == "1" - - -def test_cases_to_status(mip_order_to_submit): - # GIVEN a scout order with a trio case - project: OrderType = OrderType.MIP_DNA - order = OrderIn.parse_obj(mip_order_to_submit, project=project) - - # WHEN parsing for status - data = MipDnaSubmitter.order_to_status(order=order) - - # THEN it should pick out the case - assert len(data["families"]) == 2 - family = data["families"][0] - assert family["name"] == "family1" - assert family["data_analysis"] == Workflow.MIP_DNA - assert family["data_delivery"] == str(DataDelivery.SCOUT) - assert family["priority"] == Priority.standard.name - assert family["cohorts"] == ["Other"] - assert ( - family["synopsis"] - == "As for the synopsis it will be this overly complex sentence to prove that the synopsis field might in fact be a very long string, which we should be prepared for." - ) - assert set(family["panels"]) == {"IEM"} - assert len(family["samples"]) == 3 - - first_sample = family["samples"][0] - assert first_sample["age_at_sampling"] == 17.18192 - assert first_sample["name"] == "sample1" - assert first_sample["application"] == "WGSPCFC030" - assert first_sample["phenotype_groups"] == ["Phenotype-group"] - assert first_sample["phenotype_terms"] == ["HP:0012747", "HP:0025049"] - assert first_sample["sex"] == "female" - assert first_sample["status"] == "affected" - assert first_sample["subject_id"] == "subject1" - assert first_sample["mother"] == "sample2" - assert first_sample["father"] == "sample3" - - # ... second sample has a comment - assert isinstance(family["samples"][1]["comment"], str) - - -def test_cases_to_status_synopsis(mip_order_to_submit): - # GIVEN a scout order with a trio case where synopsis is None - modified_order: dict = deepcopy(mip_order_to_submit) - for sample in modified_order["samples"]: - sample["synopsis"] = None - - project: OrderType = OrderType.MIP_DNA - order = OrderIn.parse_obj(mip_order_to_submit, project=project) - - # WHEN parsing for status - MipDnaSubmitter.order_to_status(order=order) - - # THEN No exception should have been raised on synopsis - - -def test_store_rml(orders_api, base_store, rml_status_data, ticket_id: str): - # GIVEN a basic store with no samples and a rml order - assert base_store._get_query(table=Pool).count() == 0 - assert base_store._get_query(table=Case).count() == 0 - assert not base_store._get_query(table=Sample).first() - - submitter: RmlSubmitter = RmlSubmitter(lims=orders_api.lims, status=orders_api.status) - - # WHEN storing the order - new_pools = submitter.store_items_in_status( - customer_id=rml_status_data["customer"], - order=rml_status_data["order"], - ordered=dt.datetime.now(), - ticket_id=ticket_id, - items=rml_status_data["pools"], - ) - - # THEN it should update the database with new pools - assert len(new_pools) == 2 - - assert base_store._get_query(table=Pool).count() == base_store._get_query(table=Case).count() - assert len(base_store._get_query(table=Sample).all()) == 4 - - # ASSERT that there is one negative sample - negative_samples = 0 - for sample in base_store._get_query(table=Sample).all(): - if sample.control == "negative": - negative_samples += 1 - assert negative_samples == 1 - - new_pool = base_store._get_query(table=Pool).order_by(Pool.created_at.desc()).first() - assert new_pool == new_pools[1] - - assert new_pool.name == "pool-2" - assert new_pool.application_version.application.tag == "RMLP05R800" - assert not hasattr(new_pool, "data_analysis") - - new_case = base_store.get_cases()[0] - assert new_case.data_analysis == Workflow.FASTQ - assert new_case.data_delivery == str(DataDelivery.FASTQ) - - # and that the pool is set for invoicing but not the samples of the pool - assert not new_pool.no_invoice - for link in new_case.links: - assert link.sample.no_invoice - - -def test_store_samples(orders_api, base_store, fastq_status_data, ticket_id: str): - # GIVEN a basic store with no samples and a fastq order - assert not base_store._get_query(table=Sample).first() - assert base_store._get_query(table=Case).count() == 0 - - submitter: FastqSubmitter = FastqSubmitter(lims=orders_api.lims, status=orders_api.status) - - # WHEN storing the order - new_samples = submitter.store_items_in_status( - customer_id=fastq_status_data["customer"], - order=fastq_status_data["order"], - ordered=dt.datetime.now(), - ticket_id=ticket_id, - items=fastq_status_data["samples"], - ) - - # THEN it should store the samples and create a case for each sample - assert len(new_samples) == 2 - assert len(base_store._get_query(table=Sample).all()) == 2 - assert base_store._get_query(table=Case).count() == 2 - first_sample = new_samples[0] - assert len(first_sample.links) == 2 - family_link = first_sample.links[0] - assert family_link.case in base_store.get_cases() - assert family_link.case.data_analysis - assert family_link.case.data_delivery in [DataDelivery.FASTQ, DataDelivery.NO_DELIVERY] - - -def test_store_samples_sex_stored(orders_api, base_store, fastq_status_data, ticket_id: str): - # GIVEN a basic store with no samples and a fastq order - assert not base_store._get_query(table=Sample).first() - assert base_store._get_query(table=Case).count() == 0 - - submitter = FastqSubmitter(lims=orders_api.lims, status=orders_api.status) - - # WHEN storing the order - new_samples = submitter.store_items_in_status( - customer_id=fastq_status_data["customer"], - order=fastq_status_data["order"], - ordered=dt.datetime.now(), - ticket_id=ticket_id, - items=fastq_status_data["samples"], - ) - - # THEN the sample sex should be stored - assert new_samples[0].sex == "male" - - -def test_store_fastq_samples_non_tumour_wgs_to_mip(orders_api, base_store, fastq_status_data): - # GIVEN a basic store with no samples and a non-tumour fastq order as wgs - assert not base_store._get_query(table=Sample).first() - assert base_store._get_query(table=Case).count() == 0 - base_store.get_application_by_tag( - fastq_status_data["samples"][0]["application"] - ).prep_category = PrepCategory.WHOLE_GENOME_SEQUENCING - fastq_status_data["samples"][0]["tumour"] = False - - submitter = FastqSubmitter(lims=orders_api.lims, status=orders_api.status) - - # WHEN storing the order - new_samples = submitter.store_items_in_status( - customer_id=fastq_status_data["customer"], - order=fastq_status_data["order"], - ordered=dt.datetime.now(), - ticket_id=1234348, - items=fastq_status_data["samples"], - ) - - # THEN the analysis for the case should be MAF - assert new_samples[0].links[0].case.data_analysis == Workflow.MIP_DNA - - -def test_store_fastq_samples_tumour_wgs_to_fastq( - orders_api, base_store, fastq_status_data, ticket_id: str -): - # GIVEN a basic store with no samples and a tumour fastq order as wgs - assert not base_store._get_query(table=Sample).first() - assert base_store._get_query(table=Case).count() == 0 - base_store.get_application_by_tag( - fastq_status_data["samples"][0]["application"] - ).prep_category = PrepCategory.WHOLE_GENOME_SEQUENCING - fastq_status_data["samples"][0]["tumour"] = True - - submitter = FastqSubmitter(lims=orders_api.lims, status=orders_api.status) - - # WHEN storing the order - new_samples = submitter.store_items_in_status( - customer_id=fastq_status_data["customer"], - order=fastq_status_data["order"], - ordered=dt.datetime.now(), - ticket_id=ticket_id, - items=fastq_status_data["samples"], - ) - - # THEN the analysis for the case should be FASTQ - assert new_samples[0].links[0].case.data_analysis == Workflow.FASTQ - - -def test_store_fastq_samples_non_wgs_as_fastq( - orders_api, base_store, fastq_status_data, ticket_id: str -): - # GIVEN a basic store with no samples and a fastq order as non wgs - assert not base_store._get_query(table=Sample).first() - assert base_store._get_query(table=Case).count() == 0 - non_wgs_prep_category = PrepCategory.WHOLE_EXOME_SEQUENCING - - non_wgs_applications = base_store._get_query(table=Application).filter( - Application.prep_category == non_wgs_prep_category - ) - - assert non_wgs_applications - - for sample in fastq_status_data["samples"]: - sample["application"] = non_wgs_applications[0].tag - - submitter = FastqSubmitter(lims=orders_api.lims, status=orders_api.status) - - # WHEN storing the order - new_samples = submitter.store_items_in_status( - customer_id=fastq_status_data["customer"], - order=fastq_status_data["order"], - ordered=dt.datetime.now(), - ticket_id=ticket_id, - items=fastq_status_data["samples"], - ) - - # THEN the analysis for the case should be fastq (none) - assert new_samples[0].links[0].case.data_analysis == Workflow.FASTQ - - -def test_store_samples_bad_apptag(orders_api, base_store, fastq_status_data, ticket_id: str): - # GIVEN a basic store with no samples and a fastq order - assert not base_store._get_query(table=Sample).first() - assert base_store._get_query(table=Case).count() == 0 - - for sample in fastq_status_data["samples"]: - sample["application"] = "nonexistingtag" - - submitter = FastqSubmitter(lims=orders_api.lims, status=orders_api.status) - - # THEN it should raise OrderError - with pytest.raises(OrderError): - # WHEN storing the order - submitter.store_items_in_status( - customer_id=fastq_status_data["customer"], - order=fastq_status_data["order"], - ordered=dt.datetime.now(), - ticket_id=ticket_id, - items=fastq_status_data["samples"], - ) - - -def test_store_microbial_samples(orders_api, base_store, microbial_status_data, ticket_id: str): - # GIVEN a basic store with no samples and a microbial order and one Organism - assert not base_store._get_query(table=Sample).first() - assert base_store._get_query(table=Case).count() == 0 - assert base_store.get_all_organisms().count() == 1 - - submitter = MicrobialSubmitter(lims=orders_api.lims, status=orders_api.status) - - # WHEN storing the order - new_samples = submitter.store_items_in_status( - customer_id=microbial_status_data["customer"], - order=microbial_status_data["order"], - ordered=dt.datetime.now(), - ticket_id=ticket_id, - items=microbial_status_data["samples"], - comment="", - data_analysis=Workflow.MICROSALT, - data_delivery=DataDelivery.FASTQ_QC, - ) - - # THEN it should store the samples under a case (case) and the used previously unknown - # organisms - assert new_samples - assert base_store._get_query(table=Case).count() == 1 - assert len(new_samples) == 5 - assert len(base_store._get_query(table=Sample).all()) == 5 - assert base_store.get_all_organisms().count() == 3 - - -def test_store_microbial_case_data_analysis_stored( - orders_api, base_store, microbial_status_data, ticket_id: str -): - # GIVEN a basic store with no samples and a microbial order and one Organism - assert not base_store._get_query(table=Sample).first() - assert base_store._get_query(table=Case).count() == 0 - - submitter = MicrobialSubmitter(lims=orders_api.lims, status=orders_api.status) - - # WHEN storing the order - submitter.store_items_in_status( - customer_id=microbial_status_data["customer"], - order=microbial_status_data["order"], - ordered=dt.datetime.now(), - ticket_id=ticket_id, - items=microbial_status_data["samples"], - comment="", - data_analysis=Workflow.MICROSALT, - data_delivery=DataDelivery.FASTQ_QC, - ) - - # THEN store the samples under a case with the microbial data_analysis type on case level - assert len(base_store._get_query(table=Sample).all()) > 0 - assert base_store._get_query(table=Case).count() == 1 - - microbial_case = base_store.get_cases()[0] - assert microbial_case.data_analysis == Workflow.MICROSALT - assert microbial_case.data_delivery == str(DataDelivery.FASTQ_QC) - - -def test_store_microbial_sample_priority( - orders_api, base_store, microbial_status_data, ticket_id: str -): - # GIVEN a basic store with no samples - assert not base_store._get_query(table=Sample).first() - - submitter = MicrobialSubmitter(lims=orders_api.lims, status=orders_api.status) - - # WHEN storing the order - submitter.store_items_in_status( - customer_id=microbial_status_data["customer"], - order=microbial_status_data["order"], - ordered=dt.datetime.now(), - ticket_id=ticket_id, - items=microbial_status_data["samples"], - comment="", - data_analysis=Workflow.MICROSALT, - data_delivery=DataDelivery.FASTQ_QC, - ) - - # THEN it should store the sample priority - assert len(base_store._get_query(table=Sample).all()) > 0 - microbial_sample = base_store._get_query(table=Sample).first() - - assert microbial_sample.priority_human == "research" - - -def test_store_mip(orders_api, base_store: Store, mip_status_data, ticket_id: str): - # GIVEN a basic store with no samples or nothing in it + scout order - assert not base_store._get_query(table=Sample).first() - assert not base_store.get_cases() - - submitter: MipDnaSubmitter = MipDnaSubmitter(lims=orders_api.lims, status=orders_api.status) - - # WHEN storing the order - new_families = submitter.store_items_in_status( - customer_id=mip_status_data["customer"], - order=mip_status_data["order"], - ordered=dt.datetime.now(), - ticket_id=ticket_id, - items=mip_status_data["families"], - ) - - # THEN it should create and link samples and the case - assert len(new_families) == 2 - new_case = new_families[0] - assert new_case.name == "family1" - assert set(new_case.panels) == {"IEM"} - assert new_case.priority_human == Priority.standard.name - - assert len(new_case.links) == 3 - new_link = new_case.links[0] - assert new_case.data_analysis == Workflow.MIP_DNA - assert new_case.data_delivery == str(DataDelivery.SCOUT) - assert set(new_case.cohorts) == {"Other"} - assert ( - new_case.synopsis - == "As for the synopsis it will be this overly complex sentence to prove that the synopsis field might in fact be a very long string, which we should be prepared for." - ) - assert new_link.status == "affected" - assert new_link.mother.name == "sample2" - assert new_link.father.name == "sample3" - assert new_link.sample.name == "sample1" - assert new_link.sample.sex == "female" - assert new_link.sample.application_version.application.tag == "WGSPCFC030" - assert new_link.sample.is_tumour - assert isinstance(new_case.links[1].sample.comment, str) - - assert set(new_link.sample.phenotype_groups) == {"Phenotype-group"} - assert set(new_link.sample.phenotype_terms) == {"HP:0012747", "HP:0025049"} - assert new_link.sample.subject_id == "subject1" - - assert new_link.sample.age_at_sampling == 17.18192 - - -def test_store_mip_rna(orders_api, base_store, mip_rna_status_data, ticket_id: str): - # GIVEN a basic store with no samples or nothing in it + rna order - rna_application_tag = "RNAPOAR025" - assert not base_store._get_query(table=Sample).first() - assert not base_store.get_cases() - assert base_store.get_application_by_tag(tag=rna_application_tag) - - submitter: MipRnaSubmitter = MipRnaSubmitter(lims=orders_api.lims, status=orders_api.status) - - # WHEN storing the order - new_cases = submitter.store_items_in_status( - customer_id=mip_rna_status_data["customer"], - order=mip_rna_status_data["order"], - ordered=dt.datetime.now(), - ticket_id=ticket_id, - items=mip_rna_status_data["families"], - ) - - # THEN it should create and link samples and the casing - assert len(new_cases) == 1 - new_casing = new_cases[0] - - assert len(new_casing.links) == 2 - new_link = new_casing.links[0] - assert new_casing.data_analysis == Workflow.MIP_RNA - assert new_casing.data_delivery == str(DataDelivery.SCOUT) - assert new_link.sample.name == "sample1-rna-t1" - assert new_link.sample.application_version.application.tag == rna_application_tag - - -def test_store_metagenome_samples(orders_api, base_store, metagenome_status_data, ticket_id: str): - # GIVEN a basic store with no samples and a metagenome order - assert not base_store._get_query(table=Sample).first() - - submitter = MetagenomeSubmitter(lims=orders_api.lims, status=orders_api.status) - - # WHEN storing the order - new_samples = submitter.store_items_in_status( - customer_id=metagenome_status_data["customer"], - order=metagenome_status_data["order"], - ordered=dt.datetime.now(), - ticket_id=ticket_id, - items=metagenome_status_data["families"], - ) - - # THEN it should store the samples - assert len(new_samples) == 2 - assert len(base_store._get_query(table=Sample).all()) == 2 - - -def test_store_metagenome_samples_bad_apptag( - orders_api, base_store, metagenome_status_data, ticket_id: str -): - # GIVEN a basic store with no samples and a metagenome order - assert not base_store._get_query(table=Sample).first() - - for sample in metagenome_status_data["families"][0]["samples"]: - sample["application"] = "nonexistingtag" - - submitter = MetagenomeSubmitter(lims=orders_api.lims, status=orders_api.status) - - # THEN it should raise OrderError - with pytest.raises(OrderError): - # WHEN storing the order - submitter.store_items_in_status( - customer_id=metagenome_status_data["customer"], - order=metagenome_status_data["order"], - ordered=dt.datetime.now(), - ticket_id=ticket_id, - items=metagenome_status_data["families"], - ) - - -@pytest.mark.parametrize( - "submitter", [BalsamicSubmitter, BalsamicQCSubmitter, BalsamicUmiSubmitter] -) -def test_store_cancer_samples( - orders_api, base_store: Store, balsamic_status_data, submitter, ticket_id: str -): - # GIVEN a basic store with no samples and a cancer order - assert not base_store._get_query(table=Sample).first() - assert not base_store.get_cases() - - submitter: Submitter = submitter(lims=orders_api.lims, status=orders_api.status) - - # WHEN storing the order - new_families = submitter.store_items_in_status( - customer_id=balsamic_status_data["customer"], - order=balsamic_status_data["order"], - ordered=dt.datetime.now(), - ticket_id=ticket_id, - items=balsamic_status_data["families"], - ) - - # THEN it should create and link samples and the case - assert len(new_families) == 1 - new_case = new_families[0] - assert new_case.name == "family1" - assert new_case.data_analysis in [ - Workflow.BALSAMIC, - Workflow.BALSAMIC_QC, - Workflow.BALSAMIC_UMI, - ] - assert new_case.data_delivery == str(DataDelivery.FASTQ_ANALYSIS_SCOUT) - assert not set(new_case.panels) - assert new_case.priority_human == Priority.standard.name - - assert len(new_case.links) == 1 - new_link = new_case.links[0] - assert new_link.sample.name == "s1" - assert new_link.sample.sex == "male" - assert new_link.sample.application_version.application.tag == "WGSPCFC030" - assert new_link.sample.comment == "other Elution buffer" - assert new_link.sample.is_tumour - - -def test_store_existing_single_sample_from_trio( - orders_api, base_store, mip_status_data, ticket_id: str -): - # GIVEN a stored trio case - submitter: MipDnaSubmitter = MipDnaSubmitter(lims=orders_api.lims, status=orders_api.status) - new_families = submitter.store_items_in_status( - customer_id=mip_status_data["customer"], - order=mip_status_data["order"], - ordered=dt.datetime.now(), - ticket_id=ticket_id, - items=mip_status_data["families"], - ) - - new_case = new_families[0] - assert new_case.name == "family1" - assert set(new_case.panels) == {"IEM"} - assert new_case.priority_human == Priority.standard.name - - assert len(new_case.links) == 3 - new_link = new_case.links[0] - assert new_link.mother - assert new_link.father - name = new_link.sample.name - internal_id = new_link.sample.internal_id - assert base_store.get_sample_by_internal_id(internal_id) - - # WHEN storing a new case with one sample from the trio - for family_idx, family in enumerate(mip_status_data["families"]): - for sample_idx, sample in enumerate(family["samples"]): - if sample["name"] == name: - sample["internal_id"] = internal_id - family["name"] = "single-from-trio" - else: - family["samples"][sample_idx] = {} - - family["samples"] = list(filter(None, family["samples"])) - - if not family["samples"]: - mip_status_data["families"][family_idx] = {} - - mip_status_data["families"] = list(filter(None, mip_status_data["families"])) - - submitter: MipDnaSubmitter = MipDnaSubmitter(lims=orders_api.lims, status=orders_api.status) - new_families = submitter.store_items_in_status( - customer_id=mip_status_data["customer"], - order=mip_status_data["order"], - ordered=dt.datetime.now(), - ticket_id=ticket_id, - items=mip_status_data["families"], - ) - - # THEN there should be no complaints about missing parents - assert len(new_families) == 1 - assert len(new_families[0].links) == 1 - assert not new_families[0].links[0].mother - assert not new_families[0].links[0].father - - -def test_store_existing_case( - orders_api: OrdersAPI, base_store: Store, mip_status_data: dict, ticket_id: str -): - # GIVEN a basic store with no samples or nothing in it + scout order - assert not base_store._get_query(table=Sample).first() - assert not base_store.get_cases() - - submitter: MipDnaSubmitter = MipDnaSubmitter(lims=orders_api.lims, status=orders_api.status) - - # WHEN storing the order - submitter.store_items_in_status( - customer_id=mip_status_data["customer"], - order=mip_status_data["order"], - ordered=dt.datetime.now(), - ticket_id=ticket_id, - items=mip_status_data["families"], - ) - - base_store.session.close() - new_cases: list[Case] = base_store.get_cases() - - # Save internal id - stored_cases_internal_ids = dict([(case.name, case.internal_id) for case in new_cases]) - for case in mip_status_data["families"]: - case["internal_id"] = stored_cases_internal_ids[case["name"]] - - submitter.store_items_in_status( - customer_id=mip_status_data["customer"], - order=mip_status_data["order"], - ordered=dt.datetime.now(), - ticket_id=ticket_id, - items=mip_status_data["families"], - ) - - base_store.session.close() - rerun_cases: list[Case] = base_store.get_cases() - - # THEN the sample ticket should be appended to previos ticket and action set to analyze - assert rerun_cases[0].tickets == f"{ticket_id},{ticket_id}" - assert rerun_cases[0].action == CaseActions.ANALYZE diff --git a/tests/meta/orders/test_meta_orders_lims.py b/tests/services/orders/order_lims_service/test_order_lims_service.py similarity index 88% rename from tests/meta/orders/test_meta_orders_lims.py rename to tests/services/orders/order_lims_service/test_order_lims_service.py index b3c6c3ecaf..c22499242b 100644 --- a/tests/meta/orders/test_meta_orders_lims.py +++ b/tests/services/orders/order_lims_service/test_order_lims_service.py @@ -1,7 +1,7 @@ import pytest from cg.constants import Workflow -from cg.meta.orders.lims import build_lims_sample +from cg.services.orders.order_lims_service.order_lims_service import OrderLimsService from cg.models.lims.sample import LimsSample from cg.models.orders.constants import OrderType from cg.models.orders.order import OrderIn @@ -11,7 +11,9 @@ def test_to_lims_mip(mip_order_to_submit): # GIVEN a scout order for a trio order_data = OrderIn.parse_obj(obj=mip_order_to_submit, project=OrderType.MIP_DNA) # WHEN parsing the order to format for LIMS import - samples: list[LimsSample] = build_lims_sample(customer="cust003", samples=order_data.samples) + samples: list[LimsSample] = OrderLimsService._build_lims_sample( + customer="cust003", samples=order_data.samples + ) # THEN it should list all samples assert len(samples) == 4 @@ -43,7 +45,9 @@ def test_to_lims_fastq(fastq_order_to_submit): order_data = OrderIn.parse_obj(obj=fastq_order_to_submit, project=OrderType.FASTQ) # WHEN parsing the order to format for LIMS - samples: list[LimsSample] = build_lims_sample(customer="dummyCust", samples=order_data.samples) + samples: list[LimsSample] = OrderLimsService._build_lims_sample( + customer="dummyCust", samples=order_data.samples + ) # THEN should "work" assert len(samples) == 2 @@ -60,7 +64,9 @@ def test_to_lims_rml(rml_order_to_submit): order_data = OrderIn.parse_obj(obj=rml_order_to_submit, project=OrderType.RML) # WHEN parsing for LIMS - samples: list[LimsSample] = build_lims_sample(customer="dummyCust", samples=order_data.samples) + samples: list[LimsSample] = OrderLimsService._build_lims_sample( + customer="dummyCust", samples=order_data.samples + ) # THEN it should have found the same number of samples assert len(samples) == 4 @@ -78,7 +84,9 @@ def test_to_lims_microbial(microbial_order_to_submit): order_data = OrderIn.parse_obj(obj=microbial_order_to_submit, project=OrderType.MICROSALT) # WHEN parsing for LIMS - samples: list[LimsSample] = build_lims_sample(customer="cust000", samples=order_data.samples) + samples: list[LimsSample] = OrderLimsService._build_lims_sample( + customer="cust000", samples=order_data.samples + ) # THEN it should "work" assert len(samples) == 5 @@ -99,7 +107,9 @@ def test_to_lims_sarscov2(sarscov2_order_to_submit): order_data = OrderIn.parse_obj(obj=sarscov2_order_to_submit, project=OrderType.SARS_COV_2) # WHEN parsing for LIMS - samples: list[LimsSample] = build_lims_sample(customer="cust000", samples=order_data.samples) + samples: list[LimsSample] = OrderLimsService._build_lims_sample( + customer="cust000", samples=order_data.samples + ) # THEN it should have found the same number of samples assert len(samples) == 6 @@ -128,7 +138,9 @@ def test_to_lims_balsamic(balsamic_order_to_submit, project): order_data = OrderIn.parse_obj(obj=balsamic_order_to_submit, project=project) # WHEN parsing the order to format for LIMS import - samples: list[LimsSample] = build_lims_sample(customer="cust000", samples=order_data.samples) + samples: list[LimsSample] = OrderLimsService._build_lims_sample( + customer="cust000", samples=order_data.samples + ) # THEN it should list all samples assert len(samples) == 1 diff --git a/tests/services/orders/order_store_service/test_fastq_order_service.py b/tests/services/orders/order_store_service/test_fastq_order_service.py new file mode 100644 index 0000000000..36e76e33d2 --- /dev/null +++ b/tests/services/orders/order_store_service/test_fastq_order_service.py @@ -0,0 +1,194 @@ +import datetime as dt +import pytest +from cg.constants import DataDelivery, Workflow +from cg.constants.constants import PrepCategory +from cg.exc import OrderError +from cg.models.orders.order import OrderIn, OrderType +from cg.services.orders.store_order_services.store_fastq_order_service import StoreFastqOrderService +from cg.store.models import Application, Case, Sample +from cg.store.store import Store + + +def test_samples_to_status( + fastq_order_to_submit: dict, store_fastq_order_service: StoreFastqOrderService +): + # GIVEN fastq order with two samples + order = OrderIn.parse_obj(fastq_order_to_submit, OrderType.FASTQ) + + # WHEN parsing for status + data = store_fastq_order_service.order_to_status(order=order) + + # THEN it should pick out samples and relevant information + assert len(data["samples"]) == 2 + first_sample = data["samples"][0] + assert first_sample["name"] == "prov1" + assert first_sample["application"] == "WGSPCFC060" + assert first_sample["priority"] == "priority" + assert first_sample["tumour"] is False + assert first_sample["volume"] == "1" + + # ... and the other sample is a tumour + assert data["samples"][1]["tumour"] is True + + +def test_store_samples( + base_store: Store, + fastq_status_data: dict, + ticket_id: str, + store_fastq_order_service: StoreFastqOrderService, +): + # GIVEN a basic store with no samples and a fastq order + assert not base_store._get_query(table=Sample).first() + assert base_store._get_query(table=Case).count() == 0 + + # WHEN storing the order + new_samples = store_fastq_order_service.store_items_in_status( + customer_id=fastq_status_data["customer"], + order=fastq_status_data["order"], + ordered=dt.datetime.now(), + ticket_id=ticket_id, + items=fastq_status_data["samples"], + ) + + # THEN it should store the samples and create a case for each sample + assert len(new_samples) == 2 + assert len(base_store._get_query(table=Sample).all()) == 2 + assert base_store._get_query(table=Case).count() == 2 + first_sample = new_samples[0] + assert len(first_sample.links) == 2 + family_link = first_sample.links[0] + assert family_link.case in base_store.get_cases() + assert family_link.case.data_analysis + assert family_link.case.data_delivery in [DataDelivery.FASTQ, DataDelivery.NO_DELIVERY] + + +def test_store_samples_sex_stored( + base_store: Store, + fastq_status_data: dict, + ticket_id: str, + store_fastq_order_service: StoreFastqOrderService, +): + # GIVEN a basic store with no samples and a fastq order + assert not base_store._get_query(table=Sample).first() + assert base_store._get_query(table=Case).count() == 0 + + # WHEN storing the order + new_samples = store_fastq_order_service.store_items_in_status( + customer_id=fastq_status_data["customer"], + order=fastq_status_data["order"], + ordered=dt.datetime.now(), + ticket_id=ticket_id, + items=fastq_status_data["samples"], + ) + + # THEN the sample sex should be stored + assert new_samples[0].sex == "male" + + +def test_store_fastq_samples_non_tumour_wgs_to_mip( + base_store: Store, fastq_status_data: dict, store_fastq_order_service: StoreFastqOrderService +): + # GIVEN a basic store with no samples and a non-tumour fastq order as wgs + assert not base_store._get_query(table=Sample).first() + assert base_store._get_query(table=Case).count() == 0 + base_store.get_application_by_tag( + fastq_status_data["samples"][0]["application"] + ).prep_category = PrepCategory.WHOLE_GENOME_SEQUENCING + fastq_status_data["samples"][0]["tumour"] = False + + # WHEN storing the order + new_samples = store_fastq_order_service.store_items_in_status( + customer_id=fastq_status_data["customer"], + order=fastq_status_data["order"], + ordered=dt.datetime.now(), + ticket_id=1234348, + items=fastq_status_data["samples"], + ) + + # THEN the analysis for the case should be MAF + assert new_samples[0].links[0].case.data_analysis == Workflow.MIP_DNA + + +def test_store_fastq_samples_tumour_wgs_to_fastq( + base_store: Store, + fastq_status_data: dict, + ticket_id: str, + store_fastq_order_service: StoreFastqOrderService, +): + # GIVEN a basic store with no samples and a tumour fastq order as wgs + assert not base_store._get_query(table=Sample).first() + assert base_store._get_query(table=Case).count() == 0 + base_store.get_application_by_tag( + fastq_status_data["samples"][0]["application"] + ).prep_category = PrepCategory.WHOLE_GENOME_SEQUENCING + fastq_status_data["samples"][0]["tumour"] = True + + # WHEN storing the order + new_samples = store_fastq_order_service.store_items_in_status( + customer_id=fastq_status_data["customer"], + order=fastq_status_data["order"], + ordered=dt.datetime.now(), + ticket_id=ticket_id, + items=fastq_status_data["samples"], + ) + + # THEN the analysis for the case should be FASTQ + assert new_samples[0].links[0].case.data_analysis == Workflow.FASTQ + + +def test_store_fastq_samples_non_wgs_as_fastq( + base_store: Store, + fastq_status_data: dict, + ticket_id: str, + store_fastq_order_service: StoreFastqOrderService, +): + # GIVEN a basic store with no samples and a fastq order as non wgs + assert not base_store._get_query(table=Sample).first() + assert base_store._get_query(table=Case).count() == 0 + non_wgs_prep_category = PrepCategory.WHOLE_EXOME_SEQUENCING + + non_wgs_applications = base_store._get_query(table=Application).filter( + Application.prep_category == non_wgs_prep_category + ) + + assert non_wgs_applications + + for sample in fastq_status_data["samples"]: + sample["application"] = non_wgs_applications[0].tag + + # WHEN storing the order + new_samples = store_fastq_order_service.store_items_in_status( + customer_id=fastq_status_data["customer"], + order=fastq_status_data["order"], + ordered=dt.datetime.now(), + ticket_id=ticket_id, + items=fastq_status_data["samples"], + ) + + # THEN the analysis for the case should be fastq (none) + assert new_samples[0].links[0].case.data_analysis == Workflow.FASTQ + + +def test_store_samples_bad_apptag( + base_store: Store, + fastq_status_data: dict, + ticket_id: str, + store_fastq_order_service: StoreFastqOrderService, +): + # GIVEN a basic store with no samples and a fastq order + assert not base_store._get_query(table=Sample).first() + assert base_store._get_query(table=Case).count() == 0 + + for sample in fastq_status_data["samples"]: + sample["application"] = "nonexistingtag" + + # THEN it should raise OrderError + with pytest.raises(OrderError): + # WHEN storing the order + store_fastq_order_service.store_items_in_status( + customer_id=fastq_status_data["customer"], + order=fastq_status_data["order"], + ordered=dt.datetime.now(), + ticket_id=ticket_id, + items=fastq_status_data["samples"], + ) diff --git a/tests/services/orders/order_store_service/test_generic_order_store_service.py b/tests/services/orders/order_store_service/test_generic_order_store_service.py new file mode 100644 index 0000000000..31d6a7d9a9 --- /dev/null +++ b/tests/services/orders/order_store_service/test_generic_order_store_service.py @@ -0,0 +1,299 @@ +"""Module to test the StoreGenericOrderService class.""" + +import datetime as dt +import math +from copy import deepcopy + + +from cg.constants import DataDelivery, Priority, Workflow +from cg.constants.constants import CaseActions +from cg.models.orders.order import OrderIn, OrderType +from cg.services.orders.store_order_services.store_case_order import StoreCaseOrderService +from cg.store.models import Case, Sample +from cg.store.store import Store + + +def test_cases_to_status( + mip_order_to_submit: dict, store_generic_order_service: StoreCaseOrderService +): + # GIVEN a scout order with a trio case + project: OrderType = OrderType.MIP_DNA + order = OrderIn.parse_obj(mip_order_to_submit, project=project) + + # WHEN parsing for status + data = store_generic_order_service.order_to_status(order=order) + + # THEN it should pick out the case + assert len(data["families"]) == 2 + family = data["families"][0] + assert family["name"] == "family1" + assert family["data_analysis"] == Workflow.MIP_DNA + assert family["data_delivery"] == str(DataDelivery.SCOUT) + assert family["priority"] == Priority.standard.name + assert family["cohorts"] == ["Other"] + assert ( + family["synopsis"] + == "As for the synopsis it will be this overly complex sentence to prove that the synopsis field might in fact be a very long string, which we should be prepared for." + ) + assert set(family["panels"]) == {"IEM"} + assert len(family["samples"]) == 3 + + first_sample = family["samples"][0] + assert math.isclose(first_sample["age_at_sampling"], 17.18192, rel_tol=1e-09, abs_tol=1e-09) + assert first_sample["name"] == "sample1" + assert first_sample["application"] == "WGSPCFC030" + assert first_sample["phenotype_groups"] == ["Phenotype-group"] + assert first_sample["phenotype_terms"] == ["HP:0012747", "HP:0025049"] + assert first_sample["sex"] == "female" + assert first_sample["status"] == "affected" + assert first_sample["subject_id"] == "subject1" + assert first_sample["mother"] == "sample2" + assert first_sample["father"] == "sample3" + + # ... second sample has a comment + assert isinstance(family["samples"][1]["comment"], str) + + +def test_cases_to_status_synopsis( + mip_order_to_submit: dict, store_generic_order_service: StoreCaseOrderService +): + # GIVEN a scout order with a trio case where synopsis is None + modified_order: dict = deepcopy(mip_order_to_submit) + for sample in modified_order["samples"]: + sample["synopsis"] = None + + project: OrderType = OrderType.MIP_DNA + order = OrderIn.parse_obj(mip_order_to_submit, project=project) + + # WHEN parsing for status + store_generic_order_service.order_to_status(order=order) + + # THEN No exception should have been raised on synopsis + + +def test_store_mip( + base_store: Store, + mip_status_data: dict, + ticket_id: str, + store_generic_order_service: StoreCaseOrderService, +): + # GIVEN a basic store with no samples or nothing in it + scout order + assert not base_store._get_query(table=Sample).first() + assert not base_store.get_cases() + + # WHEN storing the order + new_families = store_generic_order_service.store_items_in_status( + customer_id=mip_status_data["customer"], + order=mip_status_data["order"], + ordered=dt.datetime.now(), + ticket_id=ticket_id, + items=mip_status_data["families"], + ) + + # THEN it should create and link samples and the case + assert len(new_families) == 2 + new_case = new_families[0] + assert new_case.name == "family1" + assert set(new_case.panels) == {"IEM"} + assert new_case.priority_human == Priority.standard.name + + assert len(new_case.links) == 3 + new_link = new_case.links[0] + assert new_case.data_analysis == Workflow.MIP_DNA + assert new_case.data_delivery == str(DataDelivery.SCOUT) + assert set(new_case.cohorts) == {"Other"} + assert ( + new_case.synopsis + == "As for the synopsis it will be this overly complex sentence to prove that the synopsis field might in fact be a very long string, which we should be prepared for." + ) + assert new_link.status == "affected" + assert new_link.mother.name == "sample2" + assert new_link.father.name == "sample3" + assert new_link.sample.name == "sample1" + assert new_link.sample.sex == "female" + assert new_link.sample.application_version.application.tag == "WGSPCFC030" + assert new_link.sample.is_tumour + assert isinstance(new_case.links[1].sample.comment, str) + + assert set(new_link.sample.phenotype_groups) == {"Phenotype-group"} + assert set(new_link.sample.phenotype_terms) == {"HP:0012747", "HP:0025049"} + assert new_link.sample.subject_id == "subject1" + assert math.isclose(new_link.sample.age_at_sampling, 17.18192, rel_tol=1e-09, abs_tol=1e-09) + + +def test_store_mip_rna( + base_store: Store, + mip_rna_status_data, + ticket_id: str, + store_generic_order_service: StoreCaseOrderService, +): + # GIVEN a basic store with no samples or nothing in it + rna order + rna_application_tag = "RNAPOAR025" + assert not base_store._get_query(table=Sample).first() + assert not base_store.get_cases() + assert base_store.get_application_by_tag(tag=rna_application_tag) + + # WHEN storing the order + new_cases = store_generic_order_service.store_items_in_status( + customer_id=mip_rna_status_data["customer"], + order=mip_rna_status_data["order"], + ordered=dt.datetime.now(), + ticket_id=ticket_id, + items=mip_rna_status_data["families"], + ) + + # THEN it should create and link samples and the casing + assert len(new_cases) == 1 + new_casing = new_cases[0] + + assert len(new_casing.links) == 2 + new_link = new_casing.links[0] + assert new_casing.data_analysis == Workflow.MIP_RNA + assert new_casing.data_delivery == str(DataDelivery.SCOUT) + assert new_link.sample.name == "sample1-rna-t1" + assert new_link.sample.application_version.application.tag == rna_application_tag + + +def test_store_cancer_samples( + base_store: Store, + balsamic_status_data: dict, + ticket_id: str, + store_generic_order_service: StoreCaseOrderService, +): + + # GIVEN a basic store with no samples and a cancer order + assert not base_store._get_query(table=Sample).first() + assert not base_store.get_cases() + + # WHEN storing the order + new_families = store_generic_order_service.store_items_in_status( + customer_id=balsamic_status_data["customer"], + order=balsamic_status_data["order"], + ordered=dt.datetime.now(), + ticket_id=ticket_id, + items=balsamic_status_data["families"], + ) + + # THEN it should create and link samples and the case + assert len(new_families) == 1 + new_case = new_families[0] + assert new_case.name == "family1" + assert new_case.data_analysis in [ + Workflow.BALSAMIC, + Workflow.BALSAMIC_QC, + Workflow.BALSAMIC_UMI, + ] + assert new_case.data_delivery == str(DataDelivery.FASTQ_ANALYSIS_SCOUT) + assert not set(new_case.panels) + assert new_case.priority_human == Priority.standard.name + + assert len(new_case.links) == 1 + new_link = new_case.links[0] + assert new_link.sample.name == "s1" + assert new_link.sample.sex == "male" + assert new_link.sample.application_version.application.tag == "WGSPCFC030" + assert new_link.sample.comment == "other Elution buffer" + assert new_link.sample.is_tumour + + +def test_store_existing_single_sample_from_trio( + base_store: Store, + mip_status_data: dict, + ticket_id: str, + store_generic_order_service: StoreCaseOrderService, +): + # GIVEN a stored trio case + + new_families = store_generic_order_service.store_items_in_status( + customer_id=mip_status_data["customer"], + order=mip_status_data["order"], + ordered=dt.datetime.now(), + ticket_id=ticket_id, + items=mip_status_data["families"], + ) + + new_case = new_families[0] + assert new_case.name == "family1" + assert set(new_case.panels) == {"IEM"} + assert new_case.priority_human == Priority.standard.name + + assert len(new_case.links) == 3 + new_link = new_case.links[0] + assert new_link.mother + assert new_link.father + name = new_link.sample.name + internal_id = new_link.sample.internal_id + assert base_store.get_sample_by_internal_id(internal_id) + + # WHEN storing a new case with one sample from the trio + for family_idx, family in enumerate(mip_status_data["families"]): + for sample_idx, sample in enumerate(family["samples"]): + if sample["name"] == name: + sample["internal_id"] = internal_id + family["name"] = "single-from-trio" + else: + family["samples"][sample_idx] = {} + + family["samples"] = list(filter(None, family["samples"])) + + if not family["samples"]: + mip_status_data["families"][family_idx] = {} + + mip_status_data["families"] = list(filter(None, mip_status_data["families"])) + + new_families = store_generic_order_service.store_items_in_status( + customer_id=mip_status_data["customer"], + order=mip_status_data["order"], + ordered=dt.datetime.now(), + ticket_id=ticket_id, + items=mip_status_data["families"], + ) + + # THEN there should be no complaints about missing parents + assert len(new_families) == 1 + assert len(new_families[0].links) == 1 + assert not new_families[0].links[0].mother + assert not new_families[0].links[0].father + + +def test_store_existing_case( + base_store: Store, + mip_status_data: dict, + ticket_id: str, + store_generic_order_service: StoreCaseOrderService, +): + # GIVEN a basic store with no samples or nothing in it + scout order + assert not base_store._get_query(table=Sample).first() + assert not base_store.get_cases() + + # WHEN storing the order + store_generic_order_service.store_items_in_status( + customer_id=mip_status_data["customer"], + order=mip_status_data["order"], + ordered=dt.datetime.now(), + ticket_id=ticket_id, + items=mip_status_data["families"], + ) + + base_store.session.close() + new_cases: list[Case] = base_store.get_cases() + + # Save internal id + stored_cases_internal_ids = dict([(case.name, case.internal_id) for case in new_cases]) + for case in mip_status_data["families"]: + case["internal_id"] = stored_cases_internal_ids[case["name"]] + + store_generic_order_service.store_items_in_status( + customer_id=mip_status_data["customer"], + order=mip_status_data["order"], + ordered=dt.datetime.now(), + ticket_id=ticket_id, + items=mip_status_data["families"], + ) + + base_store.session.close() + rerun_cases: list[Case] = base_store.get_cases() + + # THEN the sample ticket should be appended to previos ticket and action set to analyze + assert rerun_cases[0].tickets == f"{ticket_id},{ticket_id}" + assert rerun_cases[0].action == CaseActions.ANALYZE diff --git a/tests/services/orders/order_store_service/test_metagenome_store_service.py b/tests/services/orders/order_store_service/test_metagenome_store_service.py new file mode 100644 index 0000000000..7d8b91573f --- /dev/null +++ b/tests/services/orders/order_store_service/test_metagenome_store_service.py @@ -0,0 +1,51 @@ +import datetime as dt +import pytest +from cg.exc import OrderError +from cg.models.orders.order import OrderIn, OrderType +from cg.services.orders.store_order_services.store_metagenome_order import ( + StoreMetagenomeOrderService, +) +from cg.store.models import Sample +from cg.store.store import Store + + +def test_metagenome_to_status( + metagenome_order_to_submit: dict, store_metagenome_order_service: StoreMetagenomeOrderService +): + # GIVEN metagenome order with two samples + order = OrderIn.parse_obj(metagenome_order_to_submit, OrderType.METAGENOME) + + # WHEN parsing for status + data = store_metagenome_order_service.order_to_status(order=order) + case = data["families"][0] + # THEN it should pick out samples and relevant information + assert len(case["samples"]) == 2 + first_sample = case["samples"][0] + assert first_sample["name"] == "Bristol" + assert first_sample["application"] == "METLIFR020" + assert first_sample["priority"] == "standard" + assert first_sample["volume"] == "1.0" + + +def test_store_metagenome_samples_bad_apptag( + base_store: Store, + metagenome_status_data: dict, + ticket_id: str, + store_metagenome_order_service: StoreMetagenomeOrderService, +): + # GIVEN a basic store with no samples and a metagenome order + assert not base_store._get_query(table=Sample).first() + + for sample in metagenome_status_data["families"][0]["samples"]: + sample["application"] = "nonexistingtag" + + # THEN it should raise OrderError + with pytest.raises(OrderError): + # WHEN storing the order + store_metagenome_order_service.store_items_in_status( + customer_id=metagenome_status_data["customer"], + order=metagenome_status_data["order"], + ordered=dt.datetime.now(), + ticket_id=ticket_id, + items=metagenome_status_data["families"], + ) diff --git a/tests/services/orders/order_store_service/test_microbial_store_order_service.py b/tests/services/orders/order_store_service/test_microbial_store_order_service.py new file mode 100644 index 0000000000..c638fd43dd --- /dev/null +++ b/tests/services/orders/order_store_service/test_microbial_store_order_service.py @@ -0,0 +1,247 @@ +from cg.services.orders.store_order_services.store_microbial_order import StoreMicrobialOrderService +from cg.store.models import Case +import datetime as dt +from cg.constants import DataDelivery +from cg.constants.constants import Workflow +from cg.models.orders.constants import OrderType +from cg.models.orders.order import OrderIn +from cg.models.orders.sample_base import ControlEnum +from cg.models.orders.samples import SarsCov2Sample +from cg.store.models import Customer, Sample +from cg.store.store import Store + + +def test_microbial_samples_to_status( + microbial_order_to_submit: dict, store_microbial_order_service: StoreMicrobialOrderService +): + # GIVEN microbial order with three samples + order = OrderIn.parse_obj(microbial_order_to_submit, OrderType.MICROSALT) + + # WHEN parsing for status + data = store_microbial_order_service.order_to_status(order=order) + + # THEN it should pick out samples and relevant information + assert len(data["samples"]) == 5 + assert data["customer"] == "cust002" + assert data["order"] == "Microbial samples" + assert data["comment"] == "Order comment" + assert data["data_analysis"] == Workflow.MICROSALT + assert data["data_delivery"] == str(DataDelivery.FASTQ) + + # THEN first sample should contain all the relevant data from the microbial order + sample_data = data["samples"][0] + assert sample_data["priority"] == "research" + assert sample_data["name"] == "all-fields" + assert sample_data.get("internal_id") is None + assert sample_data["organism_id"] == "M.upium" + assert sample_data["reference_genome"] == "NC_111" + assert sample_data["application"] == "MWRNXTR003" + assert sample_data["comment"] == "plate comment" + assert sample_data["volume"] == "1" + + +def test_sarscov2_samples_to_status( + sarscov2_order_to_submit: dict, store_microbial_order_service: StoreMicrobialOrderService +): + # GIVEN sarscov2 order with three samples + order = OrderIn.parse_obj(sarscov2_order_to_submit, OrderType.SARS_COV_2) + + # WHEN parsing for status + data = store_microbial_order_service.order_to_status(order=order) + + # THEN it should pick out samples and relevant information + assert len(data["samples"]) == 6 + assert data["customer"] == "cust002" + assert data["order"] == "Sars-CoV-2 samples" + assert data["comment"] == "Order comment" + assert data["data_analysis"] == Workflow.MUTANT + assert data["data_delivery"] == str(DataDelivery.FASTQ) + + # THEN first sample should contain all the relevant data from the microbial order + sample_data = data["samples"][0] + assert sample_data.get("internal_id") is None + assert sample_data["priority"] == "research" + assert sample_data["application"] == "VWGDPTR001" + assert sample_data["comment"] == "plate comment" + assert sample_data["name"] == "all-fields" + assert sample_data["organism_id"] == "SARS CoV-2" + assert sample_data["reference_genome"] == "NC_111" + assert sample_data["volume"] == "1" + + +def test_store_microbial_samples( + base_store: Store, + microbial_status_data: dict, + ticket_id: str, + store_microbial_order_service: StoreMicrobialOrderService, +): + # GIVEN a basic store with no samples and a microbial order and one Organism + assert not base_store._get_query(table=Sample).first() + assert base_store._get_query(table=Case).count() == 0 + assert base_store.get_all_organisms().count() == 1 + + # WHEN storing the order + new_samples = store_microbial_order_service.store_items_in_status( + customer_id=microbial_status_data["customer"], + order=microbial_status_data["order"], + ordered=dt.datetime.now(), + ticket_id=ticket_id, + items=microbial_status_data["samples"], + comment="", + data_analysis=Workflow.MICROSALT, + data_delivery=DataDelivery.FASTQ_QC, + ) + + # THEN it should store the samples under a case (case) and the used previously unknown + # organisms + assert new_samples + assert base_store._get_query(table=Case).count() == 1 + assert len(new_samples) == 5 + assert len(base_store._get_query(table=Sample).all()) == 5 + assert base_store.get_all_organisms().count() == 3 + + +def test_store_microbial_case_data_analysis_stored( + base_store: Store, + microbial_status_data: dict, + ticket_id: str, + store_microbial_order_service: StoreMicrobialOrderService, +): + # GIVEN a basic store with no samples and a microbial order and one Organism + assert not base_store._get_query(table=Sample).first() + assert base_store._get_query(table=Case).count() == 0 + + # WHEN storing the order + store_microbial_order_service.store_items_in_status( + customer_id=microbial_status_data["customer"], + order=microbial_status_data["order"], + ordered=dt.datetime.now(), + ticket_id=ticket_id, + items=microbial_status_data["samples"], + comment="", + data_analysis=Workflow.MICROSALT, + data_delivery=DataDelivery.FASTQ_QC, + ) + + # THEN store the samples under a case with the microbial data_analysis type on case level + assert len(base_store._get_query(table=Sample).all()) > 0 + assert base_store._get_query(table=Case).count() == 1 + + microbial_case = base_store.get_cases()[0] + assert microbial_case.data_analysis == Workflow.MICROSALT + assert microbial_case.data_delivery == str(DataDelivery.FASTQ_QC) + + +def test_store_microbial_sample_priority( + base_store: Store, + microbial_status_data: dict, + ticket_id: str, + store_microbial_order_service: StoreMicrobialOrderService, +): + # GIVEN a basic store with no samples + assert not base_store._get_query(table=Sample).first() + + # WHEN storing the order + store_microbial_order_service.store_items_in_status( + customer_id=microbial_status_data["customer"], + order=microbial_status_data["order"], + ordered=dt.datetime.now(), + ticket_id=ticket_id, + items=microbial_status_data["samples"], + comment="", + data_analysis=Workflow.MICROSALT, + data_delivery=DataDelivery.FASTQ_QC, + ) + + # THEN it should store the sample priority + assert len(base_store._get_query(table=Sample).all()) > 0 + microbial_sample = base_store._get_query(table=Sample).first() + + assert microbial_sample.priority_human == "research" + + +def test_order_to_status_control_exists( + sarscov2_order_to_submit: dict, + base_store: Store, + store_microbial_order_service: StoreMicrobialOrderService, +): + # GIVEN sarscov2 order with three samples + order: OrderIn = OrderIn.parse_obj(sarscov2_order_to_submit, OrderType.SARS_COV_2) + + # WHEN transforming order to status structure + result: dict = store_microbial_order_service.order_to_status(order=order) + + # THEN check that control is in the result + sample: dict + for sample in result.get("samples"): + assert "control" in sample + + +def test_order_to_status_control_has_input_value( + sarscov2_order_to_submit: dict, + base_store: Store, + store_microbial_order_service: StoreMicrobialOrderService, +): + # GIVEN sarscov2 order with three samples with control value set + control_value = ControlEnum.positive + order: OrderIn = OrderIn.parse_obj(sarscov2_order_to_submit, OrderType.SARS_COV_2) + sample: SarsCov2Sample + for sample in order.samples: + sample.control: ControlEnum = control_value + + # WHEN transforming order to status structure + result: dict = store_microbial_order_service.order_to_status(order=order) + + # THEN check that control is in the result + sample: dict + for sample in result.get("samples"): + assert control_value in sample.get("control") + + +def test_mutant_sample_generates_fields(sarscov2_order_to_submit: dict, base_store: Store): + """Tests that Mutant orders with region and original_lab set can generate region_code and original_lab_address.""" + # GIVEN sarscov2 order with six samples, one without region_code and original_lab_address + + # WHEN parsing the order + order: OrderIn = OrderIn.parse_obj(sarscov2_order_to_submit, OrderType.SARS_COV_2) + + # THEN all samples should have region_code and original_lab_address set + for sample in order.samples: + assert sample.region_code + assert sample.original_lab_address + + +def test_store_items_in_status_control_has_stored_value( + sarscov2_order_to_submit: dict, + base_store: Store, + store_microbial_order_service: StoreMicrobialOrderService, +): + # GIVEN sarscov2 order with three samples with control value + order: OrderIn = OrderIn.parse_obj(sarscov2_order_to_submit, OrderType.SARS_COV_2) + control_value = ControlEnum.positive + sample: SarsCov2Sample + for sample in order.samples: + sample.control: ControlEnum = control_value + + status_data = store_microbial_order_service.order_to_status(order=order) + + # WHEN storing the order + store_microbial_order_service.store_items_in_status( + comment="", + customer_id=order.customer, + data_analysis=Workflow.MUTANT, + data_delivery=DataDelivery.FASTQ, + order="", + ordered=dt.datetime.now(), + ticket_id=123456, + items=status_data.get("samples"), + ) + + # THEN control should exist on the sample in the store + customer: Customer = base_store.get_customer_by_internal_id(customer_internal_id=order.customer) + sample: SarsCov2Sample + for sample in order.samples: + stored_sample: Sample = base_store.get_sample_by_customer_and_name( + customer_entry_id=[customer.id], sample_name=sample.name + ) + assert stored_sample.control == control_value diff --git a/tests/services/orders/order_store_service/test_pool_order_store_service.py b/tests/services/orders/order_store_service/test_pool_order_store_service.py new file mode 100644 index 0000000000..82cf7566a6 --- /dev/null +++ b/tests/services/orders/order_store_service/test_pool_order_store_service.py @@ -0,0 +1,85 @@ +import datetime as dt +from cg.constants import DataDelivery, Workflow +from cg.models.orders.order import OrderIn, OrderType +from cg.services.orders.store_order_services.store_pool_order import StorePoolOrderService +from cg.store.models import Case, Pool, Sample +from cg.store.store import Store + + +def test_pools_to_status( + rml_order_to_submit: dict, store_pool_order_service: StorePoolOrderService +): + # GIVEN a rml order with three samples in one pool + order = OrderIn.parse_obj(rml_order_to_submit, OrderType.RML) + + # WHEN parsing for status + data = store_pool_order_service.order_to_status(order=order) + + # THEN it should pick out the general information + assert data["customer"] == "cust000" + assert data["order"] == "#123456" + assert data["comment"] == "order comment" + + # ... and information about the pool(s) + assert len(data["pools"]) == 2 + pool = data["pools"][0] + assert pool["name"] == "pool-1" + assert pool["application"] == "RMLP05R800" + assert pool["data_analysis"] == Workflow.FASTQ + assert pool["data_delivery"] == str(DataDelivery.FASTQ) + assert len(pool["samples"]) == 2 + sample = pool["samples"][0] + assert sample["name"] == "sample1" + assert sample["comment"] == "test comment" + assert pool["priority"] == "research" + assert sample["control"] == "negative" + + +def test_store_rml( + base_store: Store, + rml_status_data: dict, + ticket_id: str, + store_pool_order_service: StorePoolOrderService, +): + # GIVEN a basic store with no samples and a rml order + assert base_store._get_query(table=Pool).count() == 0 + assert base_store._get_query(table=Case).count() == 0 + assert not base_store._get_query(table=Sample).first() + + # WHEN storing the order + new_pools = store_pool_order_service.store_items_in_status( + customer_id=rml_status_data["customer"], + order=rml_status_data["order"], + ordered=dt.datetime.now(), + ticket_id=ticket_id, + items=rml_status_data["pools"], + ) + + # THEN it should update the database with new pools + assert len(new_pools) == 2 + + assert base_store._get_query(table=Pool).count() == base_store._get_query(table=Case).count() + assert len(base_store._get_query(table=Sample).all()) == 4 + + # ASSERT that there is one negative sample + negative_samples = 0 + for sample in base_store._get_query(table=Sample).all(): + if sample.control == "negative": + negative_samples += 1 + assert negative_samples == 1 + + new_pool = base_store._get_query(table=Pool).order_by(Pool.created_at.desc()).first() + assert new_pool == new_pools[1] + + assert new_pool.name == "pool-2" + assert new_pool.application_version.application.tag == "RMLP05R800" + assert not hasattr(new_pool, "data_analysis") + + new_case = base_store.get_cases()[0] + assert new_case.data_analysis == Workflow.FASTQ + assert new_case.data_delivery == str(DataDelivery.FASTQ) + + # and that the pool is set for invoicing but not the samples of the pool + assert not new_pool.no_invoice + for link in new_case.links: + assert link.sample.no_invoice diff --git a/tests/meta/orders/test_rnafusion_submitter.py b/tests/services/orders/test_validate_order_service/test_validate_generic_order.py similarity index 76% rename from tests/meta/orders/test_rnafusion_submitter.py rename to tests/services/orders/test_validate_order_service/test_validate_generic_order.py index 465498a46e..f74f5842d0 100644 --- a/tests/meta/orders/test_rnafusion_submitter.py +++ b/tests/services/orders/test_validate_order_service/test_validate_generic_order.py @@ -1,9 +1,11 @@ import pytest from cg.exc import OrderError -from cg.meta.orders.rnafusion_submitter import RnafusionSubmitter from cg.models.orders.constants import OrderType from cg.models.orders.order import OrderIn +from cg.services.orders.validate_order_services.validate_case_order import ( + ValidateCaseOrderService, +) from cg.store.store import Store @@ -15,13 +17,13 @@ def test__validate_one_sample_per_case_multiple_samples( ### GIVEN an RNAFUSION order where the first and last sample share the same case order_data = OrderIn.parse_obj(obj=rnafusion_order_to_submit, project=OrderType.RNAFUSION) order_data.samples[-1].family_name = order_data.samples[0].family_name - rnafusion_submitter = RnafusionSubmitter(status=base_store, lims=None) + validator = ValidateCaseOrderService(base_store) ### WHEN validating that each case has only one sample ### THEN an OrderError should be raised with pytest.raises(OrderError): - rnafusion_submitter._validate_only_one_sample_per_case(order_data.samples) + validator._validate_only_one_sample_per_case(order_data.samples) def test__validate_one_sample_per_case_unique_samples( @@ -33,9 +35,9 @@ def test__validate_one_sample_per_case_unique_samples( order_data: OrderIn = OrderIn.parse_obj( obj=rnafusion_order_to_submit, project=OrderType.RNAFUSION ) - rnafusion_submitter: RnafusionSubmitter = RnafusionSubmitter(status=base_store, lims=None) + validator = ValidateCaseOrderService(base_store) ### WHEN validating that each case has only one sample - rnafusion_submitter._validate_only_one_sample_per_case(order_data.samples) + validator._validate_only_one_sample_per_case(order_data.samples) ### THEN no errors should be raised diff --git a/tests/meta/orders/test_SarsCov2Submitter_validate_order.py b/tests/services/orders/test_validate_order_service/test_validate_microbial_order_service.py similarity index 83% rename from tests/meta/orders/test_SarsCov2Submitter_validate_order.py rename to tests/services/orders/test_validate_order_service/test_validate_microbial_order_service.py index 63777197e2..2ef1fef357 100644 --- a/tests/meta/orders/test_SarsCov2Submitter_validate_order.py +++ b/tests/services/orders/test_validate_order_service/test_validate_microbial_order_service.py @@ -1,11 +1,14 @@ import pytest from cg.exc import OrderError -from cg.meta.orders.sars_cov_2_submitter import SarsCov2Submitter + from cg.models.orders.constants import OrderType from cg.models.orders.order import OrderIn from cg.models.orders.sample_base import ControlEnum from cg.models.orders.samples import SarsCov2Sample +from cg.services.orders.validate_order_services.validate_microbial_order import ( + ValidateMicrobialOrderService, +) from cg.store.store import Store from tests.store_helpers import StoreHelpers @@ -15,7 +18,7 @@ def test_validate_normal_order(sarscov2_order_to_submit: dict, base_store: Store order = OrderIn.parse_obj(sarscov2_order_to_submit, OrderType.SARS_COV_2) # WHEN validating the order - SarsCov2Submitter(status=base_store, lims=None).validate_order(order=order) + ValidateMicrobialOrderService(base_store).validate_order(order=order) # THEN it should be regarded as valid @@ -32,7 +35,7 @@ def test_validate_submitted_order( # WHEN validating the order # THEN it should be regarded as invalid with pytest.raises(OrderError): - SarsCov2Submitter(status=base_store, lims=None).validate_order(order=order) + ValidateMicrobialOrderService(base_store).validate_order(order=order) def test_validate_submitted_control_order( @@ -48,4 +51,4 @@ def test_validate_submitted_control_order( # WHEN validating the order # THEN it should be regarded as valid - SarsCov2Submitter(status=base_store, lims=None).validate_order(order=order) + ValidateMicrobialOrderService(base_store).validate_order(order=order) diff --git a/tests/meta/orders/test_PoolSubmitter_validate_order.py b/tests/services/orders/test_validate_order_service/test_validate_pool_order_service.py similarity index 78% rename from tests/meta/orders/test_PoolSubmitter_validate_order.py rename to tests/services/orders/test_validate_order_service/test_validate_pool_order_service.py index 3af6fc953b..98e138f0f6 100644 --- a/tests/meta/orders/test_PoolSubmitter_validate_order.py +++ b/tests/services/orders/test_validate_order_service/test_validate_pool_order_service.py @@ -3,10 +3,10 @@ from cg.constants import DataDelivery from cg.constants.constants import Workflow from cg.exc import OrderError -from cg.meta.orders.pool_submitter import PoolSubmitter from cg.models.orders.constants import OrderType from cg.models.orders.order import OrderIn from cg.models.orders.samples import RmlSample +from cg.services.orders.validate_order_services.validate_pool_order import ValidatePoolOrderService from cg.store.models import Customer from cg.store.store import Store from tests.store_helpers import StoreHelpers @@ -17,7 +17,7 @@ def test_validate_normal_order(rml_order_to_submit: dict, base_store: Store): order = OrderIn.parse_obj(rml_order_to_submit, OrderType.RML) # WHEN validating the order - PoolSubmitter(status=base_store, lims=None).validate_order(order=order) + ValidatePoolOrderService(status_db=base_store).validate_order(order=order) # THEN it should be regarded as valid @@ -30,7 +30,9 @@ def test_validate_case_name(rml_order_to_submit: dict, base_store: Store, helper for sample in order.samples: case = helpers.ensure_case( store=base_store, - case_name=PoolSubmitter.create_case_name(ticket=order.ticket, pool_name=sample.pool), + case_name=ValidatePoolOrderService.create_case_name( + ticket=order.ticket, pool_name=sample.pool + ), customer=customer, data_analysis=Workflow.FLUFFY, data_delivery=DataDelivery.STATINA, @@ -41,4 +43,4 @@ def test_validate_case_name(rml_order_to_submit: dict, base_store: Store, helper # WHEN validating the order # THEN it should be regarded as invalid with pytest.raises(OrderError): - PoolSubmitter(status=base_store, lims=None).validate_order(order=order) + ValidatePoolOrderService(status_db=base_store).validate_order(order=order) diff --git a/tests/store/conftest.py b/tests/store/conftest.py index c20acc27c9..034cca8362 100644 --- a/tests/store/conftest.py +++ b/tests/store/conftest.py @@ -11,8 +11,8 @@ from cg.constants.devices import DeviceType from cg.constants.priority import PriorityTerms from cg.constants.subject import PhenotypeStatus, Sex -from cg.meta.orders.pool_submitter import PoolSubmitter from cg.services.illumina.data_transfer.models import IlluminaFlowCellDTO +from cg.services.orders.store_order_services.store_pool_order import StorePoolOrderService from cg.store.models import ( Analysis, Application, @@ -514,7 +514,7 @@ def rml_pool_store( new_case = helpers.add_case( store=store, internal_id=case_id, - name=PoolSubmitter.create_case_name(ticket=ticket_id, pool_name="Test"), + name=StorePoolOrderService.create_case_name(ticket=ticket_id, pool_name="Test"), ) store.session.add(new_case)