Repository: Mutari-App/mutari-backend
Pull request #96: PBI-29-Recently Viewed
Author: login: fathanadhitama
Created: 2025-04-20T13:28:57Z | Merged: 2025-04-25T12:18:28Z

## **CHANGES:**
- Add itinerary view model
- Implement adding itinerary views and get itinerary view by user services and controllers

## **NOTES:**
- Tests have been added for both services and controllers to ensure correct functionality.
- Code coverage for the new features is now at 100%.
- Please review the itinerary view model to check if any corrections are needed.


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced the ability to track and display recently viewed itineraries for each user.
  - Users can now view a list of their 10 most recently viewed itineraries.
  - Viewing an itinerary updates its position in the recent views list.
- **Bug Fixes**
  - None.
- **Tests**
  - Added tests for viewing itineraries and retrieving recent itinerary views.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/db/itinerary.zmodel b/db/itinerary.zmodel
index a74aea703..3c9d1ae85 100644
--- a/db/itinerary.zmodel
+++ b/db/itinerary.zmodel
@@ -22,6 +22,7 @@ model Itinerary extends Base {
     tours            Tour[]
     contingencyPlans ContingencyPlan[]
     reminder         ItineraryReminder?
+    views ItineraryView[]
 }
 
 model ContingencyPlan extends Base {
@@ -136,3 +137,16 @@ model ItineraryReminder extends Base {
 
     itinerary      Itinerary       @relation(fields: [itineraryId], references: [id], onDelete: Cascade)
 }
+
+model ItineraryView extends Base {
+  userId      String
+  itineraryId String
+  viewedAt    DateTime @default(now())
+
+  user        User      @relation(fields: [userId], references: [id], onDelete: Cascade)
+  itinerary   Itinerary @relation(fields: [itineraryId], references: [id], onDelete: Cascade)
+
+  @@index([userId, viewedAt])
+  @@unique([userId, itineraryId])
+}
+
diff --git a/db/prisma/migrations/20250416163643_add_itinerary_view/migration.sql b/db/prisma/migrations/20250416163643_add_itinerary_view/migration.sql
new file mode 100644
index 000000000..b42842a2c
--- /dev/null
+++ b/db/prisma/migrations/20250416163643_add_itinerary_view/migration.sql
@@ -0,0 +1,26 @@
+-- CreateTable
+CREATE TABLE "ItineraryView" (
+    "updatedAt" TIMESTAMPTZ NOT NULL,
+    "createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "id" TEXT NOT NULL,
+    "userId" TEXT NOT NULL,
+    "itineraryId" TEXT NOT NULL,
+    "viewedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+    CONSTRAINT "ItineraryView_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "ItineraryView_id_key" ON "ItineraryView"("id");
+
+-- CreateIndex
+CREATE INDEX "ItineraryView_userId_viewedAt_idx" ON "ItineraryView"("userId", "viewedAt");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "ItineraryView_userId_itineraryId_key" ON "ItineraryView"("userId", "itineraryId");
+
+-- AddForeignKey
+ALTER TABLE "ItineraryView" ADD CONSTRAINT "ItineraryView_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "ItineraryView" ADD CONSTRAINT "ItineraryView_itineraryId_fkey" FOREIGN KEY ("itineraryId") REFERENCES "Itinerary"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/db/prisma/schema.prisma b/db/prisma/schema.prisma
index 8f118a6cc..1ac990dc8 100644
--- a/db/prisma/schema.prisma
+++ b/db/prisma/schema.prisma
@@ -79,6 +79,7 @@ model User {
   itineraryAccess  ItineraryAccess[]
   itineraryLikes   ItineraryLike[]
   tourTickets      TourTicket[]
+  itineraryViews   ItineraryView[]
 }
 
 model Ticket {
@@ -182,6 +183,7 @@ model Itinerary {
   tours            Tour[]
   contingencyPlans ContingencyPlan[]
   reminder         ItineraryReminder?
+  views            ItineraryView[]
 }
 
 model ContingencyPlan {
@@ -314,3 +316,17 @@ model ItineraryReminder {
   startDate      DateTime        @default(now()) @db.Timestamptz()
   itinerary      Itinerary       @relation(fields: [itineraryId], references: [id], onDelete: Cascade)
 }
+
+model ItineraryView {
+  updatedAt   DateTime  @updatedAt() @db.Timestamptz()
+  createdAt   DateTime  @default(now()) @db.Timestamptz()
+  id          String    @id() @unique() @default(uuid())
+  userId      String
+  itineraryId String
+  viewedAt    DateTime  @default(now())
+  user        User      @relation(fields: [userId], references: [id], onDelete: Cascade)
+  itinerary   Itinerary @relation(fields: [itineraryId], references: [id], onDelete: Cascade)
+
+  @@unique([userId, itineraryId])
+  @@index([userId, viewedAt])
+}
diff --git a/db/user.zmodel b/db/user.zmodel
index 8cecf1a93..f54155101 100644
--- a/db/user.zmodel
+++ b/db/user.zmodel
@@ -24,6 +24,7 @@ model User extends Base {
     itineraryAccess  ItineraryAccess[]
     itineraryLikes   ItineraryLike[]
     tourTickets      TourTicket[]
+    itineraryViews ItineraryView[]
 }
 
 model Ticket extends Base {
diff --git a/src/itinerary/itinerary.controller.spec.ts b/src/itinerary/itinerary.controller.spec.ts
index cf3c77b7d..1ce2b558c 100644
--- a/src/itinerary/itinerary.controller.spec.ts
+++ b/src/itinerary/itinerary.controller.spec.ts
@@ -54,6 +54,8 @@ describe('ItineraryController', () => {
     createContingencyPlan: jest.fn(),
     selectContingencyPlan: jest.fn(),
     updateContingencyPlan: jest.fn(),
+    createViewItinerary: jest.fn(),
+    getViewItinerary: jest.fn(),
   }
 
   const mockResponseUtil = {
@@ -1992,4 +1994,75 @@ describe('ItineraryController', () => {
       ).rejects.toThrow(ForbiddenException)
     })
   })
+
+  describe('createViewItinerary', () => {
+    it('should call service and return response', async () => {
+      const user = { id: 'user123' }
+      const itineraryId = 'itinerary123'
+      const createdView = { id: 'view1', itineraryId, userId: user.id }
+
+      mockItineraryService.createViewItinerary.mockResolvedValue(createdView)
+      mockResponseUtil.response.mockReturnValue({
+        statusCode: HttpStatus.CREATED,
+        message: 'Itinerary view added successfully',
+        data: createdView,
+      })
+
+      const result = await controller.createViewItinerary(
+        user as any,
+        itineraryId
+      )
+
+      expect(itineraryService.createViewItinerary).toHaveBeenCalledWith(
+        itineraryId,
+        user
+      )
+      expect(responseUtil.response).toHaveBeenCalledWith(
+        {
+          statusCode: HttpStatus.CREATED,
+          message: 'Itinerary view added successfully',
+        },
+        {
+          itinerary: createdView,
+        }
+      )
+      expect(result).toEqual({
+        statusCode: HttpStatus.CREATED,
+        message: 'Itinerary view added successfully',
+        data: createdView,
+      })
+    })
+  })
+
+  describe('getViewItinerary', () => {
+    it('should call service and return viewed itineraries', async () => {
+      const user = { id: 'user123' }
+      const itinerary = [{ itineraryId: 'a' }, { itineraryId: 'b' }]
+
+      mockItineraryService.getViewItinerary.mockResolvedValue(itinerary)
+      mockResponseUtil.response.mockReturnValue({
+        statusCode: HttpStatus.OK,
+        message: 'Itinerary views fetched successfully',
+        itinerary,
+      })
+
+      const result = await controller.getViewItinerary(user as any)
+
+      expect(itineraryService.getViewItinerary).toHaveBeenCalledWith(user)
+      expect(responseUtil.response).toHaveBeenCalledWith(
+        {
+          statusCode: HttpStatus.OK,
+          message: 'Itinerary views fetched successfully',
+        },
+        {
+          itinerary,
+        }
+      )
+      expect(result).toEqual({
+        statusCode: HttpStatus.OK,
+        message: 'Itinerary views fetched successfully',
+        itinerary,
+      })
+    })
+  })
 })
diff --git a/src/itinerary/itinerary.controller.ts b/src/itinerary/itinerary.controller.ts
index 429db559e..164890522 100644
--- a/src/itinerary/itinerary.controller.ts
+++ b/src/itinerary/itinerary.controller.ts
@@ -5,7 +5,6 @@ import {
   Post,
   Body,
   HttpStatus,
-  NotFoundException,
   Param,
   Patch,
   Delete,
@@ -130,6 +129,20 @@ export class ItineraryController {
     )
   }
 
+  @Get('views')
+  async getViewItinerary(@GetUser() user: User) {
+    const itinerary = await this.itineraryService.getViewItinerary(user)
+    return this.responseUtil.response(
+      {
+        statusCode: HttpStatus.OK,
+        message: 'Itinerary views fetched successfully',
+      },
+      {
+        itinerary,
+      }
+    )
+  }
+
   @Get(':id')
   async findOne(@Param('id') id: string, @GetUser() user: User) {
     const itinerary = await this.itineraryService.findOne(id, user)
@@ -170,6 +183,26 @@ export class ItineraryController {
     )
   }
 
+  @Post('views/:itineraryId')
+  async createViewItinerary(
+    @GetUser() user: User,
+    @Param('itineraryId') itineraryId: string
+  ) {
+    const itinerary = await this.itineraryService.createViewItinerary(
+      itineraryId,
+      user
+    )
+    return this.responseUtil.response(
+      {
+        statusCode: HttpStatus.CREATED,
+        message: 'Itinerary view added successfully',
+      },
+      {
+        itinerary,
+      }
+    )
+  }
+
   @Patch(':id')
   async updateItinerary(
     @Param('id') id: string,
diff --git a/src/itinerary/itinerary.service.spec.ts b/src/itinerary/itinerary.service.spec.ts
index f3642bb72..8f30590cd 100644
--- a/src/itinerary/itinerary.service.spec.ts
+++ b/src/itinerary/itinerary.service.spec.ts
@@ -54,6 +54,13 @@ describe('ItineraryService', () => {
       create: jest.fn(),
       delete: jest.fn(),
     },
+    itineraryView: {
+      upsert: jest.fn(),
+      findMany: jest.fn(),
+      create: jest.fn(),
+      delete: jest.fn(),
+      update: jest.fn(),
+    },
     _checkItineraryExists: jest.fn(),
     _checkContingencyCount: jest.fn(),
   }
@@ -3790,4 +3797,89 @@ describe('ItineraryService', () => {
       ).rejects.toThrow(BadRequestException)
     })
   })
+
+  describe('createViewItinerary', () => {
+    it('should update viewedAt if itinerary already viewed', async () => {
+      const userViews = [{ itineraryId: 'it-1' }]
+      mockPrismaService.itineraryView.findMany.mockResolvedValue(userViews)
+
+      await service.createViewItinerary('it-1', mockUser)
+
+      expect(mockPrismaService.itineraryView.update).toHaveBeenCalledWith({
+        where: {
+          userId_itineraryId: {
+            userId: 'user-123',
+            itineraryId: 'it-1',
+          },
+        },
+        data: { viewedAt: expect.any(Date) },
+      })
+    })
+
+    it('should delete oldest view if already 10 and add new', async () => {
+      const userViews = Array.from({ length: 10 }).map((_, i) => ({
+        id: `view-${i}`,
+        itineraryId: `it-${i}`,
+      }))
+
+      mockPrismaService.itineraryView.findMany.mockResolvedValue(userViews)
+      mockPrismaService.itineraryView.create.mockResolvedValue({})
+
+      await service.createViewItinerary('new-itinerary', mockUser)
+
+      expect(mockPrismaService.itineraryView.delete).toHaveBeenCalledWith({
+        where: { id: 'view-9' }, // assumed last in list is oldest
+      })
+      expect(mockPrismaService.itineraryView.create).toHaveBeenCalledWith({
+        data: {
+          userId: 'user-123',
+          itineraryId: 'new-itinerary',
+          viewedAt: expect.any(Date),
+        },
+      })
+    })
+
+    it('should just create view if less than 10 views', async () => {
+      const userViews = Array.from({ length: 5 }).map((_, i) => ({
+        id: `view-${i}`,
+        itineraryId: `it-${i}`,
+      }))
+
+      mockPrismaService.itineraryView.findMany.mockResolvedValue(userViews)
+      mockPrismaService.itineraryView.create.mockResolvedValue({})
+
+      await service.createViewItinerary('it-100', mockUser)
+
+      expect(mockPrismaService.itineraryView.delete).not.toHaveBeenCalled()
+      expect(mockPrismaService.itineraryView.create).toHaveBeenCalledWith({
+        data: {
+          userId: 'user-123',
+          itineraryId: 'it-100',
+          viewedAt: expect.any(Date),
+        },
+      })
+    })
+  })
+
+  describe('getViewItinerary', () => {
+    it('should return list of itinerary views ordered by viewedAt desc', async () => {
+      const user = { id: 'user123' }
+
+      const expectedResult = [
+        { itineraryId: 'a', viewedAt: new Date() },
+        { itineraryId: 'b', viewedAt: new Date() },
+      ]
+
+      mockPrismaService.itineraryView.findMany.mockResolvedValue(expectedResult)
+
+      const result = await service.getViewItinerary(user as any)
+
+      expect(mockPrismaService.itineraryView.findMany).toHaveBeenCalledWith({
+        where: { userId: user.id },
+        orderBy: { viewedAt: 'desc' },
+      })
+
+      expect(result).toEqual(expectedResult)
+    })
+  })
 })
diff --git a/src/itinerary/itinerary.service.ts b/src/itinerary/itinerary.service.ts
index 286011464..7f22f8a75 100644
--- a/src/itinerary/itinerary.service.ts
+++ b/src/itinerary/itinerary.service.ts
@@ -1335,4 +1335,57 @@ export class ItineraryService {
       return { ...updatedContingency, sections: mappedSections }
     })
   }
+
+  async createViewItinerary(itineraryId: string, user: User) {
+    const userId = user.id
+
+    const userViews = await this.prisma.itineraryView.findMany({
+      where: { userId },
+      orderBy: { viewedAt: 'desc' },
+    })
+
+    const itineraryExists = userViews.some(
+      (view) => view.itineraryId === itineraryId
+    )
+
+    if (itineraryExists) {
+      return this.prisma.itineraryView.update({
+        where: {
+          userId_itineraryId: { userId, itineraryId },
+        },
+        data: {
+          viewedAt: new Date(),
+        },
+      })
+    }
+
+    if (userViews.length >= 10) {
+      await this.prisma.itineraryView.delete({
+        where: {
+          id: userViews[userViews.length - 1].id,
+        },
+      })
+    }
+
+    return this.prisma.itineraryView.create({
+      data: {
+        userId,
+        itineraryId,
+        viewedAt: new Date(),
+      },
+    })
+  }
+
+  async getViewItinerary(user: User) {
+    const userId = user.id
+
+    return this.prisma.itineraryView.findMany({
+      where: {
+        userId: userId,
+      },
+      orderBy: {
+        viewedAt: 'desc',
+      },
+    })
+  }
 }


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

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

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

`1-6`: **Consider adding additional validation to the DTO.**

The DTO has basic string validation, but could benefit from more robust input validation.


```diff
 import { IsString } from 'class-validator'
+import { IsNotEmpty, IsUUID } from 'class-validator'

 export class CreateItineraryViewDto {
-  @IsString()
+  @IsString()
+  @IsNotEmpty({ message: 'Itinerary ID cannot be empty' })
+  @IsUUID('4', { message: 'Itinerary ID must be a valid UUID' })
   itineraryId: string
 }
```

</blockquote></details>
<details>
<summary>db/prisma/migrations/20250416163643_add_itinerary_view/migration.sql (1)</summary><blockquote>

`1-26`: **The ItineraryView table structure correctly supports the recently viewed functionality.**

The migration script creates a well-structured table with:
- Appropriate timestamps (createdAt, updatedAt, viewedAt)
- A unique constraint on userId+itineraryId to ensure one view record per user-itinerary pair
- Proper indexing on userId+viewedAt to optimize retrieval of recently viewed itineraries
- Correct foreign key constraints with CASCADE behavior



Consider using consistent timestamp types - the script uses TIMESTAMPTZ for updatedAt/createdAt but TIMESTAMP(3) for viewedAt. While functionally this works, using the same type throughout improves consistency.

</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 6d29892aa72a55ca6571702bab324490c3951395.

</details>

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

* `db/itinerary.zmodel` (2 hunks)
* `db/prisma/migrations/20250416163643_add_itinerary_view/migration.sql` (1 hunks)
* `db/prisma/schema.prisma` (3 hunks)
* `db/user.zmodel` (1 hunks)
* `src/itinerary/dto/create-view-itinerary.dto.ts` (1 hunks)
* `src/itinerary/itinerary.controller.spec.ts` (2 hunks)
* `src/itinerary/itinerary.controller.ts` (3 hunks)
* `src/itinerary/itinerary.service.spec.ts` (2 hunks)
* `src/itinerary/itinerary.service.ts` (1 hunks)

</details>

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

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

<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/create-view-itinerary.dto.ts (1)</summary>

* `CreateItineraryViewDto` (3-6)

</details>

</blockquote></details>

</details>

</details>

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

<details>
<summary>db/user.zmodel (1)</summary>

`27-27`: **Relation looks good!**

The addition of the `itineraryViews` relation to the User model follows the existing pattern of model relations and is correctly typed as an array of `ItineraryView` objects.

</details>
<details>
<summary>db/itinerary.zmodel (2)</summary>

`25-25`: **Appropriate relation field added to Itinerary model.**

The `views` relation properly connects the Itinerary model with the new ItineraryView model.

---

`141-151`: **Model design looks good with appropriate constraints.**

The ItineraryView model is well designed with:
- Appropriate fields: userId, itineraryId, and viewedAt
- Cascade delete relations to both User and Itinerary to maintain referential integrity
- A database index on [userId, viewedAt] to optimize queries for viewing history by user
- A unique constraint on [userId, itineraryId] to prevent duplicate view records

Good job implementing these database optimization features!

</details>
<details>
<summary>db/prisma/schema.prisma (3)</summary>

`319-332`: **Well-structured ItineraryView model implementation**

The `ItineraryView` model is well-designed with:
- Appropriate timestamps (`updatedAt`, `createdAt`, `viewedAt`)
- Proper relations to both `User` and `Itinerary` models with cascade delete behavior
- A unique constraint to prevent duplicate views for the same user-itinerary pair
- An index on `userId` and `viewedAt` to optimize queries for retrieving recently viewed itineraries

This implementation will effectively track which itineraries a user has viewed and when.

---

`82-82`: **Appropriate relation field added to User model**

The `itineraryViews` relation field is correctly added to the `User` model, establishing the one-to-many relationship with the `ItineraryView` model.

---

`186-186`: **Appropriate relation field added to Itinerary model**

The `views` relation field is correctly added to the `Itinerary` model, establishing the one-to-many relationship with the `ItineraryView` model.

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

`23-23`: **Correct import of the DTO for itinerary views**

The import of `CreateItineraryViewDto` is appropriately included to validate the input data for creating itinerary views.

---

`186-202`: **Correct implementation of POST endpoint for creating itinerary views**

The `createViewItinerary` endpoint is correctly implemented as a POST operation with:
- Appropriate HTTP status 201 (CREATED)
- Proper use of the `@GetUser()` decorator to get the authenticated user
- Validation of input using `CreateItineraryViewDto`
- Correct service method call passing the itinerary ID and user
- Meaningful response message

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

`57-58`: **Mock service functions correctly added**

The mock service implementations for `createViewItinerary` and `getViewItinerary` are properly added to the `mockItineraryService` object.

---

`1998-2030`: **Well-implemented test for createViewItinerary**

The test for `createViewItinerary` is comprehensive, verifying:
- That the service method is called with the correct parameters
- That the response utility is called with the expected format
- That the controller returns the expected response

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

`3797-3851`: **Tests for the new itinerary view functionality look good!**

The test cases for `createViewItinerary` and `getViewItinerary` adequately verify that:
1. The view creation correctly upserts based on the composite key (userId_itineraryId)
2. The view retrieval properly filters by userId and orders by viewedAt descending

The tests follow the arrange-act-assert pattern and contain appropriate mocks and verifications.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-20T13:31:51Z
user: login: fahmi-ramadhan
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-21T14:37:15Z
user: login: matthewhjt
body: Tolong diperhatikan terkait logic upsert di CreateView. Terima kasih.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T16:22:16Z
user: login: daffarafi
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T05:21:16Z

Review comments:
diff_hunk: @@ -1335,4 +1335,35 @@ export class ItineraryService {
       return { ...updatedContingency, sections: mappedSections }
     })
   }
+
+  async createViewItinerary(itineraryId: string, user: User) {
+    const userId = user.id
+
+    return this.prisma.itineraryView.upsert({
+      where: {
+        userId_itineraryId: { userId, itineraryId },
+      },
+      update: {
+        viewedAt: new Date(),
+      },
+      create: {
+        userId,
+        itineraryId,
+        viewedAt: new Date(),
+      },
+    })
+  }
+
+  async getViewItinerary(user: User) {
+    const userId = user.id
+
+    return this.prisma.itineraryView.findMany({
+      where: {
+        userId: userId,
+      },
+      orderBy: {
+        viewedAt: 'desc',
+      },
+    })
+  }
path: src/itinerary/itinerary.service.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add pagination and include itinerary details in the view history.**

The `getViewItinerary` method should implement pagination to handle potentially large result sets and include basic itinerary information to avoid additional queries from the client.


```diff
-async getViewItinerary(user: User) {
+async getViewItinerary(user: User, page: number = 1, limit: number = PAGINATION_LIMIT) {
   const userId = user.id
+
+  if (page < 1) {
+    throw new HttpException('Invalid page number', 400)
+  }
+
+  const skip = (page - 1) * limit
+
+  const [data, total] = await this.prisma.$transaction([
+    this.prisma.itineraryView.findMany({
+      where: {
+        userId: userId,
+      },
+      orderBy: {
+        viewedAt: 'desc',
+      },
+      take: limit,
+      skip,
+      include: {
+        itinerary: {
+          select: {
+            id: true,
+            title: true,
+            coverImage: true,
+            startDate: true,
+            endDate: true,
+          }
+        }
+      }
+    }),
+    this.prisma.itineraryView.count({
+      where: { userId }
+    })
+  ])
+
+  const totalPages = Math.ceil(total / limit) < 1 ? 1 : Math.ceil(total / limit)
+  if (page > totalPages) {
+    throw new HttpException('Page number exceeds total available pages', 400)
+  }
+
+  return {
+    data,
+    metadata: {
+      total,
+      page,
+      totalPages,
+    },
+  }

-  return this.prisma.itineraryView.findMany({
-    where: {
-      userId: userId,
-    },
-    orderBy: {
-      viewedAt: 'desc',
-    },
-  })
}
```


<!-- 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 getViewItinerary(user: User, page: number = 1, limit: number = PAGINATION_LIMIT) {
  const userId = user.id

  if (page < 1) {
    throw new HttpException('Invalid page number', 400)
  }

  const skip = (page - 1) * limit

  const [data, total] = await this.prisma.$transaction([
    this.prisma.itineraryView.findMany({
      where: {
        userId: userId,
      },
      orderBy: {
        viewedAt: 'desc',
      },
      take: limit,
      skip,
      include: {
        itinerary: {
          select: {
            id: true,
            title: true,
            coverImage: true,
            startDate: true,
            endDate: true,
          }
        }
      }
    }),
    this.prisma.itineraryView.count({
      where: { userId }
    })
  ])

  const totalPages = Math.ceil(total / limit) < 1 ? 1 : Math.ceil(total / limit)
  if (page > totalPages) {
    throw new HttpException('Page number exceeds total available pages', 400)
  }

  return {
    data,
    metadata: {
      total,
      page,
      totalPages,
    },
  }
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-20T13:31:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1335,4 +1335,35 @@ export class ItineraryService {
       return { ...updatedContingency, sections: mappedSections }
     })
   }
+
+  async createViewItinerary(itineraryId: string, user: User) {
+    const userId = user.id
+
+    return this.prisma.itineraryView.upsert({
+      where: {
+        userId_itineraryId: { userId, itineraryId },
+      },
+      update: {
+        viewedAt: new Date(),
+      },
+      create: {
+        userId,
+        itineraryId,
+        viewedAt: new Date(),
+      },
+    })
+  }
path: src/itinerary/itinerary.service.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Consider adding itinerary existence check before upsert operation.**

The `createViewItinerary` method should validate that the itinerary exists and the user has permission to view it before recording the view.


```diff
 async createViewItinerary(itineraryId: string, user: User) {
   const userId = user.id
+  
+  // Check if itinerary exists
+  const itinerary = await this.prisma.itinerary.findUnique({
+    where: { id: itineraryId },
+  })
+  
+  if (!itinerary) {
+    throw new NotFoundException(`Itinerary with ID ${itineraryId} not found`)
+  }
+  
+  // Check if user has permission to view the itinerary
+  const canView = itinerary.userId === userId || 
+    itinerary.isPublished || 
+    await this.prisma.itineraryAccess.findUnique({
+      where: { itineraryId_userId: { itineraryId, userId } },
+    })
+  
+  if (!canView) {
+    throw new ForbiddenException('You do not have permission to view this itinerary')
+  }

   return this.prisma.itineraryView.upsert({
     where: {
       userId_itineraryId: { userId, itineraryId },
     },
     update: {
       viewedAt: new Date(),
     },
     create: {
       userId,
       itineraryId,
       viewedAt: new Date(),
     },
   })
 }
```


<!-- 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 createViewItinerary(itineraryId: string, user: User) {
    const userId = user.id

    // Check if itinerary exists
    const itinerary = await this.prisma.itinerary.findUnique({
      where: { id: itineraryId },
    })

    if (!itinerary) {
      throw new NotFoundException(`Itinerary with ID ${itineraryId} not found`)
    }

    // Check if user has permission to view the itinerary
    const canView = 
      itinerary.userId === userId ||
      itinerary.isPublished ||
      await this.prisma.itineraryAccess.findUnique({
        where: { itineraryId_userId: { itineraryId, userId } },
      })

    if (!canView) {
      throw new ForbiddenException('You do not have permission to view this itinerary')
    }

    return this.prisma.itineraryView.upsert({
      where: {
        userId_itineraryId: { userId, itineraryId },
      },
      update: {
        viewedAt: new Date(),
      },
      create: {
        userId,
        itineraryId,
        viewedAt: new Date(),
      },
    })
  }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-20T13:31:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -130,6 +131,18 @@ export class ItineraryController {
     )
   }
 
+  @Get('views')
+  async getViewItinerary(@GetUser() user: User) {
+    const itinerary = await this.itineraryService.getViewItinerary(user)
+    return this.responseUtil.response(
+      {
+        statusCode: HttpStatus.CREATED,
+        message: 'Itinerary view added successfully',
+      },
+      itinerary
+    )
+  }
path: src/itinerary/itinerary.controller.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Incorrect HTTP status code and message for GET endpoint**

The GET endpoint `getViewItinerary` returns HTTP status 201 (CREATED) with a message "Itinerary view added successfully", which doesn't match the semantics of a GET operation.


Apply this fix:

```diff
  @Get('views')
  async getViewItinerary(@GetUser() user: User) {
    const itinerary = await this.itineraryService.getViewItinerary(user)
    return this.responseUtil.response(
      {
-        statusCode: HttpStatus.CREATED,
-        message: 'Itinerary view added successfully',
+        statusCode: HttpStatus.OK,
+        message: 'Itinerary views fetched 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
  @Get('views')
  async getViewItinerary(@GetUser() user: User) {
    const itinerary = await this.itineraryService.getViewItinerary(user)
    return this.responseUtil.response(
      {
        statusCode: HttpStatus.OK,
        message: 'Itinerary views fetched successfully',
      },
      itinerary
    )
  }
`````

</details>

<!-- suggestion_end -->

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

✅ Addressed in commits 473a974 to 4e53604
created_at: 2025-04-20T13:31:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1992,4 +1994,68 @@ describe('ItineraryController', () => {
       ).rejects.toThrow(ForbiddenException)
     })
   })
+
+  describe('createViewItinerary', () => {
+    it('should call service and return response', async () => {
+      const user = { id: 'user123' }
+      const dto = { itineraryId: 'itinerary123' }
+      const createdView = { id: 'view1', ...dto, userId: user.id }
+
+      mockItineraryService.createViewItinerary.mockResolvedValue(createdView)
+      mockResponseUtil.response.mockReturnValue({
+        statusCode: HttpStatus.CREATED,
+        message: 'Itinerary view added successfully',
+        data: createdView,
+      })
+
+      const result = await controller.createViewItinerary(user as any, dto)
+
+      expect(itineraryService.createViewItinerary).toHaveBeenCalledWith(
+        dto.itineraryId,
+        user
+      )
+      expect(responseUtil.response).toHaveBeenCalledWith(
+        {
+          statusCode: HttpStatus.CREATED,
+          message: 'Itinerary view added successfully',
+        },
+        createdView
+      )
+      expect(result).toEqual({
+        statusCode: HttpStatus.CREATED,
+        message: 'Itinerary view added successfully',
+        data: createdView,
+      })
+    })
+  })
+
+  describe('getViewItinerary', () => {
+    it('should call service and return viewed itineraries', async () => {
+      const user = { id: 'user123' }
+      const views = [{ itineraryId: 'a' }, { itineraryId: 'b' }]
+
+      mockItineraryService.getViewItinerary.mockResolvedValue(views)
+      mockResponseUtil.response.mockReturnValue({
+        statusCode: HttpStatus.CREATED,
+        message: 'Itinerary view added successfully',
+        data: views,
+      })
+
+      const result = await controller.getViewItinerary(user as any)
+
+      expect(itineraryService.getViewItinerary).toHaveBeenCalledWith(user)
+      expect(responseUtil.response).toHaveBeenCalledWith(
+        {
+          statusCode: HttpStatus.CREATED,
+          message: 'Itinerary view added successfully',
+        },
+        views
+      )
+      expect(result).toEqual({
+        statusCode: HttpStatus.CREATED,
+        message: 'Itinerary view added successfully',
+        data: views,
+      })
path: src/itinerary/itinerary.controller.spec.ts
user: login: fahmi-ramadhan
body: **Masalah**: HTTP status `HttpStatus.CREATED` tidak sesuai untuk operasi pembacaan (GET). Status yang lebih tepat adalah `HttpStatus.OK`.
**Saran Perbaikan**: Ganti status respons menjadi `HttpStatus.OK` agar sesuai dengan semantik HTTP.
created_at: 2025-04-21T14:33:04Z
author_association: MEMBER
diff_hunk: @@ -1335,4 +1335,35 @@ export class ItineraryService {
       return { ...updatedContingency, sections: mappedSections }
     })
   }
+
+  async createViewItinerary(itineraryId: string, user: User) {
+    const userId = user.id
+
+    return this.prisma.itineraryView.upsert({
path: src/itinerary/itinerary.service.ts
user: login: matthewhjt
body: Saya lihat di sini semua view akan disimpan oleh user. Sepertinya sebaiknya dibatasi saja supaya yang disimpan hanya 10 views terakhir saja, supaya lebih hemat resource storage, dan bahkan mungkin performance juga (terkait update).
created_at: 2025-04-21T16:21:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -170,6 +183,24 @@ export class ItineraryController {
     )
   }
 
+  @Post('views')
+  async createViewItinerary(
+    @GetUser() user: User,
+    @Body() createItineraryViewDto: CreateItineraryViewDto
path: src/itinerary/itinerary.controller.ts
user: login: daffarafi
body: Terima kasih atas implementasinya fathan! Saya sudah memeriksa kode yang diajukan dan semuanya tampak baik. Namun, saya ingin mengajukan sedikit saran mengenai penggunaan parameter di endpoint ini.

Saat ini, endpoint `createViewItinerary` menerima `itineraryId` melalui body request (`createItineraryViewDto`). Namun, karena `itineraryId` adalah identifier yang digunakan untuk menentukan resource yang spesifik, sebaiknya `itineraryId` bisa langsung diterima sebagai parameter di URL endpoint. Dengan cara ini, kita dapat membuat URL lebih deskriptif dan sesuai dengan prinsip RESTful API.

Berikut adalah contoh perubahan yang bisa dipertimbangkan:

```typescript
@Post(':itineraryId/view')
async createViewItinerary(
  @GetUser() user: User,
  @Param('itineraryId') id: string
) {
  const itinerary = await this.itineraryService.createViewItinerary(id, user)
  return this.responseUtil.response(
    {
      statusCode: HttpStatus.CREATED,
      message: 'Itinerary view added successfully',
    },
    itinerary
  )
}
```

Dengan perubahan ini, `itineraryId` akan lebih alami sebagai bagian dari URL yang menggambarkan resource yang sedang diproses. Menggunakan `@Param('itineraryId')` juga membuat endpoint ini lebih konsisten dengan standar REST, karena URL sekarang langsung mengidentifikasi resource yang dimaksud.

Saya rasa ini bisa menyederhanakan struktur endpoint dan membuatnya lebih mudah dimengerti. Terima kasih banyak atas klarifikasinya dan semoga saran ini bermanfaat!
created_at: 2025-04-23T05:15:57Z
author_association: MEMBER
diff_hunk: @@ -1335,4 +1335,35 @@ export class ItineraryService {
       return { ...updatedContingency, sections: mappedSections }
     })
   }
+
+  async createViewItinerary(itineraryId: string, user: User) {
+    const userId = user.id
+
+    return this.prisma.itineraryView.upsert({
path: src/itinerary/itinerary.service.ts
user: login: daffarafi
body: ```typescript
    if (existedTickets.length >= 5) {
      const lastExistedTicket = existedTickets[existedTickets.length - 1]
      await prisma.ticket.delete({
        where: {
          id: lastExistedTicket.id,
        },
      })
    }
```

Mungkin kode diatas bisa digunakan sebagai referensi, kode ini saya ambil dari module `pre-register` pada service `_generateTicket`. disini saya mengecek length terlebih dahulu, jika banyaknya tiket lebih besar dari jumlah maksimal tiket yang ingin saya simpan pada database, saya akan menghapus tiket terakhir.
created_at: 2025-04-23T05:20:48Z
author_association: MEMBER

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

## Walkthrough

This change introduces a new feature for tracking user views of itineraries. It adds a new `ItineraryView` model to the database schema, establishes appropriate relations with `User` and `Itinerary`, and enforces constraints to ensure a user can only have one view record per itinerary. The service and controller layers are updated to provide endpoints for creating and retrieving itinerary views, with logic to maintain only the 10 most recent views per user. Comprehensive tests are added for the new functionality.

## Changes

| File(s)                                                                                  | Change Summary                                                                                                                                                                                                                                         |
|------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| db/itinerary.zmodel, db/prisma/schema.prisma, db/user.zmodel                             | Added new `ItineraryView` model with fields for user, itinerary, and timestamps; established relations with `User` and `Itinerary`; enforced unique and indexed constraints; updated related models to include new relation fields.                    |
| db/prisma/migrations/20250416163643_add_itinerary_view/migration.sql                     | Created `ItineraryView` table with appropriate columns, constraints, indexes, and foreign key relations with cascade on delete/update.                                                                          |
| src/itinerary/itinerary.controller.ts                                                    | Added two new endpoints: one for creating an itinerary view and one for retrieving a user's viewed itineraries. Removed unused import.                                                                         |
| src/itinerary/itinerary.controller.spec.ts                                               | Added tests for the new controller endpoints, including mocks for service methods and response validation.                                                                                                      |
| src/itinerary/itinerary.service.ts                                                       | Added methods to create and retrieve itinerary views, enforcing a maximum of 10 recent views per user and updating timestamps or deleting oldest views as needed.                                              |
| src/itinerary/itinerary.service.spec.ts                                                  | Added tests for the new service methods, covering all logic branches for view creation and retrieval.                                                                                                          |

## Sequence Diagram(s)

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

    User->>Controller: POST /itineraries/views/:itineraryId
    Controller->>Service: createViewItinerary(itineraryId, user)
    Service->>DB: Find all views for user (ordered by viewedAt desc)
    alt View exists for itinerary
        Service->>DB: Update viewedAt timestamp
    else 10 views already exist
        Service->>DB: Delete oldest view
        Service->>DB: Create new view record
    else Fewer than 10 views
        Service->>DB: Create new view record
    end
    Service-->>Controller: Created/Updated view record
    Controller-->>User: 201 Created with view data

    User->>Controller: GET /itineraries/views
    Controller->>Service: getViewItinerary(user)
    Service->>DB: Find all views for user (ordered by viewedAt desc)
    Service-->>Controller: List of viewed itineraries
    Controller-->>User: 200 OK with list
```

## Poem

> Hopping through code with a view in my eye,  
> Now users can track where their memories lie.  
> Ten little carrots, each view in a row,  
> Oldest gets nibbled when new ones must grow.  
> Tests keep us safe as we leap and we bound—  
> Itinerary views, the best hop around!  
> 🥕✨

<!-- walkthrough_end -->


---

<details>
<summary>📜 Recent 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 6d29892aa72a55ca6571702bab324490c3951395 and 4e536047ef216f2c6c64b6753790dfe64d17037a.

</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` (2 hunks)
* `src/itinerary/itinerary.service.ts` (1 hunks)

</details>

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

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

</details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- 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=96):

- 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-20T13:29:09Z
body: ⚠️ **Warning:** PBI number not found in PR title. Skipping status update.
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-20T13:29:50Z
body: 🎯 **Allure Test Report is Ready!**
📊 **[View Report](https://mutari-app.github.io/mutari-backend/177/)**
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-25T06:17:02Z
body: 🎯 **Allure Test Report is Ready!**
📊 **[View Report](https://mutari-app.github.io/mutari-backend/193/)**
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-25T12:18:38Z
body: ⚠️ **Warning:** PBI number not found in PR title. Skipping status update.
author_association: NONE