Skip to content

Feature/course categories - #55

Open
TheBigWealth89 wants to merge 10 commits into
Deen-Bridge:mainfrom
TheBigWealth89:feature/course-categories
Open

Feature/course categories#55
TheBigWealth89 wants to merge 10 commits into
Deen-Bridge:mainfrom
TheBigWealth89:feature/course-categories

Conversation

@TheBigWealth89

@TheBigWealth89 TheBigWealth89 commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Description

Resolves #42

This PR overhauls the category system by migrating from hardcoded string-based categories to a dedicated Category model. It establishes a robust taxonomy with automated caching, dynamic aggregation, and role-based access control, setting the foundation for dynamic content discovery.

Changes Made

1. Data Models & Taxonomies

  • Created the new Category model (src/models/Category.js) with name, slug, description, icon, and isActive fields.
  • Updated Course and Book models to include a categoryRef reference to the Category model, while keeping the string category field synced (via pre('save') hooks) for backward compatibility.
  • Implemented slugify logic in pre('save') hooks on the Category model to automatically format the category names into URL-friendly slugs.

2. Controllers & Routes

  • Implemented categoryController.js with CRUD endpoints:
    • GET /api/categories: Fetch all active categories with dynamic stats (course count and book count) using MongoDB aggregation ($lookup and $group).
    • GET /api/categories/:slug: Fetch a single category by slug.
    • POST /api/categories: Admin-only route to create a new category.
    • PUT /api/categories/:slug: Admin-only route to update an existing category.
    • DELETE /api/categories/:slug: Admin-only route to soft-delete or disable a category.
  • Created categoryRoutes.js and wired it into app.js under /api/categories.

3. Automated Caching & Invalidation

  • Applied Redis caching (cacheMiddleware) to the read routes (GET /api/categories, GET /api/categories/:slug) with a 1-hour TTL.
  • Updated invalidateCacheMiddleware on related mutating endpoints (Course/Book creation/updates) to automatically bust the Category caches and keep the dynamic course/book counters accurate.

4. Migration & Seeding Scripts

  • src/scripts/seedCategories.js: Seed script to initialize the core disciplines (Qur'an, Hadith, Fiqh, Aqeedah, etc.) into the database. (Idempotent and safe to run multiple times).
  • src/scripts/migrateCategories.js: Data migration script to scan all existing Courses/Books, canonicalize their legacy string categories, create corresponding Category documents, and backfill the new categoryRef ObjectIds.

5. Documentation

  • Updated README.md to include instructions for running npm run seed:categories.

Testing

  • Verified Category model creation and slugification via unit tests.
  • Tested Category CRUD endpoints with admin and student roles to ensure RBAC is enforced.
  • Validated aggregation pipelines accurately return the number of courses and books per category.
  • Confirmed Redis cache is successfully set on GET and correctly busted on POST/PUT/DELETE mutations.
  • Ensured migrateCategories.js backfills existing data smoothly and is completely idempotent.

Deployment Notes

Before launching this feature, you must run the following script in the production environment to backfill legacy data:

npm run seed:categories


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

* **New Features**
  * Added category browsing with statistics, slug-based filtering, and admin CRUD.
  * Added Stellar payment quoting and path-payment support with slippage handling.
  * Introduced durable background jobs for OTP emails, payment verification, and receipt generation, plus an authenticated jobs dashboard and dead-job listings.

* **Bug Fixes**
  * Improved OTP/receipt email configuration validation and error handling.
  * Enhanced handling of transient payment verification failures via retries.

* **Documentation**
  * Updated setup guides with new job queue environment variables and the `seed:categories` workflow.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR adds category taxonomy APIs and migration tooling, a Mongo-backed background job system with OTP and receipt handling, and Stellar path-payment quoting and construction. It also updates payment verification, caching, authorization, environment configuration, scripts, and integration coverage.

Changes

Category taxonomy

Layer / File(s) Summary
Category model and legacy linkage
src/models/Category.js, src/models/Course.js, src/models/Book.js, src/scripts/migrateCategories.js, src/scripts/seedCategories.js
Adds slugged categories, category references, denormalized names, migration backfills, and idempotent Islamic-discipline seeding.
Category API and course integration
src/controllers/categoryController.js, src/controllers/courses/courseController.js
Adds category statistics/detail endpoints, admin CRUD behavior, active-category validation, and course filtering by category slug.
Category routes, authorization, and caching
src/routes/categoryRoutes.js, src/routes/courses/courseRoutes.js, src/middlewares/authMiddleware.js, src/utils/cache.js, app.js
Mounts category routes, protects admin operations, and adds category cache keys and invalidation.
Category validation and endpoint coverage
test/category*.test.js, test/migrateCategories.test.js, test-phase5.js, test-phase6.js, start-mongo.js, package.json, README.md
Adds model, migration, API, authorization, filtering, statistics, and seed-script coverage.

Background job processing

Layer / File(s) Summary
Job persistence and execution
src/models/Job.js, src/jobs/queue.js
Adds inline and Mongo queue drivers with idempotency, retries, locking, polling, terminal states, and graceful shutdown.
Payment and email handlers
src/jobs/handlers.js, services/emails/sendMail.js
Registers OTP, verification, receipt, and EmailJS handlers, including receipt generation and payment side effects.
Controller enqueueing and lifecycle wiring
src/controllers/authController.js, src/controllers/stellar/donationController.js, src/controllers/stellar/paymentController.js, server.js
Moves OTP and payment follow-up work into queued jobs and wires worker startup and shutdown.
Dashboard, configuration, and queue tests
src/routes/jobsRoutes.js, .env.example, src/config/validateEnv.js, README.md, test/jobs.test.js, test/jobHandlers.test.js
Adds token-protected job dashboard endpoints, queue configuration, and behavior tests.

Stellar path payments

Layer / File(s) Summary
Quote and path-payment primitives
src/services/stellar/stellarService.js
Adds Horizon path discovery, slippage calculations, asset conversion, strict-receive transaction construction, and transient verification handling.
Payment quote and initialization API
src/controllers/stellar/paymentController.js, src/routes/stellar/paymentRoutes.js, src/models/Transaction.js
Adds the authenticated quote endpoint and stores path-payment fields while preserving direct-payment responses.
Path-payment conversion validation
test/pathPayments.test.js
Tests slippage, XDR operation fields, fee splitting, and stroop conversion consistency.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR also adds jobs, Stellar payment flows, email refactors, and auth changes that are unrelated to category taxonomy. Move the jobs, payments, email, and auth changes into separate PRs so this one stays focused on category taxonomy.
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is related to the change set, but it's too generic to describe the main work clearly. Rename it to something like 'Add category taxonomy and CRUD endpoints' so the primary change is obvious.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed The PR covers the category model, slugging, CRUD, filtering, migration, caching, seeding, and tests requested by #42.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 19

🧹 Nitpick comments (8)
src/routes/stellar/paymentRoutes.js (1)

7-7: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

New /quote endpoint has no direct Jest coverage.

test/pathPayments.test.js only unit-tests applySlippage/calculateFeeSplit/toStroops/fromStroops and manually rebuilds XDR ops — it never exercises this route/controller through an HTTP request (e.g. via supertest), so regressions in getQuote's validation, error paths, or response payload wouldn't be caught by CI.

As per path instructions, "New or changed endpoints need Jest coverage (the runner uses --experimental-vm-modules) and consistent response shapes."

Also applies to: 19-19

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/routes/stellar/paymentRoutes.js` at line 7, Add direct Jest HTTP coverage
for the `/quote` endpoint handled by `getQuote`, using the project’s existing
request-testing approach (such as supertest). Cover successful responses,
validation failures, and controller error paths, and assert the expected
response payload shape without relying on manually rebuilt XDR operations.

Source: Path instructions

test/pathPayments.test.js (1)

96-157: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the fee-splitting builder directly
test/pathPayments.test.js still builds the two pathPaymentStrictReceive ops by hand and re-derives the split math, so it only checks XDR serialization. Add a test for buildPathPaymentTransaction(...) (or switch this case to it) so bugs in the production split logic, destination selection, or asset ordering can’t slip through.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/pathPayments.test.js` around lines 96 - 157, Update the fee-split test
to invoke the production buildPathPaymentTransaction function instead of
manually constructing both pathPaymentStrictReceive operations and recomputing
split amounts. Keep assertions for two operations, destination PLATFORM_WALLET,
correct native/USDC asset ordering, and summed destination/send-max amounts so
the production split and transaction-building logic are exercised directly.
src/controllers/courses/courseController.js (1)

63-69: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

getCourses filter is inconsistent with the create/update/detail paths.

Two small mismatches versus the rest of the category logic:

  1. It resolves { slug: category } only, whereas createCourse/updateCourse accept an ObjectId or slug.
  2. It omits isActive: true, so a soft-deleted category's slug still returns its courses — while getCategoryBySlug correctly hides inactive categories.

Not a blocker, but aligning the filter avoids callers getting different results from the two entry points.

♻️ Suggested alignment
     if (category) {
-      const cat = await Category.findOne({ slug: category });
+      const cat = await Category.findOne({ slug: category, isActive: true });
       if (!cat) {
         return res.status(200).json({ success: true, courses: [] });
       }
       query.categoryRef = cat._id;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/controllers/courses/courseController.js` around lines 63 - 69, Update the
category lookup in getCourses to accept the category filter as either an
ObjectId or slug, matching createCourse and updateCourse, and require isActive:
true. Preserve the existing empty-course response when no active category
matches, then apply the resolved category reference to the course query.
src/controllers/categoryController.js (1)

85-85: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Response shape deviates from the documented { success, message, data } contract.

The category endpoints return ad-hoc shapes ({ success, categories }, { success, category, courses, pagination }, etc.). Per the repo convention these new endpoints should use a consistent { success, message, data } envelope so clients can parse responses uniformly. Not blocking, but worth aligning while the surface is new — feel free to fold the extra fields under data.

As per path instructions: "consistent response shapes ({ success, message, data })".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/controllers/categoryController.js` at line 85, Update the category
controller endpoint responses, including the response at the categories success
path, to consistently use the documented { success, message, data } envelope.
Move category, courses, pagination, and other endpoint-specific fields under
data, and provide an appropriate message while preserving the existing success
payload values.

Source: Path instructions

src/controllers/stellar/donationController.js (1)

196-220: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the new transient-retry (202) path.

This new branch is on the payment-integrity critical path (deciding whether a donation ends up retrying vs failed), but no test in this batch exercises it. Given the emphasis on payment correctness, consider adding an integration test asserting the 202 response, status: "retrying" persistence, and that verifyPaymentOnChain gets enqueued with the expected idempotency key.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/controllers/stellar/donationController.js` around lines 196 - 220, Add
integration coverage for the transient branch in the donation controller’s
verification flow, asserting the 202 response and persisted donation status are
"retrying". Also verify that verifyPaymentOnChain is enqueued with the donation
transaction ID and idempotency key `verify:${result.hash}`.

Source: Path instructions

test/jobs.test.js (1)

1-66: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Good dedup/retry/auth coverage; mongo-driver paths remain untested.

These tests only exercise the inline driver (dedup + retry) and dashboard auth rejection. The mongo-backed path (processNext, dead-lettering after maxAttempts, and the startup active→retrying requeue in startJobs) has no coverage in this batch — worth adding once mongo-memory-server tooling is available, given this is the production driver.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/jobs.test.js` around lines 1 - 66, Add tests covering the Mongo-backed
queue path, using mongo-memory-server when available: exercise processNext,
verify failed jobs are dead-lettered after maxAttempts, and verify startJobs
requeues startup jobs from active to retrying. Keep the existing inline
deduplication, retry, and dashboard-auth tests unchanged.
src/models/Job.js (1)

1-28: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

No retention/cleanup path for completed or dead jobs.

Schema/indexes look correct for the query patterns in queue.js (compound {status, runAt} index covers processNext, unique index covers idempotency lookups). One gap: there's no TTL or archival strategy for completed/dead documents, so this collection will grow unbounded in production. Consider a TTL index on completedAt (e.g., purge completed jobs after N days) or a periodic archival job — but keep dead jobs retained longer/indefinitely for debugging.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/models/Job.js` around lines 1 - 28, Implement a retention strategy for
completed jobs in the Job schema by adding a TTL index on completedAt with the
configured retention duration, while excluding dead jobs from automatic deletion
so they remain available for debugging. Use an appropriate partial filter or
equivalent index configuration to limit expiration to completed documents.

Source: Path instructions

src/routes/jobsRoutes.js (1)

13-20: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Consider a constant-time comparison for the dashboard token.

Nice touch gating the whole router and returning 404 when the token is unset. One hardening suggestion: comparing the bearer token with !== is not constant-time, so it can leak timing information about the secret. For an admin surface exposing job internals/errors, a constant-time compare is cheap insurance.

The hand-rolled escapeHtml at Lines 5-11 is actually complete and correct for this server-rendered table, so the static-analysis XSS hint there is a false positive — no change needed.

🔒 Constant-time token check
+import crypto from "crypto";
 router.use((req, res, next) => {
   const token = process.env.JOBS_DASHBOARD_TOKEN;
   if (!token) return res.status(404).json({ success: false, message: "Not found" });
-  if (req.headers.authorization !== `Bearer ${token}`) {
+  const provided = req.headers.authorization || "";
+  const expected = `Bearer ${token}`;
+  const providedBuf = Buffer.from(provided);
+  const expectedBuf = Buffer.from(expected);
+  if (
+    providedBuf.length !== expectedBuf.length ||
+    !crypto.timingSafeEqual(providedBuf, expectedBuf)
+  ) {
     return res.status(401).json({ success: false, message: "Unauthorized" });
   }
   next();
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/routes/jobsRoutes.js` around lines 13 - 20, Update the authorization
check in the router middleware to compare the supplied bearer token and
JOBS_DASHBOARD_TOKEN using a constant-time comparison, handling differing
lengths safely before comparison. Preserve the existing 404 response for an
unset token, 401 response for invalid credentials, and next() behavior for valid
credentials.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@package.json`:
- Line 9: The package scripts no longer provide the documented npm run seed
command. Update the package.json script configuration around seed:categories to
restore the seed alias, or consistently rename the command in README.md and
migration instructions while preserving the documented seeding workflow.

In `@services/emails/sendMail.js`:
- Around line 33-43: Update sendOtpEmail and sendReceiptEmail to stop logging
plaintext email addresses; log a non-identifying reference such as an available
userId or masked/hashed email instead, while preserving the existing email
delivery behavior and transaction-hash context.
- Line 6: Remove the fallback from EMAILJS_RECEIPT_TEMPLATE_ID so it cannot
reuse EMAILJS_TEMPLATE_ID; validate that the receipt template ID is configured
and fail loudly when it is missing. Also add the required
EMAILJS_RECEIPT_TEMPLATE_ID configuration entry to .env.example.

In `@src/controllers/authController.js`:
- Around line 126-131: Update registerUser to generate a fresh OTP for each
registration instead of using the module-level generatedOtp imported from
emailRoutes.js. Remove that dependency in this flow, create the OTP within
registerUser, and pass the per-registration value to the sendOtpEmail enqueue
payload and idempotencyKey.

In `@src/controllers/categoryController.js`:
- Around line 189-214: Update deleteCategory to count Book documents with the
same categoryRef in addition to Course documents, and soft-delete the category
whenever either count is greater than zero. Preserve the existing hard-delete
behavior only when neither courses nor books reference the category.

In `@src/jobs/handlers.js`:
- Around line 56-73: Move the transaction status update and initial save out of
the beginning of the purchase-confirmation flow in the relevant job handler, so
recordSaleEarnings and the purchasedBooks/purchasedCourses/enrolledUsers updates
execute first. Only after all purchase side effects succeed should the handler
set status to confirmed, clear failureReason, assign confirmedAt, and save;
preserve queueReceipt behavior and the existing idempotent retry flow.

In `@src/jobs/queue.js`:
- Around line 41-59: The inline path in enqueue silently ignores
options.session, so session-scoped jobs do not preserve transactional behavior.
Update enqueue to handle session-scoped calls explicitly: either defer
executeInline until the session transaction resolves, or reject them in inline
mode with a clear error; document the chosen limitation or behavior. Keep
non-session inline deduplication and execution unchanged.
- Around line 123-133: Update the startup recovery query in startJobs so it only
changes active jobs whose lockedAt timestamp is older than the configured or
established lock-timeout threshold. Preserve the existing retrying status, runAt
reset, and lockedAt removal for those stale jobs, while leaving fresh active
jobs owned by other workers untouched.
- Around line 61-77: Update the enqueue flow around Job.findOneAndUpdate to
enable includeResultMetadata while preserving new: true, then use the returned
metadata to set duplicate based on lastErrorObject.updatedExisting or the
absence of upserted rather than job.attemptsMade. Continue returning the
document’s _id as id and preserve the existing queued response.

In `@src/middlewares/authMiddleware.js`:
- Around line 46-50: Update the 403 response in the role check within
authMiddleware to include data: null alongside success and message, preserving
the required { success, message, data } response shape and existing
authorization behavior.

In `@src/models/Category.js`:
- Around line 69-84: Resolve the dead `isModified('name')` condition in the
`categorySchema` save hook by explicitly choosing the intended slug behavior:
either remove the name-change check and keep slugs immutable, or regenerate the
slug through `generateUniqueSlug` when `name` is modified while preserving the
current-category exclusion.

In `@src/routes/categoryRoutes.js`:
- Around line 22-24: Extend the existing category integration Jest suite to
cover PATCH /api/categories/:id and DELETE /api/categories/:id. Add authorized
and unauthorized cases, verify category update and soft-delete behavior, and
assert cache-visible responses after each operation while preserving the
existing POST coverage.

In `@src/routes/courses/courseRoutes.js`:
- Around line 2-3: Remove the duplicate ESM import declarations for
invalidateCacheMiddleware and CACHE_KEYS in courseRoutes.js, retaining a single
import for each binding so the module parses successfully.

In `@src/routes/stellar/paymentRoutes.js`:
- Line 19: Update the controller used by the /quote route, getQuote, to return
the standard { success, message, data } envelope, moving the quote payload from
the bespoke quote property into data and providing the required message while
preserving the existing quote contents.

In `@start-mongo.js`:
- Line 9: Update the start-mongo.js bootstrap to avoid overwriting the
developer’s existing .env by writing only to a dedicated test environment file
or refusing when .env exists. Remove the hardcoded JWT_SECRET value from the
script, read the test secret from an environment variable, and document that
variable in .env.example.

In `@test-phase5.js`:
- Around line 53-60: Update the course-update request in the test flow around
courseId and updateRes to use PUT instead of PATCH, matching the registered
/api/courses/:id route so the category update is actually exercised.
- Line 19: Remove the hardcoded JWT fallback secrets and require JWT_SECRET from
the environment, failing test setup when it is absent. Update test-phase5.js:19
and both fallback uses in test/category.integration.test.js:27-28 to use the
validated environment value; document the test-only variable in .env.example if
it is not already documented.

In `@test-phase6.js`:
- Around line 19-22: Remove the hardcoded JWT secret fallback from both
token-signing calls in test-phase6.js. Read JWT_SECRET from the environment
once, fail fast with a clear error when it is missing, and reuse that validated
value for adminToken and studentToken; do not modify the script’s logging or
test structure.

In `@test/category.integration.test.js`:
- Around line 18-22: Require MONGO_URI_TEST in test/category.integration.test.js
lines 18-22, test/category.test.js lines 5-15, and test-phase5.js lines 10-16;
validate that each URI targets an explicitly test-named database, connect using
it exclusively, and perform fixture deletion only after that validation.

---

Nitpick comments:
In `@src/controllers/categoryController.js`:
- Line 85: Update the category controller endpoint responses, including the
response at the categories success path, to consistently use the documented {
success, message, data } envelope. Move category, courses, pagination, and other
endpoint-specific fields under data, and provide an appropriate message while
preserving the existing success payload values.

In `@src/controllers/courses/courseController.js`:
- Around line 63-69: Update the category lookup in getCourses to accept the
category filter as either an ObjectId or slug, matching createCourse and
updateCourse, and require isActive: true. Preserve the existing empty-course
response when no active category matches, then apply the resolved category
reference to the course query.

In `@src/controllers/stellar/donationController.js`:
- Around line 196-220: Add integration coverage for the transient branch in the
donation controller’s verification flow, asserting the 202 response and
persisted donation status are "retrying". Also verify that verifyPaymentOnChain
is enqueued with the donation transaction ID and idempotency key
`verify:${result.hash}`.

In `@src/models/Job.js`:
- Around line 1-28: Implement a retention strategy for completed jobs in the Job
schema by adding a TTL index on completedAt with the configured retention
duration, while excluding dead jobs from automatic deletion so they remain
available for debugging. Use an appropriate partial filter or equivalent index
configuration to limit expiration to completed documents.

In `@src/routes/jobsRoutes.js`:
- Around line 13-20: Update the authorization check in the router middleware to
compare the supplied bearer token and JOBS_DASHBOARD_TOKEN using a constant-time
comparison, handling differing lengths safely before comparison. Preserve the
existing 404 response for an unset token, 401 response for invalid credentials,
and next() behavior for valid credentials.

In `@src/routes/stellar/paymentRoutes.js`:
- Line 7: Add direct Jest HTTP coverage for the `/quote` endpoint handled by
`getQuote`, using the project’s existing request-testing approach (such as
supertest). Cover successful responses, validation failures, and controller
error paths, and assert the expected response payload shape without relying on
manually rebuilt XDR operations.

In `@test/jobs.test.js`:
- Around line 1-66: Add tests covering the Mongo-backed queue path, using
mongo-memory-server when available: exercise processNext, verify failed jobs are
dead-lettered after maxAttempts, and verify startJobs requeues startup jobs from
active to retrying. Keep the existing inline deduplication, retry, and
dashboard-auth tests unchanged.

In `@test/pathPayments.test.js`:
- Around line 96-157: Update the fee-split test to invoke the production
buildPathPaymentTransaction function instead of manually constructing both
pathPaymentStrictReceive operations and recomputing split amounts. Keep
assertions for two operations, destination PLATFORM_WALLET, correct native/USDC
asset ordering, and summed destination/send-max amounts so the production split
and transaction-building logic are exercised directly.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f1d67c2d-a545-4243-ab1f-69c3bd095ef1

📥 Commits

Reviewing files that changed from the base of the PR and between aebaa00 and 69a88df.

📒 Files selected for processing (38)
  • .env.example
  • README.md
  • app.js
  • package.json
  • server.js
  • services/emails/sendMail.js
  • src/config/validateEnv.js
  • src/controllers/authController.js
  • src/controllers/categoryController.js
  • src/controllers/courses/courseController.js
  • src/controllers/stellar/donationController.js
  • src/controllers/stellar/paymentController.js
  • src/jobs/handlers.js
  • src/jobs/queue.js
  • src/middlewares/authMiddleware.js
  • src/models/Book.js
  • src/models/Category.js
  • src/models/Course.js
  • src/models/Job.js
  • src/models/Transaction.js
  • src/models/User.js
  • src/routes/categoryRoutes.js
  • src/routes/courses/courseRoutes.js
  • src/routes/jobsRoutes.js
  • src/routes/stellar/paymentRoutes.js
  • src/scripts/migrateCategories.js
  • src/scripts/seedCategories.js
  • src/services/stellar/stellarService.js
  • src/utils/cache.js
  • start-mongo.js
  • test-phase5.js
  • test-phase6.js
  • test/category.integration.test.js
  • test/category.test.js
  • test/jobHandlers.test.js
  • test/jobs.test.js
  • test/migrateCategories.test.js
  • test/pathPayments.test.js

Comment thread package.json
"start": "node server.js",
"dev": "nodemon server.js",
"seed": "node src/scripts/seedDatabase.js",
"seed:categories": "node src/scripts/seedCategories.js",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve or update the documented seed command.

Replacing seed breaks the npm run seed command still documented in README.md. Restore the old alias or update the documentation and migration instructions together.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` at line 9, The package scripts no longer provide the documented
npm run seed command. Update the package.json script configuration around
seed:categories to restore the seed alias, or consistently rename the command in
README.md and migration instructions while preserving the documented seeding
workflow.

const EMAILJS_PUBLIC_KEY = process.env.EMAILJS_PUBLIC_KEY || "p_hFoYPb6o0w7206-";
const EMAILJS_SERVICE_ID = process.env.EMAILJS_SERVICE_ID;
const EMAILJS_TEMPLATE_ID = process.env.EMAILJS_TEMPLATE_ID;
const EMAILJS_RECEIPT_TEMPLATE_ID = process.env.EMAILJS_RECEIPT_TEMPLATE_ID || EMAILJS_TEMPLATE_ID;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Silent fallback to the OTP template for receipts risks sending the wrong email content.

If EMAILJS_RECEIPT_TEMPLATE_ID is unset, EMAILJS_RECEIPT_TEMPLATE_ID becomes EMAILJS_TEMPLATE_ID (the OTP template). sendTemplate's guard only checks that a templateId is truthy, so this passes validation and silently sends receipt data through the OTP template — likely rendering garbage or the wrong message to the donor. Prefer failing loudly (throw if EMAILJS_RECEIPT_TEMPLATE_ID is missing) over reusing an unrelated template. As per path instructions, this configuration belongs in .env.example.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/emails/sendMail.js` at line 6, Remove the fallback from
EMAILJS_RECEIPT_TEMPLATE_ID so it cannot reuse EMAILJS_TEMPLATE_ID; validate
that the receipt template ID is configured and fail loudly when it is missing.
Also add the required EMAILJS_RECEIPT_TEMPLATE_ID configuration entry to
.env.example.

Source: Path instructions

Comment on lines +33 to +43
export const sendOtpEmail = async (otp, email) => {
if (!email || !otp) throw new Error("Email and OTP are required to send the email.");
logger.info(`Sending OTP email to: ${email}`);
return sendTemplate(EMAILJS_TEMPLATE_ID, email, { otp });
};

export const sendReceiptEmail = async (receipt) => {
if (!receipt.email || !receipt.txHash) throw new Error("Receipt email and transaction hash are required");
logger.info(`Sending receipt for ${receipt.txHash} to: ${receipt.email}`);
return sendTemplate(EMAILJS_RECEIPT_TEMPLATE_ID, receipt.email, receipt);
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Avoid logging raw email addresses.

Sending OTP email to: ${email} and Sending receipt for ${receipt.txHash} to: ${receipt.email} write plaintext user emails to logs. Static analysis flags this too. Prefer logging a non-identifying reference (e.g., userId, or a masked/hashed email) to avoid PII retention in log stores.

🛡 Proposed fix
-  logger.info(`Sending OTP email to: ${email}`);
+  logger.info("Sending OTP email");
...
-  logger.info(`Sending receipt for ${receipt.txHash} to: ${receipt.email}`);
+  logger.info(`Sending receipt for ${receipt.txHash}`);
📝 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
export const sendOtpEmail = async (otp, email) => {
if (!email || !otp) throw new Error("Email and OTP are required to send the email.");
logger.info(`Sending OTP email to: ${email}`);
return sendTemplate(EMAILJS_TEMPLATE_ID, email, { otp });
};
export const sendReceiptEmail = async (receipt) => {
if (!receipt.email || !receipt.txHash) throw new Error("Receipt email and transaction hash are required");
logger.info(`Sending receipt for ${receipt.txHash} to: ${receipt.email}`);
return sendTemplate(EMAILJS_RECEIPT_TEMPLATE_ID, receipt.email, receipt);
};
export const sendOtpEmail = async (otp, email) => {
if (!email || !otp) throw new Error("Email and OTP are required to send the email.");
logger.info("Sending OTP email");
return sendTemplate(EMAILJS_TEMPLATE_ID, email, { otp });
};
export const sendReceiptEmail = async (receipt) => {
if (!receipt.email || !receipt.txHash) throw new Error("Receipt email and transaction hash are required");
logger.info(`Sending receipt for ${receipt.txHash}`);
return sendTemplate(EMAILJS_RECEIPT_TEMPLATE_ID, receipt.email, receipt);
};
🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 34-34: Avoid logging sensitive data
Context: logger.info(Sending OTP email to: ${email})
Note: [CWE-532] Insertion of Sensitive Information into Log File.

(log-sensitive-data)


[warning] 40-40: Avoid logging sensitive data
Context: logger.info(Sending receipt for ${receipt.txHash} to: ${receipt.email})
Note: [CWE-532] Insertion of Sensitive Information into Log File.

(log-sensitive-data)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/emails/sendMail.js` around lines 33 - 43, Update sendOtpEmail and
sendReceiptEmail to stop logging plaintext email addresses; log a
non-identifying reference such as an available userId or masked/hashed email
instead, while preserving the existing email delivery behavior and
transaction-hash context.

Source: Linters/SAST tools

Comment on lines +126 to +131
await enqueue(
"sendOtpEmail",
{ userId: user._id.toString(), otp: generatedOtp },
{ attempts: 5, backoffMs: 1000, idempotencyKey: `otp:${user._id}:${generatedOtp}` }
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether generatedOtp is defined anywhere in authController.js or delegated to a User model method.
rg -n 'generatedOtp' src/controllers/authController.js
rg -n -A5 -B5 'generateOtp|randomInt|otp' src/models/User.js

Repository: Deen-Bridge/dnb-backend

Length of output: 372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- authController.js outline ---'
ast-grep outline src/controllers/authController.js --view expanded || true

echo '--- emailRoutes.js outline ---'
ast-grep outline src/routes/emailRoutes.js --view expanded || true

echo '--- authController.js relevant lines ---'
sed -n '1,180p' src/controllers/authController.js | cat -n

echo '--- emailRoutes.js relevant lines ---'
sed -n '1,220p' src/routes/emailRoutes.js | cat -n

Repository: Deen-Bridge/dnb-backend

Length of output: 9386


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
for path in ["src/controllers/authController.js","src/routes/emailRoutes.js"]:
    p=Path(path)
    print(f"\n## {path} ({sum(1 for _ in p.open())} lines)")
PY

Repository: Deen-Bridge/dnb-backend

Length of output: 252


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## authController.js\n'
sed -n '1,170p' src/controllers/authController.js

printf '\n## emailRoutes.js\n'
sed -n '1,220p' src/routes/emailRoutes.js

Repository: Deen-Bridge/dnb-backend

Length of output: 6517


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- search OTP usage ---'
rg -n "generatedOtp|otp" src | sed -n '1,200p'

echo
echo '--- emailRoutes.js with line numbers ---'
cat -n src/routes/emailRoutes.js

echo
echo '--- authController registerUser region ---'
sed -n '103,135p' src/controllers/authController.js | cat -n

Repository: Deen-Bridge/dnb-backend

Length of output: 3870


Generate a per-registration OTP here. generatedOtp is imported as a module-level constant from src/routes/emailRoutes.js, so every signup reuses the same code until the process restarts. Move OTP creation into registerUser and pass that fresh value to enqueue().

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 131-131: Avoid logging sensitive data
Context: logger.info(✅ User registered successfully: ${email} (ID: ${user._id}))
Note: [CWE-532] Insertion of Sensitive Information into Log File.

(log-sensitive-data)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/controllers/authController.js` around lines 126 - 131, Update
registerUser to generate a fresh OTP for each registration instead of using the
module-level generatedOtp imported from emailRoutes.js. Remove that dependency
in this flow, create the OTP within registerUser, and pass the per-registration
value to the sendOtpEmail enqueue payload and idempotencyKey.

Comment on lines +189 to +214
export const deleteCategory = catchAsync(async (req, res) => {
const categoryId = req.params.id;
const category = await Category.findById(categoryId);

if (!category) {
return res.status(404).json({ success: false, message: 'Category not found' });
}

const coursesCount = await Course.countDocuments({ categoryRef: categoryId });

if (coursesCount > 0) {
category.isActive = false;
await category.save();
return res.status(200).json({
success: true,
message: 'Category soft-deleted because it has associated courses',
category
});
} else {
await Category.deleteOne({ _id: categoryId });
return res.status(200).json({
success: true,
message: 'Category hard-deleted because it has no associated courses'
});
}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Soft-delete guard ignores Books, so book-only categories get hard-deleted and orphan their categoryRef.

The intent (per issue #42) is soft deletion whenever a category is referenced. Right now deleteCategory only counts Course documents. Since Book also carries a categoryRef (see src/models/Book.js lines 14-17), a category referenced solely by books falls into the else branch and is hard-deleted — leaving those books pointing at a categoryRef that no longer exists.

Why it matters: those dangling references break any populate/lookup on books and quietly corrupt referential integrity that the migration just worked to establish.

🛡️ Proposed fix: count books too
+import Book from '../models/Book.js';
+
 export const deleteCategory = catchAsync(async (req, res) => {
   const categoryId = req.params.id;
   const category = await Category.findById(categoryId);

   if (!category) {
     return res.status(404).json({ success: false, message: 'Category not found' });
   }

-  const coursesCount = await Course.countDocuments({ categoryRef: categoryId });
+  const [coursesCount, booksCount] = await Promise.all([
+    Course.countDocuments({ categoryRef: categoryId }),
+    Book.countDocuments({ categoryRef: categoryId }),
+  ]);

-  if (coursesCount > 0) {
+  if (coursesCount + booksCount > 0) {
     category.isActive = false;
     await category.save();
     return res.status(200).json({
       success: true,
-      message: 'Category soft-deleted because it has associated courses',
+      message: 'Category soft-deleted because it has associated courses or books',
       category
     });
   } else {
📝 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
export const deleteCategory = catchAsync(async (req, res) => {
const categoryId = req.params.id;
const category = await Category.findById(categoryId);
if (!category) {
return res.status(404).json({ success: false, message: 'Category not found' });
}
const coursesCount = await Course.countDocuments({ categoryRef: categoryId });
if (coursesCount > 0) {
category.isActive = false;
await category.save();
return res.status(200).json({
success: true,
message: 'Category soft-deleted because it has associated courses',
category
});
} else {
await Category.deleteOne({ _id: categoryId });
return res.status(200).json({
success: true,
message: 'Category hard-deleted because it has no associated courses'
});
}
});
import Book from '../models/Book.js';
export const deleteCategory = catchAsync(async (req, res) => {
const categoryId = req.params.id;
const category = await Category.findById(categoryId);
if (!category) {
return res.status(404).json({ success: false, message: 'Category not found' });
}
const [coursesCount, booksCount] = await Promise.all([
Course.countDocuments({ categoryRef: categoryId }),
Book.countDocuments({ categoryRef: categoryId }),
]);
if (coursesCount + booksCount > 0) {
category.isActive = false;
await category.save();
return res.status(200).json({
success: true,
message: 'Category soft-deleted because it has associated courses or books',
category
});
} else {
await Category.deleteOne({ _id: categoryId });
return res.status(200).json({
success: true,
message: 'Category hard-deleted because it has no associated courses'
});
}
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/controllers/categoryController.js` around lines 189 - 214, Update
deleteCategory to count Book documents with the same categoryRef in addition to
Course documents, and soft-delete the category whenever either count is greater
than zero. Preserve the existing hard-delete behavior only when neither courses
nor books reference the category.

Comment thread start-mongo.js
const uri = mongod.getUri();

// Write to .env
fs.writeFileSync('.env', `MONGO_URI=${uri}\nJWT_SECRET=supersecret\nNODE_ENV=test\nPORT=5000\n`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Unconditionally overwriting .env will clobber a developer's real config, and the hardcoded JWT secret should be documented in .env.example.

Thanks for adding an easy in-memory Mongo bootstrap — really handy! Two things to fix before this lands:

  1. Destructive write: fs.writeFileSync('.env', ...) replaces the entire .env file. Anyone who runs this with an existing .env (real Mongo URI, live JWT secret, API keys) will silently lose it. Please write to a dedicated file (e.g. .env.test) or refuse to run if .env already exists.
  2. Hardcoded secret: JWT_SECRET=supersecret is a literal secret in source. Even for local/test use, configuration (including test secrets) should come from environment variables documented in .env.example rather than being baked into a committed script.

As per path instructions: "Flag hardcoded secrets, connection strings, JWT secrets, or wallet keys; all configuration belongs in environment variables documented in .env.example."

🛡️ Safer approach
-  // Write to .env
-  fs.writeFileSync('.env', `MONGO_URI=${uri}\nJWT_SECRET=supersecret\nNODE_ENV=test\nPORT=5000\n`);
+  // Write to a dedicated test env file so we never clobber a real .env
+  const jwtSecret = process.env.JWT_SECRET || 'test-only-secret';
+  fs.writeFileSync('.env.test', `MONGO_URI=${uri}\nJWT_SECRET=${jwtSecret}\nNODE_ENV=test\nPORT=5000\n`);
📝 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
fs.writeFileSync('.env', `MONGO_URI=${uri}\nJWT_SECRET=supersecret\nNODE_ENV=test\nPORT=5000\n`);
// Write to a dedicated test env file so we never clobber a real .env
const jwtSecret = process.env.JWT_SECRET || 'test-only-secret';
fs.writeFileSync('.env.test', `MONGO_URI=${uri}\nJWT_SECRET=${jwtSecret}\nNODE_ENV=test\nPORT=5000\n`);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@start-mongo.js` at line 9, Update the start-mongo.js bootstrap to avoid
overwriting the developer’s existing .env by writing only to a dedicated test
environment file or refusing when .env exists. Remove the hardcoded JWT_SECRET
value from the script, read the test secret from an environment variable, and
document that variable in .env.example.

Source: Path instructions

Comment thread test-phase5.js
await User.deleteMany({});

const user = await User.create({ name: 'Admin User', email: 'admin@phase5.com', password: 'password123', role: 'tutor' });
const token = jwt.sign({ userId: user._id, sessionId: '123' }, process.env.JWT_SECRET || "deenbridge-temp-secret-key-2024", { expiresIn: '1h' });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Remove hardcoded JWT fallback secrets.

These literals violate the configuration boundary and make tests silently use a known signing key when configuration is missing. Load a test-only JWT_SECRET from the environment and fail setup if it is absent.

  • test-phase5.js#L19-L19: remove the deenbridge-temp-secret-key-2024 fallback.
  • test/category.integration.test.js#L27-L28: remove both fallback uses.

As per path instructions, “all configuration belongs in environment variables documented in .env.example.”

📍 Affects 2 files
  • test-phase5.js#L19-L19 (this comment)
  • test/category.integration.test.js#L27-L28
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test-phase5.js` at line 19, Remove the hardcoded JWT fallback secrets and
require JWT_SECRET from the environment, failing test setup when it is absent.
Update test-phase5.js:19 and both fallback uses in
test/category.integration.test.js:27-28 to use the validated environment value;
document the test-only variable in .env.example if it is not already documented.

Source: Path instructions

Comment thread test-phase5.js
Comment on lines +53 to +60
// 4. Update course
const courseId = createRes.body.course._id;
const updateRes = await request(app)
.patch(`/api/courses/${courseId}`)
.set('Authorization', `Bearer ${token}`)
.send({
category: 'aqeedah' // Change category
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use the route’s PUT method for the update check.

Line 56 calls PATCH /api/courses/:id, but the registered route is PUT /api/courses/:id. This request currently returns 404 and the script only logs it, so the category-update flow is never exercised.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test-phase5.js` around lines 53 - 60, Update the course-update request in the
test flow around courseId and updateRes to use PUT instead of PATCH, matching
the registered /api/courses/:id route so the category update is actually
exercised.

Comment thread test-phase6.js
Comment on lines +19 to +22
const adminToken = jwt.sign({ userId: admin._id, sessionId: '123' }, process.env.JWT_SECRET || "deenbridge-temp-secret-key-2024", { expiresIn: '1h' });

const student = await User.create({ name: 'Student', email: 'student@test.com', password: 'password123', role: 'student' });
const studentToken = jwt.sign({ userId: student._id, sessionId: '456' }, process.env.JWT_SECRET || "deenbridge-temp-secret-key-2024", { expiresIn: '1h' });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Avoid hardcoding the JWT secret fallback in the test script.

The literal "deenbridge-temp-secret-key-2024" fallback appears here (lines 19 and 22). If JWT_SECRET isn't set, these tokens are signed with a secret baked into source — the same weak default that middleware falls back to. Please rely on process.env.JWT_SECRET (documented in .env.example) and fail fast if it's missing, rather than shipping the fallback in a committed file.

Also worth noting for CI health: this file uses console.log rather than assertions, so it won't fail the Jest runner on regressions — the assertion-based test/*.test.js files are the ones that actually protect these paths.

As per path instructions: "Flag hardcoded secrets, connection strings, JWT secrets, or wallet keys; all configuration belongs in environment variables documented in .env.example."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test-phase6.js` around lines 19 - 22, Remove the hardcoded JWT secret
fallback from both token-signing calls in test-phase6.js. Read JWT_SECRET from
the environment once, fail fast with a clear error when it is missing, and reuse
that validated value for adminToken and studentToken; do not modify the script’s
logging or test structure.

Source: Path instructions

Comment on lines +18 to +22
beforeAll(async () => {
await mongoose.connect(process.env.MONGO_URI);
await Category.deleteMany({});
await Course.deleteMany({});
await User.deleteMany({});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift

Isolate destructive tests from the application database.

These test paths connect to the normal MONGO_URI and then delete durable collections. Running Jest or the phase script against an incorrectly configured environment can erase real users, courses, and categories. Require a dedicated MONGO_URI_TEST, reject non-test database names, and use it exclusively in test setup.

  • test/category.integration.test.js#L18-L22: connect to and clear only the dedicated test database.
  • test/category.test.js#L5-L15: connect to and clear only the dedicated test database.
  • test-phase5.js#L10-L16: require the dedicated test database before deleting fixtures.
📍 Affects 3 files
  • test/category.integration.test.js#L18-L22 (this comment)
  • test/category.test.js#L5-L15
  • test-phase5.js#L10-L16
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/category.integration.test.js` around lines 18 - 22, Require
MONGO_URI_TEST in test/category.integration.test.js lines 18-22,
test/category.test.js lines 5-15, and test-phase5.js lines 10-16; validate that
each URI targets an explicitly test-named database, connect using it
exclusively, and perform fixture deletion only after that validation.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/config/validateEnv.js (2)

27-30: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Require JOBS_DASHBOARD_TOKEN for production.

Listing this variable as optional allows production to start without dashboard authentication configured; src/routes/jobsRoutes.js then returns 404, silently disabling the dashboard. Move it to production-required validation (while retaining test/dev flexibility) and validate that the configured token is sufficiently strong.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/config/validateEnv.js` around lines 27 - 30, Update the environment
validation in validateEnv to treat JOBS_DASHBOARD_TOKEN as required in
production while preserving its optional behavior for test and development
environments. Add production-only validation that the configured token meets the
project’s required strength criteria, and remove it from the optional-variable
list.

Source: Path instructions


38-45: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject unknown STELLAR_NETWORK values instead of defaulting them to testnet.

A typo such as mainent silently selects https://horizon-testnet.stellar.org, which can make production payment verification and path-payment flows use the wrong network. Accept only mainnet or testnet, and fail validation otherwise.

Proposed validation
 const network = process.env.STELLAR_NETWORK || "testnet";
+if (!["mainnet", "testnet"].includes(network)) {
+  throw new Error("STELLAR_NETWORK must be either mainnet or testnet");
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/config/validateEnv.js` around lines 38 - 45, Update the network
validation in validateEnv so STELLAR_NETWORK accepts only "mainnet" or
"testnet"; reject any other explicitly provided value instead of treating it as
testnet. Preserve the existing default to testnet when the variable is unset,
and only assign the corresponding HORIZON_URLS value after validation succeeds.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/config/validateEnv.js`:
- Around line 27-30: Update the environment validation in validateEnv to treat
JOBS_DASHBOARD_TOKEN as required in production while preserving its optional
behavior for test and development environments. Add production-only validation
that the configured token meets the project’s required strength criteria, and
remove it from the optional-variable list.
- Around line 38-45: Update the network validation in validateEnv so
STELLAR_NETWORK accepts only "mainnet" or "testnet"; reject any other explicitly
provided value instead of treating it as testnet. Preserve the existing default
to testnet when the variable is unset, and only assign the corresponding
HORIZON_URLS value after validation succeeds.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2de1e654-a1ad-4e1b-9e8b-410cc3334aef

📥 Commits

Reviewing files that changed from the base of the PR and between 69a88df and 08be6e3.

📒 Files selected for processing (3)
  • .env.example
  • src/config/validateEnv.js
  • src/services/stellar/stellarService.js
🚧 Files skipped from review as they are similar to previous changes (2)
  • .env.example
  • src/services/stellar/stellarService.js

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.

[Enhancement] Course category taxonomy: Category model, slugs, admin CRUD, per-category stats, and Islamic-discipline seed data

4 participants