11import { Request , Response } from "express" ;
2+ import { QueryTypes } from "sequelize" ;
23import { ControlEU } from "../domain.layer/frameworks/EU-AI-Act/controlEU.model" ;
4+ import { notifyUserAssigned , AssignmentRoleType } from "../services/inAppNotification.service" ;
35import { FileType } from "../domain.layer/models/file/file.model" ;
46import { uploadFile } from "../utils/fileUpload.utils" ;
57import {
@@ -34,6 +36,112 @@ import {
3436import logger from "../utils/logger/fileLogger" ;
3537import { hasPendingApprovalQuery } from "../utils/approvalRequest.utils" ;
3638
39+ // Helper function to get user name
40+ async function getUserNameById ( userId : number ) : Promise < string > {
41+ const result = await sequelize . query < { name : string ; surname : string } > (
42+ `SELECT name, surname FROM public.users WHERE id = :userId` ,
43+ { replacements : { userId } , type : QueryTypes . SELECT }
44+ ) ;
45+ if ( result [ 0 ] ) {
46+ return `${ result [ 0 ] . name } ${ result [ 0 ] . surname } ` . trim ( ) ;
47+ }
48+ return "Someone" ;
49+ }
50+
51+ // Helper function to notify assignment changes for EU AI Act entities
52+ async function notifyEuAiActAssignment (
53+ req : Request | RequestWithFile ,
54+ entityType : "EU AI Act Subcontrol" ,
55+ entityId : number ,
56+ entityName : string ,
57+ roleType : AssignmentRoleType ,
58+ newUserId : number ,
59+ oldUserId : number | null | undefined ,
60+ projectId ?: number ,
61+ controlId ?: number
62+ ) : Promise < void > {
63+ // Only notify if assigned to a new user
64+ if ( newUserId && newUserId !== oldUserId ) {
65+ const assignerName = await getUserNameById ( req . userId ! ) ;
66+ const baseUrl = process . env . FRONTEND_URL || "http://localhost:5173" ;
67+
68+ let urlPath : string ;
69+ let controlName : string | undefined ;
70+ let projectName : string | undefined ;
71+ let description : string | undefined ;
72+ let resolvedControlId = controlId ;
73+
74+ // Query for subcontrol description and order info from struct table
75+ const subcontrolResult = await sequelize . query < { description : string ; control_id : number ; subcontrol_order_no : number ; control_order_no : number } > (
76+ `SELECT scs.description, sc.control_id, scs.order_no as subcontrol_order_no, cs.order_no as control_order_no
77+ FROM "${ req . tenantId ! } ".subcontrols_eu sc
78+ JOIN public.subcontrols_struct_eu scs ON sc.subcontrol_meta_id = scs.id
79+ JOIN public.controls_struct_eu cs ON scs.control_id = cs.id
80+ WHERE sc.id = :entityId` ,
81+ { replacements : { entityId } , type : QueryTypes . SELECT }
82+ ) ;
83+ description = subcontrolResult [ 0 ] ?. description ;
84+ // Build subcontrol identifier like "1.1 Subcontrol title" (control.order_no.subcontrol.order_no)
85+ if ( subcontrolResult [ 0 ] ) {
86+ entityName = `${ subcontrolResult [ 0 ] . control_order_no } .${ subcontrolResult [ 0 ] . subcontrol_order_no } ${ entityName } ` ;
87+ }
88+ if ( ! controlId && subcontrolResult [ 0 ] ?. control_id ) {
89+ resolvedControlId = subcontrolResult [ 0 ] . control_id ;
90+ }
91+
92+ // Query for additional context: control name and project name
93+ if ( projectId ) {
94+ // Get project name
95+ const projectResult = await sequelize . query < { project_title : string } > (
96+ `SELECT project_title FROM "${ req . tenantId ! } ".projects WHERE id = :projectId` ,
97+ { replacements : { projectId } , type : QueryTypes . SELECT }
98+ ) ;
99+ projectName = projectResult [ 0 ] ?. project_title ;
100+
101+ // Get control name from struct table
102+ if ( resolvedControlId ) {
103+ const controlResult = await sequelize . query < { title : string } > (
104+ `SELECT cs.title
105+ FROM "${ req . tenantId ! } ".controls_eu c
106+ JOIN public.controls_struct_eu cs ON c.control_meta_id = cs.id
107+ WHERE c.id = :controlId` ,
108+ { replacements : { controlId : resolvedControlId } , type : QueryTypes . SELECT }
109+ ) ;
110+ controlName = controlResult [ 0 ] ?. title ;
111+ }
112+ }
113+
114+ if ( projectId && resolvedControlId ) {
115+ urlPath = `/project-view?projectId=${ projectId } &tab=frameworks&framework=eu-ai-act&subtab=compliance&controlId=${ resolvedControlId } &subControlId=${ entityId } ` ;
116+ } else if ( projectId ) {
117+ urlPath = `/project-view?projectId=${ projectId } &tab=frameworks&framework=eu-ai-act&subtab=compliance` ;
118+ } else {
119+ urlPath = `/project-view` ;
120+ }
121+
122+ notifyUserAssigned (
123+ req . tenantId ! ,
124+ newUserId ,
125+ {
126+ entityType,
127+ entityId,
128+ entityName,
129+ roleType,
130+ entityUrl : `${ baseUrl } ${ urlPath } ` ,
131+ } ,
132+ assignerName ,
133+ baseUrl ,
134+ {
135+ frameworkName : "EU AI Act" ,
136+ projectName,
137+ parentType : controlName ? "Control" : undefined ,
138+ parentName : controlName ,
139+ description,
140+ }
141+ ) . catch ( ( err ) => console . error ( `Failed to send ${ roleType } notification:` , err ) ) ;
142+ }
143+ }
144+
37145export async function getAssessmentsByProjectId (
38146 req : Request ,
39147 res : Response
@@ -322,8 +430,32 @@ export async function saveControls(
322430
323431 // now we need to iterate over subcontrols inside the control, and create a subcontrol for each subcontrol
324432 const subControlResp = [ ] ;
433+ // Track assignment changes for notifications (sent after transaction commits)
434+ const assignmentChanges : Array < {
435+ subcontrolId : number ;
436+ entityName : string ;
437+ roleType : AssignmentRoleType ;
438+ newUserId : number ;
439+ oldUserId : number | null ;
440+ } > = [ ] ;
441+
325442 if ( Control . subControls ) {
326443 for ( const subcontrol of JSON . parse ( Control . subControls ) ) {
444+ // Get current subcontrol data for assignment change detection
445+ const currentSubcontrolResult = ( await sequelize . query (
446+ `SELECT sc.owner, sc.reviewer, sc.approver, scs.title
447+ FROM "${ req . tenantId ! } ".subcontrols_eu sc
448+ JOIN public.subcontrols_struct_eu scs ON sc.subcontrol_meta_id = scs.id
449+ WHERE sc.id = :id;` ,
450+ {
451+ replacements : { id : parseInt ( subcontrol . id ) } ,
452+ transaction,
453+ type : QueryTypes . SELECT ,
454+ }
455+ ) ) as { owner : number | null ; reviewer : number | null ; approver : number | null ; title : string } [ ] ;
456+
457+ const currentSubcontrol = currentSubcontrolResult [ 0 ] || { owner : null , reviewer : null , approver : null , title : '' } ;
458+
327459 const evidenceFiles = ( ( req . files as UploadedFile [ ] ) || [ ] ) . filter (
328460 ( f ) => f . fieldname === `evidence_files_${ parseInt ( subcontrol . id ) } `
329461 ) ;
@@ -404,6 +536,22 @@ export async function saveControls(
404536 ) ;
405537 if ( subcontrolToSave ) {
406538 subControlResp . push ( subcontrolToSave ) ;
539+
540+ // Track assignment changes for notification
541+ const entityName = currentSubcontrol . title || `Subcontrol #${ subcontrol . id } ` ;
542+ const newOwner = subcontrol . owner ? parseInt ( String ( subcontrol . owner ) ) : null ;
543+ const newReviewer = subcontrol . reviewer ? parseInt ( String ( subcontrol . reviewer ) ) : null ;
544+ const newApprover = subcontrol . approver ? parseInt ( String ( subcontrol . approver ) ) : null ;
545+
546+ if ( newOwner && newOwner !== currentSubcontrol . owner ) {
547+ assignmentChanges . push ( { subcontrolId : parseInt ( subcontrol . id ) , entityName, roleType : "Owner" , newUserId : newOwner , oldUserId : currentSubcontrol . owner } ) ;
548+ }
549+ if ( newReviewer && newReviewer !== currentSubcontrol . reviewer ) {
550+ assignmentChanges . push ( { subcontrolId : parseInt ( subcontrol . id ) , entityName, roleType : "Reviewer" , newUserId : newReviewer , oldUserId : currentSubcontrol . reviewer } ) ;
551+ }
552+ if ( newApprover && newApprover !== currentSubcontrol . approver ) {
553+ assignmentChanges . push ( { subcontrolId : parseInt ( subcontrol . id ) , entityName, roleType : "Approver" , newUserId : newApprover , oldUserId : currentSubcontrol . approver } ) ;
554+ }
407555 }
408556 }
409557 }
@@ -419,6 +567,21 @@ export async function saveControls(
419567 ) ;
420568 await transaction . commit ( ) ;
421569
570+ // Send assignment notifications after transaction commits
571+ for ( const change of assignmentChanges ) {
572+ notifyEuAiActAssignment (
573+ req ,
574+ "EU AI Act Subcontrol" ,
575+ change . subcontrolId ,
576+ change . entityName ,
577+ change . roleType ,
578+ change . newUserId ,
579+ change . oldUserId ,
580+ Control . project_id ,
581+ controlId
582+ ) ;
583+ }
584+
422585 await logSuccess ( {
423586 eventType : "Update" ,
424587 description : `Successfully saved controls for control ID ${ controlId } ` ,
0 commit comments