diff --git a/cg/services/orders/validation/models/case.py b/cg/services/orders/validation/models/case.py index 6c7207b547..81ce1b66ed 100644 --- a/cg/services/orders/validation/models/case.py +++ b/cg/services/orders/validation/models/case.py @@ -6,6 +6,8 @@ from cg.services.orders.validation.models.discriminators import has_internal_id from cg.services.orders.validation.models.existing_sample import ExistingSample from cg.services.orders.validation.models.sample_aliases import SampleInCase +from cg.store.models import Sample as DbSample +from cg.store.store import Store NewSample = Annotated[SampleInCase, Tag("new")] ExistingSampleType = Annotated[ExistingSample, Tag("existing")] @@ -45,11 +47,17 @@ def enumerated_existing_samples(self) -> list[tuple[int, ExistingSample]]: samples.append((sample_index, sample)) return samples - def get_sample(self, sample_name: str) -> SampleInCase | None: + def get_new_sample(self, sample_name: str) -> SampleInCase | None: for _, sample in self.enumerated_new_samples: if sample.name == sample_name: return sample + def get_existing_sample_from_db(self, sample_name: str, store: Store) -> DbSample | None: + for _, sample in self.enumerated_existing_samples: + db_sample: DbSample | None = store.get_sample_by_internal_id(sample.internal_id) + if db_sample and db_sample.name == sample_name: + return db_sample + @model_validator(mode="before") def convert_empty_strings_to_none(cls, data): if isinstance(data, dict): diff --git a/cg/services/orders/validation/rules/case/utils.py b/cg/services/orders/validation/rules/case/utils.py index 350082f884..30ef3ccac7 100644 --- a/cg/services/orders/validation/rules/case/utils.py +++ b/cg/services/orders/validation/rules/case/utils.py @@ -1,3 +1,4 @@ +from cg.services.orders.validation.models.case import Case from cg.services.orders.validation.models.existing_case import ExistingCase from cg.services.orders.validation.workflows.balsamic.models.case import BalsamicCase from cg.services.orders.validation.workflows.balsamic_umi.models.case import BalsamicUmiCase @@ -34,3 +35,11 @@ def is_case_not_from_collaboration(case: ExistingCase, customer_id: str, store: db_case: DbCase | None = store.get_case_by_internal_id(case.internal_id) customer: Customer | None = store.get_customer_by_internal_id(customer_id) return db_case and customer and db_case.customer not in customer.collaborators + + +def is_sample_in_case(case: Case, sample_name: str, store: Store) -> bool: + if case.get_new_sample(sample_name): + return True + elif case.get_existing_sample_from_db(sample_name=sample_name, store=store): + return True + return False diff --git a/cg/services/orders/validation/rules/case_sample/rules.py b/cg/services/orders/validation/rules/case_sample/rules.py index e0c7a79b06..82307bea0a 100644 --- a/cg/services/orders/validation/rules/case_sample/rules.py +++ b/cg/services/orders/validation/rules/case_sample/rules.py @@ -292,48 +292,49 @@ def validate_sample_names_different_from_case_names( return errors -def validate_fathers_are_male(order: OrderWithCases, **kwargs) -> list[InvalidFatherSexError]: +def validate_fathers_are_male( + order: OrderWithCases, store: Store, **kwargs +) -> list[InvalidFatherSexError]: errors: list[InvalidFatherSexError] = [] for index, case in order.enumerated_new_cases: case_errors: list[InvalidFatherSexError] = get_father_sex_errors( - case=case, case_index=index + case=case, case_index=index, store=store ) errors.extend(case_errors) return errors def validate_fathers_in_same_case_as_children( - order: OrderWithCases, **kwargs + order: OrderWithCases, store: Store, **kwargs ) -> list[FatherNotInCaseError]: errors: list[FatherNotInCaseError] = [] for index, case in order.enumerated_new_cases: case_errors: list[FatherNotInCaseError] = get_father_case_errors( - case=case, - case_index=index, + case=case, case_index=index, store=store ) errors.extend(case_errors) return errors -def validate_mothers_are_female(order: OrderWithCases, **kwargs) -> list[InvalidMotherSexError]: +def validate_mothers_are_female( + order: OrderWithCases, store: Store, **kwargs +) -> list[InvalidMotherSexError]: errors: list[InvalidMotherSexError] = [] for index, case in order.enumerated_new_cases: case_errors: list[InvalidMotherSexError] = get_mother_sex_errors( - case=case, - case_index=index, + case=case, case_index=index, store=store ) errors.extend(case_errors) return errors def validate_mothers_in_same_case_as_children( - order: OrderWithCases, **kwargs + order: OrderWithCases, store: Store, **kwargs ) -> list[MotherNotInCaseError]: errors: list[MotherNotInCaseError] = [] for index, case in order.enumerated_new_cases: case_errors: list[MotherNotInCaseError] = get_mother_case_errors( - case=case, - case_index=index, + case=case, case_index=index, store=store ) errors.extend(case_errors) return errors diff --git a/cg/services/orders/validation/rules/case_sample/utils.py b/cg/services/orders/validation/rules/case_sample/utils.py index 9db37b3c9e..979147d510 100644 --- a/cg/services/orders/validation/rules/case_sample/utils.py +++ b/cg/services/orders/validation/rules/case_sample/utils.py @@ -28,6 +28,7 @@ SampleInCase, SampleWithRelatives, ) +from cg.services.orders.validation.rules.case.utils import is_sample_in_case from cg.services.orders.validation.rules.utils import ( get_concentration_interval, has_sample_invalid_concentration, @@ -112,12 +113,12 @@ def get_repeated_case_name_errors(order: OrderWithCases) -> list[RepeatedCaseNam def get_father_sex_errors( - case: CaseContainingRelatives, case_index: int + case: CaseContainingRelatives, case_index: int, store: Store ) -> list[InvalidFatherSexError]: errors: list[InvalidFatherSexError] = [] children: list[tuple[SampleWithRelatives, int]] = case.get_samples_with_father() for child, child_index in children: - if is_father_sex_invalid(child=child, case=case): + if is_father_sex_invalid(child=child, case=case, store=store): error: InvalidFatherSexError = create_father_sex_error( case_index=case_index, sample_index=child_index ) @@ -125,8 +126,14 @@ def get_father_sex_errors( return errors -def is_father_sex_invalid(child: SampleWithRelatives, case: CaseContainingRelatives) -> bool: - father: SampleWithRelatives | None = case.get_sample(child.father) +def is_father_sex_invalid( + child: SampleWithRelatives, case: CaseContainingRelatives, store: Store +) -> bool: + father: SampleWithRelatives | None = case.get_new_sample(child.father) + if not father: + father: DbSample | None = case.get_existing_sample_from_db( + sample_name=child.father, store=store + ) return father and father.sex != Sex.MALE @@ -135,14 +142,14 @@ def create_father_sex_error(case_index: int, sample_index: int) -> InvalidFather def get_father_case_errors( - case: CaseContainingRelatives, - case_index: int, + case: CaseContainingRelatives, case_index: int, store: Store ) -> list[FatherNotInCaseError]: errors: list[FatherNotInCaseError] = [] - children: list[tuple[SampleWithRelatives, int]] = case.get_samples_with_father() + children: list[tuple[SampleWithRelatives | ExistingSample, int]] = ( + case.get_samples_with_father() + ) for child, child_index in children: - father: SampleWithRelatives | None = case.get_sample(child.father) - if not father: + if not is_sample_in_case(case=case, sample_name=child.father, store=store): error: FatherNotInCaseError = create_father_case_error( case_index=case_index, sample_index=child_index, @@ -152,13 +159,12 @@ def get_father_case_errors( def get_mother_sex_errors( - case: CaseContainingRelatives, - case_index: int, + case: CaseContainingRelatives, case_index: int, store: Store ) -> list[InvalidMotherSexError]: errors: list[InvalidMotherSexError] = [] children: list[tuple[SampleWithRelatives, int]] = case.get_samples_with_mother() for child, child_index in children: - if is_mother_sex_invalid(child=child, case=case): + if is_mother_sex_invalid(child=child, case=case, store=store): error: InvalidMotherSexError = create_mother_sex_error( case_index=case_index, sample_index=child_index, @@ -168,14 +174,12 @@ def get_mother_sex_errors( def get_mother_case_errors( - case: CaseContainingRelatives, - case_index: int, + case: CaseContainingRelatives, case_index: int, store: Store ) -> list[MotherNotInCaseError]: errors: list[MotherNotInCaseError] = [] children: list[tuple[SampleWithRelatives, int]] = case.get_samples_with_mother() for child, child_index in children: - mother: SampleWithRelatives | None = case.get_sample(child.mother) - if not mother: + if not is_sample_in_case(case=case, sample_name=child.mother, store=store): error: MotherNotInCaseError = create_mother_case_error( case_index=case_index, sample_index=child_index ) @@ -191,8 +195,14 @@ def create_mother_case_error(case_index: int, sample_index: int) -> MotherNotInC return MotherNotInCaseError(case_index=case_index, sample_index=sample_index) -def is_mother_sex_invalid(child: SampleWithRelatives, case: CaseContainingRelatives) -> bool: - mother: SampleWithRelatives | None = case.get_sample(child.mother) +def is_mother_sex_invalid( + child: SampleWithRelatives, case: CaseContainingRelatives, store: Store +) -> bool: + mother: SampleWithRelatives | None = case.get_new_sample(child.mother) + if not mother: + mother: DbSample | None = case.get_existing_sample_from_db( + sample_name=child.mother, store=store + ) return mother and mother.sex != Sex.FEMALE diff --git a/cg/services/orders/validation/workflows/mip_dna/models/case.py b/cg/services/orders/validation/workflows/mip_dna/models/case.py index 1a77df2902..3c6dda474f 100644 --- a/cg/services/orders/validation/workflows/mip_dna/models/case.py +++ b/cg/services/orders/validation/workflows/mip_dna/models/case.py @@ -16,8 +16,8 @@ class MipDnaCase(Case): synopsis: str | None = None samples: list[Annotated[NewSample | OldSample, Discriminator(has_internal_id)]] - def get_samples_with_father(self) -> list[tuple[MipDnaSample, int]]: + def get_samples_with_father(self) -> list[tuple[MipDnaSample | ExistingSample, int]]: return [(sample, index) for index, sample in self.enumerated_samples if sample.father] - def get_samples_with_mother(self) -> list[tuple[MipDnaSample, int]]: + def get_samples_with_mother(self) -> list[tuple[MipDnaSample | ExistingSample, int]]: return [(sample, index) for index, sample in self.enumerated_samples if sample.mother] diff --git a/cg/services/orders/validation/workflows/tomte/models/case.py b/cg/services/orders/validation/workflows/tomte/models/case.py index ab4504e4ea..7b99fb37e9 100644 --- a/cg/services/orders/validation/workflows/tomte/models/case.py +++ b/cg/services/orders/validation/workflows/tomte/models/case.py @@ -16,8 +16,8 @@ class TomteCase(Case): synopsis: str | None = None samples: list[Annotated[NewSample | OldSample, Discriminator(has_internal_id)]] - def get_samples_with_father(self) -> list[tuple[TomteSample, int]]: + def get_samples_with_father(self) -> list[tuple[TomteSample | ExistingSample, int]]: return [(sample, index) for index, sample in self.enumerated_samples if sample.father] - def get_samples_with_mother(self) -> list[tuple[TomteSample, int]]: + def get_samples_with_mother(self) -> list[tuple[TomteSample | ExistingSample, int]]: return [(sample, index) for index, sample in self.enumerated_samples if sample.mother] diff --git a/tests/services/orders/validation_service/workflows/tomte/test_case_sample_rules.py b/tests/services/orders/validation_service/workflows/tomte/test_case_sample_rules.py index ed244d2330..23a2b4ba92 100644 --- a/tests/services/orders/validation_service/workflows/tomte/test_case_sample_rules.py +++ b/tests/services/orders/validation_service/workflows/tomte/test_case_sample_rules.py @@ -1,3 +1,4 @@ +from cg.constants import Sex from cg.models.orders.sample_base import StatusEnum from cg.services.orders.validation.errors.case_errors import ( InvalidGenePanelsError, @@ -7,6 +8,7 @@ DescendantAsFatherError, FatherNotInCaseError, InvalidFatherSexError, + InvalidMotherSexError, PedigreeError, SampleIsOwnFatherError, ) @@ -16,9 +18,11 @@ validate_fathers_are_male, validate_fathers_in_same_case_as_children, validate_gene_panels_exist, + validate_mothers_are_female, validate_pedigree, ) from cg.services.orders.validation.workflows.tomte.models.order import TomteOrder +from cg.store.models import Sample from cg.store.store import Store from tests.store_helpers import StoreHelpers @@ -55,11 +59,13 @@ def test_repeated_gene_panels(valid_order: TomteOrder, store_with_panels: Store) assert isinstance(errors[0], RepeatedGenePanelsError) -def test_father_must_be_male(order_with_invalid_father_sex: TomteOrder): +def test_father_must_be_male(order_with_invalid_father_sex: TomteOrder, base_store: Store): # GIVEN an order with an incorrectly specified father # WHEN validating the order - errors: list[InvalidFatherSexError] = validate_fathers_are_male(order_with_invalid_father_sex) + errors: list[InvalidFatherSexError] = validate_fathers_are_male( + order=order_with_invalid_father_sex, store=base_store + ) # THEN errors are returned assert errors @@ -68,13 +74,73 @@ def test_father_must_be_male(order_with_invalid_father_sex: TomteOrder): assert isinstance(errors[0], InvalidFatherSexError) -def test_father_in_wrong_case(order_with_father_in_wrong_case: TomteOrder): +def test_existing_father_must_be_male( + valid_order: TomteOrder, store_with_multiple_cases_and_samples: Store +): + """Tests that an order with a father which is a female sample in StatusDB gives an error.""" + + # GIVEN a sample in StatusDB with female sex + father_db_sample: Sample = store_with_multiple_cases_and_samples.session.query(Sample).first() + father_db_sample.sex = Sex.FEMALE + store_with_multiple_cases_and_samples.commit_to_store() + + # GIVEN that an order has a corresponding existing sample in one of its cases + father_sample = ExistingSample(internal_id=father_db_sample.internal_id) + valid_order.cases[0].samples.append(father_sample) + + # GIVEN that another sample in the order specifies the sample as its father + father_name = father_db_sample.name + valid_order.cases[0].samples[0].father = father_name + + # WHEN validating the order + errors: list[InvalidFatherSexError] = validate_fathers_are_male( + order=valid_order, store=store_with_multiple_cases_and_samples + ) + + # THEN errors are returned + assert errors + + # THEN the errors are about the father's sex + assert isinstance(errors[0], InvalidFatherSexError) + + +def test_existing_mother_must_be_female( + valid_order: TomteOrder, store_with_multiple_cases_and_samples: Store +): + """Tests that an order with a mother which is a male sample in StatusDB gives an error.""" + + # GIVEN a sample in StatusDB with male sex + mother_db_sample: Sample = store_with_multiple_cases_and_samples.session.query(Sample).first() + mother_db_sample.sex = Sex.MALE + store_with_multiple_cases_and_samples.commit_to_store() + + # GIVEN that an order has a corresponding existing sample in one of its cases + mother_sample = ExistingSample(internal_id=mother_db_sample.internal_id) + valid_order.cases[0].samples.append(mother_sample) + + # GIVEN that another sample in the order specifies the sample as its mother + mother_name = mother_db_sample.name + valid_order.cases[0].samples[0].mother = mother_name + + # WHEN validating the order + errors: list[InvalidMotherSexError] = validate_mothers_are_female( + order=valid_order, store=store_with_multiple_cases_and_samples + ) + + # THEN errors are returned + assert errors + + # THEN the errors are about the mother's sex + assert isinstance(errors[0], InvalidMotherSexError) + + +def test_father_in_wrong_case(order_with_father_in_wrong_case: TomteOrder, base_store: Store): # GIVEN an order with the father sample in the wrong case # WHEN validating the order errors: list[FatherNotInCaseError] = validate_fathers_in_same_case_as_children( - order_with_father_in_wrong_case + order=order_with_father_in_wrong_case, store=base_store ) # THEN an error is returned