Repository: Mutari-App/mutari-backend
Pull request #99: PBI 30 Duplicate and Edit Other's Itinerary
Author: login: mmalkindi
Created: 2025-04-21T16:02:20Z | Merged: 2025-04-28T03:00:05Z

### Changes:
- Added implementation for duplicating an existing itinerary and its contingency plans to a new user
  - `duplicateItinerary`: duplicates itinerary
  - `duplicateContingency`: duplicates contingency
- Added supporting endpoints for /itinerary/
  - `POST/id/duplicate`: duplicate itinerary and its contingencies
![image](https://github.com/user-attachments/assets/6d7f73d1-dcbc-4514-9fe3-9224acaed8d8)


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

## Summary by CodeRabbit

- **New Features**
  - Added the ability to duplicate an itinerary, including all associated sections, blocks, tags, and contingency plans, via a new endpoint.
- **Bug Fixes**
  - Improved error handling for duplication actions, ensuring appropriate responses for missing or unauthorized access.
- **Tests**
  - Introduced comprehensive tests for itinerary and contingency duplication, covering successful scenarios and error cases.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/src/itinerary/itinerary.controller.spec.ts b/src/itinerary/itinerary.controller.spec.ts
index 6785d4403..843baeea0 100644
--- a/src/itinerary/itinerary.controller.spec.ts
+++ b/src/itinerary/itinerary.controller.spec.ts
@@ -55,6 +55,8 @@ describe('ItineraryController', () => {
     createContingencyPlan: jest.fn(),
     selectContingencyPlan: jest.fn(),
     updateContingencyPlan: jest.fn(),
+    duplicateItinerary: jest.fn(),
+    duplicateContingency: jest.fn(),
     searchItineraries: jest.fn(),
     createViewItinerary: jest.fn(),
     getViewItinerary: jest.fn(),
@@ -1999,6 +2001,396 @@ describe('ItineraryController', () => {
     })
   })
 
+  describe('duplicateItineraryAndContingencies', () => {
+    it('should duplicate an itinerary and its contingencies and return a formatted response', async () => {
+      const itineraryId = 'itinerary-123'
+      const existingItinerary = {
+        id: itineraryId,
+        userId: 'different-userid',
+        title: 'Beach Trip',
+        isPublished: true,
+        description: 'An relaxing beach vacation',
+        startDate: new Date('2025-03-11'),
+        endDate: new Date('2025-03-16'),
+        coverImage: 'beach2.jpg',
+        tags: [
+          {
+            id: 'itinerarytag-1',
+            itineraryId: 'itinerary-123',
+            tagId: 'tag-123',
+            createdAt: new Date(),
+            tag: {
+              id: 'tag-123',
+              name: 'Beach',
+              description: 'Beach activities',
+              iconUrl: null,
+              createdAt: new Date(),
+              updatedAt: new Date(),
+            },
+          },
+        ],
+        sections: [
+          {
+            sectionNumber: 1,
+            title: 'Updated Day 1',
+            blocks: [
+              {
+                blockType: BLOCK_TYPE.LOCATION,
+                title: 'Updated Beach Resort',
+                description: 'Check in at the updated beach resort',
+                position: 0,
+                startTime: new Date('2025-03-11T14:00:00Z'),
+                endTime: new Date('2025-03-11T15:00:00Z'),
+                location: 'Updated Beach Resort',
+                price: 600000,
+                photoUrl: 'updated_resort.jpg',
+              },
+            ],
+          },
+        ],
+      }
+
+      const duplicatedItineraryDto: CreateItineraryDto = {
+        title: existingItinerary.title,
+        description: existingItinerary.description,
+        startDate: existingItinerary.startDate,
+        endDate: existingItinerary.endDate,
+        coverImage: existingItinerary.coverImage,
+        tags: ['tag-123'],
+        sections: [
+          {
+            sectionNumber: 1,
+            title: 'Updated Day 1',
+            blocks: [
+              {
+                blockType: BLOCK_TYPE.LOCATION,
+                title: 'Updated Beach Resort',
+                description: 'Check in at the updated beach resort',
+                position: 0,
+                startTime: new Date('2025-03-11T14:00:00Z'),
+                endTime: new Date('2025-03-11T15:00:00Z'),
+                location: 'Updated Beach Resort',
+                price: 600000,
+                photoUrl: 'updated_resort.jpg',
+              },
+            ],
+          },
+        ],
+      }
+
+      const duplicatedItinerary = {
+        id: 'itinerary-789',
+        userId: mockUser.id,
+        title: duplicatedItineraryDto.title,
+        description: duplicatedItineraryDto.description,
+        coverImage: duplicatedItineraryDto.coverImage,
+        startDate: duplicatedItineraryDto.startDate,
+        endDate: duplicatedItineraryDto.endDate,
+        isPublished: false,
+        isCompleted: false,
+        createdAt: new Date(),
+        updatedAt: new Date(),
+        sections: [
+          {
+            id: 'section-1',
+            itineraryId: 'itinerary-789',
+            sectionNumber: 1,
+            title: 'Updated Day 1',
+            createdAt: new Date(),
+            updatedAt: new Date(),
+            blocks: [
+              {
+                id: 'block-1',
+                sectionId: 'section-1',
+                position: 0,
+                blockType: BLOCK_TYPE.LOCATION,
+                title: 'Updated Beach Resort',
+                description: 'Check in at the updated beach resort',
+                startTime: new Date('2025-03-11T14:00:00Z'),
+                endTime: new Date('2025-03-11T15:00:00Z'),
+                location: 'Updated Beach Resort',
+                price: 600000,
+                photoUrl: 'updated_resort.jpg',
+                createdAt: new Date(),
+                updatedAt: new Date(),
+              },
+            ],
+          },
+        ],
+        tags: [
+          {
+            id: 'itinerarytag-1',
+            itineraryId: 'itinerary-123',
+            tagId: 'tag-123',
+            createdAt: new Date(),
+            tag: {
+              id: 'tag-123',
+              name: 'Beach',
+              description: 'Beach activities',
+              iconUrl: null,
+              createdAt: new Date(),
+              updatedAt: new Date(),
+            },
+          },
+        ],
+      }
+
+      const existingContingencyPlans = [
+        {
+          id: 'contingency-plan-123',
+          title: 'Contingency Plan 1',
+          description: 'Desc',
+          sections: [],
+        },
+        {
+          id: 'contingency-plan-456',
+          title: 'Contingency Plan 2',
+          description: 'Desc',
+          sections: [],
+        },
+      ]
+
+      const mockResponse = {
+        statusCode: HttpStatus.CREATED,
+        message: 'Itinerary duplicated succesfully',
+        itinerary: duplicatedItinerary,
+      }
+
+      mockItineraryService.findContingencyPlans.mockResolvedValue(
+        existingContingencyPlans
+      )
+      mockItineraryService.duplicateItinerary.mockResolvedValue(
+        duplicatedItinerary
+      )
+      mockResponseUtil.response.mockReturnValue(mockResponse)
+
+      const result = await controller.duplicateItineraryAndContingencies(
+        itineraryId,
+        mockUser
+      )
+
+      expect(mockItineraryService.duplicateItinerary).toHaveBeenCalledWith(
+        itineraryId,
+        mockUser
+      )
+      expect(mockItineraryService.duplicateContingency).toHaveBeenCalledTimes(
+        existingContingencyPlans.length
+      )
+      existingContingencyPlans.forEach((plan) => {
+        expect(mockItineraryService.duplicateContingency).toHaveBeenCalledWith(
+          duplicatedItinerary.id,
+          itineraryId,
+          plan.id,
+          mockUser
+        )
+      })
+      expect(mockResponseUtil.response).toHaveBeenCalledWith(
+        {
+          statusCode: HttpStatus.CREATED,
+          message: 'Itinerary duplicated successfully',
+        },
+        { duplicatedItinerary }
+      )
+      expect(result).toEqual(mockResponse)
+    })
+
+    it('should duplicate an itinerary with no contingencies and return a formatted response', async () => {
+      const itineraryId = 'itinerary-123'
+      const existingItinerary = {
+        id: itineraryId,
+        userId: 'different-userid',
+        title: 'Beach Trip',
+        isPublished: true,
+        description: 'An relaxing beach vacation',
+        startDate: new Date('2025-03-11'),
+        endDate: new Date('2025-03-16'),
+        coverImage: 'beach2.jpg',
+        tags: [
+          {
+            id: 'itinerarytag-1',
+            itineraryId: 'itinerary-123',
+            tagId: 'tag-123',
+            createdAt: new Date(),
+            tag: {
+              id: 'tag-123',
+              name: 'Beach',
+              description: 'Beach activities',
+              iconUrl: null,
+              createdAt: new Date(),
+              updatedAt: new Date(),
+            },
+          },
+        ],
+        sections: [
+          {
+            sectionNumber: 1,
+            title: 'Updated Day 1',
+            blocks: [
+              {
+                blockType: BLOCK_TYPE.LOCATION,
+                title: 'Updated Beach Resort',
+                description: 'Check in at the updated beach resort',
+                position: 0,
+                startTime: new Date('2025-03-11T14:00:00Z'),
+                endTime: new Date('2025-03-11T15:00:00Z'),
+                location: 'Updated Beach Resort',
+                price: 600000,
+                photoUrl: 'updated_resort.jpg',
+              },
+            ],
+          },
+        ],
+      }
+
+      const duplicatedItineraryDto: CreateItineraryDto = {
+        title: existingItinerary.title,
+        description: existingItinerary.description,
+        startDate: existingItinerary.startDate,
+        endDate: existingItinerary.endDate,
+        coverImage: existingItinerary.coverImage,
+        tags: ['tag-123'],
+        sections: [
+          {
+            sectionNumber: 1,
+            title: 'Updated Day 1',
+            blocks: [
+              {
+                blockType: BLOCK_TYPE.LOCATION,
+                title: 'Updated Beach Resort',
+                description: 'Check in at the updated beach resort',
+                position: 0,
+                startTime: new Date('2025-03-11T14:00:00Z'),
+                endTime: new Date('2025-03-11T15:00:00Z'),
+                location: 'Updated Beach Resort',
+                price: 600000,
+                photoUrl: 'updated_resort.jpg',
+              },
+            ],
+          },
+        ],
+      }
+
+      const duplicatedItinerary = {
+        id: 'itinerary-789',
+        userId: mockUser.id,
+        title: duplicatedItineraryDto.title,
+        description: duplicatedItineraryDto.description,
+        coverImage: duplicatedItineraryDto.coverImage,
+        startDate: duplicatedItineraryDto.startDate,
+        endDate: duplicatedItineraryDto.endDate,
+        isPublished: false,
+        isCompleted: false,
+        createdAt: new Date(),
+        updatedAt: new Date(),
+        sections: [
+          {
+            id: 'section-1',
+            itineraryId: 'itinerary-789',
+            sectionNumber: 1,
+            title: 'Updated Day 1',
+            createdAt: new Date(),
+            updatedAt: new Date(),
+            blocks: [
+              {
+                id: 'block-1',
+                sectionId: 'section-1',
+                position: 0,
+                blockType: BLOCK_TYPE.LOCATION,
+                title: 'Updated Beach Resort',
+                description: 'Check in at the updated beach resort',
+                startTime: new Date('2025-03-11T14:00:00Z'),
+                endTime: new Date('2025-03-11T15:00:00Z'),
+                location: 'Updated Beach Resort',
+                price: 600000,
+                photoUrl: 'updated_resort.jpg',
+                createdAt: new Date(),
+                updatedAt: new Date(),
+              },
+            ],
+          },
+        ],
+        tags: [
+          {
+            id: 'itinerarytag-1',
+            itineraryId: 'itinerary-123',
+            tagId: 'tag-123',
+            createdAt: new Date(),
+            tag: {
+              id: 'tag-123',
+              name: 'Beach',
+              description: 'Beach activities',
+              iconUrl: null,
+              createdAt: new Date(),
+              updatedAt: new Date(),
+            },
+          },
+        ],
+      }
+
+      const existingContingencyPlans = []
+
+      const mockResponse = {
+        statusCode: HttpStatus.CREATED,
+        message: 'Contingency duplicated successfully.',
+        itinerary: duplicatedItinerary,
+      }
+
+      mockItineraryService.findContingencyPlans.mockResolvedValue(
+        existingContingencyPlans
+      )
+      mockItineraryService.duplicateItinerary.mockResolvedValue(
+        duplicatedItinerary
+      )
+      mockResponseUtil.response.mockReturnValue(mockResponse)
+
+      const result = await controller.duplicateItineraryAndContingencies(
+        itineraryId,
+        mockUser
+      )
+
+      expect(mockItineraryService.duplicateItinerary).toHaveBeenCalledWith(
+        itineraryId,
+        mockUser
+      )
+      expect(mockItineraryService.duplicateContingency).toHaveBeenCalledTimes(
+        existingContingencyPlans.length
+      )
+      existingContingencyPlans.forEach((plan) => {
+        expect(mockItineraryService.duplicateContingency).toHaveBeenCalledWith(
+          duplicatedItinerary.id,
+          itineraryId,
+          plan.id,
+          mockUser
+        )
+      })
+      expect(mockResponseUtil.response).toHaveBeenCalledWith(
+        {
+          statusCode: HttpStatus.CREATED,
+          message: 'Itinerary duplicated successfully',
+        },
+        { duplicatedItinerary }
+      )
+      expect(result).toEqual(mockResponse)
+    })
+
+    it('should pass errors from service to the caller', async () => {
+      // Arrange
+      const mockError = new Error('Itinerary is not public')
+      mockItineraryService.duplicateItinerary.mockRejectedValue(mockError)
+
+      // Act & Assert
+      await expect(
+        controller.duplicateItineraryAndContingencies('itn-123', mockUser)
+      ).rejects.toThrow(mockError)
+
+      expect(mockItineraryService.duplicateItinerary).toHaveBeenCalledWith(
+        'itn-123',
+        mockUser
+      )
+      expect(mockResponseUtil.response).not.toHaveBeenCalled()
+    })
+  })
+
   describe('searchItineraries', () => {
     it('should search itineraries with default parameters', async () => {
       // Mock search results
diff --git a/src/itinerary/itinerary.controller.ts b/src/itinerary/itinerary.controller.ts
index a8bcadc7e..66d9850f6 100644
--- a/src/itinerary/itinerary.controller.ts
+++ b/src/itinerary/itinerary.controller.ts
@@ -513,4 +513,40 @@ export class ItineraryController {
       publishedItinerary
     )
   }
+
+  @Post(':itineraryId/duplicate')
+  async duplicateItineraryAndContingencies(
+    @Param('itineraryId') itineraryId: string,
+    @GetUser() user: User
+  ) {
+    // 1: Duplicate Itinerary
+    const duplicatedItinerary = await this.itineraryService.duplicateItinerary(
+      itineraryId,
+      user
+    )
+
+    // 2: Duplicate Contingencies
+    const existingContingencies =
+      await this.itineraryService.findContingencyPlans(itineraryId, user)
+    if (existingContingencies.length > 0) {
+      for (const plan of existingContingencies) {
+        await this.itineraryService.duplicateContingency(
+          duplicatedItinerary.id,
+          itineraryId,
+          plan.id,
+          user
+        )
+      }
+    }
+
+    return this.responseUtil.response(
+      {
+        statusCode: HttpStatus.CREATED,
+        message: 'Itinerary duplicated successfully',
+      },
+      {
+        duplicatedItinerary,
+      }
+    )
+  }
 }
diff --git a/src/itinerary/itinerary.service.spec.ts b/src/itinerary/itinerary.service.spec.ts
index 8980de60b..38246d45b 100644
--- a/src/itinerary/itinerary.service.spec.ts
+++ b/src/itinerary/itinerary.service.spec.ts
@@ -4478,6 +4478,745 @@ describe('ItineraryService', () => {
     })
   })
 
+  describe('duplicateItinerary', () => {
+    it('should duplicate an itinerary succesfully', async () => {
+      // Arrange
+      const existingItinerary = {
+        id: 'itinerary-123',
+        userId: 'different-userid',
+        title: 'Beach Trip',
+        isPublished: true,
+        description: 'An relaxing beach vacation',
+        startDate: new Date('2025-03-11'),
+        endDate: new Date('2025-03-16'),
+        coverImage: 'beach2.jpg',
+        tags: [
+          {
+            id: 'itinerarytag-1',
+            itineraryId: 'itinerary-123',
+            tagId: 'tag-123',
+            createdAt: new Date(),
+            tag: {
+              id: 'tag-123',
+              name: 'Beach',
+              description: 'Beach activities',
+              iconUrl: null,
+              createdAt: new Date(),
+              updatedAt: new Date(),
+            },
+          },
+        ],
+        sections: [
+          {
+            sectionNumber: 1,
+            title: 'Updated Day 1',
+            blocks: [
+              {
+                blockType: BLOCK_TYPE.LOCATION,
+                title: 'Updated Beach Resort',
+                description: 'Check in at the updated beach resort',
+                position: 0,
+                startTime: new Date('2025-03-11T14:00:00Z'),
+                endTime: new Date('2025-03-11T15:00:00Z'),
+                location: 'Updated Beach Resort',
+                price: 600000,
+                photoUrl: 'updated_resort.jpg',
+              },
+            ],
+          },
+        ],
+      }
+
+      const duplicatedItineraryDto: CreateItineraryDto = {
+        title: existingItinerary.title + ' (Copy)',
+        description: existingItinerary.description,
+        startDate: existingItinerary.startDate,
+        endDate: existingItinerary.endDate,
+        coverImage: existingItinerary.coverImage,
+        tags: ['tag-123'],
+        sections: [
+          {
+            sectionNumber: 1,
+            title: 'Updated Day 1',
+            blocks: [
+              {
+                blockType: BLOCK_TYPE.LOCATION,
+                title: 'Updated Beach Resort',
+                description: 'Check in at the updated beach resort',
+                position: 0,
+                startTime: new Date('2025-03-11T14:00:00Z'),
+                endTime: new Date('2025-03-11T15:00:00Z'),
+                location: 'Updated Beach Resort',
+                price: 600000,
+                photoUrl: 'updated_resort.jpg',
+              },
+            ],
+          },
+        ],
+      }
+
+      const duplicatedItinerary = {
+        id: 'itinerary-789',
+        userId: mockUser.id,
+        title: duplicatedItineraryDto.title,
+        description: duplicatedItineraryDto.description,
+        coverImage: duplicatedItineraryDto.coverImage,
+        startDate: duplicatedItineraryDto.startDate,
+        endDate: duplicatedItineraryDto.endDate,
+        isPublished: false,
+        isCompleted: false,
+        createdAt: new Date(),
+        updatedAt: new Date(),
+        sections: [
+          {
+            id: 'section-1',
+            itineraryId: 'itinerary-789',
+            sectionNumber: 1,
+            title: 'Updated Day 1',
+            createdAt: new Date(),
+            updatedAt: new Date(),
+            blocks: [
+              {
+                id: 'block-1',
+                sectionId: 'section-1',
+                position: 0,
+                blockType: BLOCK_TYPE.LOCATION,
+                title: 'Updated Beach Resort',
+                description: 'Check in at the updated beach resort',
+                startTime: new Date('2025-03-11T14:00:00Z'),
+                endTime: new Date('2025-03-11T15:00:00Z'),
+                location: 'Updated Beach Resort',
+                price: 600000,
+                photoUrl: 'updated_resort.jpg',
+                createdAt: new Date(),
+                updatedAt: new Date(),
+              },
+            ],
+          },
+        ],
+        tags: [
+          {
+            id: 'itinerarytag-1',
+            itineraryId: 'itinerary-123',
+            tagId: 'tag-123',
+            createdAt: new Date(),
+            tag: {
+              id: 'tag-123',
+              name: 'Beach',
+              description: 'Beach activities',
+              iconUrl: null,
+              createdAt: new Date(),
+              updatedAt: new Date(),
+            },
+          },
+        ],
+      }
+
+      mockPrismaService.itinerary.findUnique.mockResolvedValue(
+        existingItinerary
+      )
+      mockPrismaService.tag.findMany.mockResolvedValue([
+        { id: 'tag-123', name: 'Beach' },
+      ])
+      mockPrismaService.itinerary.create.mockResolvedValue(duplicatedItinerary)
+
+      // Act
+      const result = await service.duplicateItinerary('itinerary-123', mockUser)
+
+      // Assert
+      expect(mockPrismaService.tag.findMany).toHaveBeenCalledWith({
+        where: {
+          id: {
+            in: ['tag-123'],
+          },
+        },
+      })
+      expect(mockPrismaService.$transaction).toHaveBeenCalled()
+      expect(mockPrismaService.itinerary.create).toHaveBeenCalledWith({
+        data: {
+          userId: mockUser.id,
+          title: duplicatedItinerary.title,
+          description: duplicatedItinerary.description,
+          coverImage: duplicatedItinerary.coverImage,
+          startDate: new Date(duplicatedItinerary.startDate),
+          endDate: new Date(duplicatedItinerary.endDate),
+          tags: {
+            create: [{ tag: { connect: { id: 'tag-123' } } }],
+          },
+          sections: {
+            create: [
+              {
+                sectionNumber: 1,
+                title: 'Updated Day 1',
+                blocks: {
+                  create: [
+                    {
+                      position: 0,
+                      blockType: BLOCK_TYPE.LOCATION,
+                      title: 'Updated Beach Resort',
+                      description: 'Check in at the updated beach resort',
+                      startTime: new Date('2025-03-11T14:00:00Z'),
+                      endTime: new Date('2025-03-11T15:00:00Z'),
+                      location: 'Updated Beach Resort',
+                      price: 600000,
+                      photoUrl: 'updated_resort.jpg',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        include: {
+          sections: {
+            include: {
+              blocks: true,
+            },
+          },
+          tags: {
+            include: {
+              tag: true,
+            },
+          },
+          user: {
+            select: {
+              firstName: true,
+              id: true,
+              lastName: true,
+              photoProfile: true,
+            },
+          },
+        },
+      })
+      expect(result).toEqual(duplicatedItinerary)
+    })
+
+    it('should duplicate an itinerary with default fields succesfully', async () => {
+      // Arrange
+      const existingItinerary = {
+        id: 'itinerary-123',
+        userId: 'different-userid',
+        title: 'Beach Trip',
+        isPublished: true,
+        description: 'An relaxing beach vacation',
+        startDate: new Date('2025-03-11'),
+        endDate: new Date('2025-03-16'),
+        coverImage: 'beach2.jpg',
+        tags: [],
+        sections: [
+          {
+            sectionNumber: 1,
+            blocks: [
+              {
+                blockType: BLOCK_TYPE.LOCATION,
+                title: 'Updated Beach Resort',
+                description: 'Check in at the updated beach resort',
+                position: 0,
+              },
+            ],
+          },
+          {
+            sectionNumber: 2,
+            blocks: [],
+          },
+        ],
+      }
+
+      const duplicatedItineraryDto: CreateItineraryDto = {
+        title: existingItinerary.title + ' (Copy)',
+        description: existingItinerary.description,
+        startDate: existingItinerary.startDate,
+        endDate: existingItinerary.endDate,
+        coverImage: existingItinerary.coverImage,
+        tags: [],
+        sections: [
+          {
+            sectionNumber: 1,
+            title: 'Hari ke-1',
+            blocks: [
+              {
+                blockType: BLOCK_TYPE.LOCATION,
+                title: 'Updated Beach Resort',
+                description: 'Check in at the updated beach resort',
+                position: 0,
+                startTime: null,
+                endTime: null,
+                price: 0,
+              },
+            ],
+          },
+          {
+            sectionNumber: 2,
+            title: 'Hari ke-2',
+            blocks: [],
+          },
+        ],
+      }
+
+      const duplicatedItinerary = {
+        id: 'itinerary-789',
+        userId: mockUser.id,
+        title: duplicatedItineraryDto.title,
+        description: duplicatedItineraryDto.description,
+        coverImage: duplicatedItineraryDto.coverImage,
+        startDate: duplicatedItineraryDto.startDate,
+        endDate: duplicatedItineraryDto.endDate,
+        isPublished: false,
+        isCompleted: false,
+        createdAt: new Date(),
+        updatedAt: new Date(),
+        sections: [
+          {
+            id: 'section-1',
+            itineraryId: 'itinerary-789',
+            sectionNumber: 1,
+            title: 'Updated Day 1',
+            createdAt: new Date(),
+            updatedAt: new Date(),
+            blocks: [
+              {
+                id: 'block-1',
+                sectionId: 'section-1',
+                position: 0,
+                blockType: BLOCK_TYPE.LOCATION,
+                title: 'Hari ke-1',
+                description: 'Check in at the updated beach resort',
+                startTime: null,
+                endTime: null,
+                price: 0,
+                createdAt: new Date(),
+                updatedAt: new Date(),
+              },
+            ],
+          },
+          {
+            id: 'section-2',
+            itineraryId: 'itinerary-789',
+            sectionNumber: 2,
+            title: 'Hari ke-2',
+            createdAt: new Date(),
+            updatedAt: new Date(),
+            blocks: [],
+          },
+        ],
+        tags: [],
+      }
+
+      mockPrismaService.itinerary.findUnique.mockResolvedValue(
+        existingItinerary
+      )
+      mockPrismaService.itinerary.create.mockResolvedValue(duplicatedItinerary)
+
+      // Act
+      const result = await service.duplicateItinerary('itinerary-123', mockUser)
+
+      // Assert
+      expect(mockPrismaService.$transaction).toHaveBeenCalled()
+      expect(mockPrismaService.itinerary.create).toHaveBeenCalledWith({
+        data: {
+          userId: mockUser.id,
+          title: duplicatedItinerary.title,
+          description: duplicatedItinerary.description,
+          coverImage: duplicatedItinerary.coverImage,
+          startDate: new Date(duplicatedItinerary.startDate),
+          endDate: new Date(duplicatedItinerary.endDate),
+          sections: {
+            create: [
+              {
+                sectionNumber: 1,
+                title: 'Hari ke-1',
+                blocks: {
+                  create: [
+                    {
+                      position: 0,
+                      blockType: BLOCK_TYPE.LOCATION,
+                      title: 'Updated Beach Resort',
+                      description: 'Check in at the updated beach resort',
+                      startTime: null,
+                      endTime: null,
+                      price: 0,
+                      location: undefined,
+                      photoUrl: undefined,
+                    },
+                  ],
+                },
+              },
+              {
+                sectionNumber: 2,
+                title: 'Hari ke-2',
+                blocks: {
+                  create: [],
+                },
+              },
+            ],
+          },
+          tags: undefined,
+        },
+        include: {
+          sections: {
+            include: {
+              blocks: true,
+            },
+          },
+          tags: {
+            include: {
+              tag: true,
+            },
+          },
+          user: {
+            select: {
+              firstName: true,
+              id: true,
+              lastName: true,
+              photoProfile: true,
+            },
+          },
+        },
+      })
+      expect(result).toEqual(duplicatedItinerary)
+    })
+
+    it('should throw NotFoundException if itinerary does not exist', async () => {
+      // Arrange
+      mockPrismaService.itinerary.findUnique.mockResolvedValue(null)
+
+      // Act & Assert
+      await expect(
+        service.duplicateItinerary('non-existent-id', mockUser)
+      ).rejects.toThrow(NotFoundException)
+      expect(mockPrismaService.$transaction).not.toHaveBeenCalled()
+    })
+
+    it('should throw ForbiddenException if user does not own the itinerary and the itinerary is not public', async () => {
+      // Arrange
+      const existingItinerary = {
+        id: 'itinerary-123',
+        userId: 'another-user-id',
+        title: 'Beach Trip',
+        description: 'A relaxing beach vacation',
+        isPublished: false,
+        startDate: new Date('2025-03-10'),
+        endDate: new Date('2025-03-15'),
+        sections: [
+          {
+            sectionNumber: 1,
+            title: 'Hari ke-1', // Default title
+            blocks: [], // Empty blocks
+          },
+        ],
+        pendingInvites: [],
+        access: [],
+      }
+
+      mockPrismaService.itinerary.findUnique.mockResolvedValue(
+        existingItinerary
+      )
+
+      // Act & Assert
+      await expect(
+        service.duplicateItinerary('itinerary-123', mockUser)
+      ).rejects.toThrow(ForbiddenException)
+      expect(mockPrismaService.$transaction).not.toHaveBeenCalled()
+    })
+  })
+
+  describe('duplicateContingency', () => {
+    it('should duplicate an itinerary contingency', async () => {
+      // Arrange
+      const existingContingencyPlan = {
+        id: 'contingency-plan-123',
+        itineraryId: 'itinerary-123',
+        title: 'Plan B', // Service determines this based on contingencyCount
+        description: 'Backup plan',
+        sections: [
+          {
+            id: 'section-1',
+            sectionNumber: 1001,
+            title: 'Section 1',
+            blocks: [
+              {
+                id: 'block-1',
+                position: 0,
+                blockType: 'LOCATION',
+                title: 'Block 1',
+                description: 'Block 1 Description',
+                startTime: new Date('2025-03-10T14:00:00Z'),
+                endTime: new Date('2025-03-10T15:00:00Z'),
+                location: 'Location 1',
+                price: 100,
+                photoUrl: 'photo1.jpg',
+                routeToNext: null,
+                routeFromPrevious: null,
+              },
+            ],
+          },
+        ],
+      }
+
+      const duplicatedContingencyDto: CreateContingencyPlanDto = {
+        title: existingContingencyPlan.title,
+        description: existingContingencyPlan.description,
+        sections: [
+          {
+            sectionNumber: 1,
+            title: 'Section 1',
+            blocks: [
+              {
+                blockType: 'LOCATION',
+                title: 'Block 1',
+                description: 'Block 1 Description',
+                startTime: new Date('2025-03-10T14:00:00Z'),
+                endTime: new Date('2025-03-10T15:00:00Z'),
+                location: 'Location 1',
+                price: 100,
+                photoUrl: 'photo1.jpg',
+                position: 0,
+                routeToNext: {
+                  sourceBlockId: 'source-block-1',
+                  destinationBlockId: 'destination-block-1',
+                  distance: 5000,
+                  duration: 900,
+                  polyline: 'abc123',
+                  transportMode: TRANSPORT_MODE.DRIVE,
+                },
+              },
+            ],
+          },
+        ],
+      }
+
+      const mockItinerary = {
+        id: 'itinerary-123',
+        isPublished: true,
+      }
+
+      const mockOtherItinerary = {
+        id: 'itinerary-789',
+        userId: mockUser.id,
+      }
+
+      const contingencyCount = 0
+
+      const duplicatedContingencyPlan = {
+        id: 'contingency-plan-123',
+        itineraryId: 'itinerary-789',
+        title: 'Plan B', // Service determines this based on contingencyCount
+        description: 'Backup plan',
+        sections: [
+          {
+            id: 'section-1',
+            sectionNumber: 1001, // Modified by the service (sectionNumber + (contingencyCount + 1) * 1000)
+            title: 'Section 1',
+            blocks: [
+              {
+                id: 'block-1',
+                position: 0,
+                blockType: 'LOCATION',
+                title: 'Block 1',
+                description: 'Block 1 Description',
+                startTime: new Date('2025-03-10T14:00:00Z'),
+                endTime: new Date('2025-03-10T15:00:00Z'),
+                location: 'Location 1',
+                price: 100,
+                photoUrl: 'photo1.jpg',
+                routeToNext: null,
+                routeFromPrevious: null,
+              },
+            ],
+          },
+        ],
+      }
+
+      // Expected result with mapped section numbers
+      const expectedResult = {
+        ...duplicatedContingencyPlan,
+        sections: [
+          {
+            ...duplicatedContingencyPlan.sections[0],
+            sectionNumber: 1, // Mapped back to original (sectionNumber % 1000)
+          },
+        ],
+      }
+
+      mockPrismaService.itinerary.findUnique.mockResolvedValueOnce(
+        mockItinerary
+      )
+      mockPrismaService.itinerary.findUnique.mockResolvedValueOnce(
+        mockOtherItinerary
+      )
+      mockPrismaService.contingencyPlan.findUnique.mockResolvedValue(
+        existingContingencyPlan
+      )
+      mockPrismaService.contingencyPlan.count.mockResolvedValue(
+        contingencyCount
+      )
+      mockPrismaService.contingencyPlan.create.mockResolvedValue(
+        duplicatedContingencyPlan
+      )
+      mockPrismaService.route.create.mockResolvedValue({
+        id: 'route-1',
+        sourceBlockId: 'block-1',
+        destinationBlockId: 'block-2',
+        distance: 5000,
+        duration: 900,
+        polyline: 'abc123',
+        transportMode: TRANSPORT_MODE.DRIVE,
+      })
+
+      // Act
+      const result = await service.duplicateContingency(
+        'itinerary-789',
+        'itinerary-123',
+        'contingency-plan-123',
+        mockUser
+      )
+
+      // Assert
+      expect(mockPrismaService.$transaction).toHaveBeenCalled()
+      expect(mockPrismaService.contingencyPlan.create).toHaveBeenCalledWith({
+        data: {
+          itineraryId: mockOtherItinerary.id,
+          title: 'Plan B', // Determined by CONTINGENCY_TITLE[contingencyCount]
+          description: duplicatedContingencyDto.description,
+          sections: {
+            create: [
+              {
+                sectionNumber: 1001, // 1 + (0 + 1) * 1000
+                title: 'Section 1',
+                itinerary: {
+                  connect: { id: mockOtherItinerary.id },
+                },
+                blocks: {
+                  create: [
+                    {
+                      position: 0,
+                      blockType: 'LOCATION',
+                      title: 'Block 1',
+                      description: 'Block 1 Description',
+                      startTime: new Date('2025-03-10T14:00:00Z'),
+                      endTime: new Date('2025-03-10T15:00:00Z'),
+                      location: 'Location 1',
+                      price: 100,
+                      photoUrl: 'photo1.jpg',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        include: {
+          sections: {
+            include: {
+              blocks: {
+                include: {
+                  routeToNext: true,
+                  routeFromPrevious: true,
+                },
+              },
+            },
+          },
+        },
+      })
+
+      expect(result).toEqual(expectedResult)
+    })
+
+    it('should throw NotFoundException if contingency does not exist', async () => {
+      // Arrange
+      const existingItinerary = {
+        id: 'itinerary-123',
+        userId: 'another-user-id',
+        title: 'Beach Trip',
+        description: 'A relaxing beach vacation',
+        isPublished: true,
+        startDate: new Date('2025-03-10'),
+        endDate: new Date('2025-03-15'),
+        sections: [
+          {
+            sectionNumber: 1,
+            title: 'Hari ke-1', // Default title
+            blocks: [], // Empty blocks
+          },
+        ],
+        pendingInvites: [],
+        access: [],
+      }
+      mockPrismaService.itinerary.findUnique.mockResolvedValue(
+        existingItinerary
+      )
+      mockPrismaService.contingencyPlan.findUnique.mockResolvedValue(null)
+
+      // Act & Assert
+      await expect(
+        service.duplicateContingency(
+          'itinerary-345',
+          'itn-123',
+          'non-existant-contingency',
+          mockUser
+        )
+      ).rejects.toThrow(NotFoundException)
+      expect(mockPrismaService.$transaction).not.toHaveBeenCalled()
+    })
+
+    it('should throw NotFoundException if itinerary does not exist', async () => {
+      // Arrange
+      mockPrismaService.itinerary.findUnique.mockResolvedValue(null)
+      mockPrismaService.contingencyPlan.findUnique.mockResolvedValue(null)
+
+      // Act & Assert
+      await expect(
+        service.duplicateContingency(
+          'itinerary-345',
+          'non-existent-id',
+          '123',
+          mockUser
+        )
+      ).rejects.toThrow(NotFoundException)
+      expect(mockPrismaService.$transaction).not.toHaveBeenCalled()
+    })
+
+    it('should throw ForbiddenException if user does not own the itinerary and the itinerary is not public', async () => {
+      // Arrange
+      const existingItinerary = {
+        id: 'itinerary-123',
+        userId: 'another-user-id',
+        title: 'Beach Trip',
+        description: 'A relaxing beach vacation',
+        isPublished: false,
+        startDate: new Date('2025-03-10'),
+        endDate: new Date('2025-03-15'),
+        sections: [
+          {
+            sectionNumber: 1,
+            title: 'Hari ke-1', // Default title
+            blocks: [], // Empty blocks
+          },
+        ],
+        pendingInvites: [],
+        access: [],
+      }
+
+      mockPrismaService.itinerary.findUnique.mockResolvedValue(
+        existingItinerary
+      )
+      mockPrismaService.contingencyPlan.findUnique.mockResolvedValue(
+        'something'
+      )
+
+      // Act & Assert
+      await expect(
+        service.duplicateContingency(
+          'itinerary-345',
+          'itinerary-123',
+          '123',
+          mockUser
+        )
+      ).rejects.toThrow(ForbiddenException)
+      expect(mockPrismaService.$transaction).not.toHaveBeenCalled()
+    })
+  })
+
   describe('findTrendingItineraries', () => {
     it('should return trending itineraries ordered by most likes', async () => {
       // Arrange
diff --git a/src/itinerary/itinerary.service.ts b/src/itinerary/itinerary.service.ts
index b358accf6..67b16384f 100644
--- a/src/itinerary/itinerary.service.ts
+++ b/src/itinerary/itinerary.service.ts
@@ -323,6 +323,101 @@ export class ItineraryService {
     })
   }
 
+  async duplicateItinerary(id: string, user: User) {
+    await this._checkReadItineraryPermission(id, user)
+
+    // phase 1: duplicate the itinerary
+    const originalItinerary = await this.prisma.itinerary.findUnique({
+      where: { id: id },
+      include: {
+        sections: {
+          where: {
+            contingencyPlanId: null,
+          },
+          include: {
+            blocks: {
+              include: {
+                routeToNext: true,
+                routeFromPrevious: true,
+              },
+            },
+          },
+        },
+        tags: {
+          include: {
+            tag: true,
+          },
+        },
+      },
+    })
+    const itineraryData: CreateItineraryDto = {
+      title: originalItinerary.title + ' (Copy)',
+      description: originalItinerary.description,
+      coverImage: originalItinerary.coverImage,
+      startDate: originalItinerary.startDate,
+      endDate: originalItinerary.endDate,
+      sections: originalItinerary.sections.map((section) =>
+        this._mapSections(section)
+      ),
+      tags:
+        originalItinerary.tags && originalItinerary.tags.length > 0
+          ? originalItinerary.tags.map((tags) => tags.tag.id)
+          : [],
+    }
+    const newItinerary = await this.createItinerary(itineraryData, user)
+    return newItinerary
+  }
+
+  async duplicateContingency(
+    newItineraryId: string,
+    itineraryId: string,
+    contingencyPlanId: string,
+    user: User
+  ) {
+    // phase 2: duplicate contingency
+    const contingecyPlan = await this.findContingencyPlan(
+      itineraryId,
+      contingencyPlanId,
+      user
+    )
+    const contingencyData: CreateContingencyPlanDto = {
+      title: contingecyPlan.title,
+      description: contingecyPlan.description,
+      sections: contingecyPlan.sections.map((section) =>
+        this._mapSections(section)
+      ),
+    }
+    const newContingencyPlan = await this.createContingencyPlan(
+      newItineraryId,
+      contingencyData,
+      user
+    )
+    return newContingencyPlan
+  }
+
+  _mapSections(section: CreateSectionDto) {
+    return {
+      sectionNumber: section.sectionNumber,
+      title: section.title ?? `Hari ke-${section.sectionNumber}`,
+      blocks:
+        section.blocks && section.blocks.length > 0
+          ? section.blocks.map((block, index) => ({
+              position: index,
+              blockType: block.blockType,
+              title: block.title,
+              description: block.description,
+              startTime: block.startTime ? new Date(block.startTime) : null,
+              endTime: block.endTime ? new Date(block.endTime) : null,
+              location: block.location,
+              price: block.price ?? 0,
+              photoUrl: block.photoUrl,
+              routeToNext: block.routeToNext,
+              routeFromPrevious: block.routeFromPrevious,
+            }))
+          : [],
+    }
+  }
+
   /**
    * Checks whether itinerary with given id exists
    * @param id Id for Itinerary


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 4**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>src/itinerary/itinerary.controller.spec.ts (2)</summary><blockquote>

`1996-2073`: **Enhance test coverage for the duplicateItinerary method.**

When implementing the suggested validation of the original itinerary in the controller, you should also add a test that verifies this behavior. Currently, the test for NotFoundException only checks when the creation of the new itinerary fails, not when validating the original itinerary.


Consider adding a test similar to this:

```typescript
it('should verify the original itinerary exists before duplicating', async () => {
  const itineraryId = 'itinerary-123';
  const duplicateItineraryDto = {
    itinerary: {
      title: 'Duplicated Itinerary',
      description: 'This is a duplicated itinerary',
      startDate: new Date('2025-06-01'),
      endDate: new Date('2025-06-10'),
      sections: [],
    },
    contingencyPlans: [],
  };

  // Mock findOne to throw NotFoundException
  mockItineraryService.findOne.mockRejectedValue(
    new NotFoundException(`Itinerary with ID ${itineraryId} not found`)
  );

  await expect(
    controller.duplicateItinerary(
      itineraryId,
      mockUser,
      duplicateItineraryDto
    )
  ).rejects.toThrow(NotFoundException);

  // Verify findOne was called but createItinerary was not
  expect(mockItineraryService.findOne).toHaveBeenCalledWith(
    itineraryId,
    mockUser
  );
  expect(mockItineraryService.createItinerary).not.toHaveBeenCalled();
});
```

---

`2108-2163`: **Add test for cleanup after contingency plan creation failure.**

After implementing transaction handling in the controller, add a test that verifies the itinerary is removed if contingency plan creation fails.


Enhance your existing test to also verify cleanup behavior:

```diff
  it('should handle errors when creating contingency plans', async () => {
    const itineraryId = 'itinerary-123'
    const duplicateItineraryDto = {
      // ... existing test data ...
    }

    const mockDuplicatedItinerary = {
      // ... existing mock itinerary ...
    }

    const mockError = new Error('Failed to create contingency plan')

    mockItineraryService.createItinerary.mockResolvedValue(
      mockDuplicatedItinerary
    )
    mockItineraryService.createContingencyPlan.mockRejectedValue(mockError)
+   mockItineraryService.removeItinerary.mockResolvedValue(undefined)

    await expect(
      controller.duplicateItinerary(
        itineraryId,
        mockUser,
        duplicateItineraryDto
      )
    ).rejects.toThrow(mockError)

    expect(mockItineraryService.createItinerary).toHaveBeenCalledWith(
      duplicateItineraryDto.itinerary,
      mockUser
    )
    expect(mockItineraryService.createContingencyPlan).toHaveBeenCalledWith(
      mockDuplicatedItinerary.id,
      duplicateItineraryDto.contingencyPlans[0],
      mockUser
    )
+   // Verify cleanup was attempted
+   expect(mockItineraryService.removeItinerary).toHaveBeenCalledWith(
+     mockDuplicatedItinerary.id,
+     mockUser
+   )
  })
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between eaf5a75536514fa1566edee8781e8dabb8021ee9 and a84fe36d8efb95734af365e6e7a4433f0439fc58.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `src/itinerary/dto/duplicate-itinerary.dto.ts` (1 hunks)
* `src/itinerary/itinerary.controller.spec.ts` (1 hunks)
* `src/itinerary/itinerary.controller.ts` (2 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (2)</summary>

<details>
<summary>src/itinerary/dto/duplicate-itinerary.dto.ts (2)</summary><blockquote>

<details>
<summary>src/itinerary/dto/create-itinerary.dto.ts (1)</summary>

* `CreateItineraryDto` (12-42)

</details>
<details>
<summary>src/itinerary/dto/create-contingency-plan.dto.ts (1)</summary>

* `CreateContingencyPlanDto` (10-23)

</details>

</blockquote></details>
<details>
<summary>src/itinerary/itinerary.controller.ts (2)</summary><blockquote>

<details>
<summary>src/common/decorators/getUser.decorator.ts (1)</summary>

* `GetUser` (3-8)

</details>
<details>
<summary>src/itinerary/dto/duplicate-itinerary.dto.ts (1)</summary>

* `DuplicateItineraryDto` (6-10)

</details>

</blockquote></details>

</details>

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T16:06:05Z
user: login: daffarafi
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T04:55:54Z
user: login: mmalkindi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T07:02:59Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 5**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>src/itinerary/itinerary.service.ts (1)</summary><blockquote>

`423-441`: **Access-only collaborators cannot duplicate unpublished itineraries**

Current logic only allows duplication when:
1. user is owner, or
2. itinerary `isPublished === true`.

If the owner explicitly *shared* an unpublished itinerary with someone (row in `itineraryAccess`), that collaborator is refused duplication even though they can fully view/edit it elsewhere.

Consider:

```typescript
if (itinerary.userId !== user.id) {
  const hasAccess = await this.prisma.itineraryAccess.findUnique({ where: { itineraryId_userId: { itineraryId: id, userId: user.id } } })
  if (!hasAccess && !itinerary.isPublished) throw new ForbiddenException(...)
}
```

</blockquote></details>
<details>
<summary>src/itinerary/itinerary.service.spec.ts (1)</summary><blockquote>

`4044-4232`: **Section-number double-offset not detected in assertions**

The spec constructs `expectedResult.sections[0].sectionNumber` as `1`, but the current service code (buggy) will actually return `2001`.  
The test still passes because the mock for `contingencyPlan.create` already returns `1001` which your spec later maps to `1`.

After fixing the service you’ll need an assertion ensuring:

```typescript
expect(createdSection.sectionNumber).toBe(1001); // persisted value
expect(result.sections[0].sectionNumber).toBe(1); // mapped value returned to caller
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Lite**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between a84fe36d8efb95734af365e6e7a4433f0439fc58 and b2e5d3b2200435d54095740191341990a31885b9.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `src/itinerary/itinerary.controller.spec.ts` (2 hunks)
* `src/itinerary/itinerary.controller.ts` (2 hunks)
* `src/itinerary/itinerary.service.spec.ts` (1 hunks)
* `src/itinerary/itinerary.service.ts` (2 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* src/itinerary/itinerary.controller.ts
* src/itinerary/itinerary.controller.spec.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>src/itinerary/itinerary.service.ts (2)</summary><blockquote>

<details>
<summary>src/itinerary/dto/create-itinerary.dto.ts (1)</summary>

* `CreateItineraryDto` (12-42)

</details>
<details>
<summary>src/itinerary/dto/create-contingency-plan.dto.ts (1)</summary>

* `CreateContingencyPlanDto` (10-23)

</details>

</blockquote></details>

</details>

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T16:15:15Z
user: login: Nsyay
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-27T14:39:57Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>src/itinerary/itinerary.controller.spec.ts (1)</summary><blockquote>

`2152-2152`: **This issue was previously identified but not fully resolved.**

The inconsistency between mock response message and the expected message in assertions was previously identified in a review comment but appears to still be present in the code.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>src/itinerary/itinerary.controller.spec.ts (1)</summary><blockquote>

`2194-2371`: **Consider refactoring duplicate test cases.**

The second test case is almost identical to the first one, with the only difference being an empty array of contingency plans. Consider refactoring to reduce duplication while still testing the specific behavior difference.


```diff
- it('should duplicate an itinerary with no contingencies and return a formatted response', async () => {
-   // ... 177 lines of mostly duplicated test code ...
- })

+ it('should duplicate an itinerary with no contingencies and return a formatted response', async () => {
+   const itineraryId = 'itinerary-123'
+   const duplicatedItinerary = { id: 'itinerary-789', /* other required fields */ }
+   
+   // Mock empty contingency plans array
+   const existingContingencyPlans = []
+   
+   const mockResponse = {
+     statusCode: HttpStatus.CREATED,
+     message: 'Itinerary duplicated successfully',
+     itinerary: duplicatedItinerary,
+   }
+   
+   mockItineraryService.findContingencyPlans.mockResolvedValue(existingContingencyPlans)
+   mockItineraryService.duplicateItinerary.mockResolvedValue(duplicatedItinerary)
+   mockResponseUtil.response.mockReturnValue(mockResponse)
+   
+   const result = await controller.duplicateItineraryAndContingencies(itineraryId, mockUser)
+   
+   expect(mockItineraryService.duplicateItinerary).toHaveBeenCalledWith(itineraryId, mockUser)
+   expect(mockItineraryService.duplicateContingency).not.toHaveBeenCalled()
+   expect(mockResponseUtil.response).toHaveBeenCalledWith(
+     {
+       statusCode: HttpStatus.CREATED,
+       message: 'Itinerary duplicated successfully',
+     },
+     { duplicatedItinerary }
+   )
+   expect(result).toEqual(mockResponse)
+ })
```

This refactoring focuses on testing the key difference (no contingency plans) without repeating all the setup code, making the test more maintainable.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Lite**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 57f340fd4e095f9f0daf79d737c551836fee66ba and 8cfdbfa502e4cd1e90b965db90d262905d0f9011.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `src/itinerary/itinerary.controller.spec.ts` (2 hunks)
* `src/itinerary/itinerary.controller.ts` (2 hunks)
* `src/itinerary/itinerary.service.spec.ts` (1 hunks)
* `src/itinerary/itinerary.service.ts` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (3)</summary>

* src/itinerary/itinerary.controller.ts
* src/itinerary/itinerary.service.ts
* src/itinerary/itinerary.service.spec.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>src/itinerary/itinerary.controller.spec.ts (1)</summary><blockquote>

<details>
<summary>src/itinerary/dto/create-itinerary.dto.ts (1)</summary>

* `CreateItineraryDto` (12-42)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (3)</summary><blockquote>

<details>
<summary>src/itinerary/itinerary.controller.spec.ts (3)</summary>

`57-58`: **Good implementation of service mocks.**

The mock service implementation correctly includes the new `duplicateItinerary` and `duplicateContingency` methods needed for the itinerary duplication functionality. This ensures that the controller's interactions with these service methods can be properly tested.

---

`2173-2183`: **Good test coverage for contingency duplication.**

The test properly verifies that:
1. The `duplicateContingency` method is called for each existing contingency plan
2. It's called with the correct parameters including the new itinerary ID, original itinerary ID, contingency ID, and user
3. The number of calls matches the expected count of contingency plans

This ensures that the controller correctly handles duplicating all associated contingency plans with an itinerary.

---

`2373-2388`: **Good error handling test.**

The test properly verifies that errors from the service layer are correctly propagated through the controller. This is an important test case that ensures the controller doesn't swallow exceptions, making debugging easier and providing appropriate feedback to API consumers.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-27T16:27:38Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>src/itinerary/itinerary.service.spec.ts (2)</summary><blockquote>

`4625-4692`: **Verify test adequately checks duplicated itinerary structure**

The test asserts that the created itinerary has the expected high-level properties, but doesn't explicitly verify that all the detailed nested structures from the original itinerary are properly copied.


Consider enhancing the test assertion to explicitly verify that the deep nested structures (sections, blocks) are correctly copied with their properties preserved. For example:

```diff
// After the existing assertions in the first test case
+ // Verify sections structure
+ expect(mockPrismaService.itinerary.create).toHaveBeenCalledWith(
+   expect.objectContaining({
+     data: expect.objectContaining({
+       sections: expect.objectContaining({
+         create: expect.arrayContaining([
+           expect.objectContaining({
+             sectionNumber: 1,
+             title: 'Updated Day 1',
+             blocks: expect.objectContaining({
+               create: expect.arrayContaining([
+                 expect.objectContaining({
+                   position: 0,
+                   blockType: BLOCK_TYPE.LOCATION,
+                   title: 'Updated Beach Resort',
+                   description: 'Check in at the updated beach resort',
+                   // Additional block properties
+                 })
+               ])
+             })
+           })
+         ])
+       })
+     })
+   })
+ );
```

---

`4883-4887`: **Ensure method signature matches implementation**

The test is calling `duplicateContingency` with 4 parameters (targetItineraryId, sourceItineraryId, contingencyPlanId, user), which is a clear and logical signature. Make sure the implementation follows this pattern.


Consider updating the parameter names in both the test and implementation to make their purpose more explicit, as the current names might be confusing:

```diff
- const result = await service.duplicateContingency(
-   'itinerary-789',
-   'itinerary-123',
-   'contingency-plan-123',
-   mockUser
- )

+ const result = await service.duplicateContingency(
+   targetItineraryId: 'itinerary-789',
+   sourceItineraryId: 'itinerary-123', 
+   contingencyPlanId: 'contingency-plan-123',
+   user: mockUser
+ )
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Lite**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between ecb5468a8924d7dc88009945a831d623d518f408 and 96f166099d6fa272ea5b09b41274f4f8d2c8e1a5.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `src/itinerary/itinerary.controller.spec.ts` (2 hunks)
* `src/itinerary/itinerary.controller.ts` (1 hunks)
* `src/itinerary/itinerary.service.spec.ts` (1 hunks)
* `src/itinerary/itinerary.service.ts` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (3)</summary>

* src/itinerary/itinerary.controller.ts
* src/itinerary/itinerary.service.ts
* src/itinerary/itinerary.controller.spec.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>src/itinerary/itinerary.service.spec.ts (2)</summary><blockquote>

<details>
<summary>src/itinerary/dto/create-itinerary.dto.ts (1)</summary>

* `CreateItineraryDto` (12-42)

</details>
<details>
<summary>src/itinerary/dto/create-contingency-plan.dto.ts (1)</summary>

* `CreateContingencyPlanDto` (10-23)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (4)</summary><blockquote>

<details>
<summary>src/itinerary/itinerary.service.spec.ts (4)</summary>

`4738-4938`: **Good implementation of duplicateContingency tests**

The test properly verifies that a contingency plan can be duplicated with all its nested sections and blocks. It also correctly tests the section number transformation logic, where section numbers are stored with an offset (1001, 2002) but exposed to clients with normalized values (1, 2).

---

`4940-5032`: **Good error handling tests for duplicateContingency**

These tests cover important error cases: non-existent contingency, non-existent itinerary, and permission issues. The permission check correctly verifies that a user cannot duplicate a contingency from an unpublished itinerary they don't own.

---

`4624-4624`: <details>
<summary>✅ Verification successful</summary>

**Ensure service method implementation matches the test expectations**

The test is calling `duplicateItinerary` with just the itinerary ID and user, but a previously identified issue mentioned that the implementation was incorrectly using `this.findOne(id, user)` which has stricter permission checks than intended.


Let's verify that the implementation of duplicateItinerary in the service file uses the correct permission flow:

---

🏁 Script executed:

```shell
#!/bin/bash
# Check the implementation of duplicateItinerary to ensure it's not using findOne()
rg -A 10 -B 5 "async duplicateItinerary" src/itinerary/itinerary.service.ts
```

Length of output: 471

---



</details>

**duplicateItinerary uses correct permission check**  
Verified that `duplicateItinerary` calls `_checkReadItineraryPermission(id, user)` rather than `this.findOne(id, user)`, so it aligns with the test’s expectations. No changes needed.

---

`4481-4736`: _🛠️ Refactor suggestion_

**Good coverage of the duplicateItinerary functionality**

This test suite has comprehensive coverage for duplicating an itinerary, including successful duplication of a complex itinerary with blocks, sections, and tags, as well as error cases for non-existent itineraries and permission validation.

However, there's a missing test case for a key user flow mentioned in the previous review comments.

Add a test case that verifies a non-owner can successfully duplicate a published itinerary. This ensures the permission flow works correctly for public itineraries. The current test suite only verifies that a non-owner cannot duplicate an unpublished itinerary.

```diff
describe('duplicateItinerary', () => {
  // Existing tests...
  
+ it('should allow a non-owner to duplicate a published itinerary', async () => {
+   // Arrange
+   const existingItinerary = {
+     id: 'itinerary-123',
+     userId: 'different-userid', // Different from mockUser.id
+     title: 'Public Beach Trip',
+     isPublished: true, // Published itinerary
+     description: 'A relaxing beach vacation',
+     startDate: new Date('2025-03-11'),
+     endDate: new Date('2025-03-16'),
+     coverImage: 'beach2.jpg',
+     tags: [{
+       id: 'itinerarytag-1',
+       itineraryId: 'itinerary-123',
+       tagId: 'tag-123',
+       createdAt: new Date(),
+       tag: {
+         id: 'tag-123',
+         name: 'Beach',
+         description: 'Beach activities',
+         iconUrl: null,
+         createdAt: new Date(),
+         updatedAt: new Date(),
+       },
+     }],
+     sections: [{
+       sectionNumber: 1,
+       title: 'Day 1',
+       blocks: [{
+         blockType: BLOCK_TYPE.LOCATION,
+         title: 'Beach Resort',
+         description: 'Check in at the beach resort',
+         position: 0,
+         // Add more block properties as needed
+       }],
+     }],
+   }
+   
+   const duplicatedItinerary = {
+     id: 'itinerary-789',
+     userId: mockUser.id, // The current user becomes the owner
+     title: existingItinerary.title + ' (Copy)',
+     isPublished: false, // Duplicated as unpublished
+     // Add other necessary properties
+   }
+   
+   mockPrismaService.itinerary.findUnique.mockResolvedValue(existingItinerary)
+   mockPrismaService.tag.findMany.mockResolvedValue([{ id: 'tag-123', name: 'Beach' }])
+   mockPrismaService.itinerary.create.mockResolvedValue(duplicatedItinerary)
+   
+   // Act
+   const result = await service.duplicateItinerary('itinerary-123', mockUser)
+   
+   // Assert
+   expect(result).toEqual(duplicatedItinerary)
+   expect(mockPrismaService.itinerary.create).toHaveBeenCalled()
+   // Add more specific assertions as needed
+ })
```



> Likely an incorrect or invalid review comment.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-27T16:56:56Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 3**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>src/itinerary/itinerary.service.spec.ts (1)</summary><blockquote>

`5031-5040`: **`itinerary.findUnique` mock depends on call order – make it data-driven**

`duplicateContingency` internally calls `itinerary.findUnique` for **both**
the source and the destination itinerary.  Stubbing with two consecutive
`mockResolvedValueOnce` values couples the test to the internal call order; a
harmless refactor (e.g. swapping the checks) will break the test.

Prefer a deterministic mock:

```ts
mockPrismaService.itinerary.findUnique.mockImplementation(({ where }) => {
  if (where.id === 'itinerary-123') return mockSourceItinerary
  if (where.id === 'itinerary-789') return mockTargetItinerary
  return null
})
```

The same approach makes the intention clearer and eliminates brittle ordering
assumptions.  



Also applies to: 5074-5120

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Lite**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 96f166099d6fa272ea5b09b41274f4f8d2c8e1a5 and a373a66d8d269240077c078d5a3530fd55088b01.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `src/itinerary/itinerary.controller.spec.ts` (2 hunks)
* `src/itinerary/itinerary.controller.ts` (1 hunks)
* `src/itinerary/itinerary.service.spec.ts` (1 hunks)
* `src/itinerary/itinerary.service.ts` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (3)</summary>

* src/itinerary/itinerary.controller.ts
* src/itinerary/itinerary.controller.spec.ts
* src/itinerary/itinerary.service.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>src/itinerary/itinerary.service.spec.ts (2)</summary><blockquote>

<details>
<summary>src/itinerary/dto/create-itinerary.dto.ts (1)</summary>

* `CreateItineraryDto` (12-42)

</details>
<details>
<summary>src/itinerary/dto/create-contingency-plan.dto.ts (1)</summary>

* `CreateContingencyPlanDto` (10-23)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>src/itinerary/itinerary.service.spec.ts (1)</summary>

`4481-4528`: **Missing permission-negative assertion**

This “happy path” duplicates a published itinerary owned by another user.
Good ✅.  
However, the service used to contain a bug where a follow-up `findOne` call
overrode the permissive check and threw `ForbiddenException` (see previous
review). Please add an explicit assertion that the call **does not** throw and
that `ForbiddenException` is *not* raised, otherwise the test could again pass
only because mocks bypass the real guard.

Example:

```ts
await expect(
  service.duplicateItinerary('itinerary-123', mockUser)
).resolves.not.toThrow()
```

[to ensure coverage]

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T02:08:32Z

Review comments:
diff_hunk: @@ -0,0 +1,10 @@
+import { IsObject } from 'class-validator'
+
+import { CreateItineraryDto } from './create-itinerary.dto'
+import { CreateContingencyPlanDto } from './create-contingency-plan.dto'
+
+export class DuplicateItineraryDto {
+  @IsObject({ each: true })
+  itinerary: CreateItineraryDto
path: src/itinerary/dto/duplicate-itinerary.dto.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix incorrect validation decorator on the `itinerary` property.**

The `each: true` parameter on `@IsObject()` is meant for validating each element in an array, but `itinerary` is a single object, not an array.


```diff
- @IsObject({ each: true })
+ @IsObject()
  itinerary: CreateItineraryDto
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  @IsObject()
  itinerary: CreateItineraryDto
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-21T16:06:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,10 @@
+import { IsObject } from 'class-validator'
+
+import { CreateItineraryDto } from './create-itinerary.dto'
+import { CreateContingencyPlanDto } from './create-contingency-plan.dto'
+
+export class DuplicateItineraryDto {
+  @IsObject({ each: true })
+  itinerary: CreateItineraryDto
+  contingencyPlans: CreateContingencyPlanDto[]
path: src/itinerary/dto/duplicate-itinerary.dto.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add validation decorators to the `contingencyPlans` property.**

The `contingencyPlans` property lacks validation decorators, unlike the `itinerary` property. For consistency and proper validation, add appropriate decorators.


```diff
  itinerary: CreateItineraryDto
+ @IsArray()
+ @IsObject({ each: true })
  contingencyPlans: CreateContingencyPlanDto[]
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  @IsArray()
  @IsObject({ each: true })
  contingencyPlans: CreateContingencyPlanDto[]
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-21T16:06:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -386,4 +387,36 @@ export class ItineraryController {
       }
     )
   }
+
+  @Post(':itineraryId/duplicate')
+  async duplicateItinerary(
+    @Param('itineraryId') itineraryId: string,
+    @GetUser() user: User,
+    @Body() data: DuplicateItineraryDto
+  ) {
+    // Create new itinerary first
+    const itinerary = await this.itineraryService.createItinerary(
+      data.itinerary,
+      user
+    )
+
+    // Create contigencies
+    for (const contingencyPlan of data.contingencyPlans) {
+      await this.itineraryService.createContingencyPlan(
+        itinerary.id,
+        contingencyPlan,
+        user
+      )
+    }
+
+    return this.responseUtil.response(
+      {
+        statusCode: HttpStatus.OK,
+        message: 'Itinerary duplicated succesfully',
+      },
+      {
+        itinerary,
+      }
+    )
+  }
path: src/itinerary/itinerary.controller.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add validation for original itinerary and implement transaction handling.**

The current implementation has several issues that need addressing:

1. It doesn't verify if the original itinerary exists or if the user has permission to access it
2. There's no transaction handling - if creating a contingency plan fails after the itinerary is created, it would result in partial data
3. The HTTP status code should be 201 (Created) rather than 200 (OK) for consistency with other creation endpoints


Here's an improved implementation:

```diff
  @Post(':itineraryId/duplicate')
  async duplicateItinerary(
    @Param('itineraryId') itineraryId: string,
    @GetUser() user: User,
    @Body() data: DuplicateItineraryDto
  ) {
+   // Verify the original itinerary exists and user has access
+   await this.itineraryService.findOne(itineraryId, user)
+
    // Create new itinerary first
    const itinerary = await this.itineraryService.createItinerary(
      data.itinerary,
      user
    )

-   // Create contigencies
+   // Create contingencies
+   try {
      for (const contingencyPlan of data.contingencyPlans) {
        await this.itineraryService.createContingencyPlan(
          itinerary.id,
          contingencyPlan,
          user
        )
      }
+   } catch (error) {
+     // If creating contingency plans fails, clean up the itinerary
+     await this.itineraryService.removeItinerary(itinerary.id, user)
+     throw error;
+   }

    return this.responseUtil.response(
      {
-       statusCode: HttpStatus.OK,
-       message: 'Itinerary duplicated succesfully',
+       statusCode: HttpStatus.CREATED,
+       message: 'Itinerary duplicated successfully',
      },
      {
        itinerary,
      }
    )
  }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  @Post(':itineraryId/duplicate')
  async duplicateItinerary(
    @Param('itineraryId') itineraryId: string,
    @GetUser() user: User,
    @Body() data: DuplicateItineraryDto
  ) {
    // Verify the original itinerary exists and user has access
    await this.itineraryService.findOne(itineraryId, user)

    // Create new itinerary first
    const itinerary = await this.itineraryService.createItinerary(
      data.itinerary,
      user
    )

    // Create contingencies
    try {
      for (const contingencyPlan of data.contingencyPlans) {
        await this.itineraryService.createContingencyPlan(
          itinerary.id,
          contingencyPlan,
          user
        )
      }
    } catch (error) {
      // If creating contingency plans fails, clean up the itinerary
      await this.itineraryService.removeItinerary(itinerary.id, user)
      throw error
    }

    return this.responseUtil.response(
      {
        statusCode: HttpStatus.CREATED,
        message: 'Itinerary duplicated successfully',
      },
      {
        itinerary,
      }
    )
  }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-21T16:06:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1992,4 +1992,174 @@ describe('ItineraryController', () => {
       ).rejects.toThrow(ForbiddenException)
     })
   })
+
+  describe('duplicateItinerary', () => {
+    it('should duplicate an itinerary and return a formatted response', async () => {
+      const itineraryId = 'itinerary-123'
+      const duplicateItineraryDto = {
+        itinerary: {
+          title: 'Duplicated Itinerary',
+          description: 'This is a duplicated itinerary',
+          startDate: new Date('2025-06-01'),
+          endDate: new Date('2025-06-10'),
+          sections: [],
+        },
+        contingencyPlans: [
+          {
+            title: 'Contingency Plan 1',
+            description: 'Backup plan for day 1',
+            sections: [],
+          },
+          {
+            title: 'Contingency Plan 2',
+            description: 'Backup plan for day 2',
+            sections: [],
+          },
+        ],
+      }
+
+      const mockDuplicatedItinerary = {
+        id: 'duplicated-itinerary-123',
+        userId: mockUser.id,
+        title: duplicateItineraryDto.itinerary.title,
+        description: duplicateItineraryDto.itinerary.description,
+        startDate: duplicateItineraryDto.itinerary.startDate,
+        endDate: duplicateItineraryDto.itinerary.endDate,
+        sections: [],
+        createdAt: new Date(),
+        updatedAt: new Date(),
+      }
+
+      const mockResponse = {
+        statusCode: HttpStatus.OK,
+        message: 'Contingency updated successfully.',
+        itinerary: mockDuplicatedItinerary,
+      }
path: src/itinerary/itinerary.controller.spec.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix incorrect assertion message in test.**

The mock response message doesn't match the expected message from the controller implementation. It currently asserts "Contingency updated successfully" instead of "Itinerary duplicated succesfully".


```diff
  const mockResponse = {
    statusCode: HttpStatus.OK,
-   message: 'Contingency updated successfully.',
+   message: 'Itinerary duplicated succesfully',
    itinerary: mockDuplicatedItinerary,
  }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
      const mockResponse = {
        statusCode: HttpStatus.OK,
        message: 'Itinerary duplicated succesfully',
        itinerary: mockDuplicatedItinerary,
      }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commits 4121b5f to c325be5
created_at: 2025-04-21T16:06:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -386,4 +387,36 @@ export class ItineraryController {
       }
     )
   }
+
+  @Post(':itineraryId/duplicate')
+  async duplicateItinerary(
+    @Param('itineraryId') itineraryId: string,
+    @GetUser() user: User,
+    @Body() data: DuplicateItineraryDto
+  ) {
+    // Create new itinerary first
+    const itinerary = await this.itineraryService.createItinerary(
+      data.itinerary,
+      user
+    )
+
+    // Create contigencies
+    for (const contingencyPlan of data.contingencyPlans) {
+      await this.itineraryService.createContingencyPlan(
+        itinerary.id,
+        contingencyPlan,
+        user
+      )
+    }
+
+    return this.responseUtil.response(
+      {
+        statusCode: HttpStatus.OK,
path: src/itinerary/itinerary.controller.ts
user: login: daffarafi
body: Selain itu, saya juga ingin memberikan sedikit saran perbaikan minor mengenai status code yang digunakan. Mengingat bahwa operasi ini bertujuan untuk membuat entitas baru (itinerary baru), status HTTP yang lebih tepat menurut saya adalah `HttpStatus.CREATED` (201) daripada `HttpStatus.OK` (200). Status 201 lebih spesifik digunakan ketika sebuah resource baru berhasil dibuat.

Dengan melakukan perubahan ini, akan lebih jelas bagi frontend bahwa proses berhasil menghasilkan entitas baru. Berikut adalah bagian yang perlu diperbarui:

```typescript
return this.responseUtil.response(
  {
    statusCode: HttpStatus.CREATED, // Ubah dari OK menjadi CREATED
    message: 'Itinerary duplicated successfully',
  },
  {
    itinerary,
  }
)
```

Sebenarnya ini adalah masalah minor saja, karena perubahan ini tidak mengubah logic apapun. Terimakasih
created_at: 2025-04-23T04:28:15Z
author_association: MEMBER
diff_hunk: @@ -386,4 +387,36 @@ export class ItineraryController {
       }
     )
   }
+
+  @Post(':itineraryId/duplicate')
+  async duplicateItinerary(
+    @Param('itineraryId') itineraryId: string,
+    @GetUser() user: User,
+    @Body() data: DuplicateItineraryDto
path: src/itinerary/itinerary.controller.ts
user: login: daffarafi
body: Terima kasih atas implementasinya kindi! Saya telah memeriksa kode yang sudah dibuat.

Saya ingin memberikan saran tambahan terkait struktur endpoint ini. Saat ini, endpoint ini mengharuskan untuk menerima body yang berisi data itinerary dan contingency plans yang akan diduplikasi. Namun, jika tujuan utamanya adalah menduplikasi itinerary berdasarkan `itineraryId`, maka sepertinya kita bisa menyederhanakan endpoint ini dengan hanya menggunakan `itineraryId` sebagai parameter.

Dengan cara ini, kita bisa mengurangi ketergantungan pada body, dan membuat endpoint lebih sederhana. Misalnya, jika data itinerary yang ingin diduplikasi sudah ada di database, kita hanya perlu mengambil data tersebut berdasarkan `itineraryId`, lalu membuat duplikatnya.

Berikut adalah contoh perubahan yang bisa dipertimbangkan:

```typescript
    // Get the original itinerary
    const originalItinerary = await this.itineraryService.findOne(id, user)

    // Create the new itinerary using the existing createItinerary method
    return this.itineraryService.createItinerary(createItineraryDto, user)
```

Namun, saya juga ingin tahu mungkin ada alasan tersendiri untuk mengirimkan data itinerary dan contingency plans di body request. Apakah ada pertimbangan khusus yang membuat pendekatan ini lebih baik untuk kasus ini? Mungkin saya yang kurang paham akan konteksnya.

Semoga saran ini membantu, dan terima kasih atas kerjasamanya!

created_at: 2025-04-23T04:53:27Z
author_association: MEMBER
diff_hunk: @@ -386,4 +387,36 @@ export class ItineraryController {
       }
     )
   }
+
+  @Post(':itineraryId/duplicate')
+  async duplicateItinerary(
+    @Param('itineraryId') itineraryId: string,
+    @GetUser() user: User,
+    @Body() data: DuplicateItineraryDto
path: src/itinerary/itinerary.controller.ts
user: login: daffarafi
body: Hal ini juga bisa mengatasi potensi masalah keamanan. Ketika body request diperbolehkan untuk dikirimkan, terdapat kemungkinan bahwa pengguna yang tidak berwenang bisa mencoba untuk menduplikasi itinerary berdasarkan itineraryId yang valid, namun dengan data body yang dimanipulasi atau berbeda. Ini bisa menjadi risiko, karena mereka dapat mengirimkan body yang tidak sesuai dengan data asli yang ada di server.
created_at: 2025-04-23T04:55:11Z
author_association: MEMBER
diff_hunk: @@ -386,4 +387,36 @@ export class ItineraryController {
       }
     )
   }
+
+  @Post(':itineraryId/duplicate')
+  async duplicateItinerary(
+    @Param('itineraryId') itineraryId: string,
+    @GetUser() user: User,
+    @Body() data: DuplicateItineraryDto
path: src/itinerary/itinerary.controller.ts
user: login: mmalkindi
body: Halo Dafa, terima kasih banyak atas masukannya. Saya setuju sekali dengan kekhawatiran Anda, dan refactoring akan saya lakukan. Awalnya, saya kira service calls bisa dikurangi dengan mengambil data yang sudah di fetch di _frontend_ berhubung pengguna perlu menggunakan tombol di halaman detail itinerary yang sudah fetch semua data itinerary. Namun, _approach_ ini setelah dipikir-pikir lagi memang kurang aman sehingga perlu dilakukan refactoring.
created_at: 2025-04-23T07:02:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -294,6 +294,91 @@ export class ItineraryService {
     })
   }
 
+  async duplicateItinerary(id: string, user: User) {
+    await this._checkDuplicateItineraryPermission(id, user)
+
+    // phase 1: duplicate the itinerary
+    const originalItinerary = await this.findOne(id, user)
+    const itineraryData: CreateItineraryDto = {
+      title: originalItinerary.title,
+      description: originalItinerary.description,
+      coverImage: originalItinerary.coverImage,
+      startDate: originalItinerary.startDate,
+      endDate: originalItinerary.endDate,
+      sections: originalItinerary.sections.map((section) => ({
+        sectionNumber: section.sectionNumber,
+        title: section.title || `Hari ke-${section.sectionNumber}`,
+        blocks:
+          section.blocks && section.blocks.length > 0
+            ? section.blocks.map((block, index) => ({
+                position: index,
+                blockType: block.blockType,
+                title: block.title,
+                description: block.description,
+                startTime: block.startTime ? new Date(block.startTime) : null,
+                endTime: block.endTime ? new Date(block.endTime) : null,
+                location: block.location,
+                price: block.price || 0,
+                photoUrl: block.photoUrl,
+                routeToNext: block.routeToNext,
+                routeFromPrevious: block.routeFromPrevious,
+              }))
+            : [],
+      })),
+      tags:
+        originalItinerary.tags && originalItinerary.tags.length > 0
+          ? originalItinerary.tags.map((tags) => tags.tag.id)
+          : [],
+    }
+    const newItinerary = await this.createItinerary(itineraryData, user)
+    return newItinerary
+  }
+
+  async duplicateContingency(
+    itineraryId: string,
+    contingencyPlanId: string,
+    user: User
+  ) {
+    await this._checkDuplicateItineraryPermission(itineraryId, user)
+
+    // phase 2: duplicate contingency
+    const contingecyPlan = await this.findContingencyPlan(
+      itineraryId,
+      contingencyPlanId,
+      user
+    )
+    const contingencyData: CreateContingencyPlanDto = {
path: src/itinerary/itinerary.service.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

_⚠️ Potential issue_

**Same “public itinerary” issue in `duplicateContingency`**

`findContingencyPlan()` internally calls `_checkUpdateItineraryPermission`, forcing ownership.  
For a published itinerary the current user still hits `ForbiddenException`, negating the earlier permissive check.

Fetch the contingency plan directly (or add a dedicated permission helper) instead of re-using the *update* path.

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commits 4121b5f to c325be5
created_at: 2025-04-24T16:15:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -294,6 +294,91 @@
     })
   }
 
+  async duplicateItinerary(id: string, user: User) {
+    await this._checkDuplicateItineraryPermission(id, user)
+
+    // phase 1: duplicate the itinerary
+    const originalItinerary = await this.findOne(id, user)
+    const itineraryData: CreateItineraryDto = {
+      title: originalItinerary.title,
+      description: originalItinerary.description,
+      coverImage: originalItinerary.coverImage,
+      startDate: originalItinerary.startDate,
+      endDate: originalItinerary.endDate,
+      sections: originalItinerary.sections.map((section) => ({
+        sectionNumber: section.sectionNumber,
+        title: section.title || `Hari ke-${section.sectionNumber}`,
+        blocks:
+          section.blocks && section.blocks.length > 0
+            ? section.blocks.map((block, index) => ({
+                position: index,
+                blockType: block.blockType,
+                title: block.title,
+                description: block.description,
+                startTime: block.startTime ? new Date(block.startTime) : null,
+                endTime: block.endTime ? new Date(block.endTime) : null,
+                location: block.location,
+                price: block.price || 0,
+                photoUrl: block.photoUrl,
+                routeToNext: block.routeToNext,
+                routeFromPrevious: block.routeFromPrevious,
+              }))
+            : [],
path: src/itinerary/itinerary.service.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Copying `routeToNext`/`routeFromPrevious` objects verbatim risks PK collisions**

You forward the full persisted relation objects (including `id`, `sourceBlockId`, etc.) to `createItinerary()`.  
Inside `createItinerary()` these objects are used to create *new* `Route` rows; retaining the original IDs can violate unique-key constraints or leak internal DB identifiers.

Strip DB-generated fields before passing them, or rebuild only the raw DTO you really need (distance, duration, …).

```diff
- routeToNext: block.routeToNext,
- routeFromPrevious: block.routeFromPrevious,
+ routeToNext: block.routeToNext
+   ? {
+       distance: block.routeToNext.distance,
+       duration: block.routeToNext.duration,
+       polyline: block.routeToNext.polyline,
+       transportMode: block.routeToNext.transportMode,
+     }
+   : undefined,
+ // routeFromPrevious is never consumed by createItinerary()
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
          section.blocks && section.blocks.length > 0
            ? section.blocks.map((block, index) => ({
                position: index,
                blockType: block.blockType,
                title: block.title,
                description: block.description,
                startTime: block.startTime ? new Date(block.startTime) : null,
                endTime: block.endTime ? new Date(block.endTime) : null,
                location: block.location,
                price: block.price || 0,
                photoUrl: block.photoUrl,
                routeToNext: block.routeToNext
                  ? {
                      distance: block.routeToNext.distance,
                      duration: block.routeToNext.duration,
                      polyline: block.routeToNext.polyline,
                      transportMode: block.routeToNext.transportMode,
                    }
                  : undefined,
                // routeFromPrevious is never consumed by createItinerary()
              }))
            : [],
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-24T16:15:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -294,6 +294,91 @@
     })
   }
 
+  async duplicateItinerary(id: string, user: User) {
+    await this._checkDuplicateItineraryPermission(id, user)
+
+    // phase 1: duplicate the itinerary
+    const originalItinerary = await this.findOne(id, user)
+    const itineraryData: CreateItineraryDto = {
+      title: originalItinerary.title,
+      description: originalItinerary.description,
+      coverImage: originalItinerary.coverImage,
path: src/itinerary/itinerary.service.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

_⚠️ Potential issue_

**`findOne()` will wrongly forbid duplicating a *public* itinerary you don’t own**

`duplicateItinerary()` first passes the permission check (`_checkDuplicateItineraryPermission`) – which explicitly allows duplication of *published* itineraries even when the user is not the owner – but then immediately calls `this.findOne(id, user)`.  
`findOne()` relies on `_checkItineraryExists`, which throws a `ForbiddenException` if the user is not the owner *and* has no explicit access.  
As a result, users cannot duplicate a published itinerary they are allowed to duplicate.

```diff
- const originalItinerary = await this.findOne(id, user)
+ // fetch without ownership/access restriction – only existence needed
+ const originalItinerary = await this.prisma.itinerary.findUnique({
+   where: { id },
+   include: {
+     sections: { include: { blocks: { include: { routeToNext: true, routeFromPrevious: true } } } },
+     tags: { include: { tag: true } },
+   },
+ })
+ if (!originalItinerary) throw new NotFoundException(`Itinerary with ID ${id} not found`)
```




<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  async duplicateItinerary(id: string, user: User) {
    await this._checkDuplicateItineraryPermission(id, user)

    // phase 1: duplicate the itinerary
    // fetch without ownership/access restriction – only existence needed
    const originalItinerary = await this.prisma.itinerary.findUnique({
      where: { id },
      include: {
        sections: {
          include: {
            blocks: {
              include: { routeToNext: true, routeFromPrevious: true },
            },
          },
        },
        tags: { include: { tag: true } },
      },
    })
    if (!originalItinerary) {
      throw new NotFoundException(`Itinerary with ID ${id} not found`)
    }

    const itineraryData: CreateItineraryDto = {
      title: originalItinerary.title,
      description: originalItinerary.description,
      coverImage: originalItinerary.coverImage,
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-24T16:15:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -294,6 +294,91 @@
     })
   }
 
+  async duplicateItinerary(id: string, user: User) {
+    await this._checkDuplicateItineraryPermission(id, user)
+
+    // phase 1: duplicate the itinerary
+    const originalItinerary = await this.findOne(id, user)
+    const itineraryData: CreateItineraryDto = {
+      title: originalItinerary.title,
+      description: originalItinerary.description,
+      coverImage: originalItinerary.coverImage,
+      startDate: originalItinerary.startDate,
+      endDate: originalItinerary.endDate,
+      sections: originalItinerary.sections.map((section) => ({
+        sectionNumber: section.sectionNumber,
+        title: section.title || `Hari ke-${section.sectionNumber}`,
+        blocks:
+          section.blocks && section.blocks.length > 0
+            ? section.blocks.map((block, index) => ({
+                position: index,
+                blockType: block.blockType,
+                title: block.title,
+                description: block.description,
+                startTime: block.startTime ? new Date(block.startTime) : null,
+                endTime: block.endTime ? new Date(block.endTime) : null,
+                location: block.location,
+                price: block.price || 0,
+                photoUrl: block.photoUrl,
+                routeToNext: block.routeToNext,
+                routeFromPrevious: block.routeFromPrevious,
+              }))
+            : [],
+      })),
+      tags:
+        originalItinerary.tags && originalItinerary.tags.length > 0
+          ? originalItinerary.tags.map((tags) => tags.tag.id)
+          : [],
+    }
+    const newItinerary = await this.createItinerary(itineraryData, user)
+    return newItinerary
+  }
+
+  async duplicateContingency(
+    itineraryId: string,
+    contingencyPlanId: string,
+    user: User
+  ) {
+    await this._checkDuplicateItineraryPermission(itineraryId, user)
+
+    // phase 2: duplicate contingency
+    const contingecyPlan = await this.findContingencyPlan(
+      itineraryId,
+      contingencyPlanId,
+      user
+    )
+    const contingencyData: CreateContingencyPlanDto = {
+      title: contingecyPlan.title,
+      description: contingecyPlan.description,
+      sections: contingecyPlan.sections.map((section) => ({
+        sectionNumber: section.sectionNumber,
+        title: section.title || `Hari ke-${section.sectionNumber}`,
+        blocks:
+          section.blocks && section.blocks.length > 0
+            ? section.blocks.map((block, index) => ({
+                position: index,
+                blockType: block.blockType,
+                title: block.title,
+                description: block.description,
+                startTime: block.startTime ? new Date(block.startTime) : null,
+                endTime: block.endTime ? new Date(block.endTime) : null,
+                location: block.location,
+                price: block.price || 0,
+                photoUrl: block.photoUrl,
+                routeToNext: block.routeToNext,
+                routeFromPrevious: block.routeFromPrevious,
+              }))
+            : [],
+      })),
path: src/itinerary/itinerary.service.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Section numbers are re-offset → new contingency ends up at 2001/3001 …**

Existing contingency sections already carry the +1000 offset (e.g. `1001`).  
Passing that value to `createContingencyPlan()` means the service will *add the offset again*, so the duplicate’s first section becomes `2001`.

Convert back to the logical section number first:

```diff
- sectionNumber: section.sectionNumber,
+ sectionNumber: section.sectionNumber % 1000,
```

Do the same for every section in the map.



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        sectionNumber: section.sectionNumber % 1000,
        title: section.title || `Hari ke-${section.sectionNumber}`,
        blocks:
          section.blocks && section.blocks.length > 0
            ? section.blocks.map((block, index) => ({
                position: index,
                blockType: block.blockType,
                title: block.title,
                description: block.description,
                startTime: block.startTime ? new Date(block.startTime) : null,
                endTime: block.endTime ? new Date(block.endTime) : null,
                location: block.location,
                price: block.price || 0,
                photoUrl: block.photoUrl,
                routeToNext: block.routeToNext,
                routeFromPrevious: block.routeFromPrevious,
              }))
            : [],
      })),
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-24T16:15:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3790,4 +3790,528 @@ describe('ItineraryService', () => {
       ).rejects.toThrow(BadRequestException)
     })
   })
+
+  describe('duplicateItinerary', () => {
+    it('should duplicate an itinerary succesfully', async () => {
+      // Arrange
+      const existingItinerary = {
+        id: 'itinerary-123',
+        userId: 'different-userid',
+        title: 'Beach Trip',
+        isPublished: true,
+        description: 'An relaxing beach vacation',
+        startDate: new Date('2025-03-11'),
+        endDate: new Date('2025-03-16'),
+        coverImage: 'beach2.jpg',
+        tags: [
+          {
+            id: 'itinerarytag-1',
+            itineraryId: 'itinerary-123',
+            tagId: 'tag-123',
+            createdAt: new Date(),
+            tag: {
+              id: 'tag-123',
+              name: 'Beach',
+              description: 'Beach activities',
+              iconUrl: null,
+              createdAt: new Date(),
+              updatedAt: new Date(),
+            },
+          },
+        ],
+        sections: [
+          {
+            sectionNumber: 1,
+            title: 'Updated Day 1',
+            blocks: [
+              {
+                blockType: BLOCK_TYPE.LOCATION,
+                title: 'Updated Beach Resort',
+                description: 'Check in at the updated beach resort',
+                position: 0,
+                startTime: new Date('2025-03-11T14:00:00Z'),
+                endTime: new Date('2025-03-11T15:00:00Z'),
+                location: 'Updated Beach Resort',
+                price: 600000,
+                photoUrl: 'updated_resort.jpg',
+              },
+            ],
+          },
+        ],
+      }
+
+      const duplicatedItineraryDto: CreateItineraryDto = {
+        title: existingItinerary.title,
+        description: existingItinerary.description,
+        startDate: existingItinerary.startDate,
+        endDate: existingItinerary.endDate,
+        coverImage: existingItinerary.coverImage,
+        tags: ['tag-123'],
+        sections: [
+          {
+            sectionNumber: 1,
+            title: 'Updated Day 1',
+            blocks: [
+              {
+                blockType: BLOCK_TYPE.LOCATION,
+                title: 'Updated Beach Resort',
+                description: 'Check in at the updated beach resort',
+                position: 0,
+                startTime: new Date('2025-03-11T14:00:00Z'),
+                endTime: new Date('2025-03-11T15:00:00Z'),
+                location: 'Updated Beach Resort',
+                price: 600000,
+                photoUrl: 'updated_resort.jpg',
+              },
+            ],
+          },
+        ],
+      }
+
+      const duplicatedItinerary = {
+        id: 'itinerary-789',
+        userId: mockUser.id,
+        title: duplicatedItineraryDto.title,
+        description: duplicatedItineraryDto.description,
+        coverImage: duplicatedItineraryDto.coverImage,
+        startDate: duplicatedItineraryDto.startDate,
+        endDate: duplicatedItineraryDto.endDate,
+        isPublished: false,
+        isCompleted: false,
+        createdAt: new Date(),
+        updatedAt: new Date(),
+        sections: [
+          {
+            id: 'section-1',
+            itineraryId: 'itinerary-789',
+            sectionNumber: 1,
+            title: 'Updated Day 1',
+            createdAt: new Date(),
+            updatedAt: new Date(),
+            blocks: [
+              {
+                id: 'block-1',
+                sectionId: 'section-1',
+                position: 0,
+                blockType: BLOCK_TYPE.LOCATION,
+                title: 'Updated Beach Resort',
+                description: 'Check in at the updated beach resort',
+                startTime: new Date('2025-03-11T14:00:00Z'),
+                endTime: new Date('2025-03-11T15:00:00Z'),
+                location: 'Updated Beach Resort',
+                price: 600000,
+                photoUrl: 'updated_resort.jpg',
+                createdAt: new Date(),
+                updatedAt: new Date(),
+              },
+            ],
+          },
+        ],
+        tags: [
+          {
+            id: 'itinerarytag-1',
+            itineraryId: 'itinerary-123',
+            tagId: 'tag-123',
+            createdAt: new Date(),
+            tag: {
+              id: 'tag-123',
+              name: 'Beach',
+              description: 'Beach activities',
+              iconUrl: null,
+              createdAt: new Date(),
+              updatedAt: new Date(),
+            },
+          },
+        ],
+      }
+
+      mockPrismaService.itinerary.findUnique.mockResolvedValue(
+        existingItinerary
+      )
+      mockPrismaService.tag.findMany.mockResolvedValue([
+        { id: 'tag-123', name: 'Beach' },
+      ])
+      mockPrismaService.itinerary.create.mockResolvedValue(duplicatedItinerary)
+
+      // Act
+      const result = await service.duplicateItinerary('itinerary-123', mockUser)
+
+      // Assert
+      expect(mockPrismaService.tag.findMany).toHaveBeenCalledWith({
+        where: {
+          id: {
+            in: ['tag-123'],
+          },
+        },
+      })
+      expect(mockPrismaService.$transaction).toHaveBeenCalled()
+      expect(mockPrismaService.itinerary.create).toHaveBeenCalledWith({
+        data: {
+          userId: mockUser.id,
+          title: duplicatedItinerary.title,
+          description: duplicatedItinerary.description,
+          coverImage: duplicatedItinerary.coverImage,
+          startDate: new Date(duplicatedItinerary.startDate),
+          endDate: new Date(duplicatedItinerary.endDate),
+          tags: {
+            create: [{ tag: { connect: { id: 'tag-123' } } }],
+          },
+          sections: {
+            create: [
+              {
+                sectionNumber: 1,
+                title: 'Updated Day 1',
+                blocks: {
+                  create: [
+                    {
+                      position: 0,
+                      blockType: BLOCK_TYPE.LOCATION,
+                      title: 'Updated Beach Resort',
+                      description: 'Check in at the updated beach resort',
+                      startTime: expect.any(Date),
+                      endTime: expect.any(Date),
+                      location: 'Updated Beach Resort',
+                      price: 600000,
+                      photoUrl: 'updated_resort.jpg',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        include: {
+          sections: {
+            include: {
+              blocks: true,
+            },
+          },
+          tags: {
+            include: {
+              tag: true,
+            },
+          },
+        },
+      })
+      expect(result).toEqual(duplicatedItinerary)
+    })
path: src/itinerary/itinerary.service.spec.ts
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Happy-path test passes only because mocks bypass the real permission bug**

The test stubs `findUnique` to return an itinerary owned by *another* user but `isPublished: true`, then stubs `create` to succeed.  
Because the service under test erroneously calls `findOne()` (see earlier comment), the real implementation would currently throw `ForbiddenException`; the test hides that by mocking `$transaction` & `findOne` chain.

Please add a negative test asserting that a published itinerary *is* duplicable **after you fix the service**, and remove the permissive mock that bypasses `findOne()`.

---

I’ll inspect the service implementation to confirm where and how permissions are checked in `duplicateItinerary`.

---

🏁 Script executed:

```shell
#!/bin/bash
# Locate and display the duplicateItinerary method with surrounding lines
rg -n -A 50 "async duplicateItinerary" src/itinerary/itinerary.service.ts
```

Length of output: 2242

---

I need to inspect the permission-check helper to confirm the current behavior.

---

🏁 Script executed:

```shell
#!/bin/bash
# Display the implementation of the duplicate-itinerary permission check
rg -n -A 20 "async _checkDuplicateItineraryPermission" src/itinerary/itinerary.service.ts
```

Length of output: 722

---

I need to inspect the `findOne` method’s permission logic.

---

🏁 Script executed:

```shell
#!/bin/bash
# Display the findOne method implementation and its permission checks
rg -n -A 20 "async findOne" src/itinerary/itinerary.service.ts
```

Length of output: 691

---

I need to verify how `findOne` enforces access in `_checkItineraryExists`.

---

🏁 Script executed:

```shell
#!/bin/bash
# Display the implementation of the itinerary existence and permission check
rg -n -A 20 "async _checkItineraryExists" src/itinerary/itinerary.service.ts
```

Length of output: 718

---



</details>

**Refactor `duplicateItinerary` permission flow and tighten tests**  

Currently, `duplicateItinerary` first calls `_checkDuplicateItineraryPermission` (which allows non-owners to duplicate *published* itineraries) but then immediately calls `findOne`—and in turn `_checkItineraryExists`—which only grants access to owners or shared users, *never* to non-owners of published itineraries. This mismatch means published itineraries remain forbidden, yet your happy-path test only passes because it mocks away both permission checks and the real `findOne` call.

Please update as follows:

• In src/itinerary/itinerary.service.ts:  
  – At `async duplicateItinerary` (around line 297), replace the call to `this.findOne(id, user)` with either:  
    • the itinerary returned from `_checkDuplicateItineraryPermission` (extended to include tags & sections), or  
    • a new helper that loads full itinerary details without the strict owner/access guard.  
  – Ensure `_checkDuplicateItineraryPermission` (around line 423) includes the necessary `sections` and `tags` in its `findUnique` so you don’t need `findOne`.  

• Update existing tests in src/itinerary/itinerary.service.spec.ts:  
  – Remove any mocks that bypass the actual duplicate-permission or `findOne` logic.  
  – Stub only the Prisma calls you truly intend to simulate (e.g. a `prisma.itinerary.findUnique` returning a published/non-published fixture, then `$transaction` & `itinerary.create`).  
  – Add two focused tests:  
    1. Unauthenticated user (non-owner) *cannot* duplicate an **unpublished** itinerary (expect `ForbiddenException`).  
    2. Non-owner *can* duplicate a **published** itinerary (expect success).  

This will align your implementation with its intended behavior and prevent mocks from hiding real permission bugs.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-24T16:15:14Z
author_association: CONTRIBUTOR
path: src/itinerary/dto/duplicate-itinerary.dto.ts
user: login: Nsyay
body: Halo kindi, karena ini telah di refactor dan dto duplicate itinerary sudah tidak dipakai, sebaiknya dihapus saja
created_at: 2025-04-27T14:39:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1996,6 +1998,396 @@ describe('ItineraryController', () => {
     })
   })
 
+  describe('duplicateItineraryAndContingencies', () => {
+    it('should duplicate an itinerary and its contingencies and return a formatted response', async () => {
+      const itineraryId = 'itinerary-123'
+      const existingItinerary = {
+        id: itineraryId,
+        userId: 'different-userid',
+        title: 'Beach Trip',
+        isPublished: true,
+        description: 'An relaxing beach vacation',
+        startDate: new Date('2025-03-11'),
+        endDate: new Date('2025-03-16'),
+        coverImage: 'beach2.jpg',
+        tags: [
+          {
+            id: 'itinerarytag-1',
+            itineraryId: 'itinerary-123',
+            tagId: 'tag-123',
+            createdAt: new Date(),
+            tag: {
+              id: 'tag-123',
+              name: 'Beach',
+              description: 'Beach activities',
+              iconUrl: null,
+              createdAt: new Date(),
+              updatedAt: new Date(),
+            },
+          },
+        ],
+        sections: [
+          {
+            sectionNumber: 1,
+            title: 'Updated Day 1',
+            blocks: [
+              {
+                blockType: BLOCK_TYPE.LOCATION,
+                title: 'Updated Beach Resort',
+                description: 'Check in at the updated beach resort',
+                position: 0,
+                startTime: new Date('2025-03-11T14:00:00Z'),
+                endTime: new Date('2025-03-11T15:00:00Z'),
+                location: 'Updated Beach Resort',
+                price: 600000,
+                photoUrl: 'updated_resort.jpg',
+              },
+            ],
+          },
+        ],
+      }
+
+      const duplicatedItineraryDto: CreateItineraryDto = {
+        title: existingItinerary.title,
+        description: existingItinerary.description,
+        startDate: existingItinerary.startDate,
+        endDate: existingItinerary.endDate,
+        coverImage: existingItinerary.coverImage,
+        tags: ['tag-123'],
+        sections: [
+          {
+            sectionNumber: 1,
+            title: 'Updated Day 1',
+            blocks: [
+              {
+                blockType: BLOCK_TYPE.LOCATION,
+                title: 'Updated Beach Resort',
+                description: 'Check in at the updated beach resort',
+                position: 0,
+                startTime: new Date('2025-03-11T14:00:00Z'),
+                endTime: new Date('2025-03-11T15:00:00Z'),
+                location: 'Updated Beach Resort',
+                price: 600000,
+                photoUrl: 'updated_resort.jpg',
+              },
+            ],
+          },
+        ],
+      }
+
+      const duplicatedItinerary = {
+        id: 'itinerary-789',
+        userId: mockUser.id,
+        title: duplicatedItineraryDto.title,
+        description: duplicatedItineraryDto.description,
+        coverImage: duplicatedItineraryDto.coverImage,
+        startDate: duplicatedItineraryDto.startDate,
+        endDate: duplicatedItineraryDto.endDate,
+        isPublished: false,
+        isCompleted: false,
+        createdAt: new Date(),
+        updatedAt: new Date(),
+        sections: [
+          {
+            id: 'section-1',
+            itineraryId: 'itinerary-789',
+            sectionNumber: 1,
+            title: 'Updated Day 1',
+            createdAt: new Date(),
+            updatedAt: new Date(),
+            blocks: [
+              {
+                id: 'block-1',
+                sectionId: 'section-1',
+                position: 0,
+                blockType: BLOCK_TYPE.LOCATION,
+                title: 'Updated Beach Resort',
+                description: 'Check in at the updated beach resort',
+                startTime: new Date('2025-03-11T14:00:00Z'),
+                endTime: new Date('2025-03-11T15:00:00Z'),
+                location: 'Updated Beach Resort',
+                price: 600000,
+                photoUrl: 'updated_resort.jpg',
+                createdAt: new Date(),
+                updatedAt: new Date(),
+              },
+            ],
+          },
+        ],
+        tags: [
+          {
+            id: 'itinerarytag-1',
+            itineraryId: 'itinerary-123',
+            tagId: 'tag-123',
+            createdAt: new Date(),
+            tag: {
+              id: 'tag-123',
+              name: 'Beach',
+              description: 'Beach activities',
+              iconUrl: null,
+              createdAt: new Date(),
+              updatedAt: new Date(),
+            },
+          },
+        ],
+      }
+
+      const existingContingencyPlans = [
+        {
+          id: 'contingency-plan-123',
+          title: 'Contingency Plan 1',
+          description: 'Desc',
+          sections: [],
+        },
+        {
+          id: 'contingency-plan-456',
+          title: 'Contingency Plan 2',
+          description: 'Desc',
+          sections: [],
+        },
+      ]
+
+      const mockResponse = {
+        statusCode: HttpStatus.OK,
+        message: 'Contingency updated successfully.',
+        itinerary: duplicatedItinerary,
+      }
+
+      mockItineraryService.findContingencyPlans.mockResolvedValue(
+        existingContingencyPlans
+      )
+      mockItineraryService.duplicateItinerary.mockResolvedValue(
+        duplicatedItinerary
+      )
+      mockResponseUtil.response.mockReturnValue(mockResponse)
+
+      const result = await controller.duplicateItineraryAndContingencies(
+        itineraryId,
+        mockUser
+      )
+
+      expect(mockItineraryService.duplicateItinerary).toHaveBeenCalledWith(
+        itineraryId,
+        mockUser
+      )
+      expect(mockItineraryService.duplicateContingency).toHaveBeenCalledTimes(
+        existingContingencyPlans.length
+      )
+      existingContingencyPlans.forEach((plan) => {
+        expect(mockItineraryService.duplicateContingency).toHaveBeenCalledWith(
+          duplicatedItinerary.id,
+          itineraryId,
+          plan.id,
+          mockUser
+        )
+      })
+      expect(mockResponseUtil.response).toHaveBeenCalledWith(
+        {
+          statusCode: HttpStatus.CREATED,
+          message: 'Itinerary duplicated succesfully',
+        },
+        { duplicatedItinerary }
+      )
+      expect(result).toEqual(mockResponse)
+    })
+
+    it('should duplicate an itinerary with no contingencies and return a formatted response', async () => {
+      const itineraryId = 'itinerary-123'
+      const existingItinerary = {
+        id: itineraryId,
+        userId: 'different-userid',
+        title: 'Beach Trip',
+        isPublished: true,
+        description: 'An relaxing beach vacation',
+        startDate: new Date('2025-03-11'),
+        endDate: new Date('2025-03-16'),
+        coverImage: 'beach2.jpg',
+        tags: [
+          {
+            id: 'itinerarytag-1',
+            itineraryId: 'itinerary-123',
+            tagId: 'tag-123',
+            createdAt: new Date(),
+            tag: {
+              id: 'tag-123',
+              name: 'Beach',
+              description: 'Beach activities',
+              iconUrl: null,
+              createdAt: new Date(),
+              updatedAt: new Date(),
+            },
+          },
+        ],
+        sections: [
+          {
+            sectionNumber: 1,
+            title: 'Updated Day 1',
+            blocks: [
+              {
+                blockType: BLOCK_TYPE.LOCATION,
+                title: 'Updated Beach Resort',
+                description: 'Check in at the updated beach resort',
+                position: 0,
+                startTime: new Date('2025-03-11T14:00:00Z'),
+                endTime: new Date('2025-03-11T15:00:00Z'),
+                location: 'Updated Beach Resort',
+                price: 600000,
+                photoUrl: 'updated_resort.jpg',
+              },
+            ],
+          },
+        ],
+      }
+
+      const duplicatedItineraryDto: CreateItineraryDto = {
+        title: existingItinerary.title,
+        description: existingItinerary.description,
+        startDate: existingItinerary.startDate,
+        endDate: existingItinerary.endDate,
+        coverImage: existingItinerary.coverImage,
+        tags: ['tag-123'],
+        sections: [
+          {
+            sectionNumber: 1,
+            title: 'Updated Day 1',
+            blocks: [
+              {
+                blockType: BLOCK_TYPE.LOCATION,
+                title: 'Updated Beach Resort',
+                description: 'Check in at the updated beach resort',
+                position: 0,
+                startTime: new Date('2025-03-11T14:00:00Z'),
+                endTime: new Date('2025-03-11T15:00:00Z'),
+                location: 'Updated Beach Resort',
+                price: 600000,
+                photoUrl: 'updated_resort.jpg',
+              },
+            ],
+          },
+        ],
+      }
+
+      const duplicatedItinerary = {
+        id: 'itinerary-789',
+        userId: mockUser.id,
+        title: duplicatedItineraryDto.title,
+        description: duplicatedItineraryDto.description,
+        coverImage: duplicatedItineraryDto.coverImage,
+        startDate: duplicatedItineraryDto.startDate,
+        endDate: duplicatedItineraryDto.endDate,
+        isPublished: false,
+        isCompleted: false,
+        createdAt: new Date(),
+        updatedAt: new Date(),
+        sections: [
+          {
+            id: 'section-1',
+            itineraryId: 'itinerary-789',
+            sectionNumber: 1,
+            title: 'Updated Day 1',
+            createdAt: new Date(),
+            updatedAt: new Date(),
+            blocks: [
+              {
+                id: 'block-1',
+                sectionId: 'section-1',
+                position: 0,
+                blockType: BLOCK_TYPE.LOCATION,
+                title: 'Updated Beach Resort',
+                description: 'Check in at the updated beach resort',
+                startTime: new Date('2025-03-11T14:00:00Z'),
+                endTime: new Date('2025-03-11T15:00:00Z'),
+                location: 'Updated Beach Resort',
+                price: 600000,
+                photoUrl: 'updated_resort.jpg',
+                createdAt: new Date(),
+                updatedAt: new Date(),
+              },
+            ],
+          },
+        ],
+        tags: [
+          {
+            id: 'itinerarytag-1',
+            itineraryId: 'itinerary-123',
+            tagId: 'tag-123',
+            createdAt: new Date(),
+            tag: {
+              id: 'tag-123',
+              name: 'Beach',
+              description: 'Beach activities',
+              iconUrl: null,
+              createdAt: new Date(),
+              updatedAt: new Date(),
+            },
+          },
+        ],
+      }
+
+      const existingContingencyPlans = []
+
+      const mockResponse = {
+        statusCode: HttpStatus.OK,
+        message: 'Contingency updated successfully.',
+        itinerary: duplicatedItinerary,
+      }
+
+      mockItineraryService.findContingencyPlans.mockResolvedValue(
+        existingContingencyPlans
+      )
+      mockItineraryService.duplicateItinerary.mockResolvedValue(
+        duplicatedItinerary
+      )
+      mockResponseUtil.response.mockReturnValue(mockResponse)
+
+      const result = await controller.duplicateItineraryAndContingencies(
+        itineraryId,
+        mockUser
+      )
+
+      expect(mockItineraryService.duplicateItinerary).toHaveBeenCalledWith(
+        itineraryId,
+        mockUser
+      )
+      expect(mockItineraryService.duplicateContingency).toHaveBeenCalledTimes(
+        existingContingencyPlans.length
+      )
+      existingContingencyPlans.forEach((plan) => {
+        expect(mockItineraryService.duplicateContingency).toHaveBeenCalledWith(
+          duplicatedItinerary.id,
+          itineraryId,
+          plan.id,
+          mockUser
+        )
+      })
+      expect(mockResponseUtil.response).toHaveBeenCalledWith(
+        {
+          statusCode: HttpStatus.CREATED,
+          message: 'Itinerary duplicated succesfully',
+        },
+        { duplicatedItinerary }
+      )
+      expect(result).toEqual(mockResponse)
+    })
+
+    it('should pass errors from service to the caller', async () => {
+      // Arrange
+      const mockError = new Error('Itinerary is not public')
+      mockItineraryService.duplicateItinerary.mockRejectedValue(mockError)
+
+      // Act & Assert
+      await expect(
+        controller.duplicateItineraryAndContingencies('itn-123', mockUser)
+      ).rejects.toThrow(mockError)
+
+      expect(mockItineraryService.duplicateItinerary).toHaveBeenCalledWith(
+        'itn-123',
+        mockUser
+      )
+      expect(mockResponseUtil.response).not.toHaveBeenCalled()
+    })
+  })
path: src/itinerary/itinerary.controller.spec.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix inconsistencies in test mock responses.**

There are inconsistencies between the mock response objects and the actual assertions in the tests:

1. The mock response message in both test cases doesn't match the message used in assertions
2. The mock response HTTP status code is OK but the assertion uses CREATED
3. The word "successfully" is misspelled in the assertions


Apply these changes to fix the issues:

```diff
  const mockResponse = {
-   statusCode: HttpStatus.OK,
-   message: 'Contingency updated successfully.',
+   statusCode: HttpStatus.CREATED,
+   message: 'Itinerary duplicated successfully',
    itinerary: duplicatedItinerary,
  }
```

And update the corresponding assertion:

```diff
  expect(mockResponseUtil.response).toHaveBeenCalledWith(
    {
      statusCode: HttpStatus.CREATED,
-     message: 'Itinerary duplicated succesfully',
+     message: 'Itinerary duplicated successfully',
    },
    { duplicatedItinerary }
  )
```

These changes should be made in both test cases (lines ~2150-2154, ~2329-2333, ~2186-2187, ~2365-2366).


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
      const mockResponse = {
-        statusCode: HttpStatus.OK,
-        message: 'Contingency updated successfully.',
+        statusCode: HttpStatus.CREATED,
+        message: 'Itinerary duplicated successfully',
        itinerary: duplicatedItinerary,
      }

      expect(mockResponseUtil.response).toHaveBeenCalledWith(
        {
          statusCode: HttpStatus.CREATED,
-         message: 'Itinerary duplicated succesfully',
+         message: 'Itinerary duplicated successfully',
        },
        { duplicatedItinerary }
      )
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-27T16:27:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4478,6 +4478,745 @@ describe('ItineraryService', () => {
     })
   })
 
+  describe('duplicateItinerary', () => {
+    it('should duplicate an itinerary succesfully', async () => {
+      // Arrange
+      const existingItinerary = {
+        id: 'itinerary-123',
+        userId: 'different-userid',
+        title: 'Beach Trip',
+        isPublished: true,
+        description: 'An relaxing beach vacation',
+        startDate: new Date('2025-03-11'),
+        endDate: new Date('2025-03-16'),
+        coverImage: 'beach2.jpg',
+        tags: [
+          {
+            id: 'itinerarytag-1',
+            itineraryId: 'itinerary-123',
+            tagId: 'tag-123',
+            createdAt: new Date(),
+            tag: {
+              id: 'tag-123',
+              name: 'Beach',
+              description: 'Beach activities',
+              iconUrl: null,
+              createdAt: new Date(),
+              updatedAt: new Date(),
+            },
+          },
+        ],
+        sections: [
+          {
+            sectionNumber: 1,
+            title: 'Updated Day 1',
+            blocks: [
+              {
+                blockType: BLOCK_TYPE.LOCATION,
+                title: 'Updated Beach Resort',
+                description: 'Check in at the updated beach resort',
+                position: 0,
+                startTime: new Date('2025-03-11T14:00:00Z'),
+                endTime: new Date('2025-03-11T15:00:00Z'),
+                location: 'Updated Beach Resort',
+                price: 600000,
+                photoUrl: 'updated_resort.jpg',
+              },
+            ],
+          },
+        ],
+      }
+
+      const duplicatedItineraryDto: CreateItineraryDto = {
+        title: existingItinerary.title + ' (Copy)',
+        description: existingItinerary.description,
+        startDate: existingItinerary.startDate,
+        endDate: existingItinerary.endDate,
+        coverImage: existingItinerary.coverImage,
+        tags: ['tag-123'],
+        sections: [
+          {
+            sectionNumber: 1,
+            title: 'Updated Day 1',
+            blocks: [
+              {
+                blockType: BLOCK_TYPE.LOCATION,
+                title: 'Updated Beach Resort',
+                description: 'Check in at the updated beach resort',
+                position: 0,
+                startTime: new Date('2025-03-11T14:00:00Z'),
+                endTime: new Date('2025-03-11T15:00:00Z'),
+                location: 'Updated Beach Resort',
+                price: 600000,
+                photoUrl: 'updated_resort.jpg',
+              },
+            ],
+          },
+        ],
+      }
+
+      const duplicatedItinerary = {
+        id: 'itinerary-789',
+        userId: mockUser.id,
+        title: duplicatedItineraryDto.title,
+        description: duplicatedItineraryDto.description,
+        coverImage: duplicatedItineraryDto.coverImage,
+        startDate: duplicatedItineraryDto.startDate,
+        endDate: duplicatedItineraryDto.endDate,
+        isPublished: false,
+        isCompleted: false,
+        createdAt: new Date(),
+        updatedAt: new Date(),
+        sections: [
+          {
+            id: 'section-1',
+            itineraryId: 'itinerary-789',
+            sectionNumber: 1,
+            title: 'Updated Day 1',
+            createdAt: new Date(),
+            updatedAt: new Date(),
+            blocks: [
+              {
+                id: 'block-1',
+                sectionId: 'section-1',
+                position: 0,
+                blockType: BLOCK_TYPE.LOCATION,
+                title: 'Updated Beach Resort',
+                description: 'Check in at the updated beach resort',
+                startTime: new Date('2025-03-11T14:00:00Z'),
+                endTime: new Date('2025-03-11T15:00:00Z'),
+                location: 'Updated Beach Resort',
+                price: 600000,
+                photoUrl: 'updated_resort.jpg',
+                createdAt: new Date(),
+                updatedAt: new Date(),
+              },
+            ],
+          },
+        ],
+        tags: [
+          {
+            id: 'itinerarytag-1',
+            itineraryId: 'itinerary-123',
+            tagId: 'tag-123',
+            createdAt: new Date(),
+            tag: {
+              id: 'tag-123',
+              name: 'Beach',
+              description: 'Beach activities',
+              iconUrl: null,
+              createdAt: new Date(),
+              updatedAt: new Date(),
+            },
+          },
+        ],
+      }
+
+      mockPrismaService.itinerary.findUnique.mockResolvedValue(
+        existingItinerary
+      )
+      mockPrismaService.tag.findMany.mockResolvedValue([
+        { id: 'tag-123', name: 'Beach' },
+      ])
+      mockPrismaService.itinerary.create.mockResolvedValue(duplicatedItinerary)
+
+      // Act
+      const result = await service.duplicateItinerary('itinerary-123', mockUser)
+
+      // Assert
+      expect(mockPrismaService.tag.findMany).toHaveBeenCalledWith({
+        where: {
+          id: {
+            in: ['tag-123'],
+          },
+        },
+      })
+      expect(mockPrismaService.$transaction).toHaveBeenCalled()
+      expect(mockPrismaService.itinerary.create).toHaveBeenCalledWith({
+        data: {
+          userId: mockUser.id,
+          title: duplicatedItinerary.title,
+          description: duplicatedItinerary.description,
+          coverImage: duplicatedItinerary.coverImage,
+          startDate: new Date(duplicatedItinerary.startDate),
+          endDate: new Date(duplicatedItinerary.endDate),
+          tags: {
+            create: [{ tag: { connect: { id: 'tag-123' } } }],
+          },
+          sections: {
+            create: [
+              {
+                sectionNumber: 1,
+                title: 'Updated Day 1',
+                blocks: {
+                  create: [
+                    {
+                      position: 0,
+                      blockType: BLOCK_TYPE.LOCATION,
+                      title: 'Updated Beach Resort',
+                      description: 'Check in at the updated beach resort',
+                      startTime: new Date('2025-03-11T14:00:00Z'),
+                      endTime: new Date('2025-03-11T15:00:00Z'),
+                      location: 'Updated Beach Resort',
+                      price: 600000,
+                      photoUrl: 'updated_resort.jpg',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        include: {
+          sections: {
+            include: {
+              blocks: true,
+            },
+          },
+          tags: {
+            include: {
+              tag: true,
+            },
+          },
+          user: {
+            select: {
+              firstName: true,
+              id: true,
+              lastName: true,
+              photoProfile: true,
+            },
+          },
+        },
+      })
+      expect(result).toEqual(duplicatedItinerary)
+    })
+
+    it('should duplicate an itinerary with default fields succesfully', async () => {
+      // Arrange
+      const existingItinerary = {
+        id: 'itinerary-123',
+        userId: 'different-userid',
+        title: 'Beach Trip',
+        isPublished: true,
+        description: 'An relaxing beach vacation',
+        startDate: new Date('2025-03-11'),
+        endDate: new Date('2025-03-16'),
+        coverImage: 'beach2.jpg',
+        tags: [],
+        sections: [
+          {
+            sectionNumber: 1,
+            blocks: [
+              {
+                blockType: BLOCK_TYPE.LOCATION,
+                title: 'Updated Beach Resort',
+                description: 'Check in at the updated beach resort',
+                position: 0,
+              },
+            ],
+          },
+          {
+            sectionNumber: 2,
+            blocks: [],
+          },
+        ],
+      }
+
+      const duplicatedItineraryDto: CreateItineraryDto = {
+        title: existingItinerary.title + ' (Copy)',
+        description: existingItinerary.description,
+        startDate: existingItinerary.startDate,
+        endDate: existingItinerary.endDate,
+        coverImage: existingItinerary.coverImage,
+        tags: [],
+        sections: [
+          {
+            sectionNumber: 1,
+            title: 'Hari ke-1',
+            blocks: [
+              {
+                blockType: BLOCK_TYPE.LOCATION,
+                title: 'Updated Beach Resort',
+                description: 'Check in at the updated beach resort',
+                position: 0,
+                startTime: null,
+                endTime: null,
+                price: 0,
+              },
+            ],
+          },
+          {
+            sectionNumber: 2,
+            title: 'Hari ke-2',
+            blocks: [],
+          },
+        ],
+      }
+
+      const duplicatedItinerary = {
+        id: 'itinerary-789',
+        userId: mockUser.id,
+        title: duplicatedItineraryDto.title,
+        description: duplicatedItineraryDto.description,
+        coverImage: duplicatedItineraryDto.coverImage,
+        startDate: duplicatedItineraryDto.startDate,
+        endDate: duplicatedItineraryDto.endDate,
+        isPublished: false,
+        isCompleted: false,
+        createdAt: new Date(),
+        updatedAt: new Date(),
+        sections: [
+          {
+            id: 'section-1',
+            itineraryId: 'itinerary-789',
+            sectionNumber: 1,
+            title: 'Updated Day 1',
+            createdAt: new Date(),
+            updatedAt: new Date(),
+            blocks: [
+              {
+                id: 'block-1',
+                sectionId: 'section-1',
+                position: 0,
path: src/itinerary/itinerary.service.spec.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Section title inconsistency will cause false negatives**

In `duplicatedItinerary` the first section is given the title *“Updated Day 1”*, yet the test later asserts that
`Prisma.itinerary.create` is invoked with section title *“Hari ke-1”* (default fallback).

These two expectations are mutually exclusive – the same call cannot satisfy both.
Please align the fixture and expectations (or use `expect.any(String)` / `expect.objectContaining`) so the test checks the
actual requirement instead of the exact hard-coded value.  
  


Also applies to: 4826-4834

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-28T02:08:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4478,6 +4478,745 @@ describe('ItineraryService', () => {
     })
   })
 
+  describe('duplicateItinerary', () => {
+    it('should duplicate an itinerary succesfully', async () => {
+      // Arrange
+      const existingItinerary = {
+        id: 'itinerary-123',
+        userId: 'different-userid',
+        title: 'Beach Trip',
+        isPublished: true,
+        description: 'An relaxing beach vacation',
+        startDate: new Date('2025-03-11'),
+        endDate: new Date('2025-03-16'),
+        coverImage: 'beach2.jpg',
+        tags: [
+          {
+            id: 'itinerarytag-1',
+            itineraryId: 'itinerary-123',
+            tagId: 'tag-123',
+            createdAt: new Date(),
+            tag: {
+              id: 'tag-123',
+              name: 'Beach',
+              description: 'Beach activities',
+              iconUrl: null,
+              createdAt: new Date(),
+              updatedAt: new Date(),
+            },
+          },
+        ],
+        sections: [
+          {
+            sectionNumber: 1,
+            title: 'Updated Day 1',
+            blocks: [
+              {
+                blockType: BLOCK_TYPE.LOCATION,
+                title: 'Updated Beach Resort',
+                description: 'Check in at the updated beach resort',
+                position: 0,
+                startTime: new Date('2025-03-11T14:00:00Z'),
+                endTime: new Date('2025-03-11T15:00:00Z'),
+                location: 'Updated Beach Resort',
+                price: 600000,
+                photoUrl: 'updated_resort.jpg',
+              },
+            ],
+          },
+        ],
+      }
+
+      const duplicatedItineraryDto: CreateItineraryDto = {
+        title: existingItinerary.title + ' (Copy)',
+        description: existingItinerary.description,
+        startDate: existingItinerary.startDate,
+        endDate: existingItinerary.endDate,
+        coverImage: existingItinerary.coverImage,
+        tags: ['tag-123'],
+        sections: [
+          {
+            sectionNumber: 1,
+            title: 'Updated Day 1',
+            blocks: [
+              {
+                blockType: BLOCK_TYPE.LOCATION,
+                title: 'Updated Beach Resort',
+                description: 'Check in at the updated beach resort',
+                position: 0,
+                startTime: new Date('2025-03-11T14:00:00Z'),
+                endTime: new Date('2025-03-11T15:00:00Z'),
+                location: 'Updated Beach Resort',
+                price: 600000,
+                photoUrl: 'updated_resort.jpg',
+              },
+            ],
+          },
+        ],
+      }
+
+      const duplicatedItinerary = {
+        id: 'itinerary-789',
+        userId: mockUser.id,
+        title: duplicatedItineraryDto.title,
+        description: duplicatedItineraryDto.description,
+        coverImage: duplicatedItineraryDto.coverImage,
+        startDate: duplicatedItineraryDto.startDate,
+        endDate: duplicatedItineraryDto.endDate,
+        isPublished: false,
+        isCompleted: false,
+        createdAt: new Date(),
+        updatedAt: new Date(),
+        sections: [
+          {
+            id: 'section-1',
+            itineraryId: 'itinerary-789',
+            sectionNumber: 1,
+            title: 'Updated Day 1',
+            createdAt: new Date(),
+            updatedAt: new Date(),
+            blocks: [
+              {
+                id: 'block-1',
+                sectionId: 'section-1',
+                position: 0,
+                blockType: BLOCK_TYPE.LOCATION,
+                title: 'Updated Beach Resort',
+                description: 'Check in at the updated beach resort',
+                startTime: new Date('2025-03-11T14:00:00Z'),
+                endTime: new Date('2025-03-11T15:00:00Z'),
+                location: 'Updated Beach Resort',
+                price: 600000,
+                photoUrl: 'updated_resort.jpg',
+                createdAt: new Date(),
+                updatedAt: new Date(),
+              },
+            ],
+          },
+        ],
+        tags: [
+          {
+            id: 'itinerarytag-1',
+            itineraryId: 'itinerary-123',
+            tagId: 'tag-123',
+            createdAt: new Date(),
+            tag: {
+              id: 'tag-123',
+              name: 'Beach',
+              description: 'Beach activities',
+              iconUrl: null,
+              createdAt: new Date(),
+              updatedAt: new Date(),
+            },
+          },
+        ],
+      }
+
+      mockPrismaService.itinerary.findUnique.mockResolvedValue(
+        existingItinerary
+      )
+      mockPrismaService.tag.findMany.mockResolvedValue([
+        { id: 'tag-123', name: 'Beach' },
+      ])
+      mockPrismaService.itinerary.create.mockResolvedValue(duplicatedItinerary)
+
+      // Act
+      const result = await service.duplicateItinerary('itinerary-123', mockUser)
+
+      // Assert
+      expect(mockPrismaService.tag.findMany).toHaveBeenCalledWith({
+        where: {
+          id: {
+            in: ['tag-123'],
+          },
+        },
+      })
+      expect(mockPrismaService.$transaction).toHaveBeenCalled()
+      expect(mockPrismaService.itinerary.create).toHaveBeenCalledWith({
+        data: {
+          userId: mockUser.id,
+          title: duplicatedItinerary.title,
+          description: duplicatedItinerary.description,
+          coverImage: duplicatedItinerary.coverImage,
+          startDate: new Date(duplicatedItinerary.startDate),
+          endDate: new Date(duplicatedItinerary.endDate),
+          tags: {
+            create: [{ tag: { connect: { id: 'tag-123' } } }],
+          },
+          sections: {
+            create: [
+              {
+                sectionNumber: 1,
+                title: 'Updated Day 1',
+                blocks: {
+                  create: [
+                    {
+                      position: 0,
+                      blockType: BLOCK_TYPE.LOCATION,
+                      title: 'Updated Beach Resort',
+                      description: 'Check in at the updated beach resort',
+                      startTime: new Date('2025-03-11T14:00:00Z'),
+                      endTime: new Date('2025-03-11T15:00:00Z'),
+                      location: 'Updated Beach Resort',
+                      price: 600000,
+                      photoUrl: 'updated_resort.jpg',
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+        },
+        include: {
+          sections: {
+            include: {
+              blocks: true,
+            },
+          },
+          tags: {
+            include: {
+              tag: true,
+            },
+          },
+          user: {
+            select: {
+              firstName: true,
+              id: true,
+              lastName: true,
+              photoProfile: true,
+            },
+          },
+        },
+      })
+      expect(result).toEqual(duplicatedItinerary)
path: src/itinerary/itinerary.service.spec.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Extremely brittle deep-equality assertions**

The `toHaveBeenCalledWith` snapshot tries to match the entire nested object graph
(including freshly generated `Date` instances) verbatim:

```ts
expect(mockPrismaService.itinerary.create).toHaveBeenCalledWith({ ... huge object ... })
```

1. A single, unrelated field added in the service layer will break the test.
2. `Date` objects created by the service and those re-created here are only *value*-equal, not *reference*-equal;
   using `new Date(duplicatedItinerary.startDate)` works now but is easy to forget next time.
3. The test file has grown to ~5400 lines and is hard to maintain.

Consider:

```ts
expect(mockPrismaService.itinerary.create).toHaveBeenCalledWith(
  expect.objectContaining({
    data: expect.objectContaining({
      userId: mockUser.id,
      title: expect.stringMatching(/\(Copy\)$/),
      sections: expect.any(Object),
      // …
    }),
    include: expect.any(Object),
  }),
)
```

`expect.objectContaining` + `expect.any` keep the test future-proof and
focus it on the contract rather than the implementation details.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-28T02:08:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4478,6 +4478,745 @@ describe('ItineraryService', () => {
     })
   })
 
+  describe('duplicateItinerary', () => {
+    it('should duplicate an itinerary succesfully', async () => {
+      // Arrange
+      const existingItinerary = {
+        id: 'itinerary-123',
+        userId: 'different-userid',
+        title: 'Beach Trip',
+        isPublished: true,
+        description: 'An relaxing beach vacation',
+        startDate: new Date('2025-03-11'),
+        endDate: new Date('2025-03-16'),
+        coverImage: 'beach2.jpg',
+        tags: [
+          {
+            id: 'itinerarytag-1',
+            itineraryId: 'itinerary-123',
+            tagId: 'tag-123',
+            createdAt: new Date(),
+            tag: {
+              id: 'tag-123',
+              name: 'Beach',
+              description: 'Beach activities',
+              iconUrl: null,
+              createdAt: new Date(),
+              updatedAt: new Date(),
+            },
+          },
+        ],
+        sections: [
+          {
+            sectionNumber: 1,
+            title: 'Updated Day 1',
+            blocks: [
+              {
+                blockType: BLOCK_TYPE.LOCATION,
+                title: 'Updated Beach Resort',
+                description: 'Check in at the updated beach resort',
+                position: 0,
+                startTime: new Date('2025-03-11T14:00:00Z'),
+                endTime: new Date('2025-03-11T15:00:00Z'),
+                location: 'Updated Beach Resort',
+                price: 600000,
+                photoUrl: 'updated_resort.jpg',
+              },
+            ],
+          },
+        ],
+      }
+
+      const duplicatedItineraryDto: CreateItineraryDto = {
+        title: existingItinerary.title + ' (Copy)',
+        description: existingItinerary.description,
+        startDate: existingItinerary.startDate,
+        endDate: existingItinerary.endDate,
+        coverImage: existingItinerary.coverImage,
+        tags: ['tag-123'],
+        sections: [
+          {
+            sectionNumber: 1,
+            title: 'Updated Day 1',
+            blocks: [
+              {
+                blockType: BLOCK_TYPE.LOCATION,
+                title: 'Updated Beach Resort',
+                description: 'Check in at the updated beach resort',
+                position: 0,
+                startTime: new Date('2025-03-11T14:00:00Z'),
+                endTime: new Date('2025-03-11T15:00:00Z'),
+                location: 'Updated Beach Resort',
+                price: 600000,
+                photoUrl: 'updated_resort.jpg',
+              },
+            ],
+          },
+        ],
+      }
+
+      const duplicatedItinerary = {
+        id: 'itinerary-789',
+        userId: mockUser.id,
+        title: duplicatedItineraryDto.title,
+        description: duplicatedItineraryDto.description,
+        coverImage: duplicatedItineraryDto.coverImage,
+        startDate: duplicatedItineraryDto.startDate,
+        endDate: duplicatedItineraryDto.endDate,
+        isPublished: false,
+        isCompleted: false,
+        createdAt: new Date(),
+        updatedAt: new Date(),
+        sections: [
+          {
+            id: 'section-1',
+            itineraryId: 'itinerary-789',
+            sectionNumber: 1,
+            title: 'Updated Day 1',
+            createdAt: new Date(),
+            updatedAt: new Date(),
+            blocks: [
+              {
+                id: 'block-1',
+                sectionId: 'section-1',
+                position: 0,
+                blockType: BLOCK_TYPE.LOCATION,
+                title: 'Updated Beach Resort',
+                description: 'Check in at the updated beach resort',
+                startTime: new Date('2025-03-11T14:00:00Z'),
+                endTime: new Date('2025-03-11T15:00:00Z'),
+                location: 'Updated Beach Resort',
+                price: 600000,
+                photoUrl: 'updated_resort.jpg',
+                createdAt: new Date(),
+                updatedAt: new Date(),
+              },
+            ],
+          },
+        ],
+        tags: [
+          {
+            id: 'itinerarytag-1',
+            itineraryId: 'itinerary-123',
+            tagId: 'tag-123',
path: src/itinerary/itinerary.service.spec.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Tag objects still point to the *original* itinerary**

`duplicatedItinerary.tags[*].itineraryId` is hard-coded to `"itinerary-123"` (the source).  
After duplication the join table record should reference the **new** itinerary (`"itinerary-789"` in this fixture).  
Keeping the old id makes the test pass even if the production code writes the wrong FK, masking a real regression.

```diff
-            itineraryId: 'itinerary-123',
+            itineraryId: 'itinerary-789',
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
          {
            id: 'itinerarytag-1',
            itineraryId: 'itinerary-789',
            tagId: 'tag-123',
          },
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-28T02:08:32Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-04-21T16:02:26Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This update introduces functionality for duplicating itineraries along with their associated contingency plans. In the controller, a new POST endpoint is implemented to handle the duplication request, which first duplicates the selected itinerary and then iterates through its contingency plans to duplicate each for the new itinerary. Corresponding service methods are added to perform deep copying of itineraries and contingency plans, including nested sections, blocks, and tags, with permission checks and proper ownership assignment. Comprehensive tests for these new methods and controller logic are also included, covering both successful and error scenarios.

## Changes

| File(s)                                | Change Summary                                                                                                                      |
|---------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------|
| `src/itinerary/itinerary.controller.ts` | Added a new POST endpoint `/:itineraryId/duplicate` and the `duplicateItineraryAndContingencies` method to duplicate an itinerary and its contingency plans. |
| `src/itinerary/itinerary.service.ts`  | Added `duplicateItinerary` and `duplicateContingency` methods to handle deep copying of itineraries and contingency plans, including nested entities and permission checks. Added a helper method `_mapSections` for mapping sections and blocks. |
| `src/itinerary/itinerary.controller.spec.ts` | Added tests for the new controller method, covering successful duplication with and without contingencies, and error propagation. Updated mock service with new methods. |
| `src/itinerary/itinerary.service.spec.ts`    | Added test suites for `duplicateItinerary` and `duplicateContingency`, including tests for successful duplication, permission errors, and not found cases. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant Controller
    participant Service
    participant DB

    User->>Controller: POST /:itineraryId/duplicate
    Controller->>Service: duplicateItinerary(itineraryId, user)
    Service->>DB: Fetch itinerary (with sections, blocks, tags)
    Service->>Service: Deep copy itinerary data
    Service->>DB: Create new itinerary
    Service-->>Controller: Duplicated itinerary

    Controller->>Service: findContingencyPlans(itineraryId)
    Service->>DB: Fetch contingency plans
    Service-->>Controller: Contingency plan list

    alt If contingency plans exist
        loop For each contingency
            Controller->>Service: duplicateContingency(newItineraryId, itineraryId, contingencyPlanId, user)
            Service->>DB: Fetch contingency plan (with sections, blocks)
            Service->>Service: Deep copy contingency data
            Service->>DB: Create new contingency plan
            Service-->>Controller: Duplicated contingency plan
        end
    end

    Controller-->>User: HTTP 201 with duplicated itinerary
```

## Poem

> A hop, a skip, a copy anew,  
> Itineraries and plans, now doubled for you!  
> With sections and tags, and contingencies too,  
> The code now ensures your journeys accrue.  
> Through tests and through logic, the rabbits have run,  
> Duplication is handled—our work here is done!  
> 🐇✨

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- DwQgtGAEAqAWCWBnSTIEMB26CuAXA9mAOYCmGJATmriQCaQDG+Ats2bgFyQAOFk+AIwBWJBrngA3EsgEBPRvlqU0AgfFwA6NPEgQAfACgjoCEYDEZyAAUASpETZWaCrKNwSPbABsvkCiQBHbGlcSHFcLzpIACIrACEASUgAZgAGSAARbG4veAZqD0x6AFFadUgAeVxYSgByZATxcioXABpo+2wBZnUaejlIbERKSCcvAGt4DDL0ZFtIDEcBEYBOFdaUDFwKRWwGaUgAM2wMMXh8DDRc3HkCSFps3PyadCwSAA8kJqIUJuUXdBeC4/ADu6lgv2QaEQiHwDHgBXoTC2U1Ip3kOUwiA0MBqjFgmFIyCmDC82CUYTx8GYORIbC21HOWHwh3ujzyjIuoxI1UUyAcDAh0MgAAMHjkOTRGlN/rIRUd8HxxU9GRgfuoZS14AciqLlZKSABhC7fMgMOUKpXs56ohQotVmjFeLE4gCCtDK4guVy8sg21Q88zQHv52W4iu+kDItHDU1wyBOSj4AdFAHoNc1nLJU/KdngSBsSWSymr0AsSCDRVYKgBlaCQdO0VP654keXR2NbML4I5oeHXAq/TVZtkSm0XHHuYZRjAE0509hQ6ndlA0nZSSkeAR98bRxhobgqeDXbXIFmbyAAWTwzh0B7HnKwAzIKlypaGlGQdxIh0OeW1Wy+qOKovBm/ynq89ABvAfDIqa6I8M6GDYriHjHD49w8toXhQtMCgUoKhIHPkT4eP4EjaiCUTUBevD4BRSj0AAqjYAAyXCwLguDcIgHCpqmRDgl0GhMMwqbXrgt5gK63DcKmzA3hQ8BgNuDC7tMqbcN4XipmsGhGPoxjgFAe7nmgeCEGiyh9AorDsFwvD8MIojiFIMjyEwSYqGomjaLoYCGCYUBwKgqCYDgBDEGQ1lRKJ9KcH4aCVg4TgAgMnnKKo6haDohlGaYBiIBQDDpn8LTZmB5UiSaOw+JQGiINwogaPGHAGNEHUGBYkCugkUWZjZKXMCO56EQ6iBuHiNCIKEDjqGhiqitKmYuMaWy1ZEFDyiCwofDQ0xRGC1RluQlZitIDBKcs8oCECamWheYrWgUy2yq60xrfB8LSPKbC8rQk4IKG82bKS5IHNU/geNNoT5MMvFGAAjDiABUKM1ns+wwuhwGSkykBHRCcGomap5o1wjT2DyCbcGWzBwuMUafDNtqVSOhNhGgRC4fQwxnBciAbLqaCQLkM38KyHxfLaxMOghmLIYD0MhKMDNfniwwUBR+zcv9yBPQ+UplVm8q6gbIFGiaJPovKdz+Lg2AUFgIstoiQ4rfIuqINS3iDq7XrMpLfZE1bcvmohmA4pTUhKX+EMEqEKZwRtIz5D4+v+yQr3ldt4IXmzAIJBkkGDJr/o1Fgac4Xqz00J91vmvKhyLSQwd2l9TqR5AlPQpr8aUjRKb+I1AseHgx7qPIqBV4deciwKWPIGwMJc4UeEppn9AF36JfVIPeLJ/gdV8PbjvIReHxNWIUTNxQw1cVEw/hshJD6QYABMqPo5j0iIDj/v4w5hgHsss0TfUQOTbuicQjqw8IgfYlwlI9hBDUfw+djYAgJMgYB7cG6d0VqhMIKt6ZqX1n+D6ocwGyCsEhRAtseynydq8KMNIbjoAoFQT2eEY7wDjurGi5sDT1zDhaaePoogAC9KA9nEMvKOoRe6UH7imRAaA2B+GkM/act976Rl1Iw8g9Bh7eAUcSLAKY/wUHFjDN+yQv7FA4YtOih4iCPkgZTGG/DQi8IvJrbWJB6g10NlnDBFo/qwEUBokQYhkAc3CpQHYFBy4eEPsfXWET6DOK5gUdWqAEmLV1LQfABxgGwx9BeJ+o9BjiGuLIN+7pPRMh9DvFMJDGZ+LyB4QQ0TQg7RprQN2dwizgyCRbbOJsS6CNbMIqh8oRDizaUcE4/NFaTWkCkucRJ0AegUDHVeD0Uzb1xuOLAhwgSVmGSWH4ACuSjUoaTHUeFnEjHyXwOctA3xEDft1V0XgaBUADl+GReIlCkmcI+M8kt3jhgoDZJxXQngznCKeAyPUPRRFOkQ8Wc0XjnXgVdEgAAKWomdxkuHerQGZDzEC1A2ISgAlJAAAvHoSAABvSAGguWQAAL70vlFMUURUSrb1KsOFw1V1pH02g1K+LU6FYsjCmKZL1QkUqpacU88pUmbXSZEjmC9f4l1efYBBt58DYgMFABpURFnhL5KMg0ZKLRm0zhqxuK5WkMyiCKZ1NZKD+IFVgEUwqxUezDbKSV2xpX1Uas1eMIp9IdWiAZAqobRXbyjSnCg8q2rJq6pYXq/UYq80cMNAEo1NnSCMK6cslZqx1hnDGfAcY0wcG3gkJsmdtrCmDIxT1eJfWhM+tmxNuI8nTE7Aohg+xuD93Ckcouu88TmQDCiVs9APxJM3JXH0sCqZa06Xq+gKqjbiotHcS6rcXgu1rl01kKir68O1FvUJbpDj/OOaqH4hzQmFlCPbJSJA3KAl8FLFmpZQGOgjufW+F5FTwEEpcXwRyhi2hFOQyl9z0Q0KxGOhIrJMAeWw+HBWyBwO4H/b8GKZ4Y5RjbgrEuVcM53vdRensmdNzMAOXiU6QEr1u0zYQm5gdcFhwgqgJqFAdFRGGEEdgCIfDyDiTteaSI904gAGJTGackptU6NEOydrhSAAAJaA0ArCQHfqkRGGiR4vwJnPToM6jXL1UaQZdHhN7u1lPcagaA37mELX86yTIgUXlBc6AF4WJZMxhXCvgWlbp5CRRqatVqrw8gydsxiXARTQlkKcL9Z6PbqpI6eQlHbaBcBmkpNUGwt1cCYprflmxGDOhhEtYdNUY1bXayG4qEbyrDazFmvr8qRTtU6latNQ2M1vo6fsWV8aEb5p+X1KyALZNlpGqyMaRJJqoDKL+XLX4QQ9kxTDTo80IuDZFaE0bEqluvzjQwSb+XSWhNNnhU9lt7SzKVo61szr5Q4uVjNfdIm4sLtCZAAYPDDiyFtHvROGsA1HsODyQU8culKSQ1cXz5UNgSCuPAAZ01ObcyFnhATlORaYqORzJOjt/Bdi3bMfgIJmhCzCOoSI6BZLRlnsdDohLjTcFkPS6IjWMDJbFjUXmkkHaCxLv4YJ9ByAzVky5cLGxbpqxp1BLmKFKaXIhjAh6hrsbeBK4AvO6FfCFJ/OZP5RxtReF5tTcuOwwSlhFAAOXwLgTT+BEzFHeLOgOAqH1Ujh0UkpwemZfCN5SX36HQ8UDUOijAEeo9MhjxeDn5BwQjHwNz/dRzgF8HKJJhFSBFdv3cMDgobGwfYBBp4u3tzCNiaoTB2YsJ4RuziVgIT0dKDPvjvvOBGOdZY9wDj/dCGCe+Cg/LJCJcC6ng2EwE4/cKMyxI/g1Xuo6c6jrX36DjG4m0CEEMQauuuSLGYMsKxkBCUskOMMUIAxEapAAL52XgvHX3Dj3y7ByCGH4HIHpQ2HVxAltC10fxWR5nhzunGDMTBiuT8DDxeAp0CygVBmLAt0hytx/ht18BEx93L3QyDxDzD2mDzxIDnQLxQFjxSWP34Br3j2KWwSTwo1T0hhoP90z2zyUFz0j2YOjzYKL01lFh3GQCkx6BhHxgeGhh7HL2aEQAQFpkWiOQom9jUFqRxDiGDwhHByXgZmsCUkQGGgPX8X3HTgenwMMyA1Jy8BJzJwpyZFTzpx8MHyUQHkTioGQj7EBRLiQNin8EHGY3YQ8GGgpGZwPkVH8DEH80knsG2D2CM2kF8IuEsW41RwqR5DPkfmkBMSXmoEFAvmhRciiB8wU3EAOA5meT4EiKMRIGdEBSCwLR6lCxiwFgHW81EGiwhTi0vgjCiHhRSwYDSyaImkyxtSghVnBz1AugJWJS+3PVpU5S5Ta0FXuyewqkWzn1ezlQTSc2Oi7zg2tz/ltyOWh0JUdxLiUEOFd1CDjk90QFgNFDoND3D0kJYIuBFFTxFFEPJ3EKYKBIwETUWPRWWOxQ71xSUHxXgGWE2NY2Px2K5Q0H2ODXTUe0zRexW3ewuOZ0txuPILuLX04MYyoN+OD3+MYMBOj1BPBJzyhOjyTRmwgDACMAJPPSOIalONzWmxTQ22LW21LVSnkErSIgWOgAu0v0K1OFgB2GASgPtVoFVz+1B0mTdWPxBIJkoEKHhKGO63PX9UPX2G1U6xQmb11O+2PXdysVCERwgmiMyUoGUO9luXMTxBXx0xQz/R3SOGxxqGJG8VOGLFZn7j5nCMJQ+CwKPwB2v1oR+IN1IUEJN3kTtDq2yKhEvxFENGiNK1lAyAIBumI0l1tHKX8EiFJy7E+O1KFiF2mFtDFwlylw6DuBTHCEiFTyUGYIUBrNLHjNi11EzIwMuIhGgiSx2Ck3mN3mpBCDUR4jtBjhsjuBFAyAKHlG6RclN1CF4HWS1gODzFAgwB0UfBnMFSnNQNiJTEP393Pz1K1JXHPzVxKKYQ3jvVfXPR4zHk1ib0HQNNTJtmdMzkLNAPwXh3kAX0FFjJnMhCphQJLnvI2GGlkhRwjI8DjB7AZwrFFBLOvX+w7lwwwArPwH3OcjSOZxXJmjXOQGRE3KiG3N3JoBop6VP1px9HQ3PzYwovoUYFLOhhBT/Kvw33CkTBGEIouThyXTgxTC3XqR4CUlJxeBqC8Ck2dJFAAH0sL/UUCe0oQzTeyQi/5FRuNxzBjdQ5y0C1ZNhey8RryANAgO9/B6A4M/CLhCxozyRbR3lPlMI3iTFUKA4+cIhHkkQLhNzbQKc4FthUQ7sOK2wnJuKgdMUtTkAgrhjhymBRyfhzx2i5iIJdQTy/EyJcC8KryrKbzCY7z0DLVejfl/kxjnLhiwUBjz5zwJjYUpikt69ZjGiUVMtLxstIk+06B8sVTZitiPYqsatMj6siBGtNZmtWsg0Ote4LSPYrTA0BsBTw0iSRSE0xqJr6ApqlqCtEAis5rMTwLzRCVTpnVO1askqGsicsw3rlrURd9j8KKfq6s/rS5KANrKA8Ttqush1LTTitrDiFtz1hTrTX4zqoBxr/pctprRQDKDwjLAVCUbKMAuASKCh8amQqLIawVoa/U4bDr5tCSTiUbJsxTU1DADAgom0YcLJJS3Y4p7JEpkpds0piMvIspfJcoAoObjJbIehcA9LydEA9LyJKI6A9LGLYV/J2bOa0AAAOAAFix2SAADZaBdafwBAVgABWAAdmSH1rQEOBNqtpIGNpIBtrQH1v1uSGSEOFSG9pWEOAYCtt1q1ulogHh3fhICttoGSAEHfhs39uSBjqtv1tSGtptrTsRhWERntuzpWFSDQGSERl1t1qtstrDs5oYGSHfnLujv1t1tSBtvfkzqtt9v1p/ERhtsRgYFoCbuLpIH1sRmDH1qtvyBtoEErplttqdrTsOFoA7vTqtsOEDtSAGUOBtpWFoDtptuDqtpLpNqx1duNu3Cnojt1oYHnoEDeKttSCjv1t7sRhIALstuNpjstrXvfmNvfgLpjtSBXtszs0Mk5tEAEFTuNt1r1pWHfn1u3toAYFLoALWFHr1uLtoG/uSFoH3t1sODTtDuAZlpWGNsOERmNuNvTq3uIbQBbqjrQHLvToECHpbsNsNt1toHfgQZIGHqtrPqgCLrtrQDIbNvYeNugbTqbt3qbrYatqLrbv/qwdvtLoEFszDvDqgDinUAVu1OVuA1VtoD0r3H0CAA=== -->

<!-- internal state end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=Mutari-App/mutari-backend&utm_content=99):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-21T16:03:10Z
body: 🎯 **Allure Test Report is Ready!**
📊 **[View Report](https://mutari-app.github.io/mutari-backend/181/)**
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-25T16:02:34Z
body: 🎯 **Allure Test Report is Ready!**
📊 **[View Report](https://mutari-app.github.io/mutari-backend/198/)**
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-27T16:55:10Z
body: 🎯 **Allure Test Report is Ready!**
📊 **[View Report](https://mutari-app.github.io/mutari-backend/210/)**
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-28T02:06:52Z
body: 🎯 **Allure Test Report is Ready!**
📊 **[View Report](https://mutari-app.github.io/mutari-backend/214/)**
author_association: NONE