@@ -421,6 +421,176 @@ describe('RecreationAssetService', () => {
421421 } ) ;
422422 } ) ;
423423
424+ describe ( 'bulkCreateAssets' , ( ) => {
425+ it ( 'should validate all unique asset codes then create all assets in a transaction' , async ( ) => {
426+ vi . spyOn ( service , 'findAssetCodeById' ) . mockResolvedValue ( {
427+ asset_code : 1 ,
428+ description : 'Bridge' ,
429+ } ) ;
430+
431+ const createdAssets = [
432+ {
433+ asset_id : 10n ,
434+ parent_id : null ,
435+ asset_tag : null ,
436+ rec_resource_id : 'REC-1' ,
437+ asset_code : 1 ,
438+ asset_name : 'Asset A' ,
439+ asset_comment : null ,
440+ legacy_structure_id : null ,
441+ asset_length : null ,
442+ asset_width : null ,
443+ asset_area : null ,
444+ actual_value : null ,
445+ installation_date : null ,
446+ } ,
447+ {
448+ asset_id : 11n ,
449+ parent_id : null ,
450+ asset_tag : null ,
451+ rec_resource_id : 'REC-1' ,
452+ asset_code : 1 ,
453+ asset_name : 'Asset B' ,
454+ asset_comment : null ,
455+ legacy_structure_id : null ,
456+ asset_length : null ,
457+ asset_width : null ,
458+ asset_area : null ,
459+ actual_value : null ,
460+ installation_date : null ,
461+ } ,
462+ ] ;
463+
464+ prismaMock . $transaction . mockResolvedValue ( createdAssets ) ;
465+
466+ const dto = {
467+ assets : [
468+ { asset_code : 1 , rec_resource_id : 'REC-1' , asset_name : 'Asset A' } ,
469+ { asset_code : 1 , rec_resource_id : 'REC-1' , asset_name : 'Asset B' } ,
470+ ] ,
471+ } ;
472+
473+ const result = await service . bulkCreateAssets ( dto as any ) ;
474+
475+ // Only one unique code (1), so findAssetCodeById called once
476+ expect ( service . findAssetCodeById ) . toHaveBeenCalledTimes ( 1 ) ;
477+ expect ( service . findAssetCodeById ) . toHaveBeenCalledWith ( 1 ) ;
478+
479+ expect ( prismaMock . $transaction ) . toHaveBeenCalledTimes ( 1 ) ;
480+
481+ expect ( result ) . toHaveLength ( 2 ) ;
482+ expect ( result [ 0 ] . asset_id ) . toBe ( 10 ) ;
483+ expect ( result [ 1 ] . asset_id ) . toBe ( 11 ) ;
484+ } ) ;
485+
486+ it ( 'should call findAssetCodeById for each unique asset_code' , async ( ) => {
487+ vi . spyOn ( service , 'findAssetCodeById' ) . mockResolvedValue ( {
488+ asset_code : 1 ,
489+ description : 'Bridge' ,
490+ } ) ;
491+
492+ prismaMock . $transaction . mockResolvedValue ( [
493+ {
494+ asset_id : 20n ,
495+ rec_resource_id : 'REC-2' ,
496+ asset_code : 2 ,
497+ parent_id : null ,
498+ asset_tag : null ,
499+ asset_name : null ,
500+ asset_comment : null ,
501+ legacy_structure_id : null ,
502+ asset_length : null ,
503+ asset_width : null ,
504+ asset_area : null ,
505+ actual_value : null ,
506+ installation_date : null ,
507+ } ,
508+ ] ) ;
509+
510+ const dto = {
511+ assets : [
512+ { asset_code : 1 , rec_resource_id : 'REC-2' } ,
513+ { asset_code : 2 , rec_resource_id : 'REC-2' } ,
514+ { asset_code : 1 , rec_resource_id : 'REC-2' } , // duplicate
515+ ] ,
516+ } ;
517+
518+ await service . bulkCreateAssets ( dto as any ) ;
519+
520+ // 2 unique codes: 1 and 2
521+ expect ( service . findAssetCodeById ) . toHaveBeenCalledTimes ( 2 ) ;
522+ } ) ;
523+
524+ it ( 'should throw NotFoundException if any asset code is invalid' , async ( ) => {
525+ vi . spyOn ( service , 'findAssetCodeById' ) . mockRejectedValue (
526+ new NotFoundException ( 'Asset code not found' ) ,
527+ ) ;
528+
529+ const dto = {
530+ assets : [ { asset_code : 999 , rec_resource_id : 'REC-1' } ] ,
531+ } ;
532+
533+ await expect ( service . bulkCreateAssets ( dto as any ) ) . rejects . toThrow ( ) ;
534+ } ) ;
535+
536+ it ( 'should map all returned assets including null optional fields' , async ( ) => {
537+ vi . spyOn ( service , 'findAssetCodeById' ) . mockResolvedValue ( {
538+ asset_code : 5 ,
539+ description : 'Bench' ,
540+ } ) ;
541+
542+ prismaMock . $transaction . mockResolvedValue ( [
543+ {
544+ asset_id : 30n ,
545+ parent_id : 5n ,
546+ asset_tag : 'T-30' ,
547+ rec_resource_id : 'REC-3' ,
548+ asset_code : 5 ,
549+ asset_name : 'My Bench' ,
550+ asset_comment : 'A comment' ,
551+ legacy_structure_id : 'LEG-30' ,
552+ asset_length : 2.5 ,
553+ asset_width : 1.2 ,
554+ asset_area : 3.0 ,
555+ actual_value : 750 ,
556+ installation_date : new Date ( '2022-06-15T00:00:00.000Z' ) ,
557+ } ,
558+ ] ) ;
559+
560+ const dto = {
561+ assets : [
562+ {
563+ asset_code : 5 ,
564+ rec_resource_id : 'REC-3' ,
565+ parent_id : 5 ,
566+ asset_tag : 'T-30' ,
567+ asset_name : 'My Bench' ,
568+ asset_comment : 'A comment' ,
569+ legacy_structure_id : 'LEG-30' ,
570+ asset_length : 2.5 ,
571+ asset_width : 1.2 ,
572+ asset_area : 3.0 ,
573+ actual_value : 750 ,
574+ installation_date : '2022-06-15' ,
575+ } ,
576+ ] ,
577+ } ;
578+
579+ const result = await service . bulkCreateAssets ( dto as any ) ;
580+
581+ expect ( result [ 0 ] ) . toMatchObject ( {
582+ asset_id : 30 ,
583+ parent_id : 5 ,
584+ asset_tag : 'T-30' ,
585+ rec_resource_id : 'REC-3' ,
586+ asset_code : 5 ,
587+ asset_name : 'My Bench' ,
588+ actual_value : 750 ,
589+ installation_date : '2022-06-15' ,
590+ } ) ;
591+ } ) ;
592+ } ) ;
593+
424594 describe ( 'RecreationAssetService - findAllAssets' , ( ) => {
425595 // eslint-disable-next-line @typescript-eslint/no-unused-vars
426596 let prismaService : PrismaService ;
0 commit comments