feat: Add typed API contract and explanation payloads for recommendat…#554
Conversation
…ions - Create explicit DTOs for track and artist recommendations - Add RecommendationExplanationDto with source, reason, and confidence - Replace implicit 'any' types with strongly-typed responses - Implement confidence calculation with proper normalization - Add comprehensive unit tests (16 tests, 100% pass rate) - Update controller with typed responses and API decorators - Update cache service to use typed DTOs - Fix confidence calculation bug (proper score normalization) - Fix confidence range to 1-100 (was 0-100) Breaking Changes: - Response structure changed from array to wrapper object - Clients must access response.recommendations instead of array directly - 'source' field moved to 'explanation.source' Files Modified: - backend/src/recommendations/recommendations.service.ts - backend/src/recommendations/recommendations.controller.ts - backend/src/recommendations/recommendation-cache.service.ts - backend/src/recommendations/recommendations.service.spec.ts Files Created: - backend/src/recommendations/dto/recommendation-response.dto.ts - backend/src/recommendations/recommendations.controller.spec.ts - backend/src/recommendations/TESTING_CHECKLIST.md Test Results: - 16/16 unit tests passed - 100% type safety coverage - All acceptance criteria met Closes #[issue-number]
|
@od-hunter is attempting to deploy a commit to the olufunbiik's projects Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughThis PR implements a typed API contract for the recommendations service by introducing DTOs with explanation metadata, updating service/controller/cache layers to use strongly-typed responses, and adding comprehensive test coverage and documentation to validate the changes. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (10)
backend/src/recommendations/recommendations.controller.spec.ts (1)
199-224: Drop theas anycast by typing the mock against the entity.The
as anyon Line 210 is hiding a real type mismatch: the entity may not declareuserId(it's commonly derived/joined elsewhere). Worth importingRecommendationFeedbackand typingmockFeedbackagainst it so the test breaks loudly if the entity shape drifts.♻️ Proposed refactor
+import { RecommendationFeedback } from "./entities/recommendation-feedback.entity"; ... - const mockFeedback = { + const mockFeedback: Partial<RecommendationFeedback> = { id: "feedback-1", userId: "user-1", trackId: "track-1", feedback: "up" as const, createdAt: new Date(), updatedAt: new Date(), }; - service.recordFeedback.mockResolvedValue(mockFeedback as any); + service.recordFeedback.mockResolvedValue(mockFeedback as RecommendationFeedback);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/recommendations/recommendations.controller.spec.ts` around lines 199 - 224, The mockFeedback object is currently cast with "as any", hiding a type mismatch; import the RecommendationFeedback entity type and declare mockFeedback as RecommendationFeedback (or a Partial<RecommendationFeedback> if not all fields are present) so the test is strongly typed; update the test to remove "as any" and ensure the mocked shape matches RecommendationFeedback, and keep the service.recordFeedback mock (service.recordFeedback) and the controller.submitFeedback call unchanged so the test will fail if the entity shape drifts.backend/src/recommendations/TESTING_CHECKLIST.md (1)
25-73: Minor: code fences taggedbashcontain JSON pseudo-schemas, not shell commands.The blocks at Lines 25-49 and 52-73 mix a
# GET ...comment with a JSON-shaped response template inside abash fence. Re-tagging astext (or splitting the curl/HTTP call from the response shape) avoids confusing readers who copy/paste expecting runnable bash.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/recommendations/TESTING_CHECKLIST.md` around lines 25 - 73, The fenced blocks that document "GET /recommendations/tracks" and "GET /recommendations/artists" are tagged as ```bash but contain JSON response schemas; change the fence language to ```json or ```text (or split into two fences: one ```bash for the curl/HTTP example and a separate ```json for the response shape) so readers won't expect runnable shell commands when copying the JSON schema; update the two blocks surrounding the "GET /recommendations/tracks" and "GET /recommendations/artists" headings accordingly.backend/src/recommendations/recommendation-cache.service.ts (2)
28-45: Cached set may be smaller than requestedlimit, returning truncated results.When
setTrackRecommendationswas previously called with N items and a later request asks forlimit > N, this method returns the cached N items instead of refetching. This is pre-existing behavior, but worth confirming it's intentional now that the response is wrapped withtotal(wheretotalwill reflect the truncated cached count rather than actual DB total).Consider falling through to a fresh fetch when
entry.data.length < limit:♻️ Optional fix
- if (entry && entry.expiresAt > Date.now()) { + if (entry && entry.expiresAt > Date.now() && entry.data.length >= limit) { this.logger.debug(`Track recommendations cache hit for user ${userId}`); return entry.data.slice(0, limit); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/recommendations/recommendation-cache.service.ts` around lines 28 - 45, The cached result can be smaller than the requested limit and currently returns the smaller set; update getTrackRecommendations to treat a cached entry whose entry.data.length < limit as a cache miss (i.e., delete/ignore the cached entry and return null so caller will fetch fresh data) — modify logic in getTrackRecommendations around the trackCache entry check and expiration handling (refer to getTrackRecommendations, setTrackRecommendations, trackCache, and entry.expiresAt) to fall through to a fresh fetch when cached length < limit.
16-17: Operational note: in-memory Maps don't scale across instances.This is pre-existing, but flagging since the cache surface is now formalized via DTOs: an in-memory
Mapcache won't be shared across replicas, leading to inconsistent recommendations and uneven cache hit rates in a horizontally scaled deployment. There's also no max-size or LRU eviction, so the maps grow unboundedly with active users (entries are only purged on access via the TTL check).If/when the service is scaled out, consider migrating to a shared cache (e.g., Redis via
@nestjs/cache-managerwithcache-manager-redis-store).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/recommendations/recommendation-cache.service.ts` around lines 16 - 17, The current in-memory Maps (trackCache and artistCache holding CacheEntry<TrackRecommendationDto> and CacheEntry<ArtistRecommendationDto>) won't work in a multi-instance deployment and have no eviction; replace these Maps with a shared cache (inject Nest's CACHE_MANAGER or a CacheService backed by Redis via cache-manager-redis-store), store entries under consistent keys (same key scheme you currently use for trackCache/artistCache), set sensible TTLs and max-size/eviction policies (LRU) in the Redis/cache-manager configuration, and update methods that read/write the Map to use cache-manager.get/set/del while retaining the CacheEntry shape or map it to native cache values; remove the unbounded Map fields once all callsites (e.g., methods referencing trackCache and artistCache) are updated.backend/src/recommendations/dto/recommendation-response.dto.ts (2)
7-12: Extract the source literal type to a reusable constant/type alias.The
'popular' | 'collaborative' | 'content-based'union is duplicated in theenum:array on the decorator (Line 9), the field type (Line 12), and in test files (e.g.,recommendations.controller.spec.tsLine 228). Extracting it to a single source-of-truth avoids drift if a new source is added later.♻️ Proposed refactor
import { ApiProperty } from '@nestjs/swagger'; +export const RECOMMENDATION_SOURCES = ['popular', 'collaborative', 'content-based'] as const; +export type RecommendationSource = (typeof RECOMMENDATION_SOURCES)[number]; + /** * Explanation metadata for why a recommendation was made */ export class RecommendationExplanationDto { `@ApiProperty`({ description: 'Source of the recommendation', - enum: ['popular', 'collaborative', 'content-based'], + enum: RECOMMENDATION_SOURCES, example: 'collaborative', }) - source: 'popular' | 'collaborative' | 'content-based'; + source: RecommendationSource;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/recommendations/dto/recommendation-response.dto.ts` around lines 7 - 12, Extract the literal union into a single exported type and array constant and use them for the ApiProperty and the field: create an exported type alias (e.g., RecommendationSource) equal to 'popular'|'collaborative'|'content-based' and an exported const array (e.g., RECOMMENDATION_SOURCES as const) containing those strings, then replace the inline union on the source field and the enum array in the `@ApiProperty` with RecommendationSource and RECOMMENDATION_SOURCES respectively (update RecommendationResponseDto or the class containing the source property); also update tests (e.g., recommendations.controller.spec.ts) to import and use the new exported type/constant so all usages share the same source-of-truth.
150-154: Consider typinggeneratedAtas ISO-8601 with format hints.Optional: add
format: 'date-time'to the@ApiPropertyso OpenAPI consumers (codegen/clients) can deserialize it as a date instead of a plain string. Same applies to Line 173–177 inArtistRecommendationsResponseDto.♻️ Proposed refactor
`@ApiProperty`({ description: 'Timestamp when recommendations were generated', example: '2024-01-15T10:30:00Z', + format: 'date-time', }) generatedAt: string;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/recommendations/dto/recommendation-response.dto.ts` around lines 150 - 154, Add the OpenAPI date-time format to the generatedAt properties so codegen/clients treat them as ISO-8601 timestamps: update the `@ApiProperty` on the generatedAt field in RecommendationResponseDto (property name generatedAt) to include format: 'date-time' and do the same for the generatedAt property in ArtistRecommendationsResponseDto (class name ArtistRecommendationsResponseDto) so both Swagger schemas indicate an ISO-8601 date-time instead of a plain string.FINAL_TEST_REPORT.md (1)
1-364: Consolidate or relocate the multiple overlapping reports.This PR adds four overlapping markdown reports at the repository root:
FINAL_TEST_REPORT.md,TEST_EXECUTION_SUMMARY.md,VALIDATION_REPORT.md, plusIMPLEMENTATION_SUMMARY.mdandbackend/src/recommendations/TESTING_CHECKLIST.md. They duplicate the same test counts, sample responses, breaking-change notes, and bug-fix snippets.Recommendations:
- Consolidate into a single doc (e.g.,
backend/src/recommendations/README.mdordocs/recommendations.md) covering the contract, breaking changes, and testing notes that are useful for future maintainers.- Move transient/CI-style reports (test pass counts, durations, "PRODUCTION READY" verdicts, star ratings) into the PR description rather than committing them — they go stale immediately and add review noise.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@FINAL_TEST_REPORT.md` around lines 1 - 364, Multiple overlapping test/report files (FINAL_TEST_REPORT.md, TEST_EXECUTION_SUMMARY.md, VALIDATION_REPORT.md, IMPLEMENTATION_SUMMARY.md, backend/src/recommendations/TESTING_CHECKLIST.md) should be consolidated into a single canonical docs file and transient CI output removed from the repo; create or update one maintainer-facing document (suggested: backend/src/recommendations/README.md or docs/recommendations.md) that includes API contract, breaking-change notes, explanation of bug fixes and stable testing guidance, move ephemeral items (test pass counts, durations, "PRODUCTION READY" verdicts, star ratings) into the PR description or CI artifacts, delete the duplicate markdown files, and update any internal links or README references to point to the new consolidated file.backend/src/recommendations/recommendations.service.spec.ts (1)
73-82: Tightenconfidence: expect.any(Number)to assert the documented [1, 100] bound.Optional: the controller spec asserts
confidenceis within[1, 100], but the service spec only asserts it's a number. Since the confidence normalization fix is one of the headline changes, asserting the bound here too would catch regressions earlier (closer to where the math lives).♻️ Suggested tweak
expect(result.recommendations[0]).toMatchObject({ id: "track-1", title: "Popular Track", explanation: { source: "popular", reason: expect.any(String), - confidence: expect.any(Number), + confidence: expect.any(Number), }, }); + const c = result.recommendations[0].explanation.confidence; + expect(c).toBeGreaterThanOrEqual(1); + expect(c).toBeLessThanOrEqual(100);Also applies to: 191-196
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/recommendations/recommendations.service.spec.ts` around lines 73 - 82, Tighten the confidence assertion in recommendations.service.spec.ts by replacing the loose expect.any(Number) check with explicit bounds checks: after extracting the confidence from result.recommendations[0] (or from the matched recommendation in the second case around lines 191-196), add assertions that confidence is >= 1 and <= 100 (e.g., expect(confidence).toBeGreaterThanOrEqual(1); expect(confidence).toBeLessThanOrEqual(100)), keeping the rest of the toMatchObject for id/title/explanation intact so the service spec enforces the documented [1,100] normalization.backend/src/recommendations/recommendations.service.ts (2)
287-301: Tighten thesourceparameter type inmapTrackRowto prevent silent fallthrough.
source: stringaccepts any value; if a future caller passes anything other than"popular" | "collaborative" | "content", the cast on Line 288 succeeds andbuildExplanationwill look upreasons[source]returningundefined, producing a DTO that violates its contract (noreasontext). All current callers pass one of those three literals, so making this a union is essentially free.♻️ Proposed change
- private mapTrackRow(row: RecommendationTrackRow, source: string): TrackRecommendationDto { - const sourceType = source === 'content' ? 'content-based' : source as 'popular' | 'collaborative' | 'content-based'; - + private mapTrackRow( + row: RecommendationTrackRow, + source: 'popular' | 'collaborative' | 'content', + ): TrackRecommendationDto { + const sourceType: RecommendationExplanationDto['source'] = + source === 'content' ? 'content-based' : source; + return {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/recommendations/recommendations.service.ts` around lines 287 - 301, Change the mapTrackRow signature so the source parameter is a discriminated union ('popular' | 'collaborative' | 'content') instead of string to prevent invalid values; update the sourceType logic in mapTrackRow (still using source === 'content' ? 'content-based' : source) to reflect the narrowed type and ensure buildExplanation(sourceType, ...) only ever receives the expected keys, and update any callers that pass a string to use one of the three literal types; reference: mapTrackRow, buildExplanation, RecommendationTrackRow, TrackRecommendationDto.
93-96: Magic number for the artist top-N limit.
slice(0, 10)hardcodes the artist result size. The track endpoint accepts alimitquery (Line 27 of the controller);getArtistRecommendationsdoes not, and the constant is buried in the middle of the pipeline. Promoting this to a named constant (or controller-supplied parameter) would make it easier to discover and tune later.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/recommendations/recommendations.service.ts` around lines 93 - 96, The artist result size is hardcoded with slice(0, 10); update getArtistRecommendations to accept a configurable limit (or use a named constant) and replace slice(0, 10) with slice(0, limit). For example, add a limit parameter with a default (e.g., limit = DEFAULT_TOP_ARTISTS) or introduce a DEFAULT_TOP_ARTISTS const near the top of recommendations.service.ts, then use that variable in the pipeline and ensure callers (the controller endpoint) pass the controller's limit query parameter through to getArtistRecommendations; keep the call to this.buildArtistRecommendation unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@backend/src/recommendations/recommendations.service.ts`:
- Around line 43-46: The response builder currently stamps generatedAt with new
Date() even on cache hits, which misrepresents freshness; change the flow so
cached payloads preserve their original generatedAt instead of being re-stamped:
when you call cacheService.getTrackRecommendations/getArtistRecommendations and
get a hit, read generatedAt from the cached object and pass it into
buildTrackRecommendationsResponse/buildArtistRecommendationsResponse (or update
those functions to accept an optional generatedAt parameter or to use
generatedAt from the payload if present); ensure the code that writes to the
cache stores the original generatedAt alongside the recommendation data (and
apply the same change to the other occurrence around lines 335-352).
- Around line 326-333: buildArtistRecommendation currently hardcodes
explanation.source to 'content-based' and uses artist.score normalized against a
fixed max, which misattributes source and saturates confidence; change the
caller (getArtistRecommendations) to compute a dominant source for each artist
by inspecting contributing tracks' track.explanation.source (e.g., majority or
highest cumulative score per source) and pass that dominantSource into
buildArtistRecommendation, then modify buildArtistRecommendation to call
buildExplanation with that source instead of the hardcoded 'content-based' and
adjust the confidence normalization to be per-artist (e.g., divide artist.score
by trackCount * maxExpectedScore or by the observed max artist score in the
current result set) so confidence varies meaningfully.
In `@IMPLEMENTATION_SUMMARY.md`:
- Around line 13-15: Documentation currently claims confidence is 0-100 and
shows formula min(round((score / 100) * 100), 100), but the code
(RecommendationExplanationDto with minimum: 1, maximum: 100 and the service
using Math.max(1, Math.min(confidence, 100))) normalizes against a
source-dependent expected max (20 for "collaborative", 50 otherwise); update the
docs to state the confidence range is 1–100 and replace the formula with:
confidence = clamp(round((score / expectedMax) * 100), 1, 100) where expectedMax
= 20 for collaborative recommendations and 50 for other sources, and make the
same text changes wherever the old 0-100 range and incorrect formula appear.
---
Nitpick comments:
In `@backend/src/recommendations/dto/recommendation-response.dto.ts`:
- Around line 7-12: Extract the literal union into a single exported type and
array constant and use them for the ApiProperty and the field: create an
exported type alias (e.g., RecommendationSource) equal to
'popular'|'collaborative'|'content-based' and an exported const array (e.g.,
RECOMMENDATION_SOURCES as const) containing those strings, then replace the
inline union on the source field and the enum array in the `@ApiProperty` with
RecommendationSource and RECOMMENDATION_SOURCES respectively (update
RecommendationResponseDto or the class containing the source property); also
update tests (e.g., recommendations.controller.spec.ts) to import and use the
new exported type/constant so all usages share the same source-of-truth.
- Around line 150-154: Add the OpenAPI date-time format to the generatedAt
properties so codegen/clients treat them as ISO-8601 timestamps: update the
`@ApiProperty` on the generatedAt field in RecommendationResponseDto (property
name generatedAt) to include format: 'date-time' and do the same for the
generatedAt property in ArtistRecommendationsResponseDto (class name
ArtistRecommendationsResponseDto) so both Swagger schemas indicate an ISO-8601
date-time instead of a plain string.
In `@backend/src/recommendations/recommendation-cache.service.ts`:
- Around line 28-45: The cached result can be smaller than the requested limit
and currently returns the smaller set; update getTrackRecommendations to treat a
cached entry whose entry.data.length < limit as a cache miss (i.e.,
delete/ignore the cached entry and return null so caller will fetch fresh data)
— modify logic in getTrackRecommendations around the trackCache entry check and
expiration handling (refer to getTrackRecommendations, setTrackRecommendations,
trackCache, and entry.expiresAt) to fall through to a fresh fetch when cached
length < limit.
- Around line 16-17: The current in-memory Maps (trackCache and artistCache
holding CacheEntry<TrackRecommendationDto> and
CacheEntry<ArtistRecommendationDto>) won't work in a multi-instance deployment
and have no eviction; replace these Maps with a shared cache (inject Nest's
CACHE_MANAGER or a CacheService backed by Redis via cache-manager-redis-store),
store entries under consistent keys (same key scheme you currently use for
trackCache/artistCache), set sensible TTLs and max-size/eviction policies (LRU)
in the Redis/cache-manager configuration, and update methods that read/write the
Map to use cache-manager.get/set/del while retaining the CacheEntry shape or map
it to native cache values; remove the unbounded Map fields once all callsites
(e.g., methods referencing trackCache and artistCache) are updated.
In `@backend/src/recommendations/recommendations.controller.spec.ts`:
- Around line 199-224: The mockFeedback object is currently cast with "as any",
hiding a type mismatch; import the RecommendationFeedback entity type and
declare mockFeedback as RecommendationFeedback (or a
Partial<RecommendationFeedback> if not all fields are present) so the test is
strongly typed; update the test to remove "as any" and ensure the mocked shape
matches RecommendationFeedback, and keep the service.recordFeedback mock
(service.recordFeedback) and the controller.submitFeedback call unchanged so the
test will fail if the entity shape drifts.
In `@backend/src/recommendations/recommendations.service.spec.ts`:
- Around line 73-82: Tighten the confidence assertion in
recommendations.service.spec.ts by replacing the loose expect.any(Number) check
with explicit bounds checks: after extracting the confidence from
result.recommendations[0] (or from the matched recommendation in the second case
around lines 191-196), add assertions that confidence is >= 1 and <= 100 (e.g.,
expect(confidence).toBeGreaterThanOrEqual(1);
expect(confidence).toBeLessThanOrEqual(100)), keeping the rest of the
toMatchObject for id/title/explanation intact so the service spec enforces the
documented [1,100] normalization.
In `@backend/src/recommendations/recommendations.service.ts`:
- Around line 287-301: Change the mapTrackRow signature so the source parameter
is a discriminated union ('popular' | 'collaborative' | 'content') instead of
string to prevent invalid values; update the sourceType logic in mapTrackRow
(still using source === 'content' ? 'content-based' : source) to reflect the
narrowed type and ensure buildExplanation(sourceType, ...) only ever receives
the expected keys, and update any callers that pass a string to use one of the
three literal types; reference: mapTrackRow, buildExplanation,
RecommendationTrackRow, TrackRecommendationDto.
- Around line 93-96: The artist result size is hardcoded with slice(0, 10);
update getArtistRecommendations to accept a configurable limit (or use a named
constant) and replace slice(0, 10) with slice(0, limit). For example, add a
limit parameter with a default (e.g., limit = DEFAULT_TOP_ARTISTS) or introduce
a DEFAULT_TOP_ARTISTS const near the top of recommendations.service.ts, then use
that variable in the pipeline and ensure callers (the controller endpoint) pass
the controller's limit query parameter through to getArtistRecommendations; keep
the call to this.buildArtistRecommendation unchanged.
In `@backend/src/recommendations/TESTING_CHECKLIST.md`:
- Around line 25-73: The fenced blocks that document "GET
/recommendations/tracks" and "GET /recommendations/artists" are tagged as
```bash but contain JSON response schemas; change the fence language to ```json
or ```text (or split into two fences: one ```bash for the curl/HTTP example and
a separate ```json for the response shape) so readers won't expect runnable
shell commands when copying the JSON schema; update the two blocks surrounding
the "GET /recommendations/tracks" and "GET /recommendations/artists" headings
accordingly.
In `@FINAL_TEST_REPORT.md`:
- Around line 1-364: Multiple overlapping test/report files
(FINAL_TEST_REPORT.md, TEST_EXECUTION_SUMMARY.md, VALIDATION_REPORT.md,
IMPLEMENTATION_SUMMARY.md, backend/src/recommendations/TESTING_CHECKLIST.md)
should be consolidated into a single canonical docs file and transient CI output
removed from the repo; create or update one maintainer-facing document
(suggested: backend/src/recommendations/README.md or docs/recommendations.md)
that includes API contract, breaking-change notes, explanation of bug fixes and
stable testing guidance, move ephemeral items (test pass counts, durations,
"PRODUCTION READY" verdicts, star ratings) into the PR description or CI
artifacts, delete the duplicate markdown files, and update any internal links or
README references to point to the new consolidated file.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: bb3a146b-ce19-47ab-b955-759f90c3bd8f
📒 Files selected for processing (11)
FINAL_TEST_REPORT.mdIMPLEMENTATION_SUMMARY.mdTEST_EXECUTION_SUMMARY.mdVALIDATION_REPORT.mdbackend/src/recommendations/TESTING_CHECKLIST.mdbackend/src/recommendations/dto/recommendation-response.dto.tsbackend/src/recommendations/recommendation-cache.service.tsbackend/src/recommendations/recommendations.controller.spec.tsbackend/src/recommendations/recommendations.controller.tsbackend/src/recommendations/recommendations.service.spec.tsbackend/src/recommendations/recommendations.service.ts
Closes #495