diff --git a/compliance-api/src/compliance_api/models/order.py b/compliance-api/src/compliance_api/models/order.py index 1e0f4b4b..ac1fff02 100644 --- a/compliance-api/src/compliance_api/models/order.py +++ b/compliance-api/src/compliance_api/models/order.py @@ -3,7 +3,7 @@ import enum from sqlalchemy import Boolean, Column, DateTime, Enum, ForeignKey, Index, Integer, String -from sqlalchemy.orm import relationship +from sqlalchemy.orm import joinedload, relationship from compliance_api.models.inspection import InspectionRequirement from compliance_api.utils.constant import DELETE_DIC_PARAMS @@ -284,18 +284,24 @@ def get_orders_by_case_files(cls, case_file_ids: list[int]): @classmethod def get_by_inspection_id(cls, inspection_id): - """Find all orders by inspection id that have entries in the requirement map for the given inspection.""" - return ( - cls.query.join( - OrderInspectionRequirementMap, - OrderInspectionRequirementMap.order_id - == cls.id, - ) + """Find all orders by inspection id that have entries in the requirement map for the given inspection. + + Uses a two-query approach to ensure all requirement maps are loaded for each order, + not just the ones for the queried inspection (important for linked orders). + """ + # First, get order IDs that have at least one requirement for this inspection + # Using a completely separate query to avoid contaminating the relationship context + order_ids = ( + db.session.query(OrderInspectionRequirementMap.order_id) .join( InspectionRequirement, InspectionRequirement.id == OrderInspectionRequirementMap.inspection_requirement_id, ) + .join( + cls, + cls.id == OrderInspectionRequirementMap.order_id, + ) .filter( InspectionRequirement.inspection_id == inspection_id, OrderInspectionRequirementMap.is_active.is_(True), @@ -303,6 +309,25 @@ def get_by_inspection_id(cls, inspection_id): cls.is_deleted.is_(False), ) .distinct() + .all() + ) + + # Extract just the IDs from the result tuples + order_id_list = [order_id[0] for order_id in order_ids] + + if not order_id_list: + return [] + + # Expire all objects in the session to ensure fresh load + # This prevents SQLAlchemy from using cached relationship data + db.session.expire_all() + + # Load the full orders with ALL their requirement maps using a fresh query + # Use joinedload to eagerly load all requirement maps without any filters + return ( + db.session.query(cls) + .filter(cls.id.in_(order_id_list)) + .options(joinedload(cls.order_requirement_maps)) .order_by(cls.created_date.desc()) .all() ) diff --git a/compliance-api/src/compliance_api/schemas/administrative_penalty.py b/compliance-api/src/compliance_api/schemas/administrative_penalty.py index f35f2b50..68a33a6a 100644 --- a/compliance-api/src/compliance_api/schemas/administrative_penalty.py +++ b/compliance-api/src/compliance_api/schemas/administrative_penalty.py @@ -102,7 +102,7 @@ class Meta(AutoSchemaBase.Meta): # pylint: disable=too-few-public-methods inspection_requirement = fields.Nested( InspectionRequirementSchema(), - only=("id", "summary", "inspection_id"), + only=("id", "summary", "inspection_id", "requirement_source_details"), ) diff --git a/compliance-api/src/compliance_api/schemas/charge_recommendation.py b/compliance-api/src/compliance_api/schemas/charge_recommendation.py index 97140151..ec381993 100644 --- a/compliance-api/src/compliance_api/schemas/charge_recommendation.py +++ b/compliance-api/src/compliance_api/schemas/charge_recommendation.py @@ -133,7 +133,7 @@ class Meta: # pylint: disable=too-few-public-methods inspection_requirement = fields.Nested( InspectionRequirementSchema(), - only=("id", "summary"), + only=("id", "summary", "requirement_source_details"), ) diff --git a/compliance-api/src/compliance_api/schemas/order.py b/compliance-api/src/compliance_api/schemas/order.py index 81a91254..10e9b0cc 100644 --- a/compliance-api/src/compliance_api/schemas/order.py +++ b/compliance-api/src/compliance_api/schemas/order.py @@ -71,7 +71,7 @@ class Meta(AutoSchemaBase.Meta): # pylint: disable=too-few-public-methods inspection_requirement = fields.Nested( InspectionRequirementSchema(), - only=("id", "summary"), + only=("id", "summary", "requirement_source_details"), ) diff --git a/compliance-api/src/compliance_api/schemas/restorative_justice.py b/compliance-api/src/compliance_api/schemas/restorative_justice.py index 36357c3c..00b9c5cd 100644 --- a/compliance-api/src/compliance_api/schemas/restorative_justice.py +++ b/compliance-api/src/compliance_api/schemas/restorative_justice.py @@ -60,7 +60,7 @@ class Meta: # pylint: disable=too-few-public-methods inspection_requirement = fields.Nested( InspectionRequirementSchema(), - only=("id", "summary"), + only=("id", "summary", "requirement_source_details"), ) inspection_requirement_id = fields.Integer() diff --git a/compliance-api/src/compliance_api/schemas/violation_ticket.py b/compliance-api/src/compliance_api/schemas/violation_ticket.py index bc067377..ea1e5595 100644 --- a/compliance-api/src/compliance_api/schemas/violation_ticket.py +++ b/compliance-api/src/compliance_api/schemas/violation_ticket.py @@ -82,7 +82,7 @@ class Meta(AutoSchemaBase.Meta): # pylint: disable=too-few-public-methods inspection_requirement = fields.Nested( InspectionRequirementSchema(), - only=("id", "summary"), + only=("id", "summary", "requirement_source_details"), ) diff --git a/compliance-api/src/compliance_api/schemas/warning_letter.py b/compliance-api/src/compliance_api/schemas/warning_letter.py index 28a1e4e2..0961643e 100644 --- a/compliance-api/src/compliance_api/schemas/warning_letter.py +++ b/compliance-api/src/compliance_api/schemas/warning_letter.py @@ -63,7 +63,7 @@ class Meta(AutoSchemaBase.Meta): # pylint: disable=too-few-public-methods inspection_requirement = fields.Nested( InspectionRequirementSchema(), - only=("id", "summary"), + only=("id", "summary", "requirement_source_details"), ) diff --git a/compliance-api/src/compliance_api/services/order/order.py b/compliance-api/src/compliance_api/services/order/order.py index 41f653d4..be3841bd 100644 --- a/compliance-api/src/compliance_api/services/order/order.py +++ b/compliance-api/src/compliance_api/services/order/order.py @@ -76,6 +76,7 @@ def create_order(cls, order_data: dict) -> OrderModel: created_order = OrderModel.create(order_obj, session) cls.insert_or_update_inspection_requirements( created_order.id, + inspection_id, order_data.get("inspection_requirement_ids", []), session, ) @@ -122,6 +123,7 @@ def update_order(cls, order_id: int, update_data: dict) -> OrderModel: updated_order = OrderModel.update_order(order_id, update_data, session) cls.insert_or_update_inspection_requirements( updated_order.id, + inspection.id, update_data.get("inspection_requirement_ids", []), session, ) @@ -153,12 +155,17 @@ def delete_order(cls, order_id: int) -> OrderModel: @classmethod def insert_or_update_inspection_requirements( - cls, order_id: int, inspection_requirement_ids: list[int], session=None + cls, order_id: int, inspection_id: int, inspection_requirement_ids: list[int], session=None ): - """Insert/Update inspection requirements associated with a given order.""" + """Insert/Update inspection requirements associated with a given order for a specific inspection. + + This method only modifies requirement maps that belong to the specified inspection_id, + leaving requirement maps from other inspections (in case of linked orders) untouched. + """ if inspection_requirement_ids is not None: - existing_requirements = OrderInspectionRequirementMapModel.get_by_order_id( - order_id + # Only get existing requirements for this specific inspection + existing_requirements = OrderInspectionRequirementMapModel.get_by_inspection_and_order_id( + inspection_id, order_id ) existing_requirement_ids = { req.inspection_requirement_id for req in existing_requirements @@ -292,7 +299,8 @@ def link(cls, order_id: int, link: dict) -> OrderModel: "Order is already linked to the given inspection requirements" ) with session_scope() as session: - cls.insert_or_update_inspection_requirements( + # Use bulk_insert to ADD requirements without deleting existing ones + OrderInspectionRequirementMapModel.bulk_insert( order_id, requirement_ids, session ) return order diff --git a/compliance-web/src/components/App/Inspections/Profile/Enforcements/EnforcementUtils.ts b/compliance-web/src/components/App/Inspections/Profile/Enforcements/EnforcementUtils.ts index 44a88975..ff83bbee 100644 --- a/compliance-web/src/components/App/Inspections/Profile/Enforcements/EnforcementUtils.ts +++ b/compliance-web/src/components/App/Inspections/Profile/Enforcements/EnforcementUtils.ts @@ -152,70 +152,83 @@ export const formatRequirementSources = ( violationTicket?: ViolationTicket, restorativeJustice?: RestorativeJustice ): string[] => { - const orderRequirementIds = order?.order_requirement_maps?.map( - (map) => map.inspection_requirement_id - ); - const warningLetterRequirementIds = - warningLetter?.warning_letter_requirement_maps?.map( - (map) => map.inspection_requirement_id - ); + // Collect all requirement maps from all enforcement types + const allRequirementMaps = [ + ...(order?.order_requirement_maps || []).map(map => map.inspection_requirement), + ...(warningLetter?.warning_letter_requirement_maps || []).map(map => map.inspection_requirement), + ...(administrativePenalty?.administrative_penalty_requirement_maps || []).map(map => map.inspection_requirement), + ...(chargeRecommendation?.charge_recommendation_requirement_maps || []).map(map => map.inspection_requirement), + ...(violationTicket?.violation_ticket_requirement_maps || []).map(map => map.inspection_requirement), + ...(restorativeJustice?.restorative_justice_requirement_maps || []).map(map => map.inspection_requirement), + ]; - const administrativePenaltyRequirementIds = administrativePenalty?.administrative_penalty_requirement_maps?.map( - (map) => map.inspection_requirement_id + // Check if any requirement has source details from API + const hasSourceDetailsFromAPI = allRequirementMaps.some( + req => req.requirement_source_details && req.requirement_source_details.length > 0 ); - const chargeRecommendationRequirementIds = chargeRecommendation?.charge_recommendation_requirement_maps?.map( - (map) => map.inspection_requirement_id - ); + const result: string[] = []; - const violationTicketRequirementIds = violationTicket?.violation_ticket_requirement_maps?.map( - (map) => map.inspection_requirement_id - ); + if (hasSourceDetailsFromAPI) { + allRequirementMaps.forEach((requirement) => { + const sourceMap = new Map(); - const restorativeJusticeRequirementIds = restorativeJustice?.restorative_justice_requirement_maps?.map( - (map) => map.inspection_requirement_id - ); + requirement.requirement_source_details?.forEach((source) => { + const sourceId = source.requirement_source_id; + const sourceName = source.requirement_source?.name || ""; + const number = source.condition_number ?? source.section_number ?? ""; - const requirementIds = [ - ...(orderRequirementIds || []), - ...(warningLetterRequirementIds || []), - ...(administrativePenaltyRequirementIds || []), - ...(chargeRecommendationRequirementIds || []), - ...(violationTicketRequirementIds || []), - ...(restorativeJusticeRequirementIds || []), - ]; + if (!sourceMap.has(sourceId)) { + sourceMap.set(sourceId, { name: sourceName, numbers: [] }); + } - const requirements = requirementEnforcements.filter((requirement) => - requirementIds?.includes(requirement.id) - ); + if (number) { + sourceMap.get(sourceId)?.numbers.push(`#${number.trim()}`); + } + }); - const result: string[] = []; + sourceMap.forEach((value) => { + if (value.numbers.length > 0) { + result.push(`${value.name}, ${value.numbers.join(", ")}`); + } else { + result.push(value.name); + } + }); + }); + } else { + // Fallback to the old way if no source details are available from API for data safety + const requirementIds = allRequirementMaps.map(req => req.id); + const requirements = requirementEnforcements.filter((requirement) => + requirementIds?.includes(requirement.id) + ); - requirements.forEach((requirement) => { - const sourceMap = new Map(); + requirements.forEach((requirement) => { + const sourceMap = new Map(); - requirement.requirement_source_details.forEach((source) => { - const sourceId = source.requirement_source_id; - const sourceName = source.requirement_source?.name || ""; - const number = source.condition_number ?? source.section_number ?? ""; + requirement.requirement_source_details?.forEach((source) => { + const sourceId = source.requirement_source_id; + const sourceName = source.requirement_source?.name || ""; + const number = source.condition_number ?? source.section_number ?? ""; - if (!sourceMap.has(sourceId)) { - sourceMap.set(sourceId, { name: sourceName, numbers: [] }); - } + if (!sourceMap.has(sourceId)) { + sourceMap.set(sourceId, { name: sourceName, numbers: [] }); + } - if (number) { - sourceMap.get(sourceId)?.numbers.push(`#${number.trim()}`); - } - }); + if (number) { + sourceMap.get(sourceId)?.numbers.push(`#${number.trim()}`); + } + }); - sourceMap.forEach((value) => { - if (value.numbers.length > 0) { - result.push(`${value.name}, ${value.numbers.join(", ")}`); - } else { - result.push(value.name); - } + sourceMap.forEach((value) => { + if (value.numbers.length > 0) { + result.push(`${value.name}, ${value.numbers.join(", ")}`); + } else { + result.push(value.name); + } + }); }); - }); + } + return result; }; diff --git a/compliance-web/src/components/App/Inspections/Profile/Enforcements/Orders/OrderCreationOptions.tsx b/compliance-web/src/components/App/Inspections/Profile/Enforcements/Orders/OrderCreationOptions.tsx index 367f0c40..8a487341 100644 --- a/compliance-web/src/components/App/Inspections/Profile/Enforcements/Orders/OrderCreationOptions.tsx +++ b/compliance-web/src/components/App/Inspections/Profile/Enforcements/Orders/OrderCreationOptions.tsx @@ -137,8 +137,7 @@ const OrderCreationOptions: FC = ({ }} /> - {/* Hiding linking for now */} - {false && } label={ @@ -162,7 +161,7 @@ const OrderCreationOptions: FC = ({ mb: 2, alignItems: 'flex-start' }} - />} + /> {/* Existing Order Selection */} diff --git a/compliance-web/src/models/AdministrativePenalty.ts b/compliance-web/src/models/AdministrativePenalty.ts index 9b3e7bd5..dc5fb845 100644 --- a/compliance-web/src/models/AdministrativePenalty.ts +++ b/compliance-web/src/models/AdministrativePenalty.ts @@ -1,5 +1,6 @@ import { Inspection } from './Inspection'; import { InspectionRequirement } from './InspectionRequirement'; +import { InspectionRequirementSource } from './InspectionRequirementSource'; import { Option } from './common'; export interface AdministrativePenalty { @@ -24,6 +25,7 @@ interface AdministrativePenaltyRequirementMap { id: number; summary: string; inspection_id: number; + requirement_source_details: InspectionRequirementSource[]; }; } diff --git a/compliance-web/src/models/ChargeRecommendation.ts b/compliance-web/src/models/ChargeRecommendation.ts index e61eb472..ef244582 100644 --- a/compliance-web/src/models/ChargeRecommendation.ts +++ b/compliance-web/src/models/ChargeRecommendation.ts @@ -1,3 +1,4 @@ +import { InspectionRequirementSource } from "./InspectionRequirementSource"; import { Option } from "./common"; export interface ChargeRecommendation { @@ -25,6 +26,7 @@ interface ChargeRecommendationRequirementMap { inspection_requirement: { id: number; summary: string; + requirement_source_details: InspectionRequirementSource[]; }; } diff --git a/compliance-web/src/models/InspectionOrder.ts b/compliance-web/src/models/InspectionOrder.ts index d686cee9..05a22f35 100644 --- a/compliance-web/src/models/InspectionOrder.ts +++ b/compliance-web/src/models/InspectionOrder.ts @@ -1,6 +1,8 @@ import { OrderApproval } from "./OrderApproval"; import { StaffUser } from "./Staff"; +import { InspectionRequirementSource } from "./InspectionRequirementSource"; + export interface InspectionOrder { issuing_officer?: StaffUser; section?: { @@ -40,6 +42,7 @@ interface OrderRequirementMap { inspection_requirement: { id: number; summary: string; + requirement_source_details: InspectionRequirementSource[]; }; } diff --git a/compliance-web/src/models/InspectionWarningLetter.ts b/compliance-web/src/models/InspectionWarningLetter.ts index fb2f7ff9..b382dd06 100644 --- a/compliance-web/src/models/InspectionWarningLetter.ts +++ b/compliance-web/src/models/InspectionWarningLetter.ts @@ -1,3 +1,4 @@ +import { InspectionRequirementSource } from "./InspectionRequirementSource"; import { StaffUser } from "./Staff"; import { WarningLetterApproval } from "./WarningLetterApproval"; @@ -33,6 +34,7 @@ interface WarningLetterRequirementMap { inspection_requirement: { id: number; summary: string; + requirement_source_details: InspectionRequirementSource[]; }; } diff --git a/compliance-web/src/models/RestorativeJustice.ts b/compliance-web/src/models/RestorativeJustice.ts index 18234bd5..2dcbb298 100644 --- a/compliance-web/src/models/RestorativeJustice.ts +++ b/compliance-web/src/models/RestorativeJustice.ts @@ -1,3 +1,5 @@ +import { InspectionRequirementSource } from "./InspectionRequirementSource"; + export interface RestorativeJusticeRequirementMap { id: number; restorative_justice_id: number; @@ -5,6 +7,7 @@ export interface RestorativeJusticeRequirementMap { inspection_requirement: { id: number; summary: string; + requirement_source_details: InspectionRequirementSource[] }; } diff --git a/compliance-web/src/models/ViolationTicket.ts b/compliance-web/src/models/ViolationTicket.ts index a5d8e8be..c869f57a 100644 --- a/compliance-web/src/models/ViolationTicket.ts +++ b/compliance-web/src/models/ViolationTicket.ts @@ -1,3 +1,5 @@ +import { InspectionRequirementSource } from "./InspectionRequirementSource"; + export interface ViolationTicket { violation_ticket_requirement_maps: ViolationTicketRequirementMap[]; id: number; @@ -19,6 +21,7 @@ interface ViolationTicketRequirementMap { inspection_requirement: { id: number; summary: string; + requirement_source_details: InspectionRequirementSource[]; }; }