Skip to content

feat: Add typed API contract and explanation payloads for recommendat…#554

Merged
OlufunbiIK merged 1 commit into
OlufunbiIK:mainfrom
od-hunter:feature/recommendations-typed-api-contract
Apr 27, 2026
Merged

feat: Add typed API contract and explanation payloads for recommendat…#554
OlufunbiIK merged 1 commit into
OlufunbiIK:mainfrom
od-hunter:feature/recommendations-typed-api-contract

Conversation

@od-hunter
Copy link
Copy Markdown
Contributor

@od-hunter od-hunter commented Apr 27, 2026

Closes #495

…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]
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 27, 2026

@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.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 27, 2026

📝 Walkthrough

Walkthrough

This 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

Cohort / File(s) Summary
DTO Definitions
backend/src/recommendations/dto/recommendation-response.dto.ts
Introduces new NestJS/Swagger DTO classes: RecommendationExplanationDto (with source, reason, and 1–100 confidence bounds), TrackRecommendationDto, ArtistRecommendationDto, and response wrappers (TrackRecommendationsResponseDto, ArtistRecommendationsResponseDto) with recommendations arrays, total count, and timestamp.
Service Layer
backend/src/recommendations/recommendations.service.ts, backend/src/recommendations/recommendation-cache.service.ts
Service methods now return typed DTO wrappers and generate explanation metadata (source, reason, confidence) for each recommendation. Cache service refactored from untyped map to two strongly-typed generic maps (trackCache, artistCache) with updated method signatures.
Controller Layer
backend/src/recommendations/recommendations.controller.ts
Added explicit return types (Promise<TrackRecommendationsResponseDto>, Promise<ArtistRecommendationsResponseDto>) and Swagger @ApiResponse decorators for both GET endpoints.
Test Coverage
backend/src/recommendations/recommendations.controller.spec.ts, backend/src/recommendations/recommendations.service.spec.ts
New comprehensive controller unit tests verifying DTO response shapes, explanation fields, and confidence bounds (1–100). Expanded service tests with cache injection, typed assertions, and explanation metadata validation across recommendation sources.
Documentation
FINAL_TEST_REPORT.md, IMPLEMENTATION_SUMMARY.md, TEST_EXECUTION_SUMMARY.md, VALIDATION_REPORT.md, backend/src/recommendations/TESTING_CHECKLIST.md
Records test execution results (16/16 Jest tests passing, 100% coverage), implementation contract details, validation status including two confidence-related bug fixes, breaking API response shape change (array → wrapped object), and manual testing/deployment instructions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 A fuzzy tale of types so clear,
Explanations now appear!
Confidence bounds and sources bright,
DTOs wrapped, responses typed right.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title is partially related to the changeset. It accurately captures the main feature (typed API contract and explanation payloads for recommendations), but is truncated mid-word, making it incomplete and unclear. Complete the truncated title to fully describe the feature, e.g., 'feat: Add typed API contract and explanation payloads for recommendations service'.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed The PR fully satisfies issue #495 requirements: explicit DTOs created, typed controller responses implemented, explanation metadata with source/reason/confidence added, strongly typed payloads throughout, and comprehensive unit/controller tests provided (16 tests, 100% pass).
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #495 objectives. Core DTO/service/controller changes address typed contracts; cache service typing and tests support this work; documentation/checklists aid understanding but remain within scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown

@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: 3

🧹 Nitpick comments (10)
backend/src/recommendations/recommendations.controller.spec.ts (1)

199-224: Drop the as any cast by typing the mock against the entity.

The as any on Line 210 is hiding a real type mismatch: the entity may not declare userId (it's commonly derived/joined elsewhere). Worth importing RecommendationFeedback and typing mockFeedback against 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 tagged bash contain 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 a bash fence. Re-tagging as text (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 requested limit, returning truncated results.

When setTrackRecommendations was previously called with N items and a later request asks for limit > 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 with total (where total will 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 Map cache 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-manager with cache-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 the enum: array on the decorator (Line 9), the field type (Line 12), and in test files (e.g., recommendations.controller.spec.ts Line 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 typing generatedAt as ISO-8601 with format hints.

Optional: add format: 'date-time' to the @ApiProperty so OpenAPI consumers (codegen/clients) can deserialize it as a date instead of a plain string. Same applies to Line 173–177 in ArtistRecommendationsResponseDto.

♻️ 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, plus IMPLEMENTATION_SUMMARY.md and backend/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.md or docs/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: Tighten confidence: expect.any(Number) to assert the documented [1, 100] bound.

Optional: the controller spec asserts confidence is 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 the source parameter type in mapTrackRow to prevent silent fallthrough.

source: string accepts any value; if a future caller passes anything other than "popular" | "collaborative" | "content", the cast on Line 288 succeeds and buildExplanation will look up reasons[source] returning undefined, producing a DTO that violates its contract (no reason text). 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 a limit query (Line 27 of the controller); getArtistRecommendations does 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

📥 Commits

Reviewing files that changed from the base of the PR and between cc45920 and 85b9ea5.

📒 Files selected for processing (11)
  • FINAL_TEST_REPORT.md
  • IMPLEMENTATION_SUMMARY.md
  • TEST_EXECUTION_SUMMARY.md
  • VALIDATION_REPORT.md
  • backend/src/recommendations/TESTING_CHECKLIST.md
  • backend/src/recommendations/dto/recommendation-response.dto.ts
  • backend/src/recommendations/recommendation-cache.service.ts
  • backend/src/recommendations/recommendations.controller.spec.ts
  • backend/src/recommendations/recommendations.controller.ts
  • backend/src/recommendations/recommendations.service.spec.ts
  • backend/src/recommendations/recommendations.service.ts

Comment thread backend/src/recommendations/recommendations.service.ts
Comment thread backend/src/recommendations/recommendations.service.ts
Comment thread IMPLEMENTATION_SUMMARY.md
@OlufunbiIK OlufunbiIK merged commit bdda6cb into OlufunbiIK:main Apr 27, 2026
1 of 3 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.

Recommendations Typed API Contract and Explanation Payloads

2 participants