Skip to content

Commit

Permalink
add new mutant validation rule (#4075)
Browse files Browse the repository at this point in the history
## Description
Closes Clinical-Genomics/improve-order-flow#102
Fixes the sample name validation rule for Mutant orders to exclude control samples from the name check

### Added
- Validation function `validate_non_control_sample_names_available` that verifies that sample names used in an order are not in the store excluding control samples
- Utils function `get_sample_name_not_available_errors` used in new validation function
- Mutant order fixture
- aliases to group samples with organism and samples orders with control samples
- tests for new validation functions

### Changed
- Refactored validation function `validate_sample_names_available` to use the new utils function

### Fixed

- Changed some balsamic structure in the order validation service
- Reverted some deletions in json order fixtures from previous prs
  • Loading branch information
diitaz93 authored Jan 14, 2025
1 parent 5db8b83 commit 549d59d
Show file tree
Hide file tree
Showing 19 changed files with 238 additions and 32 deletions.
5 changes: 5 additions & 0 deletions cg/services/order_validation_service/errors/sample_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ class SampleNameNotAvailableError(SampleError):
message: str = "Sample name already used in previous order"


class SampleNameNotAvailableControlError(SampleError):
field: str = "name"
message: str = "Sample name already in use. Only control samples are allowed repeated names"


class ContainerNameRepeatedError(SampleError):
field: str = "container_name"
message: str = "Tube names must be unique among samples"
Expand Down
12 changes: 11 additions & 1 deletion cg/services/order_validation_service/models/order_aliases.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
from cg.services.order_validation_service.workflows.fluffy.models.order import FluffyOrder
from cg.services.order_validation_service.workflows.metagenome.models.sample import MetagenomeSample
from cg.services.order_validation_service.workflows.microbial_fastq.models.order import (
MicrobialFastqOrder,
)
from cg.services.order_validation_service.workflows.microsalt.models.order import MicrosaltOrder
from cg.services.order_validation_service.workflows.mutant.models.order import MutantOrder
from cg.services.order_validation_service.workflows.rml.models.order import RmlOrder
from cg.services.order_validation_service.workflows.taxprofiler.models.sample import (
TaxprofilerSample,
)

OrderWithIndexedSamples = FluffyOrder | RmlOrder
OrderWithNonHumanSamples = MutantOrder | MicrosaltOrder
OrderWithSamplesFromOrganism = MutantOrder | MicrosaltOrder
OrderWithControlSamples = (
MetagenomeSample | MicrobialFastqOrder | MicrosaltOrder | MutantOrder | TaxprofilerSample
)
3 changes: 0 additions & 3 deletions cg/services/order_validation_service/models/sample_aliases.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,15 @@
)
from cg.services.order_validation_service.workflows.fastq.models.sample import FastqSample
from cg.services.order_validation_service.workflows.fluffy.models.sample import FluffySample
from cg.services.order_validation_service.workflows.microsalt.models.sample import MicrosaltSample
from cg.services.order_validation_service.workflows.mip_dna.models.sample import MipDnaSample
from cg.services.order_validation_service.workflows.mip_rna.models.sample import MipRnaSample
from cg.services.order_validation_service.workflows.mutant.models.sample import MutantSample
from cg.services.order_validation_service.workflows.rml.models.sample import RmlSample
from cg.services.order_validation_service.workflows.rna_fusion.models.sample import RnaFusionSample
from cg.services.order_validation_service.workflows.tomte.models.sample import TomteSample

HumanSample = (
BalsamicSample | BalsamicUmiSample | FastqSample | MipDnaSample | RnaFusionSample | TomteSample
)
NonHumanSample = MutantSample | MicrosaltSample

IndexedSample = FluffySample | RmlSample

Expand Down
37 changes: 24 additions & 13 deletions cg/services/order_validation_service/rules/sample/rules.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from cg.models.orders.constants import OrderType
from cg.models.orders.sample_base import ControlEnum
from cg.services.order_validation_service.errors.sample_errors import (
ApplicationArchivedError,
ApplicationNotCompatibleError,
Expand All @@ -22,14 +23,16 @@
WellPositionRmlMissingError,
)
from cg.services.order_validation_service.models.order_aliases import (
OrderWithControlSamples,
OrderWithIndexedSamples,
OrderWithNonHumanSamples,
OrderWithSamplesFromOrganism,
)
from cg.services.order_validation_service.models.sample_aliases import IndexedSample
from cg.services.order_validation_service.rules.sample.utils import (
PlateSamplesValidator,
get_indices_for_repeated_sample_names,
get_indices_for_tube_repeated_container_name,
get_sample_name_not_available_errors,
has_multiple_applications,
has_multiple_priorities,
is_container_name_missing,
Expand Down Expand Up @@ -155,11 +158,11 @@ def validate_concentration_required_if_skip_rc(


def validate_organism_exists(
order: OrderWithNonHumanSamples, store: Store, **kwargs
order: OrderWithSamplesFromOrganism, store: Store, **kwargs
) -> list[OrganismDoesNotExistError]:
"""
Validate that the organisms of all samples in the order exist in the database.
Only applicable to order types with non-human samples.
Only applicable to Microsalt and Mutant orders.
"""
errors: list[OrganismDoesNotExistError] = []
for sample_index, sample in order.enumerated_samples:
Expand Down Expand Up @@ -208,16 +211,24 @@ def validate_sample_names_available(
) -> list[SampleNameNotAvailableError]:
"""
Validate that the sample names do not exists in the database under the same customer.
Applicable to all order types.
Applicable to all orders without control samples.
"""
errors: list[SampleNameNotAvailableError] = []
customer = store.get_customer_by_internal_id(order.customer)
for sample_index, sample in order.enumerated_samples:
if store.get_sample_by_customer_and_name(
sample_name=sample.name, customer_entry_id=[customer.id]
):
error = SampleNameNotAvailableError(sample_index=sample_index)
errors.append(error)
errors: list[SampleNameNotAvailableError] = get_sample_name_not_available_errors(
order=order, store=store, has_order_control=False
)
return errors


def validate_non_control_sample_names_available(
order: OrderWithControlSamples, store: Store, **kwargs
) -> list[SampleNameNotAvailableError]:
"""
Validate that non-control sample names do not exists in the database under the same customer.
Applicable to all orders with control samples.
"""
errors: list[SampleNameNotAvailableError] = get_sample_name_not_available_errors(
order=order, store=store, has_order_control=True
)
return errors


Expand All @@ -226,7 +237,7 @@ def validate_sample_names_unique(
) -> list[SampleNameRepeatedError]:
"""
Validate that all the sample names are unique within the order.
Applicable to all order types except Mutant orders.
Applicable to all order types.
"""
sample_indices: list[int] = get_indices_for_repeated_sample_names(order)
return [SampleNameRepeatedError(sample_index=sample_index) for sample_index in sample_indices]
Expand Down
44 changes: 43 additions & 1 deletion cg/services/order_validation_service/rules/sample/utils.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import re
from collections import Counter

from cg.models.orders.sample_base import ContainerEnum
from cg.models.orders.sample_base import ContainerEnum, ControlEnum
from cg.services.order_validation_service.constants import ALLOWED_SKIP_RC_BUFFERS
from cg.services.order_validation_service.errors.sample_errors import (
BufferInvalidError,
ConcentrationInvalidIfSkipRCError,
ConcentrationRequiredError,
OccupiedWellError,
SampleError,
SampleNameNotAvailableControlError,
SampleNameNotAvailableError,
WellPositionMissingError,
)
from cg.services.order_validation_service.models.order_with_samples import OrderWithSamples
Expand Down Expand Up @@ -79,6 +82,45 @@ def get_indices_for_repeated_sample_names(order: OrderWithSamples) -> list[int]:
return indices


def get_sample_name_not_available_errors(
order: OrderWithSamples, store: Store, has_order_control: bool
) -> list[SampleError]:
"""Return errors for non-control samples with names already used in the database."""
errors: list[SampleError] = []
customer = store.get_customer_by_internal_id(order.customer)
for sample_index, sample in order.enumerated_samples:
if store.get_sample_by_customer_and_name(
sample_name=sample.name, customer_entry_id=[customer.id]
):
if is_sample_name_allowed_to_be_repeated(has_control=has_order_control, sample=sample):
continue
error = get_appropriate_sample_name_available_error(
has_control=has_order_control, sample_index=sample_index
)
errors.append(error)
return errors


def is_sample_name_allowed_to_be_repeated(has_control: bool, sample: Sample) -> bool:
"""
Return whether a sample name can be used if it is already in the database.
This is the case when the order has control samples and the sample is a control.
"""
return has_control and sample.control in [ControlEnum.positive, ControlEnum.negative]


def get_appropriate_sample_name_available_error(
has_control: bool, sample_index: int
) -> SampleError:
"""
Return the appropriate error for a sample name that is not available based on whether the
order has control samples or not.
"""
if has_control:
return SampleNameNotAvailableControlError(sample_index=sample_index)
return SampleNameNotAvailableError(sample_index=sample_index)


def is_tube_container_name_redundant(sample: Sample, counter: Counter) -> bool:
return sample.container == ContainerEnum.tube and counter.get(sample.container_name) > 1

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from cg.services.order_validation_service.errors.case_sample_errors import CaptureKitMissingError
from cg.services.order_validation_service.workflows.balsamic.models.order import BalsamicOrder
from cg.services.order_validation_service.workflows.balsamic.rules.case_sample.utils import (
from cg.services.order_validation_service.workflows.balsamic.utils import (
is_sample_missing_capture_kit,
)
from cg.store.store import Store
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
validate_well_positions_required,
validate_wells_contain_at_most_one_sample,
)
from cg.services.order_validation_service.workflows.balsamic.rules.case_sample.rules import (
from cg.services.order_validation_service.workflows.balsamic.rules import (
validate_capture_kit_panel_requirement,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
validate_application_exists,
validate_applications_not_archived,
validate_container_name_required,
validate_sample_names_available,
validate_non_control_sample_names_available,
validate_sample_names_unique,
validate_tube_container_name_unique,
validate_volume_interval,
Expand All @@ -18,7 +18,7 @@
validate_application_exists,
validate_applications_not_archived,
validate_container_name_required,
validate_sample_names_available,
validate_non_control_sample_names_available,
validate_sample_names_unique,
validate_tube_container_name_unique,
validate_volume_interval,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
validate_application_exists,
validate_applications_not_archived,
validate_container_name_required,
validate_sample_names_available,
validate_non_control_sample_names_available,
validate_sample_names_unique,
validate_tube_container_name_unique,
validate_volume_interval,
Expand All @@ -18,7 +18,7 @@
validate_application_exists,
validate_applications_not_archived,
validate_container_name_required,
validate_sample_names_available,
validate_non_control_sample_names_available,
validate_sample_names_unique,
validate_tube_container_name_unique,
validate_volume_interval,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
validate_application_exists,
validate_applications_not_archived,
validate_container_name_required,
validate_non_control_sample_names_available,
validate_organism_exists,
validate_sample_names_available,
validate_sample_names_unique,
validate_tube_container_name_unique,
validate_volume_interval,
Expand All @@ -19,8 +19,8 @@
validate_application_exists,
validate_applications_not_archived,
validate_container_name_required,
validate_non_control_sample_names_available,
validate_organism_exists,
validate_sample_names_available,
validate_sample_names_unique,
validate_tube_container_name_unique,
validate_volume_interval,
Expand Down
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
validate_application_exists,
validate_applications_not_archived,
validate_container_name_required,
validate_non_control_sample_names_available,
validate_organism_exists,
validate_sample_names_available,
validate_sample_names_unique,
validate_tube_container_name_unique,
validate_volume_interval,
Expand All @@ -19,9 +19,9 @@
validate_application_exists,
validate_applications_not_archived,
validate_container_name_required,
validate_non_control_sample_names_available,
validate_organism_exists,
validate_volume_required,
validate_sample_names_available,
validate_sample_names_unique,
validate_tube_container_name_unique,
validate_volume_interval,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
validate_application_exists,
validate_applications_not_archived,
validate_container_name_required,
validate_sample_names_available,
validate_non_control_sample_names_available,
validate_sample_names_unique,
validate_tube_container_name_unique,
validate_volume_interval,
Expand All @@ -18,7 +18,7 @@
validate_application_exists,
validate_applications_not_archived,
validate_container_name_required,
validate_sample_names_available,
validate_non_control_sample_names_available,
validate_sample_names_unique,
validate_tube_container_name_unique,
validate_volume_interval,
Expand Down
9 changes: 9 additions & 0 deletions tests/fixture_plugins/orders_fixtures/order_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from cg.services.order_validation_service.workflows.microsalt.models.order import MicrosaltOrder
from cg.services.order_validation_service.workflows.mip_dna.models.order import MipDnaOrder
from cg.services.order_validation_service.workflows.mip_rna.models.order import MipRnaOrder
from cg.services.order_validation_service.workflows.mutant.models.order import MutantOrder
from cg.services.order_validation_service.workflows.pacbio_long_read.models.order import PacbioOrder
from cg.services.order_validation_service.workflows.rml.models.order import RmlOrder

Expand Down Expand Up @@ -88,6 +89,14 @@ def mip_rna_order(mip_rna_order_to_submit: dict) -> MipRnaOrder:
return mip_rna_order


@pytest.fixture
def mutant_order(sarscov2_order_to_submit: dict, ticket_id_as_int: int) -> MutantOrder:
"""Parse mutant order example."""
order = MutantOrder.model_validate(sarscov2_order_to_submit)
order._generated_ticket_id = ticket_id_as_int
return order


@pytest.fixture
def pacbio_order(pacbio_order_to_submit: dict, ticket_id_as_int: int) -> PacbioOrder:
order = PacbioOrder.model_validate(pacbio_order_to_submit)
Expand Down
Loading

0 comments on commit 549d59d

Please sign in to comment.