@@ -4,9 +4,6 @@ import { createHash } from 'node:crypto';
44
55import { UfabcParserConnector } from '@/connectors/ufabc-parser.js' ;
66import { ComponentModel , type Component } from '@/models/Component.js' ;
7- import { SubjectModel } from '@/models/Subject.js' ;
8- import { TeacherModel } from '@/models/Teacher.js' ;
9- import { syncComponentsSchema } from '@/schemas/sync/components.js' ;
107import { syncEnrolledSchema } from '@/schemas/sync/enrolled.js' ;
118import { syncEnrollmentsSchema } from '@/schemas/sync/enrollments.js' ;
129
@@ -170,227 +167,6 @@ const plugin: FastifyPluginAsyncZodOpenApi = async (app) => {
170167 }
171168 ) ;
172169
173- app . put (
174- '/components' ,
175- {
176- schema : syncComponentsSchema ,
177- preHandler : ( request , reply ) => request . isAdmin ( reply ) ,
178- } ,
179- async ( request , reply ) => {
180- const { season, hash, ignoreErrors } = request . body ;
181- const componentsWithTeachers = await connector . getComponentsV2 ( season ) ;
182-
183- const teacherCache = new Map ( ) ;
184- const errors : Array < SyncError > = [ ] ;
185-
186- const findTeacher = async ( name : string | null ) => {
187- if ( ! name ) return null ;
188- const normalizedName = name
189- . toLowerCase ( )
190- . normalize ( 'NFD' )
191- . replace ( / \u0300 - \u036f / g, '' ) ;
192- if ( teacherCache . has ( normalizedName ) )
193- return teacherCache . get ( normalizedName ) ;
194- // @ts -ignore Complex Type Mismatch
195- const teacher = await TeacherModel . findByFuzzName ( normalizedName ) ;
196- if ( ! teacher && normalizedName !== '0' ) {
197- app . log . warn ( {
198- originalName : name ,
199- normalizedName,
200- } , 'Teacher not found' ) ;
201- errors . push ( {
202- original : name ,
203- parserError : [ 'Teacher not found in database' ] ,
204- metadata : { normalizedName } ,
205- type : 'TEACHER_NOT_FOUND' ,
206- } ) ;
207- teacherCache . set ( normalizedName , null ) ;
208- return null ;
209- }
210- if ( teacher && ! teacher . alias . includes ( normalizedName ) ) {
211- await TeacherModel . findByIdAndUpdate ( teacher . _id , {
212- $addToSet : { alias : [ normalizedName , name . toLowerCase ( ) ] } ,
213- } ) ;
214- }
215- teacherCache . set ( normalizedName , teacher ?. _id ?? null ) ;
216- return teacher ?. _id ?? null ;
217- } ;
218-
219- const components = await Promise . all (
220- componentsWithTeachers . map ( async ( c ) => {
221- const professorTeacher = c . teachers . find (
222- ( t ) => t . role === 'professor' && ! t . isSecondary
223- ) ;
224- const practiceTeacher = c . teachers . find (
225- ( t ) => t . role === 'practice' && ! t . isSecondary
226- ) ;
227-
228- const [ teoria , pratica ] = await Promise . all ( [
229- findTeacher ( professorTeacher ?. name ?? null ) ,
230- findTeacher ( practiceTeacher ?. name ?? null ) ,
231- ] ) ;
232- const dbComponent = await ComponentModel . findOne ( {
233- season,
234- $or : [
235- { uf_cod_turma : c . ufClassroomCode . toUpperCase ( ) } ,
236- { uf_cod_turma : c . ufClassroomCode } ,
237- ] ,
238- } ) . lean ( ) ;
239- let subjectId = null ;
240- if ( ! dbComponent ) {
241- app . log . warn ( {
242- msg : 'Component not found in database' ,
243- uf_cod_turma : c . ufClassroomCode ,
244- } ) ;
245- errors . push ( {
246- original : c . ufClassroomCode ,
247- parserError : [ 'Component not found in database' ] ,
248- type : 'MATCHING_FAILED' ,
249- } ) ;
250- // Normalize the subject code (strip year, uppercase, etc.)
251- const codeMatch = c . ufComponentCode . match ( / ^ ( .* ?) - \d { 2 } $ / ) ;
252- const normalizedCode = codeMatch ? codeMatch [ 1 ] : c . ufComponentCode ;
253- const subject = await SubjectModel . findOne ( {
254- uf_subject_code : { $in : [ normalizedCode ] } ,
255- } ) . lean ( ) ;
256- if ( ! subject ) {
257- app . log . warn ( {
258- msg : 'Subject not found for unmatched component' ,
259- component : c ,
260- } ) ;
261- errors . push ( {
262- original : c . name ,
263- parserError : [ 'Subject not found for unmatched component' ] ,
264- type : 'MATCHING_FAILED' ,
265- } ) ;
266- }
267- subjectId = subject ?. _id ;
268- return {
269- disciplina_id : null ,
270- campus : c . campus ,
271- disciplina : c . name ,
272- turno : c . shift ,
273- turma : c . componentClass ,
274- uf_cod_turma : c . ufClassroomCode ,
275- year : Number ( season . split ( ':' ) [ 0 ] ) ,
276- quad : Number ( season . split ( ':' ) [ 1 ] ) ,
277- codigo : c . ufComponentCode ,
278- season,
279- teoria,
280- pratica,
281- subject : subjectId ,
282- after_kick : [ ] ,
283- before_kick : [ ] ,
284- alunos_matriculados : [ ] ,
285- obrigatorias : [ ] ,
286- ideal_quad : false ,
287- vagas : c . vacancies ,
288- kind : 'file' as 'file' | 'api' ,
289- flag : 'upsert' ,
290- ignoreErrors,
291- } ;
292- }
293- return {
294- ...dbComponent ,
295- campus : c . campus ,
296- disciplina : c . name ,
297- turno : c . shift ,
298- turma : c . componentClass ,
299- uf_cod_turma : c . ufClassroomCode ,
300- teoria,
301- pratica,
302- ignoreErrors,
303- } ;
304- } )
305- ) ;
306-
307- if ( ! ignoreErrors && errors . length > 0 ) {
308- const teacherErrors = errors . filter (
309- ( e ) => e . type === 'TEACHER_NOT_FOUND'
310- ) ;
311- const matchingErrors = errors . filter (
312- ( e ) => e . type === 'MATCHING_FAILED'
313- ) ;
314- return reply . status ( 403 ) . send ( {
315- msg : 'Errors found while verifying components' ,
316- errors : {
317- missingTeachers : [ ...new Set ( teacherErrors . map ( ( e ) => e . original ) ) ] ,
318- unmatchedComponents : matchingErrors ,
319- } ,
320- totalErrors : errors . length ,
321- breakdown : {
322- teacherErrors : teacherErrors . length ,
323- matchingErrors : matchingErrors . length ,
324- } ,
325- } ) ;
326- }
327-
328- const componentHash = createHash ( 'md5' )
329- . update ( JSON . stringify ( components ) )
330- . digest ( 'hex' ) ;
331- if ( componentHash !== hash ) {
332- return {
333- hash : componentHash ,
334- errors,
335- total : components . length ,
336- payload : components ,
337- } ;
338- }
339-
340- // Dispatch all jobs, always passing flag and ignoreErrors
341- const dispatchPromises = components . map ( async ( componentData ) => {
342- try {
343- if ( ! componentData . subject ) {
344- request . log . warn ( {
345- error : 'Component data is missing' ,
346- component : componentData . disciplina ,
347- msg : 'Component data is missing' ,
348- } ) ;
349- throw new Error ( 'Component data is missing' ) ;
350- }
351-
352- // fuck this
353- // @ts -expect-error
354- await app . job . dispatch ( 'ComponentsTeachersSync' , componentData ) ;
355- return { success : true , component : componentData . disciplina } ;
356- } catch ( error ) {
357- const errorMsg = `Failed to dispatch component: ${ componentData . disciplina } ` ;
358- if ( ignoreErrors ) {
359- request . log . warn ( {
360- error : error instanceof Error ? error . message : String ( error ) ,
361- component : componentData . disciplina ,
362- msg : `${ errorMsg } (ignored)` ,
363- } ) ;
364- return {
365- success : false ,
366- component : componentData . disciplina ,
367- ignored : true ,
368- } ;
369- }
370- request . log . error ( {
371- error : error instanceof Error ? error . message : String ( error ) ,
372- component : componentData . disciplina ,
373- msg : errorMsg ,
374- } ) ;
375- throw error ;
376- }
377- } ) ;
378- const dispatchResults = await Promise . all ( dispatchPromises ) ;
379- const successfulDispatches = dispatchResults . filter (
380- ( r ) => r . success
381- ) . length ;
382- const ignoredErrors = dispatchResults . filter (
383- ( r ) => ! r . success && r . ignored
384- ) . length ;
385- return reply . send ( {
386- dispatched : true ,
387- msg : 'Component teacher sync jobs dispatched' ,
388- totalComponents : components . length ,
389- successfulDispatches,
390- ignoredErrors : ignoreErrors ? ignoredErrors : 0 ,
391- } ) ;
392- }
393- ) ;
394170
395171 app . put (
396172 '/enrolled' ,
0 commit comments