Skip to content

Feature/protocol pdf export#65

Merged
MartinStenhagen merged 24 commits into
mainfrom
feature/protocol-pdf-export
Apr 28, 2026
Merged

Feature/protocol pdf export#65
MartinStenhagen merged 24 commits into
mainfrom
feature/protocol-pdf-export

Conversation

@MartinStenhagen

@MartinStenhagen MartinStenhagen commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Protocol PDF generation, download and archival with stored archived files.
    • Protocol viewing/dashboard with per-paragraph views and HTMX-powered UI.
    • Case Officer dashboard and sidebar layout for case workflows.
    • Upload support for generated files into case records.
  • Security & Access Control

    • Confidentiality-based hiding of decision details for restricted cases.
    • New role-gated endpoints and stricter checks to prevent unauthorized edits.
  • Bug Fixes

    • Deterministic ordering of case records by creation date.

@coderabbitai

coderabbitai Bot commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@MartinStenhagen has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 35 minutes and 34 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 045847f2-ba9a-40e9-a375-05823f22f50b

📥 Commits

Reviewing files that changed from the base of the PR and between 0bca7bf and 29d7c89.

📒 Files selected for processing (3)
  • src/main/java/backendlab/team4you/protocol/ProtocolPdfService.java
  • src/main/java/backendlab/team4you/protocol/ProtocolRepository.java
  • src/test/java/backendlab/team4you/meeting/MeetingServiceTest.java
📝 Walkthrough

Walkthrough

Adds protocol PDF generation and archival (including viewer-aware paragraph filtering and archival CaseFile linking), a new upload API for generated files, repository locking and guards to prevent edits on archived protocols/meetings with protocols, new exceptions and handlers, UI fragments for case-officer/protocol dashboards, PDFBox dependency, migrations, and tests.

Changes

Cohort / File(s) Summary
PDF generation & archival
pom.xml, src/main/java/backendlab/team4you/protocol/ProtocolPdfService.java, src/main/java/backendlab/team4you/protocol/ProtocolArchiveService.java, src/main/java/backendlab/team4you/protocol/ProtocolArchiveController.java
Adds PDFBox dependency and services/controllers to generate PDFs, validate readiness, upload generated PDFs as CaseFiles, and link archived files to Protocol.
Protocol model & locking
src/main/java/backendlab/team4you/protocol/Protocol.java, src/main/java/backendlab/team4you/protocol/ProtocolRepository.java, src/main/java/backendlab/team4you/protocol/ProtocolService.java
Adds archivedPdfFile relationship, isReadyForPdf(), and repository method with pessimistic lock; blocks edits when protocol is archived.
Viewer-facing protocol views & DTOs
src/main/java/backendlab/team4you/protocol/ProtocolViewService.java, src/main/java/backendlab/team4you/protocol/ProtocolParagraphViewDto.java, src/main/java/backendlab/team4you/protocol/ProtocolViewController.java, src/main/java/backendlab/team4you/protocol/ProtocolController.java, src/main/resources/templates/fragments/*, src/main/resources/templates/protocol-view.html
Implements viewer-specific paragraph DTOs and filtering based on confidentiality, adds controllers and templates/fragments for dashboard/protocol views and updates templates to consume paragraphViews.
CaseFile upload API & S3 handling
src/main/java/backendlab/team4you/casefile/CaseFileService.java, src/test/.../CaseFileServiceTest.java
Adds uploadGeneratedFile(byte[]) API with validation, S3 upload-if-absent, numbering, confidentiality assignment, and cleanup on failures; tests extended for new flows.
CaseRecord numbering & repository
src/main/java/backendlab/team4you/caserecord/CaseRecordService.java, src/main/java/backendlab/team4you/caserecord/CaseRecordRepository.java
Adds findOrCreateAnnualProtocolCase and year-specific allocator; new repository lookup findByRegistryAndTitle and deterministic ordering for assigned-user paging.
Meeting protection & repository lock
src/main/java/backendlab/team4you/meeting/MeetingService.java, src/main/java/backendlab/team4you/meeting/MeetingRepository.java
Injects ProtocolRepository, centralizes locked fetch that rejects meetings with existing protocol (throws MeetingHasProtocolException), and adds findByIdWithLock.
New exceptions & handlers
src/main/java/backendlab/team4you/exceptions/*.java, src/main/java/backendlab/team4you/exceptions/GlobalRestExceptionHandler.java, src/main/java/backendlab/team4you/exceptions/GlobalViewExceptionHandler.java
Adds ProtocolNotReadyForPdfException, ProtocolAlreadyArchivedException, MeetingHasProtocolException and maps them to appropriate REST/MVC handlers/statuses.
Controllers & security tweaks
src/main/java/backendlab/team4you/protocol/*Controller.java, src/main/java/backendlab/team4you/controller/CaseOfficerController.java, src/main/java/backendlab/team4you/config/SecurityConfig.java
New ProtocolPdfController, DashboardProtocolController, ProtocolViewController; protocol controllers obtain current user and paragraphViews; closeCase now accepts Long and uses null-safe ownership check; security adds exact /case-officer matcher.
Application seeding & user repo
src/main/java/backendlab/team4you/Team4youApplication.java, src/main/java/backendlab/team4you/user/UserRepository.java
Switches dev seeding to CommandLineRunner with per-user existsByName checks; adds existsByName repository method.
DB migration
src/main/resources/db/migration/V24__archived_pdf_file_id.sql
Adds nullable archived_pdf_file_id FK to case_file with unique constraint.
Templates & UI
src/main/resources/templates/*, src/main/resources/templates/fragments/* (dashboard-layout, case-officer-layout, dashboard-protocols, admin-protocols, side-navbar, case-officer-sidenav, protocol-view)
Adds case-officer layout/side nav, dashboard protocol fragments with HTMX integration, updates sidebar/nav with role-gated links and additional stylesheets, and templates to reflect confidentiality-aware rendering and archive controls.
Tests added/updated
src/test/java/backendlab/team4you/... (ProtocolPdfServiceTest, ProtocolViewServiceTest, ProtocolArchiveServiceTest, updates to ProtocolServiceTest, ProtocolControllerTest, MeetingServiceTest, CaseFileServiceTest)
Adds comprehensive tests for PDF generation, view filtering, archive flow, meeting protection, and cleans up mocks to use locked fetches and updated APIs.

Sequence Diagram

sequenceDiagram
    actor Admin
    participant ProtocolArchiveController
    participant ProtocolArchiveService
    participant ProtocolPdfService
    participant CaseRecordService
    participant CaseFileService
    participant S3Service
    participant Database

    Admin->>ProtocolArchiveController: POST /admin/protocols/{id}/archive-pdf
    ProtocolArchiveController->>ProtocolArchiveService: archiveProtocolPdf(protocolId, user)
    
    ProtocolArchiveService->>Database: findWithLockById(protocolId)
    Database-->>ProtocolArchiveService: Protocol (locked)
    
    alt Protocol already archived
        ProtocolArchiveService-->>ProtocolArchiveController: return existing CaseFile
    else Protocol not ready
        ProtocolArchiveService-->>ProtocolArchiveController: throw ProtocolNotReadyForPdfException
    else Ready for PDF
        ProtocolArchiveService->>CaseRecordService: findOrCreateAnnualProtocolCase(registry, year, user)
        CaseRecordService-->>ProtocolArchiveService: CaseRecord
        
        ProtocolArchiveService->>ProtocolPdfService: generatePdf(protocolId, adminUser)
        ProtocolPdfService->>Database: getProtocol with paragraphs
        Database-->>ProtocolPdfService: Protocol + paragraphs
        ProtocolPdfService-->>ProtocolArchiveService: byte[] (PDF bytes)
        
        ProtocolArchiveService->>CaseFileService: uploadGeneratedFile(caseRecord, filename, bytes, CONFIDENTIAL/OPEN)
        CaseFileService->>S3Service: uploadFileIfAbsent(s3Key, bytes)
        S3Service-->>CaseFileService: success
        CaseFileService->>Database: save(CaseFile)
        Database-->>CaseFileService: CaseFile (persisted)
        CaseFileService-->>ProtocolArchiveService: CaseFile
        
        ProtocolArchiveService->>Database: protocol.setArchivedPdfFile(caseFile)
        ProtocolArchiveService->>Database: save(protocol)
        Database-->>ProtocolArchiveService: Protocol (updated)
        ProtocolArchiveService-->>ProtocolArchiveController: CaseFile
    end
    
    ProtocolArchiveController-->>Admin: return fragment with success message
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • gvaguirres
  • JohanHiths

Poem

🐰 I hopped through code and inked a PDF,

Locked the protocol, kept secrets deft;
Paragraphs rendered, numbers assigned,
Archived and safe, each file well-lined,
The rabbit cheers — another feature shipped!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feature/protocol pdf export' is partially related to the changeset. While PDF export/archiving is a significant feature, the PR encompasses far more: protocol viewing, dashboard UI, case file management, security configurations, database migrations, and multiple exception handlers. The title captures one aspect but not the full scope of changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/protocol-pdf-export

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

🧹 Nitpick comments (1)
src/main/resources/templates/fragments/admin-protocols.html (1)

140-145: Drive the empty state from paragraphViews too.

The loop now renders paragraphViews, but the empty-state message still checks selectedProtocol.paragraphs. That split source of truth can produce stale UI if the view model ever changes.

♻️ Suggested tweak
-                <div th:if="${`#lists.isEmpty`(selectedProtocol.paragraphs)}">
+                <div th:if="${`#lists.isEmpty`(paragraphViews)}">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/templates/fragments/admin-protocols.html` around lines 140
- 145, The empty-state check currently uses selectedProtocol.paragraphs while
the list rendering uses paragraphViews; change the empty-state condition to use
paragraphViews (i.e., use `#lists.isEmpty`(paragraphViews)) so the message is
driven from the same view model as the th:each that iterates paragraphViews;
update the th:if on the div that shows "Protokollet saknar paragrafer." to
reference paragraphViews instead of selectedProtocol.paragraphs to avoid a split
source of truth.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/backendlab/team4you/casefile/CaseFileService.java`:
- Around line 209-247: The pessimistic DB lock from findByIdWithLock is held
across the S3 call (s3Service.uploadFileIfAbsent), causing unnecessary
contention; change the flow so you acquire the lock only to call
allocateNextDocumentNumber (and persist the increment), compute
documentReference and s3Key (using buildUniqueS3Key), then commit/close that
transaction to release the lock before performing s3Service.uploadFileIfAbsent;
after the upload, start a new transaction, reload the CaseRecord without a lock
(or attach by id), construct the CaseFile with the reserved document number and
save via caseFileRepository.saveAndFlush—this ensures the lock is not held
during the potentially slow S3 upload while still reserving document numbers
safely.
- Around line 216-218: Add a null-guard for the byte[] before checking its
length: in CaseFileService (the method that currently does "if (bytes.length >
MAX_FILE_SIZE_BYTES) { throw new FileTooLargeException(MAX_FILE_SIZE_BYTES); }")
first check "if (bytes == null)" and throw a clear validation exception (e.g.,
IllegalArgumentException or your project's validation exception type) with a
message like "file bytes must not be null", then proceed to the existing length
check using MAX_FILE_SIZE_BYTES and throw FileTooLargeException as before.

In `@src/main/java/backendlab/team4you/caserecord/CaseRecordRepository.java`:
- Around line 24-25: The query in CaseRecordRepository.findByAssignedUserId is
non-deterministic with unsorted PageRequests; modify the `@Query` to include a
stable ORDER BY (e.g., ORDER BY c.id or ORDER BY c.createdAt, depending on the
intended canonical ordering) so paging is deterministic, or alternatively
validate/require the incoming Pageable to include a stable Sort and throw/handle
if absent; update the `@Query` annotation for findByAssignedUserId (or add
repository-level validation) to enforce the chosen deterministic ordering.

In `@src/main/java/backendlab/team4you/caserecord/CaseRecordService.java`:
- Around line 147-158: The current findOrCreateAnnualProtocolCase uses
registry.getName() for the lookup and is not atomic; change the lookup to use an
immutable identifier (registry.getId()) by adding/using a repository method like
findByRegistryIdAndYear (or findByRegistryAndYear) instead of
findByRegistryAndTitle, and make creation atomic by adding a DB uniqueness
constraint/unique index on (registry_id, year) and handling concurrent inserts:
keep `@Transactional` on findOrCreateAnnualProtocolCase/createAnnualProtocolCase,
attempt to save the new CaseRecord, and if a DataIntegrityViolationException (or
unique constraint violation) occurs, catch it and re-query using the stable key
to return the existing row; alternatively use a pessimistic lock on the registry
record before checking/creating to serialize concurrent creators.

In `@src/main/java/backendlab/team4you/config/SecurityConfig.java`:
- Around line 46-50: Add explicit role protection for the bare "/case-officer"
path by adding a requestMatchers entry for "/case-officer" that calls
hasRole(CASE_OFFICER) before the existing requestMatchers("/case-officer/**").
Locate the authorization chain in SecurityConfig where
requestMatchers("/case-officer/**").hasRole(CASE_OFFICER) is declared and insert
.requestMatchers("/case-officer").hasRole(CASE_OFFICER) immediately before it so
the standalone endpoint is restricted to the CASE_OFFICER role.

In `@src/main/java/backendlab/team4you/controller/CaseOfficerController.java`:
- Around line 62-71: In closeCase, validate and parse the incoming id safely and
guard against a null assigned user: wrap Long.parseLong(id) in a try/catch and
throw a ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid case id") on
NumberFormatException instead of letting it propagate; after fetching caseRecord
from caseRecordRepository in closeCase, check if caseRecord.getAssignedUser() is
null and throw a ResponseStatusException(HttpStatus.FORBIDDEN, "Case has no
assigned officer") (or another appropriate 4xx) before calling getId(); keep
existing userRepository lookup and the assigned-user-id equality check but
perform these new checks first to avoid NPEs and 500s.

In
`@src/main/java/backendlab/team4you/exceptions/ProtocolNotReadyForPdfException.java`:
- Around line 6-8: The exception message in ProtocolNotReadyForPdfException(Long
protocolId) is misleading because Protocol.isReadyForPdf() requires both a
decision and non-blank decision text; update the super(...) message to reflect
both requirements (e.g., "Alla paragrafer måste ha beslut och beslutstext innan
PDF kan skapas.") so users know missing decision text is also a cause; keep the
constructor signature and this.protocolId assignment unchanged.

In `@src/main/java/backendlab/team4you/meeting/MeetingService.java`:
- Around line 396-399: ensureMeetingHasNoProtocol currently calls
protocolRepository.existsByMeetingId(meetingId) without validating meetingId;
add a null check at the top of ensureMeetingHasNoProtocol and throw the same
domain validation error used elsewhere for missing meeting id (e.g. the existing
MeetingValidationException or IllegalArgumentException) when meetingId is null
so repository layer is not invoked; keep the rest of the logic
(protocolRepository.existsByMeetingId and MeetingHasProtocolException)
unchanged.

In `@src/main/java/backendlab/team4you/protocol/ProtocolArchiveService.java`:
- Around line 39-44: The null-check on Protocol.getArchivedPdfFile() is
non-atomic and can race under concurrency; change the read to an atomic locked
fetch: add a repository method (e.g., ProtocolRepository.findByIdForUpdate(Long
id)) annotated with a pessimistic write lock (or use EntityManager.find with
LockModeType.PESSIMISTIC_WRITE), then in ProtocolArchiveService replace the call
to protocolRepository.findById(...) with the locked fetch (still throwing
ProtocolNotFoundException if missing), perform the getArchivedPdfFile()
null-check only after acquiring the lock, and only then create/upload and save
the archived file to ensure idempotent behavior and avoid duplicate/orphan file
records.
- Around line 60-76: The code is archiving an admin-rendered PDF as
ConfidentialityLevel.OPEN; change upload to use the protocol's actual
confidentiality level instead of OPEN. In ProtocolArchiveService, when calling
protocolPdfService.generatePdf(protocolId, createSystemAdminUser()) and then
caseFileService.uploadGeneratedFile(..., ConfidentialityLevel.OPEN,
currentUser), replace ConfidentialityLevel.OPEN with the correct confidentiality
derived from the protocol (e.g., protocol.getConfidentialityLevel() or a helper
that computes the effective confidentiality for protocol), ensuring the stored
CaseFile uses that confidentiality level rather than OPEN.

In `@src/main/java/backendlab/team4you/protocol/ProtocolController.java`:
- Around line 69-80: Both POST handlers createProtocol and
updateParagraphDecision render the same fragment but omit the paragraphViews
model attribute, so the fragment's ${paragraphViews} is empty after POST; fix by
calling model.addAttribute("paragraphViews",
protocolViewService.getParagraphsForViewer(protocolId, currentUser)) in both
createProtocol(...) and updateParagraphDecision(...) before returning the
fragment (use the same currentUser retrieval as in viewProtocol and the
protocolId used in the response).

In `@src/main/java/backendlab/team4you/protocol/ProtocolPdfController.java`:
- Around line 31-43: The downloadPdf endpoint currently regenerates the PDF with
the caller’s permissions by calling protocolPdfService.generatePdf(protocolId,
currentUser); update it to serve the same bytes as the archive flow instead:
either return the archived file from ProtocolArchiveService (e.g., use a method
like ProtocolArchiveService.getArchivedPdf(protocolId) if available) or
regenerate using the same fixed system-admin user that ProtocolArchiveService
uses (obtain the system/admin user via the existing user service or archive
service helper and call protocolPdfService.generatePdf(protocolId,
systemAdminUser)), and then return those bytes from downloadPdf so the
downloaded PDF matches the archived copy.

In `@src/main/java/backendlab/team4you/protocol/ProtocolPdfService.java`:
- Around line 116-120: In splitText, avoid adding an empty string when the first
word exceeds maxCharsPerLine by only pushing currentLine to lines when it
contains content; modify the branch in ProtocolPdfService.splitText that
currently does "if (currentLine.length() + word.length() + 1 > maxCharsPerLine)
{ lines.add(currentLine.toString()); currentLine = new StringBuilder(word); }"
so that you check currentLine.length() > 0 before calling lines.add(...), and
otherwise just set currentLine = new StringBuilder(word); this ensures long
first words don't produce an empty line while preserving existing wrapping logic
for subsequent words.

In `@src/main/java/backendlab/team4you/protocol/ProtocolService.java`:
- Around line 104-112: The check for archived status on Protocol is racy; before
calling paragraph.updateDecision(decisionType, decisionText) you must re-read
the Protocol under a database lock or use optimistic locking so another
transaction cannot archive it concurrently. Fix options: (A) perform a locked
read of the Protocol (e.g., using EntityManager.lock(...) or a repository method
that loads the Protocol with PESSIMISTIC_WRITE/PESSIMISTIC_READ) inside the same
transaction, re-check protocol.getArchivedPdfFile() and throw
ProtocolAlreadyArchivedException if set; or (B) add an `@Version` field to the
Protocol entity to enable optimistic locking and handle OptimisticLockException
on commit (retry or surface an error). Apply the chosen change in the
ProtocolService method that contains protocol.getArchivedPdfFile() /
paragraph.updateDecision(...) and ensure the Archived check is performed on the
locked/locked-versioned entity before mutating the paragraph.

In `@src/main/java/backendlab/team4you/Team4youApplication.java`:
- Around line 53-77: The current seed logic only runs when repository.count() ==
0 so new CASE_OFFICER accounts (UserEntity officer1/officer2) aren’t added to
existing DBs; instead, change the seeding to check for each username and only
create if missing (e.g., use repository.existsByUsername(username) or
repository.findByUsername(...) to detect absence) and then create officer1 and
officer2 with the same setPasswordHash, setRole, setEmail and repository.save
calls; update the initialization code around repository.count() to perform
per-username checks for "officer1" and "officer2" and seed only those that don’t
exist.

In `@src/test/java/backendlab/team4you/protocol/ProtocolControllerTest.java`:
- Around line 41-45: The test is missing stubs for UserService and
ProtocolViewService used by the controller; in the ProtocolControllerTest method
stub userService.getCurrentUser(principal) to return your testUser and stub
protocolViewService.getParagraphsForViewer(100L, testUser) to return an empty
list (or desired paragraph list), then assert the model contains the
"paragraphViews" attribute (and its contents) to verify integration; locate the
MockitoBean mocks protocolViewService and userService and add when(...)
returns(...) stubs for getCurrentUser(...) and getParagraphsForViewer(...) and
an assertion that model.get("paragraphViews") matches the stubbed value.

---

Nitpick comments:
In `@src/main/resources/templates/fragments/admin-protocols.html`:
- Around line 140-145: The empty-state check currently uses
selectedProtocol.paragraphs while the list rendering uses paragraphViews; change
the empty-state condition to use paragraphViews (i.e., use
`#lists.isEmpty`(paragraphViews)) so the message is driven from the same view
model as the th:each that iterates paragraphViews; update the th:if on the div
that shows "Protokollet saknar paragrafer." to reference paragraphViews instead
of selectedProtocol.paragraphs to avoid a split source of truth.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9039d608-1994-4eee-8496-9bfc90acb9e4

📥 Commits

Reviewing files that changed from the base of the PR and between a93784a and 5b2d728.

📒 Files selected for processing (51)
  • .github/workflows/ci.yml
  • pom.xml
  • src/main/java/backendlab/team4you/Team4youApplication.java
  • src/main/java/backendlab/team4you/casefile/CaseFileService.java
  • src/main/java/backendlab/team4you/casefile/access/CaseFileAccessService.java
  • src/main/java/backendlab/team4you/caserecord/CaseRecordRepository.java
  • src/main/java/backendlab/team4you/caserecord/CaseRecordService.java
  • src/main/java/backendlab/team4you/config/CustomAuthenticationSuccessHandler.java
  • src/main/java/backendlab/team4you/config/SecurityConfig.java
  • src/main/java/backendlab/team4you/controller/AdminController.java
  • src/main/java/backendlab/team4you/controller/CaseOfficerController.java
  • src/main/java/backendlab/team4you/exceptions/CaseRecordNotFoundException.java
  • src/main/java/backendlab/team4you/exceptions/GlobalRestExceptionHandler.java
  • src/main/java/backendlab/team4you/exceptions/GlobalViewExceptionHandler.java
  • src/main/java/backendlab/team4you/exceptions/MeetingHasProtocolException.java
  • src/main/java/backendlab/team4you/exceptions/ProtocolAlreadyArchivedException.java
  • src/main/java/backendlab/team4you/exceptions/ProtocolNotReadyForPdfException.java
  • src/main/java/backendlab/team4you/meeting/MeetingService.java
  • src/main/java/backendlab/team4you/protocol/DashboardProtocolController.java
  • src/main/java/backendlab/team4you/protocol/Protocol.java
  • src/main/java/backendlab/team4you/protocol/ProtocolArchiveController.java
  • src/main/java/backendlab/team4you/protocol/ProtocolArchiveService.java
  • src/main/java/backendlab/team4you/protocol/ProtocolController.java
  • src/main/java/backendlab/team4you/protocol/ProtocolParagraph.java
  • src/main/java/backendlab/team4you/protocol/ProtocolParagraphViewDto.java
  • src/main/java/backendlab/team4you/protocol/ProtocolPdfController.java
  • src/main/java/backendlab/team4you/protocol/ProtocolPdfService.java
  • src/main/java/backendlab/team4you/protocol/ProtocolService.java
  • src/main/java/backendlab/team4you/protocol/ProtocolViewController.java
  • src/main/java/backendlab/team4you/protocol/ProtocolViewService.java
  • src/main/java/backendlab/team4you/user/UserRepository.java
  • src/main/java/backendlab/team4you/user/UserRole.java
  • src/main/resources/db/migration/V24__archived_pdf_file_id.sql
  • src/main/resources/templates/case-officer-layout.html
  • src/main/resources/templates/case-officer.html
  • src/main/resources/templates/dashboard-layout.html
  • src/main/resources/templates/fragments/admin-protocols.html
  • src/main/resources/templates/fragments/case-officer-cases.html
  • src/main/resources/templates/fragments/case-officer-sidenav.html
  • src/main/resources/templates/fragments/dashboard-protocols.html
  • src/main/resources/templates/fragments/navbar.html
  • src/main/resources/templates/fragments/side-navbar.html
  • src/main/resources/templates/protocol-view.html
  • src/test/java/backendlab/team4you/casefile/CaseFileServiceTest.java
  • src/test/java/backendlab/team4you/meeting/MeetingControllerTest.java
  • src/test/java/backendlab/team4you/meeting/MeetingServiceTest.java
  • src/test/java/backendlab/team4you/protocol/ProtocolArchiveServiceTest.java
  • src/test/java/backendlab/team4you/protocol/ProtocolControllerTest.java
  • src/test/java/backendlab/team4you/protocol/ProtocolPdfServiceTest.java
  • src/test/java/backendlab/team4you/protocol/ProtocolServiceTest.java
  • src/test/java/backendlab/team4you/protocol/ProtocolViewServiceTest.java

Comment thread src/main/java/backendlab/team4you/casefile/CaseFileService.java
Comment thread src/main/java/backendlab/team4you/casefile/CaseFileService.java
Comment thread src/main/java/backendlab/team4you/caserecord/CaseRecordRepository.java Outdated
Comment thread src/main/java/backendlab/team4you/caserecord/CaseRecordService.java
Comment thread src/main/java/backendlab/team4you/config/SecurityConfig.java
Comment thread src/main/java/backendlab/team4you/protocol/ProtocolPdfService.java
Comment thread src/main/java/backendlab/team4you/protocol/ProtocolService.java Outdated
Comment thread src/main/java/backendlab/team4you/Team4youApplication.java Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (3)
src/main/java/backendlab/team4you/Team4youApplication.java (1)

80-80: ⚠️ Potential issue | 🟡 Minor

Match the existence check to the field being queried.

seedUser(...) persists username and displayName separately, but this guard calls existsByName(username). That happens to work for dev and admin, but not for officer1/officer2, where the saved name is caseofficer1/caseofficer2. In practice, those users can be re-seeded on every startup or trip a uniqueness constraint.

Suggested fix
-		if (repository.existsByName(username)) {
+		if (repository.existsByName(displayName)) {
 			return;
 		}

If username is the real unique identifier, adding existsByUsername(...) would be even clearer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/backendlab/team4you/Team4youApplication.java` at line 80, The
startup guard is checking repository.existsByName(username) while seedUser(...)
persists username and displayName separately, causing a mismatch for users like
"caseofficer1"; change the existence check to match the actual unique identifier
(e.g., call repository.existsByUsername(username) instead of
existsByName(username)) and, if not present, add an existsByUsername(String
username) method to the repository interface; ensure the seedUser call in
Team4youApplication uses the same field (username) for both saving and checking
to avoid reseeding duplicates.
src/main/java/backendlab/team4you/casefile/CaseFileService.java (1)

209-250: ⚠️ Potential issue | 🟠 Major

Don't hold the case lock through the S3 upload.

findByIdWithLock(...) keeps a pessimistic lock on the case record while uploadFileIfAbsent(...) runs. A slow upload will block every other document operation on that case much longer than needed. Reserve the document number under lock, release that transaction, then perform the S3 upload and persist the CaseFile in a follow-up transaction.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/backendlab/team4you/casefile/CaseFileService.java` around lines
209 - 250, The code currently holds the pessimistic lock (findByIdWithLock)
while performing the S3 upload (s3Service.uploadFileIfAbsent), which can block
other operations; refactor so you reserve the document number under the lock and
immediately release the transaction (use allocateNextDocumentNumber while inside
the locked transaction), then outside that transaction perform
s3Service.uploadFileIfAbsent(s3Key, bytes, normalizedContentType) and only after
a successful upload start a new transaction to create and persist the CaseFile
via caseFileRepository.saveAndFlush; keep identifiers findByIdWithLock,
allocateNextDocumentNumber, buildUniqueS3Key, s3Service.uploadFileIfAbsent and
caseFileRepository.saveAndFlush to locate where to split transactional
boundaries.
src/main/java/backendlab/team4you/caserecord/CaseRecordService.java (1)

153-157: ⚠️ Potential issue | 🟠 Major

Make the annual protocol case lookup/create atomic on a stable key.

This still depends on a formatted title and a non-atomic find...orElseGet(...). Two concurrent archive requests can both miss the row and create duplicates, and a registry code change would also break the lookup for an already-created annual case. Please switch this flow to a stable (registryId, year) lookup backed by a DB uniqueness constraint, then handle the insert race by re-reading on conflict.

Also applies to: 175-195

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/backendlab/team4you/caserecord/CaseRecordService.java` around
lines 153 - 157, Change the annual protocol lookup/creation in CaseRecordService
to use a stable key (registry id + year) instead of the formatted title:
add/find by (registry.getId(), year) via caseRecordRepository (introduce a
repository method like findByRegistryIdAndYear), add a DB uniqueness constraint
on (registry_id, year), and modify createAnnualProtocolCase flow to attempt
insert and, on unique-key conflict, re-query by (registryId, year) and return
the existing row; ensure all references that previously used title-based lookup
(including the code around createAnnualProtocolCase) are updated to use the
stable lookup and re-read-on-conflict semantics to avoid duplicate
creations/races.
🧹 Nitpick comments (3)
src/test/java/backendlab/team4you/protocol/ProtocolArchiveServiceTest.java (2)

160-167: setField helper is brittle for inherited fields and hard to debug on failure.

Line 162 uses getDeclaredField, which fails if id moves to a superclass. Consider a small hierarchy-aware lookup and a more specific exception message.

Diff suggestion
@@
     private void setField(Object target, String fieldName, Object value) {
         try {
-            Field field = target.getClass().getDeclaredField(fieldName);
+            Field field = findField(target.getClass(), fieldName);
+            if (field == null) {
+                throw new NoSuchFieldException("Field '" + fieldName + "' not found on " + target.getClass().getName());
+            }
             field.setAccessible(true);
             field.set(target, value);
-        } catch (Exception exception) {
-            throw new RuntimeException(exception);
+        } catch (ReflectiveOperationException exception) {
+            throw new IllegalStateException("Failed to set field '" + fieldName + "'", exception);
         }
     }
+
+    private Field findField(Class<?> type, String fieldName) {
+        Class<?> current = type;
+        while (current != null) {
+            try {
+                return current.getDeclaredField(fieldName);
+            } catch (NoSuchFieldException ignored) {
+                current = current.getSuperclass();
+            }
+        }
+        return null;
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/backendlab/team4you/protocol/ProtocolArchiveServiceTest.java`
around lines 160 - 167, The setField helper uses
target.getClass().getDeclaredField(fieldName) which fails for inherited fields;
change setField to walk the class hierarchy (loop on Class<?> c =
target.getClass(); c != null; c = c.getSuperclass()) and call getDeclaredField
on each until found, then setAccessible(true) and set the value; also catch
NoSuchFieldException separately and throw a clearer exception (e.g., new
IllegalStateException("Field '"+fieldName+"' not found on "+target.getClass(),
e)) so failures include the field name and target class for easier debugging
while keeping other exceptions wrapped as before.

77-84: Add fail-fast interaction assertions in exception-path tests.

Line 77 and Line 103 assert exception type, but they do not assert that downstream collaborators stayed untouched. Adding that check makes these tests stricter against side effects.

Diff suggestion
@@
     void archiveProtocolPdf_shouldThrow_whenProtocolNotFound() {
         when(protocolRepository.findWithLockById(1L)).thenReturn(Optional.empty());

         assertThatThrownBy(() -> protocolArchiveService.archiveProtocolPdf(1L, currentUser))
                 .isInstanceOf(ProtocolNotFoundException.class);
+        verifyNoInteractions(protocolPdfService, caseRecordService, caseFileService);
+        verify(protocolRepository, never()).save(any());
     }
@@
     void archiveProtocolPdf_shouldThrow_whenProtocolNotReady() {
         when(protocolRepository.findWithLockById(1L)).thenReturn(Optional.of(protocol));

         assertThatThrownBy(() -> protocolArchiveService.archiveProtocolPdf(1L, currentUser))
                 .isInstanceOf(ProtocolNotReadyForPdfException.class);
+        verifyNoInteractions(protocolPdfService, caseRecordService, caseFileService);
+        verify(protocolRepository, never()).save(any());
     }

Also applies to: 103-110

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/backendlab/team4you/protocol/ProtocolArchiveServiceTest.java`
around lines 77 - 84, The test
archiveProtocolPdf_shouldThrow_whenProtocolNotFound currently only asserts the
exception; add fail-fast interaction assertions to ensure no downstream
collaborators are touched: after calling
protocolArchiveService.archiveProtocolPdf(1L, currentUser) and asserting
ProtocolNotFoundException, add verifyNoInteractions/verifyNoMoreInteractions
checks against the mocked collaborators used by this test class (e.g.,
pdfArchiveService, fileStorageService, auditService) and
verifyZeroInteractions/verifyNoMoreInteractions(protocolRepository) as
appropriate; apply the same pattern to the other exception-path test around
lines 103-110 to ensure no side effects.
src/main/java/backendlab/team4you/protocol/ProtocolController.java (1)

66-70: Extract repeated model population to a helper to reduce drift.

The same list attributes are re-added in three handlers. A shared helper keeps responses consistent when attributes evolve.

Diff suggestion
@@
         model.addAttribute(
                 "completedMeetingsWithoutProtocol",
                 meetingRepository.findCompletedMeetingsWithoutProtocol()
         );
-        model.addAttribute("protocols", protocolRepository.findAll());
+        model.addAttribute("protocols", protocolRepository.findAll());

         return "fragments/admin-protocols :: content";
@@
-        model.addAttribute(
-                "completedMeetingsWithoutProtocol",
-                meetingRepository.findCompletedMeetingsWithoutProtocol()
-        );
-        model.addAttribute("protocols", protocolRepository.findAll());
+        populateProtocolListModel(model);

         return "fragments/admin-protocols :: content";
@@
-        model.addAttribute(
-                "completedMeetingsWithoutProtocol",
-                meetingRepository.findCompletedMeetingsWithoutProtocol()
-        );
-        model.addAttribute("protocols", protocolRepository.findAll());
+        populateProtocolListModel(model);

         return "fragments/admin-protocols :: content";
@@
-        model.addAttribute(
-                "completedMeetingsWithoutProtocol",
-                meetingRepository.findCompletedMeetingsWithoutProtocol()
-        );
-        model.addAttribute("protocols", protocolRepository.findAll());
+        populateProtocolListModel(model);

         return "fragments/admin-protocols :: content";
     }
+
+    private void populateProtocolListModel(Model model) {
+        model.addAttribute(
+                "completedMeetingsWithoutProtocol",
+                meetingRepository.findCompletedMeetingsWithoutProtocol()
+        );
+        model.addAttribute("protocols", protocolRepository.findAll());
+    }

Also applies to: 86-90, 117-121

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/backendlab/team4you/protocol/ProtocolController.java` around
lines 66 - 70, The controller repeatedly adds the same model attributes
(meetingRepository.findCompletedMeetingsWithoutProtocol() and
protocolRepository.findAll()) in multiple handlers; extract that logic into a
private helper in ProtocolController (e.g., private void
populateModelWithProtocolsAndMeetings(Model model)) that performs
model.addAttribute("completedMeetingsWithoutProtocol",
meetingRepository.findCompletedMeetingsWithoutProtocol()) and
model.addAttribute("protocols", protocolRepository.findAll()), and replace the
repeated blocks in each handler method with a single call to
populateModelWithProtocolsAndMeetings(model) to keep responses consistent as
attributes evolve.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/backendlab/team4you/meeting/MeetingService.java`:
- Around line 396-402: The advisory check in ensureMeetingHasNoProtocol(...) is
racy; make the "no protocol" invariant enforced by serializing on the Meeting
row: in createProtocolForCompletedMeeting(...) (or inside
ensureMeetingHasNoProtocol) load the Meeting entity using a DB-level lock (e.g.,
JPA OPTIMISTIC lock via `@Version` on Meeting and validate version, or use a
PESSIMISTIC_WRITE lock via EntityManager.find(...,
LockModeType.PESSIMISTIC_WRITE) or repository method) and then re-check
protocolRepository.existsByMeetingId(meetingId) (or check a joined/derived
relation) before inserting the protocol; reference ensureMeetingHasNoProtocol,
createProtocolForCompletedMeeting, Meeting, and
protocolRepository.existsByMeetingId when making the change.
- Around line 267-270: The protocol guard is invoked after loading the Meeting,
which allows a null meetingId to reach the repository; move the call to
ensureMeetingHasNoProtocol(meetingId) to occur before getMeetingById(meetingId)
in removeAgendaItem so the guard runs first; apply the same reorder to the other
methods showing the same pattern (the other occurrences around lines 298-300 and
325-327) so ensureMeetingHasNoProtocol(...) always runs before
getMeetingById(...).

In `@src/main/java/backendlab/team4you/protocol/ProtocolPdfService.java`:
- Around line 178-185: writeLine currently sets a PDType1Font
(PDType1Font(FontName)) which only supports WinAnsi and will throw on Unicode
characters; change writeLine to use a Unicode-capable font (e.g., load a
TrueType/OTF and set PDType0Font via PDType0Font.load(document, fontStream))
before calling content.showText(text) or implement normalization/fallback to
strip/replace unsupported codepoints; update the content.setFont(...) call and
ensure the loaded PDType0Font instance is reused (cache it) rather than
recreating for every call.

---

Duplicate comments:
In `@src/main/java/backendlab/team4you/casefile/CaseFileService.java`:
- Around line 209-250: The code currently holds the pessimistic lock
(findByIdWithLock) while performing the S3 upload
(s3Service.uploadFileIfAbsent), which can block other operations; refactor so
you reserve the document number under the lock and immediately release the
transaction (use allocateNextDocumentNumber while inside the locked
transaction), then outside that transaction perform
s3Service.uploadFileIfAbsent(s3Key, bytes, normalizedContentType) and only after
a successful upload start a new transaction to create and persist the CaseFile
via caseFileRepository.saveAndFlush; keep identifiers findByIdWithLock,
allocateNextDocumentNumber, buildUniqueS3Key, s3Service.uploadFileIfAbsent and
caseFileRepository.saveAndFlush to locate where to split transactional
boundaries.

In `@src/main/java/backendlab/team4you/caserecord/CaseRecordService.java`:
- Around line 153-157: Change the annual protocol lookup/creation in
CaseRecordService to use a stable key (registry id + year) instead of the
formatted title: add/find by (registry.getId(), year) via caseRecordRepository
(introduce a repository method like findByRegistryIdAndYear), add a DB
uniqueness constraint on (registry_id, year), and modify
createAnnualProtocolCase flow to attempt insert and, on unique-key conflict,
re-query by (registryId, year) and return the existing row; ensure all
references that previously used title-based lookup (including the code around
createAnnualProtocolCase) are updated to use the stable lookup and
re-read-on-conflict semantics to avoid duplicate creations/races.

In `@src/main/java/backendlab/team4you/Team4youApplication.java`:
- Line 80: The startup guard is checking repository.existsByName(username) while
seedUser(...) persists username and displayName separately, causing a mismatch
for users like "caseofficer1"; change the existence check to match the actual
unique identifier (e.g., call repository.existsByUsername(username) instead of
existsByName(username)) and, if not present, add an existsByUsername(String
username) method to the repository interface; ensure the seedUser call in
Team4youApplication uses the same field (username) for both saving and checking
to avoid reseeding duplicates.

---

Nitpick comments:
In `@src/main/java/backendlab/team4you/protocol/ProtocolController.java`:
- Around line 66-70: The controller repeatedly adds the same model attributes
(meetingRepository.findCompletedMeetingsWithoutProtocol() and
protocolRepository.findAll()) in multiple handlers; extract that logic into a
private helper in ProtocolController (e.g., private void
populateModelWithProtocolsAndMeetings(Model model)) that performs
model.addAttribute("completedMeetingsWithoutProtocol",
meetingRepository.findCompletedMeetingsWithoutProtocol()) and
model.addAttribute("protocols", protocolRepository.findAll()), and replace the
repeated blocks in each handler method with a single call to
populateModelWithProtocolsAndMeetings(model) to keep responses consistent as
attributes evolve.

In `@src/test/java/backendlab/team4you/protocol/ProtocolArchiveServiceTest.java`:
- Around line 160-167: The setField helper uses
target.getClass().getDeclaredField(fieldName) which fails for inherited fields;
change setField to walk the class hierarchy (loop on Class<?> c =
target.getClass(); c != null; c = c.getSuperclass()) and call getDeclaredField
on each until found, then setAccessible(true) and set the value; also catch
NoSuchFieldException separately and throw a clearer exception (e.g., new
IllegalStateException("Field '"+fieldName+"' not found on "+target.getClass(),
e)) so failures include the field name and target class for easier debugging
while keeping other exceptions wrapped as before.
- Around line 77-84: The test
archiveProtocolPdf_shouldThrow_whenProtocolNotFound currently only asserts the
exception; add fail-fast interaction assertions to ensure no downstream
collaborators are touched: after calling
protocolArchiveService.archiveProtocolPdf(1L, currentUser) and asserting
ProtocolNotFoundException, add verifyNoInteractions/verifyNoMoreInteractions
checks against the mocked collaborators used by this test class (e.g.,
pdfArchiveService, fileStorageService, auditService) and
verifyZeroInteractions/verifyNoMoreInteractions(protocolRepository) as
appropriate; apply the same pattern to the other exception-path test around
lines 103-110 to ensure no side effects.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 209dfbe5-5db4-471a-b806-d7b51b927e72

📥 Commits

Reviewing files that changed from the base of the PR and between 5b2d728 and cbfa13d.

📒 Files selected for processing (19)
  • src/main/java/backendlab/team4you/Team4youApplication.java
  • src/main/java/backendlab/team4you/casefile/CaseFileService.java
  • src/main/java/backendlab/team4you/caserecord/CaseRecordRepository.java
  • src/main/java/backendlab/team4you/caserecord/CaseRecordService.java
  • src/main/java/backendlab/team4you/config/SecurityConfig.java
  • src/main/java/backendlab/team4you/controller/CaseOfficerController.java
  • src/main/java/backendlab/team4you/exceptions/ProtocolNotReadyForPdfException.java
  • src/main/java/backendlab/team4you/meeting/MeetingService.java
  • src/main/java/backendlab/team4you/protocol/ProtocolArchiveService.java
  • src/main/java/backendlab/team4you/protocol/ProtocolController.java
  • src/main/java/backendlab/team4you/protocol/ProtocolPdfController.java
  • src/main/java/backendlab/team4you/protocol/ProtocolPdfService.java
  • src/main/java/backendlab/team4you/protocol/ProtocolRepository.java
  • src/main/java/backendlab/team4you/protocol/ProtocolService.java
  • src/main/java/backendlab/team4you/user/UserRepository.java
  • src/test/java/backendlab/team4you/protocol/ProtocolArchiveServiceTest.java
  • src/test/java/backendlab/team4you/protocol/ProtocolControllerTest.java
  • src/test/java/backendlab/team4you/protocol/ProtocolPdfServiceTest.java
  • src/test/java/backendlab/team4you/protocol/ProtocolServiceTest.java
✅ Files skipped from review due to trivial changes (2)
  • src/main/java/backendlab/team4you/config/SecurityConfig.java
  • src/main/java/backendlab/team4you/user/UserRepository.java
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/main/java/backendlab/team4you/exceptions/ProtocolNotReadyForPdfException.java
  • src/main/java/backendlab/team4you/protocol/ProtocolPdfController.java
  • src/main/java/backendlab/team4you/caserecord/CaseRecordRepository.java
  • src/test/java/backendlab/team4you/protocol/ProtocolControllerTest.java
  • src/test/java/backendlab/team4you/protocol/ProtocolServiceTest.java

Comment thread src/main/java/backendlab/team4you/meeting/MeetingService.java
Comment thread src/main/java/backendlab/team4you/meeting/MeetingService.java Outdated
Comment thread src/main/java/backendlab/team4you/protocol/ProtocolPdfService.java Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
src/main/java/backendlab/team4you/protocol/ProtocolPdfService.java (1)

43-51: Avoid redundant protocol fetch in PDF generation path.

Line 43 already loads Protocol, but Line 50 calls ProtocolViewService.getParagraphsForViewer(protocolId, viewer), which (per src/main/java/backendlab/team4you/protocol/ProtocolViewService.java:21-27) fetches the same protocol again. This adds an avoidable DB roundtrip for every PDF export.

♻️ Suggested direction
- List<ProtocolParagraphViewDto> paragraphViews =
-         protocolViewService.getParagraphsForViewer(protocolId, viewer);
+ List<ProtocolParagraphViewDto> paragraphViews =
+         protocolViewService.getParagraphsForViewer(protocol, viewer);

Then add/adjust an overload in ProtocolViewService that maps from an already loaded Protocol.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/backendlab/team4you/protocol/ProtocolPdfService.java` around
lines 43 - 51, Protocol is loaded in ProtocolPdfService via
protocolRepository.findById(...) but you still call
ProtocolViewService.getParagraphsForViewer(protocolId, viewer) which causes a
duplicate DB fetch; add an overload in ProtocolViewService (e.g.,
getParagraphsForViewer(Protocol protocol, Viewer viewer) or similar) that maps
paragraphs from an already-loaded Protocol and update ProtocolPdfService to call
that overload instead of passing protocolId; also implement the new overload in
ProtocolViewService to reuse the existing mapping logic and remove any
protocolRepository lookups inside that path.
src/test/java/backendlab/team4you/meeting/MeetingServiceTest.java (1)

167-355: Consider extracting a test helper for repeated lock/protocol stubbing.

The same two stubs are repeated across many tests. A small helper (e.g., stubUnlockedMeeting(10L)) would reduce duplication and improve readability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/backendlab/team4you/meeting/MeetingServiceTest.java` around
lines 167 - 355, Introduce a small test helper to centralize the repeated
stubbing of meeting lock and protocol checks (e.g., a private method
stubUnlockedMeeting(Long meetingId) in MeetingServiceTest) that calls
meetingRepository.findByIdWithLock(meetingId) -> Optional.of(meeting) and
protocolRepository.existsByMeetingId(meetingId) -> false; replace all duplicated
when(...) lines in tests (references: meetingRepository.findByIdWithLock and
protocolRepository.existsByMeetingId) with a single call to that helper to
reduce duplication and improve readability.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/backendlab/team4you/meeting/MeetingRepository.java`:
- Around line 32-33: The query method in MeetingRepository uses a named
parameter :id but doesn't explicitly bind it; update the method signature for
findByIdWithLock in the MeetingRepository interface to add an explicit
`@Param`("id") on the Long id parameter so the JPQL :id is bound reliably (i.e.,
annotate the id parameter of findByIdWithLock with `@Param`("id")).

In `@src/main/java/backendlab/team4you/protocol/ProtocolPdfService.java`:
- Around line 123-126: The current wrapping logic in ProtocolPdfService only
breaks on whitespace (it uses currentLine, word and maxCharsPerLine) so very
long single tokens (URLs/identifiers) can overflow; modify the wrapping routine
to detect when a token's length > maxCharsPerLine and split that token into
substrings/chunks of at most maxCharsPerLine (appending the currentLine if
non-empty before splitting), adding each chunk to lines (or starting a new
currentLine with the remainder) so no produced line exceeds maxCharsPerLine;
ensure you update handling around lines.add(currentLine.toString()) and
currentLine = new StringBuilder(word) to use the chunking logic for oversized
words.
- Around line 195-203: The page-break check in writeLine incorrectly only
compares currentY to BOTTOM_MARGIN, which can allow the next line (fontSize + 6
height) to overflow; update the check in writeLine(String text, boolean bold,
float fontSize) to reserve space for the upcoming line by calling addNewPage()
when currentY - (fontSize + 6) <= BOTTOM_MARGIN (instead of currentY <=
BOTTOM_MARGIN), keeping the rest of the method (setting font, showText,
newLineAtOffset, and decrementing currentY) unchanged and ensuring addNewPage
positions content/cursor as the fresh top of a page.

In `@src/test/java/backendlab/team4you/meeting/MeetingServiceTest.java`:
- Around line 376-391: The test
updateMeeting_shouldThrowMeetingHasProtocolException_whenMeetingHasProtocol uses
the wrong expected exception and lacks stubbing; change the assertion to expect
MeetingHasProtocolException (isInstanceOf(MeetingHasProtocolException.class))
and stub the locked lookup to return a meeting that has a protocol (mock meeting
with protocol present) via meetingRepository.findByIdWithLock(...) returning
Optional.of(mockMeetingWithProtocol); keep verifying
meetingRepository.findByIdWithLock(10L) was called and that
meetingRepository.save(...) was never invoked to ensure the protocol guard
triggers.

---

Nitpick comments:
In `@src/main/java/backendlab/team4you/protocol/ProtocolPdfService.java`:
- Around line 43-51: Protocol is loaded in ProtocolPdfService via
protocolRepository.findById(...) but you still call
ProtocolViewService.getParagraphsForViewer(protocolId, viewer) which causes a
duplicate DB fetch; add an overload in ProtocolViewService (e.g.,
getParagraphsForViewer(Protocol protocol, Viewer viewer) or similar) that maps
paragraphs from an already-loaded Protocol and update ProtocolPdfService to call
that overload instead of passing protocolId; also implement the new overload in
ProtocolViewService to reuse the existing mapping logic and remove any
protocolRepository lookups inside that path.

In `@src/test/java/backendlab/team4you/meeting/MeetingServiceTest.java`:
- Around line 167-355: Introduce a small test helper to centralize the repeated
stubbing of meeting lock and protocol checks (e.g., a private method
stubUnlockedMeeting(Long meetingId) in MeetingServiceTest) that calls
meetingRepository.findByIdWithLock(meetingId) -> Optional.of(meeting) and
protocolRepository.existsByMeetingId(meetingId) -> false; replace all duplicated
when(...) lines in tests (references: meetingRepository.findByIdWithLock and
protocolRepository.existsByMeetingId) with a single call to that helper to
reduce duplication and improve readability.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5d513162-bae8-408c-9593-d988f4a60285

📥 Commits

Reviewing files that changed from the base of the PR and between cbfa13d and 0bca7bf.

⛔ Files ignored due to path filters (2)
  • src/main/resources/fonts/NotoSans-Bold.ttf is excluded by !**/*.ttf
  • src/main/resources/fonts/NotoSans-Regular.ttf is excluded by !**/*.ttf
📒 Files selected for processing (5)
  • src/main/java/backendlab/team4you/Team4youApplication.java
  • src/main/java/backendlab/team4you/meeting/MeetingRepository.java
  • src/main/java/backendlab/team4you/meeting/MeetingService.java
  • src/main/java/backendlab/team4you/protocol/ProtocolPdfService.java
  • src/test/java/backendlab/team4you/meeting/MeetingServiceTest.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/backendlab/team4you/Team4youApplication.java

Comment thread src/main/java/backendlab/team4you/meeting/MeetingRepository.java
Comment thread src/main/java/backendlab/team4you/protocol/ProtocolPdfService.java
Comment thread src/main/java/backendlab/team4you/protocol/ProtocolPdfService.java Outdated
Comment thread src/test/java/backendlab/team4you/meeting/MeetingServiceTest.java
@MartinStenhagen
MartinStenhagen merged commit 73447cd into main Apr 28, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants