Skip to content

Journal API adjustments #1010

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 38 commits into from
Jun 20, 2025
Merged

Journal API adjustments #1010

merged 38 commits into from
Jun 20, 2025

Conversation

kadamidev
Copy link
Contributor

@kadamidev kadamidev commented Jun 12, 2025

  • Fix some perm bugs, only allowing associate editors to take certain actions (chiefs should also be allowed)
  • Filter accept editor decision return vals
  • Refactor emails for sg templates
  • Emit notifs/emails for journal actions

Summary by CodeRabbit

  • New Features

    • Added support for inviting referees via email, including notifications and emails for referee invitation, acceptance, and decline.
    • Enhanced notifications and email communications for key journal submission events, such as assignment, acceptance, rejections, and revision requests.
    • Introduced new utility functions for time formatting and indefinite article prefixing in text.
  • Improvements

    • Refined journal submission workflow to include richer metadata in notifications and emails.
    • Expanded editor role permissions for submission actions and referee invitations.
    • Consolidated and strongly typed email templates and payloads for journal communications.
    • Improved error handling and logging for email and notification dispatch.
    • Enriched journal editor data with current workload metrics.
    • Refactored email templates to unify submission data into structured objects and removed direct submission links in emails.
  • Bug Fixes

    • Aligned API response formats for editor invite decisions.
    • Addressed database deadlock issues in test teardown.
  • Chores

    • Updated environment variable examples for email template configuration.
    • Removed unused dependencies and updated import paths for consistency.
  • Tests

    • Updated tests to reflect input property renaming and revised permission logic for editor roles.

Copy link

coderabbitai bot commented Jun 12, 2025

## Walkthrough

This update refactors and enhances the journal submission, referee management, and email notification systems. It introduces new, strongly-typed email payloads, restructures email templates to use consolidated submission objects, updates notification payloads, and improves error handling and logging. Several controller and service methods are extended for richer metadata, improved role permissions, and robust communication workflows.

## Changes

| File(s) / Path(s)                                                                 | Change Summary |
|-----------------------------------------------------------------------------------|---------------|
| `src/services/email.ts`                                                           | Deleted legacy email service module. |
| `src/services/email/email.ts`, `src/services/email/journalEmailTypes.ts`          | Introduced new email service and strongly-typed journal email payloads. |
| `src/controllers/communities/submissions.ts`, `src/controllers/doi/mint.ts`,<br>`src/services/journals/JournalInviteService.ts`, `src/services/journals/JournalRefereeManagementService.ts`, `src/workers/doiSubmissionQueue.ts` | Updated import paths for email utilities and ensured email sending is awaited; improved error handling. |
| `src/controllers/journals/invites/editorInviteDecision.ts`                        | Standardized response payload for editor invite acceptance. |
| `src/controllers/journals/referees/inviteReferee.ts`,<br>`src/services/journals/JournalRefereeManagementService.ts`,<br>`test/integration/journals/journalRefereeManagement.test.ts` | Renamed `managerId` to `managerUserId` for clarity; updated related test cases. |
| `src/controllers/journals/submissions/index.ts`,<br>`src/services/journals/JournalSubmissionService.ts`,<br>`src/services/journals/JournalRevisionService.ts` | Enhanced revision and rejection workflows with richer metadata, notifications, and emails; added helper methods for submission metadata. |
| `src/routes/v1/journals/index.ts`,<br>`test/integration/journals/journalSubmission.test.ts` | Expanded editor role permissions for submission actions; removed test preventing chief editor from accepting submissions. |
| `src/services/Notifications/NotificationService.ts`,<br>`src/services/Notifications/notificationPayloadTypes.ts` | Updated referee invite notification payload to use `inviteToken` and `dueDateHrs`. |
| `src/templates/emails/journals/*.tsx`                                             | Refactored all journal-related email templates to use consolidated `SubmissionExtended` and structured editor/assigner objects; removed individual submission props and direct submission links. |
| `.env.example`                                                                    | Added `SENDGRID_TEMPLATE_ID_MAP` variable for email templates. |
| `src/utils.ts`                                                                    | Added `prependIndefiniteArticle` utility function. |
| `src/utils/clock.ts`                                                              | Added `getRelativeTime` utility for human-readable time differences. |
| `package.json`                                                                    | Removed alias for `date-fn-latest`, relying directly on `date-fns`. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant Controller
    participant JournalService
    participant EmailService
    participant NotificationService

    User->>Controller: Action (e.g., accept invite, submit revision)
    Controller->>JournalService: Process action
    JournalService->>JournalService: Fetch extended submission data
    JournalService->>EmailService: sendEmail(payload)
    EmailService-->>JournalService: Email sent/logged
    JournalService->>NotificationService: emitNotification(payload)
    NotificationService-->>JournalService: Notification emitted
    JournalService-->>Controller: Result
    Controller-->>User: Response

Possibly related PRs

  • Desci Journal #1004: Introduces the foundational backend and API for Desci Journals, directly related as this PR extends and modifies the journal invitation and submission workflows established there.

Poem

In the warren of code, where submissions hop,
New emails now leap, with a type-safe plop.
Editors and referees get news with delight,
As rabbits refactor through day and night.
With tokens and roles, and a template or two,
This patch brings fresh carrots for the whole review crew! 🥕


<!-- walkthrough_end -->


---

<details>
<summary>📜 Recent 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 f06da22e0e59aa4571e0f7c5e239d27027d4bfd8 and e3decdb0051795934eafa3531d7a80c334425258.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `sync-server/yarn.lock` is excluded by `!**/yarn.lock`, `!**/*.lock`

</details>

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

* `desci-server/src/controllers/journals/submissions/index.ts` (5 hunks)
* `desci-server/test/integration/analytics.test.ts` (5 hunks)
* `desci-server/test/integration/journals/journalSubmission.test.ts` (3 hunks)
* `desci-server/test/integration/notifications.test.ts` (1 hunks)
* `sync-server/package.json` (2 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* desci-server/test/integration/notifications.test.ts

</details>

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

* sync-server/package.json
* desci-server/test/integration/journals/journalSubmission.test.ts
* desci-server/src/controllers/journals/submissions/index.ts

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: build-and-test

</details>

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

<details>
<summary>desci-server/test/integration/analytics.test.ts (2)</summary>

`370-370`: **Test suite disabled - verify if this is intentional for the PR.**

The entire "Analytics aggregate calculation" test suite has been skipped. Based on the AI summary, this aligns with other test suite skips during this update cycle, but please confirm this is intentional and not a temporary debugging measure.



If this is a permanent skip, consider adding a comment explaining why these tests are disabled and when they might be re-enabled.

---

`395-395`: **Date offset adjustments improve test reliability.**

The consistent change from 5 to 6 days ago for test data creation within the "last 7 days" scenarios is a good fix. This ensures test data falls clearly within the expected time boundaries and should reduce flaky test behavior due to timing edge cases.



The changes maintain consistency across all affected test data creation calls:
- Mock nodes, users, and ORCID users creation
- User interaction logging  
- Node viewing and liking activities

This systematic adjustment demonstrates attention to test reliability.


Also applies to: 403-403, 410-410, 425-425, 446-446, 459-459, 466-466

</details>

</blockquote></details>

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


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

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_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=desci-labs/nodes&utm_content=1010):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `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 explain this code block.`
  -	`@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 explain its main purpose.`
  - `@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.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

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 docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this 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 -->

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
desci-server/src/routes/v1/journals/index.ts (1)

155-160: Avoid magic-array repetition – extract a shared constant for editor‐action routes

Exactly the same [EditorRole.ASSOCIATE_EDITOR, EditorRole.CHIEF_EDITOR] literal now appears four times. Repeating this array:

  • bloats the diff every time the list changes,
  • invites accidental divergence between routes,
  • and slightly hurts readability.

Consider defining it once – e.g. const EDITOR_ACTION_ROLES: EditorRole[] = [EditorRole.ASSOCIATE_EDITOR, EditorRole.CHIEF_EDITOR]; – then passing it to ensureJournalRole(...) everywhere.

-import { EditorRole } from '@prisma/client';
+import { EditorRole } from '@prisma/client';
+// Roles allowed to perform submission-level editorial actions
+const EDITOR_ACTION_ROLES: EditorRole[] = [
+  EditorRole.ASSOCIATE_EDITOR,
+  EditorRole.CHIEF_EDITOR,
+];
...
-    ensureJournalRole([EditorRole.ASSOCIATE_EDITOR, EditorRole.CHIEF_EDITOR]),
+    ensureJournalRole(EDITOR_ACTION_ROLES),

A micro-refactor like this is low-risk, keeps the file DRY, and makes future permission tweaks a one-liner.

Also applies to: 166-169, 176-179, 221-224

desci-server/src/controllers/journals/invites/editorInviteDecision.ts (1)

34-38: DRY up the picked invite fields

Both the accept and decline branches call
_.pick(invite, ['id', 'role', 'inviterId', 'journalId', 'decisionAt', 'accepted', 'declined']).

Extract the field list into a constant to avoid duplication and the risk that one branch drifts from the other:

 const logger = parentLogger.child({ module: 'Journals::EditorInviteDecisionController' });

+const INVITE_RESPONSE_FIELDS: Array<keyof Invite> = [
+  'id',
+  'role',
+  'inviterId',
+  'journalId',
+  'decisionAt',
+  'accepted',
+  'declined',
+];
+
 ...
-      return sendSuccess(
-        res,
-        { invite: _.pick(invite, ['id', 'role', 'inviterId', 'journalId', 'decisionAt', 'accepted', 'declined']) },
-        'Editor invitation accepted successfully.',
-      );
+      return sendSuccess(
+        res,
+        { invite: _.pick(invite, INVITE_RESPONSE_FIELDS) },
+        'Editor invitation accepted successfully.',
+      );
 ...
-          invite: _.pick(invite, ['id', 'role', 'inviterId', 'journalId', 'decisionAt', 'accepted', 'declined']),
+          invite: _.pick(invite, INVITE_RESPONSE_FIELDS),

Makes both paths symmetrical, keeps the literal list in one place, and eases future field changes.

Also applies to: 44-48

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 33914c3 and e89a6c2.

📒 Files selected for processing (2)
  • desci-server/src/controllers/journals/invites/editorInviteDecision.ts (1 hunks)
  • desci-server/src/routes/v1/journals/index.ts (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
desci-server/src/controllers/journals/invites/editorInviteDecision.ts (1)
desci-server/src/core/api.ts (1)
  • sendSuccess (5-8)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: Cursor BugBot
  • GitHub Check: build-and-test

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

✅ BugBot reviewed your changes and found no bugs!


Was this report helpful? Give feedback by reacting with 👍 or 👎

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 30

🔭 Outside diff range comments (2)
desci-server/src/services/journals/JournalInviteService.ts (1)

68-69: Missing crypto import – compile-time failure

crypto.randomUUID() is used to generate the invite token, but crypto is not imported in this file.
Add:

import crypto from 'node:crypto';

(or import * as crypto from 'crypto') near the top.

desci-server/src/services/journals/JournalSubmissionService.ts (1)

186-206: Check that the target editor exists before dereferencing editor.user

editor can be null when an invalid editorId is supplied for the journal.
Subsequent lines access editor.user.*, which will throw and skip the intended error handling.

-  const editor = await prisma.journalEditor.findFirst({...});
-  // ...
-  await NotificationService.emitOnJournalSubmissionAssignedToEditor({ editor });
+  const editor = await prisma.journalEditor.findFirst({...});
+  if (!editor) {
+    throw new NotFoundError('Editor not found for this journal');
+  }
🧹 Nitpick comments (25)
.env.example (2)

67-69: Clarify expected format for SENDGRID_TEMPLATE_ID_MAP.

Storing a “map” in a single env-var is brittle unless the consumers parse a well-defined format (e.g. JSON or key1=id1,key2=id2). Add a short comment describing the exact syntax and required escaping rules, otherwise onboarding & CI configuration will be error-prone.


201-202: Remove superfluous blank lines to silence dotenv-linter.

- 
-

Only one trailing newline is sufficient.

desci-server/src/utils.ts (1)

309-319: Edge-cases missing in prependIndefiniteArticle.

The simple vowel-check fails for words whose pronunciation starts with a vowel sound but a consonant letter (“hour”, “MBA”, “honest”, etc.). If email copy may include such cases, consider switching to a small phonetic helper (e.g. an-array) or at least add a regex for common exceptions.

Implementation is otherwise fine.

desci-server/src/services/Notifications/NotificationService.ts (1)

608-616: Clarify and document the new dueDateHrs semantic

Switching from an absolute Date to a relative dueDateHrs: number helps avoid TZ problems, but leaves room for mis-interpretation (is it “hours from creation”, “working hours”, etc.).
Add a short JSDoc or comment here (and in the corresponding type) that states precisely:

• the reference point (invite creation vs. now)
• that it is UTC hours, not local-time hours.

This will save downstream consumers from off-by-X-hours bugs.

Also applies to: 622-640

desci-server/src/services/journals/JournalInviteService.ts (1)

95-106: Consider propagating email-send failures

sendEmail is correctly awaited, but any rejection is swallowed by the outer try/catch, logged, and the function still returns the invite as “successful”.
If email delivery is business-critical, re-throw or surface a 5xx so the caller can retry, otherwise note explicitly that silent failures are acceptable.

desci-server/src/controllers/journals/submissions/index.ts (1)

329-330: Misleading log context

The log tag uses 'acceptSubmission', but we are in rejectSubmissionController, which will confuse log searches.

-    logger.error({ fn: 'acceptSubmission', error: e, submissionId }, 'Notification push failed');
+    logger.error({ fn: 'rejectSubmission', error: e, submissionId }, 'Notification push failed');
desci-server/src/templates/emails/journals/FinalRejectionDecision.tsx (1)

37-40: Empty link breaks CTA

Button href={''} renders as an anchor with empty URL → clicking reloads inbox page.
Either supply a meaningful link (e.g. author guidelines / submission criteria) or remove the button altogether to avoid user confusion.

desci-server/src/templates/emails/journals/RefereeDeclinedEmail.tsx (2)

1-1: Remove unused imports to prevent dead-code + TS lint noise

EditorRole and JournalSubmission are not referenced anywhere in this file after the refactor.
Leaving them in will trigger eslint/ts-server “unused import” diagnostics and bloats the bundle.

-import { EditorRole, Journal, JournalSubmission } from '@prisma/client';
+import { Journal } from '@prisma/client';

44-46: Guard against missing / malformed author arrays

submission.authors.join(', ') will throw if authors is null, undefined, or not an array (possible when data comes from Prisma raw JSON fields).
Consider a safe fallback:

-<Text className="text-md text-center">Authors: {submission.authors.join(', ')}</Text>
+<Text className="text-md text-center">
+  Authors: {(submission.authors ?? []).join(', ') || 'N/A'}
+</Text>
desci-server/src/templates/emails/journals/RefereeReviewReminder.tsx (1)

29-31: Null-safety for authors array

Handle potential null/undefined as suggested previously to avoid runtime crashes in production emails.

desci-server/src/templates/emails/journals/OverdueAlertEditor.tsx (1)

32-35: Duplicate authors join() safety

As in other templates, wrap submission.authors with a fallback to prevent crashes if data is malformed.

desci-server/src/templates/emails/journals/ExternalRefereeInvite.tsx (2)

10-13: refereeName prop is declared but never used

The value is passed in from the email service yet ignored inside the template. Either include the name in the copy (e.g., greeting) or remove the prop to keep the payload minimal.


33-36: Author list null-safety

Apply the same guarded join pattern to avoid runtime errors if authors is not an array.

desci-server/src/templates/emails/journals/RefereeAccepted.tsx (1)

1-3: Unused EditorRole import

EditorRole is imported but never referenced after the refactor. Remove it to avoid a TypeScript no-unused-vars error and keep bundles lean.

desci-server/src/templates/emails/journals/RevisionSubmittedConfirmation.tsx (1)

1-3: Remove orphaned EditorRole import

EditorRole is no longer used in this file.

desci-server/src/templates/emails/journals/RefereeReassigned.tsx (1)

12-13: refereeEmail prop is declared but never used

Either utilise this value in the email body (e.g., “({refereeEmail})”) or drop the prop entirely to avoid dead code.

desci-server/src/templates/emails/journals/SubmissionReassigned.tsx (2)

10-17: Unused editor prop

editor is defined in SubmissionReassignedEmailProps but never referenced, leading to an unused variable warning and unnecessary payload bloat. Remove it or incorporate it into the email copy.

-  editor: {
-    name: string;
-    userId: number;
-  };

1-3: Extraneous EditorRole import

EditorRole isn’t used after the refactor—safe to delete.

desci-server/src/templates/emails/journals/MajorRevisionRequest.tsx (1)

5-5: Import extension may break TS builds

Using .js in a TSX file can confuse TypeScript path resolution in some tsconfig setups. Prefer importing the .ts source (compiler will rewrite) or omit the extension.

desci-server/src/templates/emails/journals/SubmissionAssigned.tsx (2)

1-1: Unused import

EditorRole is not referenced; remove to keep the bundle lean and silence linters.


10-14: Dead prop: editor

The editor prop is declared but never used in the component. Drop it (and its callers) or render relevant info to avoid stale API surface.

desci-server/src/templates/emails/journals/RefereeInvite.tsx (1)

1-1: Remove unused EditorRole import

It isn’t referenced.

desci-server/src/templates/emails/journals/SubmissionAcceped.tsx (1)

10-14: Unused editor prop

Declared but unused; prune or surface editor info.

desci-server/src/services/email/email.ts (1)

32-38: Nested ternary hurts readability

Consider extracting deploymentEnvironmentString into a small helper with a switch for clarity and easier extension.

desci-server/src/services/journals/JournalSubmissionService.ts (1)

368-378: Variable shadowing makes the code harder to follow

const submission = … is declared twice (outer scope and inside the try-block).
Rename the inner one (e.g. fullSubmission) to avoid accidental misuse.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between e89a6c2 and d6bea98.

📒 Files selected for processing (33)
  • .env.example (2 hunks)
  • desci-server/src/controllers/communities/submissions.ts (1 hunks)
  • desci-server/src/controllers/doi/mint.ts (1 hunks)
  • desci-server/src/controllers/journals/referees/inviteReferee.ts (1 hunks)
  • desci-server/src/controllers/journals/submissions/index.ts (3 hunks)
  • desci-server/src/services/Notifications/NotificationService.ts (2 hunks)
  • desci-server/src/services/Notifications/notificationPayloadTypes.ts (1 hunks)
  • desci-server/src/services/email.ts (0 hunks)
  • desci-server/src/services/email/email.ts (1 hunks)
  • desci-server/src/services/email/journalEmailTypes.ts (1 hunks)
  • desci-server/src/services/journals/JournalInviteService.ts (2 hunks)
  • desci-server/src/services/journals/JournalRefereeManagementService.ts (11 hunks)
  • desci-server/src/services/journals/JournalRevisionService.ts (3 hunks)
  • desci-server/src/services/journals/JournalSubmissionService.ts (12 hunks)
  • desci-server/src/templates/emails/journals/DeskRejection.tsx (1 hunks)
  • desci-server/src/templates/emails/journals/ExternalRefereeInvite.tsx (2 hunks)
  • desci-server/src/templates/emails/journals/FinalRejectionDecision.tsx (1 hunks)
  • desci-server/src/templates/emails/journals/MajorRevisionRequest.tsx (1 hunks)
  • desci-server/src/templates/emails/journals/MinorRevisionRequest.tsx (1 hunks)
  • desci-server/src/templates/emails/journals/OverdueAlertEditor.tsx (1 hunks)
  • desci-server/src/templates/emails/journals/RefereeAccepted.tsx (1 hunks)
  • desci-server/src/templates/emails/journals/RefereeDeclinedEmail.tsx (3 hunks)
  • desci-server/src/templates/emails/journals/RefereeInvite.tsx (2 hunks)
  • desci-server/src/templates/emails/journals/RefereeReassigned.tsx (2 hunks)
  • desci-server/src/templates/emails/journals/RefereeReviewReminder.tsx (2 hunks)
  • desci-server/src/templates/emails/journals/RevisionSubmittedConfirmation.tsx (1 hunks)
  • desci-server/src/templates/emails/journals/SubmissionAcceped.tsx (1 hunks)
  • desci-server/src/templates/emails/journals/SubmissionAssigned.tsx (1 hunks)
  • desci-server/src/templates/emails/journals/SubmissionReassigned.tsx (2 hunks)
  • desci-server/src/utils.ts (1 hunks)
  • desci-server/src/utils/clock.ts (2 hunks)
  • desci-server/src/workers/doiSubmissionQueue.ts (1 hunks)
  • desci-server/test/integration/journals/journalSubmission.test.ts (0 hunks)
💤 Files with no reviewable changes (2)
  • desci-server/test/integration/journals/journalSubmission.test.ts
  • desci-server/src/services/email.ts
✅ Files skipped from review due to trivial changes (4)
  • desci-server/src/workers/doiSubmissionQueue.ts
  • desci-server/src/controllers/doi/mint.ts
  • desci-server/src/controllers/journals/referees/inviteReferee.ts
  • desci-server/src/services/email/journalEmailTypes.ts
🧰 Additional context used
🪛 dotenv-linter (3.3.0)
.env.example

[warning] 202-202: [ExtraBlankLine] Extra blank line detected

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build-and-test
🔇 Additional comments (5)
desci-server/src/controllers/communities/submissions.ts (1)

20-20: Path change looks correct – verify runtime resolution.

The project imports transpiled .js files; ensure desci-server/src/services/email/email.ts is emitted to email.js in the same folder so Node can resolve it in production.

desci-server/src/services/Notifications/notificationPayloadTypes.ts (1)

117-119: Renamed fields require cascading updates.

dueDatedueDateHrs and new inviteToken will break existing code & templates that still expect a Date. Please grep for dueDate in referee-invite contexts and update all usages.

#!/bin/bash
# find stale usages outside this file
rg -n "dueDate[^A-Za-z]" | grep -v notificationPayloadTypes.ts
desci-server/src/services/email/email.ts (1)

162-173: Environment check uses non-standard value

NODE_ENV is usually "development", not "dev". Ensure the condition matches your deployment conventions or make it inclusive (['dev', 'development'].includes(...)).

desci-server/src/services/journals/JournalSubmissionService.ts (1)

441-447: Potential runtime when assignedEditor.user is undefined

The service trusts that every journalEditor has a linked user.
Insert a guard or add a NOT NULL join in the query to avoid Cannot read property 'name'….

desci-server/src/services/journals/JournalRefereeManagementService.ts (1)

34-40: crypto.randomUUID() used without import – may fail in older Node runtimes

Add import crypto from 'crypto'; (or use randomUUID from "node:crypto") to satisfy TS type-checking and non-global environments.

Comment on lines +77 to +83
type: EmailTypes.REVISION_SUBMITTED,
payload: {
email: submission.assignedEditor.email,
journal: submission.journal,
submission: submissionExtended,
},
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

assignedEditor.email is undefined

assignedEditor is a JournalEditor, which has no email field.
Fetch the linked User or reuse the editor object already queried:

const editorUser = await prisma.user.findUnique({ where: { id: submission.assignedEditorId } });

await sendEmail({
  type: EmailTypes.REVISION_SUBMITTED,
  payload: {
    email: editorUser.email,
    journal: submission.journal,
    submission: submissionExtended,
  },
});
🤖 Prompt for AI Agents
In desci-server/src/services/journals/JournalRevisionService.ts around lines 77
to 83, the code incorrectly accesses assignedEditor.email, but assignedEditor
lacks an email field. To fix this, fetch the User linked to assignedEditor by
querying prisma.user with assignedEditorId, then use the retrieved user's email
in the payload for sendEmail instead of assignedEditor.email.

Comment on lines 43 to 86
try {
// Notification logic
const submission = await prisma.journalSubmission.findUnique({
where: { id: submissionId },
include: {
journal: true,
author: true,
assignedEditor: true,
},
});

const editor = await prisma.journalEditor.findUnique({
where: {
userId_journalId: {
userId: submission.assignedEditorId,
journalId,
},
},
});

const submissionExtendedResult = await journalSubmissionService.getSubmissionExtendedData(submissionId);
if (submissionExtendedResult.isErr()) {
throw new Error('Failed to get submission extended data');
}
const submissionExtended = submissionExtendedResult.value;

await NotificationService.emitOnRevisionSubmittedToEditor({
journal: submission.journal,
editor,
submission: submission,
submissionTitle: submissionExtended.title,
author: submission.author,
});
sendEmail({
type: EmailTypes.REVISION_SUBMITTED,
payload: {
email: submission.assignedEditor.email,
journal: submission.journal,
submission: submissionExtended,
},
});
} catch (e) {
logger.error({ fn: 'createRevision', error: e, submissionId }, 'Notification push failed');
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Guard against unassigned submissions

If submission.assignedEditorId is null, the subsequent queries will throw.
Add an early check and return an err(...) so the caller sees a sensible error instead of a 500.

if (!submission?.assignedEditorId) {
  throw new Error('Submission has no assigned editor');
}
🤖 Prompt for AI Agents
In desci-server/src/services/journals/JournalRevisionService.ts between lines 43
and 86, the code assumes submission.assignedEditorId is always set, but if it is
null, subsequent queries will fail and cause a 500 error. Add an early check
after fetching the submission to verify submission.assignedEditorId is not null
or undefined. If it is missing, return an error or throw a new Error with a
clear message like 'Submission has no assigned editor' to provide a sensible
error response to the caller.

Comment on lines +272 to +284
include: {
journal: true,
author: true,
assignedEditor: true,
},
});

const submissionExtendedResult = await journalSubmissionService.getSubmissionExtendedData(submissionId);
if (submissionExtendedResult.isErr()) {
throw new Error('Failed to get submission extended data');
}
const submissionExtended = submissionExtendedResult.value;

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Missing null-guard for assignedEditor in acceptance path

submission.assignedEditor is assumed present, yet the DB relation is optional.
Add an explicit check similar to:

if (!submission.assignedEditor) {
  throw new Error('Assigned editor record missing');
}
🤖 Prompt for AI Agents
In desci-server/src/services/journals/JournalSubmissionService.ts around lines
272 to 284, the code assumes submission.assignedEditor is always present, but
since the database relation is optional, you need to add a null check for
assignedEditor. Insert an explicit check after fetching submission to verify if
assignedEditor exists, and if not, throw an error with a clear message like
'Assigned editor record missing' to handle this case safely.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🔭 Outside diff range comments (1)
desci-server/src/controllers/admin/metrics/publishMetrics.ts (1)

56-62: Shadowed variable hides outer scope value
guestSignUpSuccessRate is redeclared inside the if block, shadowing the variable defined earlier (lines 42-47). This can confuse readers and static–analysis tools. Rename the inner variable to make its intent explicit.

-    const [prevTotalUsers, prevPublishers, prevPublishersInCommunity, guestSignUpSuccessRate] = await Promise.all([
+    const [
+      prevTotalUsers,
+      prevPublishers,
+      prevPublishersInCommunity,
+      prevGuestSignUpSuccessRate,
+    ] = await Promise.all([
@@
-        guestSignUpSuccessRate,
+        guestSignUpSuccessRate: prevGuestSignUpSuccessRate,
♻️ Duplicate comments (3)
desci-server/src/services/journals/JournalSubmissionService.ts (3)

259-264: Error masking persists – propagate or surface partial failure
Same issue flagged in previous review: the catch block still only logs the error and returns success to the caller, silently hiding notification/email failures.
Please propagate the error (or return an object that indicates partial success) so API clients can retry or alert.


324-326: Null-guard for assignedEditor still missing

submission.assignedEditor is queried but never null-checked. When the relation is optional, this will throw in production for submissions created pre-assignment.

-        name: submission.assignedEditor.name,
-        userId: submission.assignedEditor.id,
+        name: submission.assignedEditor?.name ?? 'Unknown editor',
+        userId: submission.assignedEditor?.id ?? 0,

Better: throw a domain error earlier if assignedEditor is absent.


535-538: targetVersionIndex can be negative – add bounds check

submission.version is treated as 1-based while array indices are 0-based. For the first version this yields length - 1, but for any higher version the index can become negative and crash.

-  const targetVersionIndex = researchObject.versions.length - submission.version;
-  const targetVersion = researchObject.versions[targetVersionIndex];
+  const targetVersionIndex =
+    researchObject.versions.length - submission.version;
+  if (targetVersionIndex < 0 || targetVersionIndex >= researchObject.versions.length) {
+    return err(new Error('Requested version out of bounds'));
+  }
+  const targetVersion = researchObject.versions[targetVersionIndex];
🧹 Nitpick comments (2)
desci-server/src/controllers/admin/metrics/publishMetrics.ts (1)

32-35: Minor typo in comment
previouseprevious – helps keep inline docs tidy.

-// hard check the from and to dates are valid, to prevent previouse period from being bogus ...
+// Hard-check the from and to dates are valid to prevent previous period from being bogus ...
desci-server/src/services/journals/JournalSubmissionService.ts (1)

406-414: Variable shadowing & redundant query

Inside the requestRevision try-block you re-query submission and shadow the outer const.
This increases query load and makes debugging harder.

-    // Notification logic
-    const submission = await prisma.journalSubmission.findUnique({
+    // Refresh with relations needed for notifications
+    const freshSubmission = await prisma.journalSubmission.findUnique({
       where: { id: submissionId },
       include: {
         journal: true,
         author: true,
       },
     });

Use freshSubmission (or rename the original) to avoid shadowing.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 927b43f and 179f03c.

⛔ Files ignored due to path filters (1)
  • desci-server/yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (16)
  • desci-server/package.json (0 hunks)
  • desci-server/src/controllers/admin/analytics.ts (1 hunks)
  • desci-server/src/controllers/admin/metrics/featureAdoptionMetrics.ts (1 hunks)
  • desci-server/src/controllers/admin/metrics/publishMetrics.ts (1 hunks)
  • desci-server/src/controllers/admin/metrics/userEngagements.ts (1 hunks)
  • desci-server/src/controllers/doi/mint.ts (2 hunks)
  • desci-server/src/controllers/journals/submissions/index.ts (3 hunks)
  • desci-server/src/services/interactionLog.ts (1 hunks)
  • desci-server/src/services/journals/JournalRefereeManagementService.ts (9 hunks)
  • desci-server/src/services/journals/JournalRevisionService.ts (3 hunks)
  • desci-server/src/services/journals/JournalSubmissionService.ts (12 hunks)
  • desci-server/src/utils/clock.ts (2 hunks)
  • desci-server/src/workers/doiSubmissionQueue.ts (2 hunks)
  • desci-server/test/integration/analytics.test.ts (1 hunks)
  • desci-server/test/integration/util.test.ts (1 hunks)
  • desci-server/test/util.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • desci-server/package.json
✅ Files skipped from review due to trivial changes (7)
  • desci-server/src/controllers/admin/metrics/featureAdoptionMetrics.ts
  • desci-server/src/controllers/admin/analytics.ts
  • desci-server/test/integration/util.test.ts
  • desci-server/test/util.ts
  • desci-server/src/services/interactionLog.ts
  • desci-server/src/controllers/admin/metrics/userEngagements.ts
  • desci-server/test/integration/analytics.test.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • desci-server/src/workers/doiSubmissionQueue.ts
  • desci-server/src/controllers/doi/mint.ts
  • desci-server/src/utils/clock.ts
  • desci-server/src/controllers/journals/submissions/index.ts
  • desci-server/src/services/journals/JournalRevisionService.ts
  • desci-server/src/services/journals/JournalRefereeManagementService.ts
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build-and-test
🔇 Additional comments (1)
desci-server/src/controllers/admin/metrics/publishMetrics.ts (1)

1-1: Correct import – LGTM
Switching to the canonical date-fns package resolves the broken import path.

Comment on lines +215 to +221
const editor = await prisma.journalEditor.findFirst({
where: { userId: payload.editorId, journalId: submission.journalId },
include: {
user: true,
},
});

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Guard against a missing editor record before continuing

editor can be null if the supplied editorId does not map to an editor of the target journal.
All subsequent notification / email logic relies on editor.user. Add an explicit check and fail fast.

   const editor = await prisma.journalEditor.findFirst({
     where: { userId: payload.editorId, journalId: submission.journalId },
     include: {
       user: true,
     },
   });

+  if (!editor) {
+    throw new NotFoundError('Editor record not found for this journal');
+  }
📝 Committable suggestion

‼️ 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.

Suggested change
const editor = await prisma.journalEditor.findFirst({
where: { userId: payload.editorId, journalId: submission.journalId },
include: {
user: true,
},
});
const editor = await prisma.journalEditor.findFirst({
where: { userId: payload.editorId, journalId: submission.journalId },
include: {
user: true,
},
});
if (!editor) {
throw new NotFoundError('Editor record not found for this journal');
}
🤖 Prompt for AI Agents
In desci-server/src/services/journals/JournalSubmissionService.ts around lines
215 to 221, the code fetches an editor record that may be null if the editorId
does not correspond to an editor of the journal. Add an explicit check after the
prisma query to verify if editor is null, and if so, throw an error or return
early to fail fast before any notification or email logic that uses editor.user
is executed.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
desci-server/src/services/journals/JournalManagementService.ts (1)

146-161: Consider using a cleaner type definition approach.

The intersection type approach works but could be simplified for better maintainability.

Consider this cleaner approach:

-}> & {
-  editors: Array<
-    Prisma.JournalEditorGetPayload<{
-      include: {
-        user: {
-          select: {
-            id: true;
-            name: true;
-            email: true;
-            orcid: true;
-          };
-        };
-      };
-    }> & { currentWorkload: number }
-  >;
-};
+}>;

+export type JournalDetailsWithWorkload = JournalDetails & {
+  editors: Array<JournalDetails['editors'][0] & { currentWorkload: number }>;
+};

Then update the function return type accordingly.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between d54bfcd and 91ea185.

📒 Files selected for processing (1)
  • desci-server/src/services/journals/JournalManagementService.ts (3 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build-and-test
🔇 Additional comments (1)
desci-server/src/services/journals/JournalManagementService.ts (1)

8-8: LGTM! Import addition is appropriate.

The SubmissionStatus import is correctly added and used in the workload calculation logic.

@kadamidev kadamidev merged commit 69e8fe7 into develop Jun 20, 2025
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