Skip to content

feat: Add POST /api/v1/analysis/:offerId/enhanced — Paid Expert Analysis ("The Closer") - #39

Merged
Tambeej merged 5 commits into
mainfrom
code-pilot-backend-impl-20260507-141025
May 7, 2026
Merged

feat: Add POST /api/v1/analysis/:offerId/enhanced — Paid Expert Analysis ("The Closer")#39
Tambeej merged 5 commits into
mainfrom
code-pilot-backend-impl-20260507-141025

Conversation

@Tambeej

@Tambeej Tambeej commented May 7, 2026

Copy link
Copy Markdown
Owner

Overview

Implements "The Closer" — the paid expert analysis monetisation feature. Authenticated, paying users can now request a full AI-powered enhanced mortgage analysis report for any of their uploaded bank offers.


Completed Tasks

1. POST /api/v1/analysis/:offerId/enhanced endpoint

  • Registered in src/routes/analysis.js under the existing /api/v1/analysis router prefix.
  • Full middleware chain: protect → paidAccess → paidEndpointLimiter → validateOfferId → generateEnhancedReport.

2. paidAccess middleware (src/middleware/paidAccess.js)

  • Guards the endpoint by checking req.user.paidAnalyses === true.
  • Returns 403 Forbidden with a descriptive message if the user has not paid.
  • Must be placed after the protect middleware (which attaches req.user).

3. Fetch user's latest portfolio

  • Controller calls portfolioService.getUserPortfolio(userId) to retrieve the user's current portfolio model.
  • A null portfolio is handled gracefully — the report generation falls back to rule-based logic.

4. reportService.generateEnhancedReport(offerId, userId, offer, portfolio)

  • Builds a rate/payment comparison between the bank offer and the user's portfolio model (annuity formula).
  • Calls GPT-4o-mini via aiService.callGPT with a structured system + user prompt.
  • AI output is fully sanitised before use (field types, length limits, enum validation).
  • Falls back to a rule-based report if OpenAI is unavailable or returns invalid data.
  • Persists the result to offer.analysis.enhanced in Firestore via offerService.updateEnhancedAnalysis.

5. Store enhanced report in offer.analysis.enhanced

  • reportService calls updateEnhancedAnalysis(offerId, enhancedReport) to persist the report.
  • Storage errors are caught and logged without blocking the HTTP response.

6. Return full enhanced report data

  • 201 Created for newly generated reports.
  • 200 OK for cached reports (idempotent — avoids duplicate AI calls).
  • Response shape:
{
  "success": true,
  "message": "Enhanced report generated successfully",
  "data": {
    "tricks": [ { "nameHe", "nameEn", "descriptionHe", "descriptionEn", "applicability", "riskLevel", "potentialSavings" } ],
    "negotiationScript": "...",
    "insights": [ { "titleHe", "titleEn", "bodyHe", "bodyEn", "icon" } ],
    "comparison": { "rateDelta", "monthlySaving", "totalSaving", "loanAmount", "termYears", "bankRate", "portfolioRate", "trackComparison" },
    "generatedAt": "ISO 8601",
    "generatedBy": "ai | rule-based-fallback",
    "processingTimeMs": 1234
  }
}

Advanced Consultant Logic

Feature Implementation
Mortgage Tricks 3–5 strategies always including the Enticement Track (מסלול פיתיון) — take a high-interest track to lower others, then refinance
Negotiation Script Word-for-word Hebrew script with actual loan numbers, bank name, and market rate comparison
Strategic Insights 3–5 insights explaining the why behind each recommendation (e.g., matching tracks to expected future funds)

Security & Reliability

  • Authentication: protect middleware validates Firebase ID token.
  • Authorisation: paidAccess middleware enforces subscription gate.
  • Rate limiting: paidEndpointLimiter (5 req/min per user) prevents abuse.
  • Input validation: validateOfferId validates the :offerId path parameter.
  • Ownership check: Controller verifies the offer belongs to the requesting user (404 vs 403 distinction).
  • AI fallback: Rule-based report generated if OpenAI is unavailable.
  • Idempotency: Cached report returned if already generated (no duplicate AI calls).
  • PII protection: AI prompts contain only financial data; [שם] placeholder used for borrower name.

Files Changed

File Change
src/routes/analysis.js Added POST /:offerId/enhanced route with full middleware chain
src/controllers/analysisController.js New generateEnhancedReport controller + buildResponseData helper
src/services/reportService.js Full AI + fallback report generation, comparison builder, sanitisers
src/middleware/paidAccess.js Subscription gate middleware
__tests__/analysisEnhanced.test.js Integration tests for the new endpoint
__tests__/reportService.test.js Unit tests for report service
__tests__/paidAccess.test.js Unit tests for paidAccess middleware

Tambeej added 5 commits May 7, 2026 14:22
…ccess middleware

- Implement paidAccess middleware checking user.paidAnalyses flag
- Implement protect (auth) middleware with Firebase JWT verification
- Implement rateLimit middleware (5 req/min for paid endpoint)
- Implement analysisValidator with validateOfferId
- Implement offerService.findByIdAndUserId for ownership validation
- Implement portfolioService.getUserPortfolio for portfolio data
- Implement reportService.generateEnhancedReport with AI + fallback
- Implement analysisController.generateEnhancedReport controller
- Wire up analysis routes with full middleware chain
- Add comprehensive tests for the enhanced endpoint
…ysis

Implements task 2: fetch user's latest portfolio for use in the enhanced
analysis report generation flow.

- Create src/services/portfolioService.js with getUserPortfolio(userId)
  - Queries 'portfolios' Firestore collection for user's latest portfolio
    (ordered by updatedAt desc, falls back to createdAt desc)
  - Falls back to 'wizardInputs' collection to derive portfolio context
    from wizard submission data when no saved portfolio exists
  - Returns null gracefully when neither source has data
  - Includes computeAverageRate() helper for rate calculations
  - Full JSDoc documentation and structured logging
- Add __tests__/portfolioService.test.js with comprehensive unit tests
  covering all code paths (portfolio found, wizard fallback, null return,
  error handling, edge cases)
Task 3: Wire reportService.generateEnhancedReport(offerId, userId, offer, portfolio)
into the enhanced analysis controller flow.

- Controller calls reportService.generateEnhancedReport with all 4 required args:
  offerId, userId, offer (from offerService), portfolio (from portfolioService)
- reportService handles AI generation (GPT-4o-mini) with rule-based fallback
- Report is persisted to offer.analysis.enhanced via updateEnhancedAnalysis
- Controller returns 201 on new generation, 200 on cached report
- Added __tests__/reportServiceIntegration.test.js for task-3-specific coverage
- Enhance updateEnhancedAnalysis() in offerService.js to use a Firestore
  transaction with an idempotent "if not exists" guard, preventing
  concurrent requests from overwriting an already-stored enhanced report
- Restore full offerService.js (merging main branch CRUD functions with
  the new updateEnhancedAnalysis implementation)
- Add comprehensive unit tests for updateEnhancedAnalysis covering:
  - Stores report when analysis.enhanced does not exist
  - Skips write and returns existing report when already stored
  - Throws when offerId is missing
  - Handles Firestore transaction errors gracefully
- Update __tests__/offerService.test.js with updateEnhancedAnalysis tests
- Handle null return from findByIdAndUserId with proper NotFoundError/ForbiddenError
- Ensure full enhanced report data is returned with all required fields
- Add offer existence check before ownership check for correct error codes
- Validate enhanced report completeness before returning to client
- Return 201 for newly generated reports, 200 for cached reports
- Include all fields: tricks, negotiationScript, insights, comparison,
  generatedAt, generatedBy, processingTimeMs in response data
@Tambeej
Tambeej merged commit b2c7246 into main May 7, 2026
1 check failed
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.

1 participant