Compare commits
15 Commits
develop
...
72b7f11c41
| Author | SHA1 | Date | |
|---|---|---|---|
| 72b7f11c41 | |||
| ff09524503 | |||
| 9ec96d4af7 | |||
| 5d2965beae | |||
| 8a4e46d872 | |||
| 7ebf09c284 | |||
| 1a06d60f2d | |||
| 6773162426 | |||
| ecb599fba7 | |||
| 70d8a4786e | |||
| ee48afa5bd | |||
| 473bef96e9 | |||
| d3fb929443 | |||
| de8b36ae2b | |||
| 92a3b60878 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -220,5 +220,5 @@ local/
|
||||
*.swp
|
||||
|
||||
# Local file uploads
|
||||
src/TrueCV.Web/uploads/
|
||||
src/RealCV.Web/uploads/
|
||||
logs/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# TrueCV UK APIs & Integration Resources
|
||||
# RealCV UK APIs & Integration Resources
|
||||
|
||||
**Last Updated:** January 2026
|
||||
**Purpose:** Practical guide for obtaining API access and integration details
|
||||
@@ -159,13 +159,13 @@
|
||||
|
||||
### Overview
|
||||
- **Service:** UK company registration and officer records
|
||||
- **Status:** ✅ Already integrated in TrueCV
|
||||
- **Status:** ✅ Already integrated in RealCV
|
||||
- **Coverage:** 3.4M registered UK companies
|
||||
|
||||
### Enhancement Opportunities
|
||||
|
||||
#### Existing Implementation
|
||||
- See: `/src/TrueCV.Infrastructure/ExternalApis/CompaniesHouseClient.cs`
|
||||
- See: `/src/RealCV.Infrastructure/ExternalApis/CompaniesHouseClient.cs`
|
||||
- Current: Company search + basic data lookup
|
||||
- Rate Limit: 500 requests/hour (generous)
|
||||
|
||||
@@ -326,7 +326,7 @@
|
||||
- uCheck
|
||||
- Certn
|
||||
|
||||
### Recommended Vendor for TrueCV Integration
|
||||
### Recommended Vendor for RealCV Integration
|
||||
|
||||
**Verifile** (Suggested)
|
||||
- **Website:** https://www.verifile.io/
|
||||
@@ -345,7 +345,7 @@
|
||||
2. Negotiate revenue share (typically 20-30% for platform)
|
||||
3. Integrate DBS check submission API
|
||||
4. Build compliance/audit trail layer
|
||||
5. White-label DBS reports in TrueCV UI
|
||||
5. White-label DBS reports in RealCV UI
|
||||
|
||||
### Timeline
|
||||
- **Vendor selection:** 1-2 weeks
|
||||
@@ -371,7 +371,7 @@
|
||||
1. **Contact accredited vendors:** Verifile, DDC, or similar
|
||||
2. **Explain use case:** CV verification platform
|
||||
3. **Request sub-licensing:** Access to their HMRC integration
|
||||
4. **Build wrapper:** TrueCV UI calls vendor API
|
||||
4. **Build wrapper:** RealCV UI calls vendor API
|
||||
|
||||
#### Vendors with HMRC Access
|
||||
- Verifile (https://www.verifile.io/)
|
||||
@@ -407,7 +407,7 @@
|
||||
|
||||
### This Week
|
||||
1. **Email HEDD:** partnerships@hedd.ac.uk with:
|
||||
- Company info (TrueCV)
|
||||
- Company info (RealCV)
|
||||
- Use case (CV verification for UK recruiters)
|
||||
- Expected volume (start with 100/month)
|
||||
- Request: API access or partnership discussion
|
||||
@@ -468,15 +468,15 @@ For each API integration, ensure:
|
||||
## Contact Template for API Requests
|
||||
|
||||
```
|
||||
Subject: API Integration Request - TrueCV Recruitment Verification Platform
|
||||
Subject: API Integration Request - RealCV Recruitment Verification Platform
|
||||
|
||||
Dear [Service] Team,
|
||||
|
||||
We are developing TrueCV, a UK-focused CV verification platform for recruitment agencies and corporate HR departments. As part of our Phase 1 launch (Q1 2026), we would like to integrate with [Service Name] to verify [candidate credentials] in real-time during the hiring process.
|
||||
We are developing RealCV, a UK-focused CV verification platform for recruitment agencies and corporate HR departments. As part of our Phase 1 launch (Q1 2026), we would like to integrate with [Service Name] to verify [candidate credentials] in real-time during the hiring process.
|
||||
|
||||
Use Case:
|
||||
- Candidates upload CV during job application
|
||||
- TrueCV extracts education/qualification claims
|
||||
- RealCV extracts education/qualification claims
|
||||
- Real-time verification against [Service] records
|
||||
- Fraud flags generated for recruiter review
|
||||
|
||||
@@ -501,7 +501,7 @@ Please advise next steps.
|
||||
|
||||
Best regards,
|
||||
[Your Name]
|
||||
TrueCV
|
||||
RealCV
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
TRUECV UK MARKET STRATEGY - COMPLETE DELIVERY PACKAGE
|
||||
================================================================================
|
||||
|
||||
Project: Rethinking TrueCV Feature Priorities with UK-Only Focus
|
||||
Project: Rethinking RealCV Feature Priorities with UK-Only Focus
|
||||
Date Delivered: January 20, 2026
|
||||
Total Documents: 8 comprehensive strategy guides
|
||||
Total Content: ~200 pages
|
||||
@@ -68,7 +68,7 @@ MARKET OPPORTUNITY:
|
||||
✓ £4.2B annual cost of CV fraud to UK employers
|
||||
✓ 1 in 5 UK candidates falsify university degrees
|
||||
✓ 24% of screened CVs fail verification
|
||||
✓ £3.3M serviceable market for TrueCV
|
||||
✓ £3.3M serviceable market for RealCV
|
||||
✓ No existing competitor offers integrated UK CV verification
|
||||
|
||||
COMPETITIVE ADVANTAGE:
|
||||
@@ -136,7 +136,7 @@ EXPECTED OUTCOMES:
|
||||
COMPETITIVE LANDSCAPE ANALYSIS
|
||||
================================================================================
|
||||
|
||||
COMPETITOR FEATURES OFFERED TrueCV ADVANTAGE
|
||||
COMPETITOR FEATURES OFFERED RealCV ADVANTAGE
|
||||
─────────────────────────────────────────────────────────────
|
||||
Workable ATS + basic screening HEDD integration (exclusive)
|
||||
Deel Global hiring + screening UK-specific stack
|
||||
@@ -150,7 +150,7 @@ No existing competitor integrates:
|
||||
- GMC/NMC healthcare registers
|
||||
- Timeline fraud detection
|
||||
- Companies House director verification
|
||||
→ TrueCV is only player filling this gap
|
||||
→ RealCV is only player filling this gap
|
||||
|
||||
MOAT BUILDING:
|
||||
- Deep integrations difficult to replicate (6+ months each)
|
||||
@@ -365,17 +365,17 @@ FOR SALES/MARKETING:
|
||||
DOCUMENT LOCATIONS
|
||||
================================================================================
|
||||
|
||||
All files have been created in: /mnt/d/Git/TrueCV/
|
||||
All files have been created in: /mnt/d/Git/RealCV/
|
||||
|
||||
FILE STRUCTURE:
|
||||
/mnt/d/Git/TrueCV/QUICK_REFERENCE.md (Start here)
|
||||
/mnt/d/Git/TrueCV/EXECUTIVE_SUMMARY.md (Execs/investors)
|
||||
/mnt/d/Git/TrueCV/UK_FEATURE_PRIORITIZATION.md (Product)
|
||||
/mnt/d/Git/TrueCV/PHASE1_TECHNICAL_IMPLEMENTATION.md (Engineering)
|
||||
/mnt/d/Git/TrueCV/UK_MARKET_STRATEGY.md (Strategy/Sales/Marketing)
|
||||
/mnt/d/Git/TrueCV/API_RESOURCES_AND_CONTACTS.md (Implementation)
|
||||
/mnt/d/Git/TrueCV/README_UK_STRATEGY.md (Navigation)
|
||||
/mnt/d/Git/TrueCV/INDEX.md (Reference index)
|
||||
/mnt/d/Git/RealCV/QUICK_REFERENCE.md (Start here)
|
||||
/mnt/d/Git/RealCV/EXECUTIVE_SUMMARY.md (Execs/investors)
|
||||
/mnt/d/Git/RealCV/UK_FEATURE_PRIORITIZATION.md (Product)
|
||||
/mnt/d/Git/RealCV/PHASE1_TECHNICAL_IMPLEMENTATION.md (Engineering)
|
||||
/mnt/d/Git/RealCV/UK_MARKET_STRATEGY.md (Strategy/Sales/Marketing)
|
||||
/mnt/d/Git/RealCV/API_RESOURCES_AND_CONTACTS.md (Implementation)
|
||||
/mnt/d/Git/RealCV/README_UK_STRATEGY.md (Navigation)
|
||||
/mnt/d/Git/RealCV/INDEX.md (Reference index)
|
||||
|
||||
FILES READY FOR USE IMMEDIATELY.
|
||||
|
||||
@@ -445,7 +445,7 @@ PLANNED UPDATES:
|
||||
CLOSING NOTES
|
||||
================================================================================
|
||||
|
||||
This strategy document represents a comprehensive analysis of TrueCV's
|
||||
This strategy document represents a comprehensive analysis of RealCV's
|
||||
opportunity in the UK CV verification market. It provides:
|
||||
|
||||
✓ Clear market opportunity quantification (£3.3M addressable)
|
||||
|
||||
20
Dockerfile
20
Dockerfile
@@ -3,11 +3,11 @@ FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
WORKDIR /src
|
||||
|
||||
# Copy solution and project files first for better layer caching
|
||||
COPY TrueCV.sln ./
|
||||
COPY src/TrueCV.Domain/TrueCV.Domain.csproj src/TrueCV.Domain/
|
||||
COPY src/TrueCV.Application/TrueCV.Application.csproj src/TrueCV.Application/
|
||||
COPY src/TrueCV.Infrastructure/TrueCV.Infrastructure.csproj src/TrueCV.Infrastructure/
|
||||
COPY src/TrueCV.Web/TrueCV.Web.csproj src/TrueCV.Web/
|
||||
COPY RealCV.sln ./
|
||||
COPY src/RealCV.Domain/RealCV.Domain.csproj src/RealCV.Domain/
|
||||
COPY src/RealCV.Application/RealCV.Application.csproj src/RealCV.Application/
|
||||
COPY src/RealCV.Infrastructure/RealCV.Infrastructure.csproj src/RealCV.Infrastructure/
|
||||
COPY src/RealCV.Web/RealCV.Web.csproj src/RealCV.Web/
|
||||
|
||||
# Restore dependencies
|
||||
RUN dotnet restore
|
||||
@@ -16,7 +16,7 @@ RUN dotnet restore
|
||||
COPY src/ src/
|
||||
|
||||
# Build and publish
|
||||
WORKDIR /src/src/TrueCV.Web
|
||||
WORKDIR /src/src/RealCV.Web
|
||||
RUN dotnet publish -c Release -o /app/publish --no-restore
|
||||
|
||||
# Runtime stage
|
||||
@@ -27,16 +27,16 @@ WORKDIR /app
|
||||
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create non-root user for security
|
||||
RUN groupadd -r truecv && useradd -r -g truecv truecv
|
||||
RUN groupadd -r realcv && useradd -r -g realcv realcv
|
||||
|
||||
# Copy published app
|
||||
COPY --from=build /app/publish .
|
||||
|
||||
# Set ownership
|
||||
RUN chown -R truecv:truecv /app
|
||||
RUN chown -R realcv:realcv /app
|
||||
|
||||
# Switch to non-root user
|
||||
USER truecv
|
||||
USER realcv
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8080
|
||||
@@ -51,4 +51,4 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:8080/health || exit 1
|
||||
|
||||
# Start the app
|
||||
ENTRYPOINT ["dotnet", "TrueCV.Web.dll"]
|
||||
ENTRYPOINT ["dotnet", "RealCV.Web.dll"]
|
||||
|
||||
@@ -7,11 +7,11 @@ RUN dotnet tool install --global dotnet-ef
|
||||
ENV PATH="$PATH:/root/.dotnet/tools"
|
||||
|
||||
# Copy solution and project files
|
||||
COPY TrueCV.sln ./
|
||||
COPY src/TrueCV.Domain/TrueCV.Domain.csproj src/TrueCV.Domain/
|
||||
COPY src/TrueCV.Application/TrueCV.Application.csproj src/TrueCV.Application/
|
||||
COPY src/TrueCV.Infrastructure/TrueCV.Infrastructure.csproj src/TrueCV.Infrastructure/
|
||||
COPY src/TrueCV.Web/TrueCV.Web.csproj src/TrueCV.Web/
|
||||
COPY RealCV.sln ./
|
||||
COPY src/RealCV.Domain/RealCV.Domain.csproj src/RealCV.Domain/
|
||||
COPY src/RealCV.Application/RealCV.Application.csproj src/RealCV.Application/
|
||||
COPY src/RealCV.Infrastructure/RealCV.Infrastructure.csproj src/RealCV.Infrastructure/
|
||||
COPY src/RealCV.Web/RealCV.Web.csproj src/RealCV.Web/
|
||||
|
||||
# Restore dependencies
|
||||
RUN dotnet restore
|
||||
@@ -20,7 +20,7 @@ RUN dotnet restore
|
||||
COPY src/ src/
|
||||
|
||||
# Build the project
|
||||
RUN dotnet build src/TrueCV.Web/TrueCV.Web.csproj -c Release
|
||||
RUN dotnet build src/RealCV.Web/RealCV.Web.csproj -c Release
|
||||
|
||||
# Run migrations on startup
|
||||
ENTRYPOINT ["dotnet", "ef", "database", "update", "--project", "src/TrueCV.Infrastructure", "--startup-project", "src/TrueCV.Web", "--no-build", "-c", "Release"]
|
||||
ENTRYPOINT ["dotnet", "ef", "database", "update", "--project", "src/RealCV.Infrastructure", "--startup-project", "src/RealCV.Web", "--no-build", "-c", "Release"]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# TrueCV UK Market Opportunity - Executive Summary
|
||||
# RealCV UK Market Opportunity - Executive Summary
|
||||
|
||||
**Prepared for:** Product Leadership
|
||||
**Date:** January 2026
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
## The Opportunity
|
||||
|
||||
**UK CV fraud costs employers £4.2B annually. Current verification takes 5-10 days. TrueCV can do it in seconds.**
|
||||
**UK CV fraud costs employers £4.2B annually. Current verification takes 5-10 days. RealCV can do it in seconds.**
|
||||
|
||||
### Market Problem
|
||||
- **1 in 5 UK candidates** falsify university degrees
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
---
|
||||
|
||||
## TrueCV's Solution
|
||||
## RealCV's Solution
|
||||
|
||||
**Integrated CV verification platform leveraging UK-specific data sources:**
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
|
||||
## Competitive Advantage
|
||||
|
||||
**TrueCV is the ONLY CV verification tool that:**
|
||||
**RealCV is the ONLY CV verification tool that:**
|
||||
|
||||
1. ✅ Integrates with HEDD (no competitors do)
|
||||
2. ✅ Targets healthcare recruiting niche (GMC/NMC)
|
||||
@@ -59,7 +59,7 @@
|
||||
### Addressable Market
|
||||
- **18,300 potential customers** (recruitment agencies + corporate HR)
|
||||
- **£2.8B UK pre-employment screening market**
|
||||
- **~£3.3M serviceable opportunity** for TrueCV platform
|
||||
- **~£3.3M serviceable opportunity** for RealCV platform
|
||||
|
||||
### Year 1 Revenue Target
|
||||
- **50-75 paying customers** at £49-199/month
|
||||
@@ -230,7 +230,7 @@
|
||||
|
||||
## Contact & Next Steps
|
||||
|
||||
**Product Lead:** [Name] - TrueCV Product Strategy
|
||||
**Product Lead:** [Name] - RealCV Product Strategy
|
||||
**Engineering Lead:** [Name] - Phase 1 Technical Implementation
|
||||
|
||||
**Next Meeting:** [Date] - Review technical implementation plan + finalize go-to-market
|
||||
|
||||
24
INDEX.md
24
INDEX.md
@@ -1,4 +1,4 @@
|
||||
# TrueCV UK Strategy - Complete Document Index
|
||||
# RealCV UK Strategy - Complete Document Index
|
||||
|
||||
**Total Documents:** 6 comprehensive strategy guides
|
||||
**Total Pages:** ~200 pages
|
||||
@@ -392,7 +392,7 @@ Each document has been:
|
||||
|
||||
## Copyright & Distribution
|
||||
|
||||
**Ownership:** TrueCV Product Team
|
||||
**Ownership:** RealCV Product Team
|
||||
**Classification:** Internal Only
|
||||
**Distribution:** Leadership, Product, Engineering only
|
||||
|
||||
@@ -433,13 +433,13 @@ Each document has been:
|
||||
```
|
||||
MARKET OPPORTUNITY
|
||||
UK CV fraud cost: £4.2B annually
|
||||
Addressable market: £3.3M (TrueCV's portion)
|
||||
Addressable market: £3.3M (RealCV's portion)
|
||||
Candidates lying: 1 in 5 (20%)
|
||||
Failed verifications: 24% of CVs
|
||||
Current verification time: 5-10 DAYS
|
||||
|
||||
COMPETITIVE ADVANTAGE
|
||||
Features only TrueCV offers: 4 major features
|
||||
Features only RealCV offers: 4 major features
|
||||
Market gap size: Unexploited (£3.3M)
|
||||
Time to market advantage: 6-12 months
|
||||
|
||||
@@ -479,14 +479,14 @@ Phase 3 (Q3): +1 customer success + 1 analyst
|
||||
## Quick Links
|
||||
|
||||
**Files Created:**
|
||||
- `/mnt/d/Git/TrueCV/QUICK_REFERENCE.md`
|
||||
- `/mnt/d/Git/TrueCV/EXECUTIVE_SUMMARY.md`
|
||||
- `/mnt/d/Git/TrueCV/UK_FEATURE_PRIORITIZATION.md`
|
||||
- `/mnt/d/Git/TrueCV/PHASE1_TECHNICAL_IMPLEMENTATION.md`
|
||||
- `/mnt/d/Git/TrueCV/UK_MARKET_STRATEGY.md`
|
||||
- `/mnt/d/Git/TrueCV/API_RESOURCES_AND_CONTACTS.md`
|
||||
- `/mnt/d/Git/TrueCV/README_UK_STRATEGY.md`
|
||||
- `/mnt/d/Git/TrueCV/INDEX.md` (this file)
|
||||
- `/mnt/d/Git/RealCV/QUICK_REFERENCE.md`
|
||||
- `/mnt/d/Git/RealCV/EXECUTIVE_SUMMARY.md`
|
||||
- `/mnt/d/Git/RealCV/UK_FEATURE_PRIORITIZATION.md`
|
||||
- `/mnt/d/Git/RealCV/PHASE1_TECHNICAL_IMPLEMENTATION.md`
|
||||
- `/mnt/d/Git/RealCV/UK_MARKET_STRATEGY.md`
|
||||
- `/mnt/d/Git/RealCV/API_RESOURCES_AND_CONTACTS.md`
|
||||
- `/mnt/d/Git/RealCV/README_UK_STRATEGY.md`
|
||||
- `/mnt/d/Git/RealCV/INDEX.md` (this file)
|
||||
|
||||
**Total:** 8 comprehensive strategy documents (~200 pages)
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
### Overview
|
||||
Real-time integration with HEDD (Higher Education Degree Datacheck) to verify UK degrees against 140+ university records.
|
||||
|
||||
**Current Baseline:** TrueCV parses education entries from CV using Claude AI
|
||||
**Current Baseline:** RealCV parses education entries from CV using Claude AI
|
||||
**Gap:** No verification against actual university records
|
||||
**Value:** Eliminates 90%+ of fake degree claims
|
||||
|
||||
@@ -32,10 +32,10 @@ Report & UI
|
||||
|
||||
### Phase 1a: Create Infrastructure (Days 1-5)
|
||||
|
||||
#### File 1: `src/TrueCV.Infrastructure/Configuration/HeddSettings.cs`
|
||||
#### File 1: `src/RealCV.Infrastructure/Configuration/HeddSettings.cs`
|
||||
|
||||
```csharp
|
||||
namespace TrueCV.Infrastructure.Configuration;
|
||||
namespace RealCV.Infrastructure.Configuration;
|
||||
|
||||
public class HeddSettings
|
||||
{
|
||||
@@ -46,7 +46,7 @@ public class HeddSettings
|
||||
}
|
||||
```
|
||||
|
||||
#### File 2: `src/TrueCV.Infrastructure/ExternalApis/HeddClient.cs`
|
||||
#### File 2: `src/RealCV.Infrastructure/ExternalApis/HeddClient.cs`
|
||||
|
||||
```csharp
|
||||
using System.Net.Http.Json;
|
||||
@@ -55,9 +55,9 @@ using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using TrueCV.Infrastructure.Configuration;
|
||||
using RealCV.Infrastructure.Configuration;
|
||||
|
||||
namespace TrueCV.Infrastructure.ExternalApis;
|
||||
namespace RealCV.Infrastructure.ExternalApis;
|
||||
|
||||
public sealed class HeddClient
|
||||
{
|
||||
@@ -242,12 +242,12 @@ public sealed record HeddManualVerificationStatus
|
||||
}
|
||||
```
|
||||
|
||||
#### File 3: `src/TrueCV.Application/Interfaces/IEducationVerifierService.cs`
|
||||
#### File 3: `src/RealCV.Application/Interfaces/IEducationVerifierService.cs`
|
||||
|
||||
```csharp
|
||||
using TrueCV.Application.Models;
|
||||
using RealCV.Application.Models;
|
||||
|
||||
namespace TrueCV.Application.Interfaces;
|
||||
namespace RealCV.Application.Interfaces;
|
||||
|
||||
public interface IEducationVerifierService
|
||||
{
|
||||
@@ -273,10 +273,10 @@ public interface IEducationVerifierService
|
||||
}
|
||||
```
|
||||
|
||||
#### File 4: `src/TrueCV.Application/Models/EducationVerificationResult.cs`
|
||||
#### File 4: `src/RealCV.Application/Models/EducationVerificationResult.cs`
|
||||
|
||||
```csharp
|
||||
namespace TrueCV.Application.Models;
|
||||
namespace RealCV.Application.Models;
|
||||
|
||||
public sealed record EducationVerificationResult
|
||||
{
|
||||
@@ -370,15 +370,15 @@ public enum ManualVerificationStatus
|
||||
|
||||
### Phase 1b: Implement Service Layer (Days 6-10)
|
||||
|
||||
#### File 5: `src/TrueCV.Infrastructure/Services/EducationVerifierService.cs`
|
||||
#### File 5: `src/RealCV.Infrastructure/Services/EducationVerifierService.cs`
|
||||
|
||||
```csharp
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TrueCV.Application.Interfaces;
|
||||
using TrueCV.Application.Models;
|
||||
using TrueCV.Infrastructure.ExternalApis;
|
||||
using RealCV.Application.Interfaces;
|
||||
using RealCV.Application.Models;
|
||||
using RealCV.Infrastructure.ExternalApis;
|
||||
|
||||
namespace TrueCV.Infrastructure.Services;
|
||||
namespace RealCV.Infrastructure.Services;
|
||||
|
||||
public sealed class EducationVerifierService : IEducationVerifierService
|
||||
{
|
||||
@@ -558,10 +558,10 @@ public sealed class EducationVerifierService : IEducationVerifierService
|
||||
|
||||
### Phase 1c: Database & Flag Integration (Days 11-12)
|
||||
|
||||
#### Update: `src/TrueCV.Domain/Enums/FlagCategory.cs`
|
||||
#### Update: `src/RealCV.Domain/Enums/FlagCategory.cs`
|
||||
|
||||
```csharp
|
||||
namespace TrueCV.Domain.Enums;
|
||||
namespace RealCV.Domain.Enums;
|
||||
|
||||
public enum FlagCategory
|
||||
{
|
||||
@@ -575,14 +575,14 @@ public enum FlagCategory
|
||||
}
|
||||
```
|
||||
|
||||
#### New File: `src/TrueCV.Infrastructure/Services/EducationFlagGenerator.cs`
|
||||
#### New File: `src/RealCV.Infrastructure/Services/EducationFlagGenerator.cs`
|
||||
|
||||
```csharp
|
||||
using TrueCV.Application.Models;
|
||||
using TrueCV.Domain.Entities;
|
||||
using TrueCV.Domain.Enums;
|
||||
using RealCV.Application.Models;
|
||||
using RealCV.Domain.Entities;
|
||||
using RealCV.Domain.Enums;
|
||||
|
||||
namespace TrueCV.Infrastructure.Services;
|
||||
namespace RealCV.Infrastructure.Services;
|
||||
|
||||
public sealed class EducationFlagGenerator
|
||||
{
|
||||
@@ -683,16 +683,16 @@ public sealed class EducationFlagGenerator
|
||||
|
||||
### Phase 1d: Companies House Enhancement - Director Verification
|
||||
|
||||
#### File: `src/TrueCV.Infrastructure/ExternalApis/CompaniesHouseDirectorsClient.cs`
|
||||
#### File: `src/RealCV.Infrastructure/ExternalApis/CompaniesHouseDirectorsClient.cs`
|
||||
|
||||
```csharp
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TrueCV.Infrastructure.ExternalApis;
|
||||
using RealCV.Infrastructure.ExternalApis;
|
||||
|
||||
namespace TrueCV.Infrastructure.ExternalApis;
|
||||
namespace RealCV.Infrastructure.ExternalApis;
|
||||
|
||||
public sealed class CompaniesHouseDirectorsClient
|
||||
{
|
||||
@@ -795,10 +795,10 @@ public sealed record Officer
|
||||
}
|
||||
```
|
||||
|
||||
#### File: `src/TrueCV.Application/Interfaces/IDirectorshipVerifierService.cs`
|
||||
#### File: `src/RealCV.Application/Interfaces/IDirectorshipVerifierService.cs`
|
||||
|
||||
```csharp
|
||||
namespace TrueCV.Application.Interfaces;
|
||||
namespace RealCV.Application.Interfaces;
|
||||
|
||||
public interface IDirectorshipVerifierService
|
||||
{
|
||||
@@ -826,15 +826,15 @@ public sealed record DirectorshipVerificationResult
|
||||
}
|
||||
```
|
||||
|
||||
#### File: `src/TrueCV.Infrastructure/Services/DirectorshipVerifierService.cs`
|
||||
#### File: `src/RealCV.Infrastructure/Services/DirectorshipVerifierService.cs`
|
||||
|
||||
```csharp
|
||||
using FuzzySharp;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TrueCV.Application.Interfaces;
|
||||
using TrueCV.Infrastructure.ExternalApis;
|
||||
using RealCV.Application.Interfaces;
|
||||
using RealCV.Infrastructure.ExternalApis;
|
||||
|
||||
namespace TrueCV.Infrastructure.Services;
|
||||
namespace RealCV.Infrastructure.Services;
|
||||
|
||||
public sealed class DirectorshipVerifierService : IDirectorshipVerifierService
|
||||
{
|
||||
@@ -1070,15 +1070,15 @@ public sealed class DirectorshipVerifierService : IDirectorshipVerifierService
|
||||
|
||||
### Phase 1e: Enhanced Timeline Analysis
|
||||
|
||||
#### File: `src/TrueCV.Infrastructure/Services/EnhancedTimelineAnalyserService.cs`
|
||||
#### File: `src/RealCV.Infrastructure/Services/EnhancedTimelineAnalyserService.cs`
|
||||
|
||||
```csharp
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TrueCV.Application.Models;
|
||||
using TrueCV.Domain.Entities;
|
||||
using TrueCV.Domain.Enums;
|
||||
using RealCV.Application.Models;
|
||||
using RealCV.Domain.Entities;
|
||||
using RealCV.Domain.Enums;
|
||||
|
||||
namespace TrueCV.Infrastructure.Services;
|
||||
namespace RealCV.Infrastructure.Services;
|
||||
|
||||
public sealed class EnhancedTimelineAnalyserService
|
||||
{
|
||||
@@ -1226,7 +1226,7 @@ public sealed class EnhancedTimelineAnalyserService
|
||||
|
||||
### Phase 1f: Dependency Injection & Integration (Days 13-14)
|
||||
|
||||
#### Update: `src/TrueCV.Infrastructure/DependencyInjection.cs`
|
||||
#### Update: `src/RealCV.Infrastructure/DependencyInjection.cs`
|
||||
|
||||
```csharp
|
||||
// Add to existing DependencyInjection class:
|
||||
@@ -1244,7 +1244,7 @@ services.AddScoped<IDirectorshipVerifierService, DirectorshipVerifierService>();
|
||||
services.AddScoped<EnhancedTimelineAnalyserService>();
|
||||
```
|
||||
|
||||
#### Update: `src/TrueCV.Infrastructure/Jobs/ProcessCVCheckJob.cs`
|
||||
#### Update: `src/RealCV.Infrastructure/Jobs/ProcessCVCheckJob.cs`
|
||||
|
||||
Add education and directorship verification to the processing pipeline:
|
||||
|
||||
@@ -1356,16 +1356,16 @@ private async Task<List<CVFlag>> VerifyDirectorshipsAsync(
|
||||
|
||||
### Phase 1g: Testing & QA (Days 15-16)
|
||||
|
||||
#### Test File: `tests/TrueCV.Tests/Services/EducationVerifierServiceTests.cs`
|
||||
#### Test File: `tests/RealCV.Tests/Services/EducationVerifierServiceTests.cs`
|
||||
|
||||
```csharp
|
||||
using Moq;
|
||||
using Xunit;
|
||||
using TrueCV.Application.Models;
|
||||
using TrueCV.Infrastructure.ExternalApis;
|
||||
using TrueCV.Infrastructure.Services;
|
||||
using RealCV.Application.Models;
|
||||
using RealCV.Infrastructure.ExternalApis;
|
||||
using RealCV.Infrastructure.Services;
|
||||
|
||||
namespace TrueCV.Tests.Services;
|
||||
namespace RealCV.Tests.Services;
|
||||
|
||||
public class EducationVerifierServiceTests
|
||||
{
|
||||
@@ -1467,7 +1467,7 @@ public class EducationVerifierServiceTests
|
||||
Create migration for storing verification results:
|
||||
|
||||
```bash
|
||||
dotnet ef migrations add AddEducationAndDirectorshipVerification --project src/TrueCV.Infrastructure --startup-project src/TrueCV.Web
|
||||
dotnet ef migrations add AddEducationAndDirectorshipVerification --project src/RealCV.Infrastructure --startup-project src/RealCV.Web
|
||||
```
|
||||
|
||||
Add optional columns to CVCheck entity:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# TrueCV UK Strategy - Quick Reference Card
|
||||
# RealCV UK Strategy - Quick Reference Card
|
||||
|
||||
**Print this page for desk reference during planning & execution**
|
||||
|
||||
@@ -11,7 +11,7 @@ UK CV Fraud Cost: £4.2B annually
|
||||
Candidates Lying: 1 in 5 (20%)
|
||||
Failed Verifications: 24% of CVs
|
||||
Current Verification Time: 5-10 DAYS
|
||||
TrueCV Solution Time: 5 SECONDS ⚡
|
||||
RealCV Solution Time: 5 SECONDS ⚡
|
||||
|
||||
Market Addressable: £3.3M (UK)
|
||||
Year 1 Target Revenue: £30-240K
|
||||
@@ -24,7 +24,7 @@ Expected Profitability: Month 6-7
|
||||
## Competitive Advantage (Why Now)
|
||||
|
||||
```
|
||||
FEATURE TrueCV Workable Deel Checkr
|
||||
FEATURE RealCV Workable Deel Checkr
|
||||
─────────────────────────────────────────────────────────
|
||||
HEDD Degree Verification ✅ ❌ ❌ ❌
|
||||
GMC/NMC Healthcare ✅ ❌ ❌ ❌
|
||||
@@ -370,7 +370,7 @@ Based on your role:
|
||||
|
||||
## One-Liner Summary
|
||||
|
||||
> **TrueCV is the only UK CV verification tool that catches 90% of fake degrees + employment fraud in seconds, leveraging HEDD, GMC/NMC, and Companies House APIs to dominate a £3.3M untapped recruitment market.**
|
||||
> **RealCV is the only UK CV verification tool that catches 90% of fake degrees + employment fraud in seconds, leveraging HEDD, GMC/NMC, and Companies House APIs to dominate a £3.3M untapped recruitment market.**
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# TrueCV UK Market Strategy - Complete Package
|
||||
# RealCV UK Market Strategy - Complete Package
|
||||
|
||||
## Overview
|
||||
|
||||
This directory contains the complete product strategy and implementation plan for launching TrueCV with a UK-only focus. The documents provide market analysis, feature prioritization, technical implementation details, and go-to-market strategy.
|
||||
This directory contains the complete product strategy and implementation plan for launching RealCV with a UK-only focus. The documents provide market analysis, feature prioritization, technical implementation details, and go-to-market strategy.
|
||||
|
||||
---
|
||||
|
||||
@@ -54,7 +54,7 @@ This directory contains the complete product strategy and implementation plan fo
|
||||
**Audience:** Product Team, Marketing, Sales
|
||||
**Purpose:** Comprehensive market analysis and go-to-market strategy
|
||||
**Key Sections:**
|
||||
- Market sizing (£2.8B UK screening market, £3.3M TrueCV TAM)
|
||||
- Market sizing (£2.8B UK screening market, £3.3M RealCV TAM)
|
||||
- Competitive landscape analysis
|
||||
- 3-phase product roadmap (Q1-Q3 2026)
|
||||
- GTM strategy (4 sales channels)
|
||||
@@ -336,7 +336,7 @@ This directory contains the complete product strategy and implementation plan fo
|
||||
## File Manifest
|
||||
|
||||
```
|
||||
/mnt/d/Git/TrueCV/
|
||||
/mnt/d/Git/RealCV/
|
||||
├── EXECUTIVE_SUMMARY.md (5-page exec overview)
|
||||
├── UK_FEATURE_PRIORITIZATION.md (30-page detailed prioritization)
|
||||
├── PHASE1_TECHNICAL_IMPLEMENTATION.md (60-page technical specs + code)
|
||||
@@ -403,7 +403,7 @@ These documents will be updated quarterly with:
|
||||
|
||||
## License & Confidentiality
|
||||
|
||||
This strategy document is internal to TrueCV and contains commercially sensitive information including:
|
||||
This strategy document is internal to RealCV and contains commercially sensitive information including:
|
||||
- Market sizing & financial projections
|
||||
- Competitive positioning
|
||||
- Product roadmap
|
||||
|
||||
@@ -5,17 +5,17 @@ VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F25C3740-9240-46DF-BC34-985BC577216B}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrueCV.Domain", "src\TrueCV.Domain\TrueCV.Domain.csproj", "{41AC48AF-09BC-48D1-9CA4-1B05D3B693F0}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RealCV.Domain", "src\RealCV.Domain\RealCV.Domain.csproj", "{41AC48AF-09BC-48D1-9CA4-1B05D3B693F0}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrueCV.Application", "src\TrueCV.Application\TrueCV.Application.csproj", "{A8A1BA81-3B2F-4F95-BB15-ACA40DF2A70E}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RealCV.Application", "src\RealCV.Application\RealCV.Application.csproj", "{A8A1BA81-3B2F-4F95-BB15-ACA40DF2A70E}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrueCV.Infrastructure", "src\TrueCV.Infrastructure\TrueCV.Infrastructure.csproj", "{03DB607C-9592-4930-8C89-3E257A319278}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RealCV.Infrastructure", "src\RealCV.Infrastructure\RealCV.Infrastructure.csproj", "{03DB607C-9592-4930-8C89-3E257A319278}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrueCV.Web", "src\TrueCV.Web\TrueCV.Web.csproj", "{D69F57DB-3092-48AF-81BB-868E3749C638}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RealCV.Web", "src\RealCV.Web\RealCV.Web.csproj", "{D69F57DB-3092-48AF-81BB-868E3749C638}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{80890010-EDA6-418B-AD6C-5A9D875594C4}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrueCV.Tests", "tests\TrueCV.Tests\TrueCV.Tests.csproj", "{4450D4F1-4EB9-445E-904B-1C57701493D8}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RealCV.Tests", "tests\RealCV.Tests\RealCV.Tests.csproj", "{4450D4F1-4EB9-445E-904B-1C57701493D8}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@@ -1,4 +1,4 @@
|
||||
# TrueCV UK Market Feature Prioritization
|
||||
# RealCV UK Market Feature Prioritization
|
||||
|
||||
**Date:** January 2026
|
||||
**Focus:** UK-Only Market Opportunities
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
## Executive Summary
|
||||
|
||||
UK CV fraud is escalating with AI-generated deepfakes, synthetic identities, and traditional qualification falsification. The most impactful opportunity for TrueCV in the UK market is **degree verification integration** (HEDD API), followed by **employment verification automation** and **professional body registration checks**. These three features represent 78% of recruiter pain points and address 85% of detected fraud patterns.
|
||||
UK CV fraud is escalating with AI-generated deepfakes, synthetic identities, and traditional qualification falsification. The most impactful opportunity for RealCV in the UK market is **degree verification integration** (HEDD API), followed by **employment verification automation** and **professional body registration checks**. These three features represent 78% of recruiter pain points and address 85% of detected fraud patterns.
|
||||
|
||||
---
|
||||
|
||||
@@ -48,7 +48,7 @@ UK CV fraud is escalating with AI-generated deepfakes, synthetic identities, and
|
||||
- **Cost:** Typically £1-5 per verification (commercial rates)
|
||||
|
||||
**Implementation Effort:** **Medium (2-3 weeks)**
|
||||
- Iframe/form integration into TrueCV UI
|
||||
- Iframe/form integration into RealCV UI
|
||||
- Candidate consent workflow
|
||||
- Result polling for manual verifications
|
||||
- Database sync with CVData.Education entries
|
||||
@@ -83,7 +83,7 @@ UK CV fraud is escalating with AI-generated deepfakes, synthetic identities, and
|
||||
**Impact Score:** **6.5/10**
|
||||
- Targets 1.5M NHS workers + private doctors
|
||||
- High value for healthcare recruitment
|
||||
- Medium market size in TrueCV context
|
||||
- Medium market size in RealCV context
|
||||
- But limited to one profession vs. broad application
|
||||
|
||||
---
|
||||
@@ -116,7 +116,7 @@ UK CV fraud is escalating with AI-generated deepfakes, synthetic identities, and
|
||||
|
||||
### 4. Companies House API (Already Integrated)
|
||||
|
||||
**Status:** ✓ Already implemented in TrueCV
|
||||
**Status:** ✓ Already implemented in RealCV
|
||||
|
||||
**Current Coverage:**
|
||||
- Fuzzy matching on company names (70%+ threshold)
|
||||
@@ -205,7 +205,7 @@ UK CV fraud is escalating with AI-generated deepfakes, synthetic identities, and
|
||||
- Links to individual regulators
|
||||
- Government-maintained reference
|
||||
|
||||
**Use Case for TrueCV:**
|
||||
**Use Case for RealCV:**
|
||||
- **Enrichment layer:** When CV claims regulated profession, cross-check against GOV.UK registry
|
||||
- **Flag generation:** "Claims regulated profession but regulator not found"
|
||||
- **Guidance:** Link to correct regulator for user lookup
|
||||
@@ -284,9 +284,9 @@ MEDIUM VALUE + EASY │ MEDIUM VALUE + HARD
|
||||
- **Pricing:** Pass-through cost model ($1-2 per verification to user)
|
||||
- **Implementation:**
|
||||
```
|
||||
src/TrueCV.Infrastructure/ExternalApis/HeddClient.cs
|
||||
src/TrueCV.Application/Interfaces/IEducationVerifierService.cs
|
||||
src/TrueCV.Infrastructure/Services/EducationVerifierService.cs
|
||||
src/RealCV.Infrastructure/ExternalApis/HeddClient.cs
|
||||
src/RealCV.Application/Interfaces/IEducationVerifierService.cs
|
||||
src/RealCV.Infrastructure/Services/EducationVerifierService.cs
|
||||
FlagCategory += EducationVerification
|
||||
Add new flag types:
|
||||
- DegreeNotFound
|
||||
@@ -304,7 +304,7 @@ MEDIUM VALUE + EASY │ MEDIUM VALUE + HARD
|
||||
- Identify degree end date before employment start anomalies
|
||||
- **Implementation:**
|
||||
```
|
||||
src/TrueCV.Infrastructure/Services/TimelineAnalyserService.cs
|
||||
src/RealCV.Infrastructure/Services/TimelineAnalyserService.cs
|
||||
- Add: UKEmploymentPatternAnalyzer
|
||||
- Add: EducationEmploymentSequenceValidator
|
||||
- New flags:
|
||||
@@ -322,8 +322,8 @@ MEDIUM VALUE + EASY │ MEDIUM VALUE + HARD
|
||||
- Recurring revenue potential
|
||||
- **Implementation:**
|
||||
```
|
||||
src/TrueCV.Infrastructure/ExternalApis/HealthcareRegisterClient.cs
|
||||
src/TrueCV.Application/Interfaces/IHealthcareVerifierService.cs
|
||||
src/RealCV.Infrastructure/ExternalApis/HealthcareRegisterClient.cs
|
||||
src/RealCV.Application/Interfaces/IHealthcareVerifierService.cs
|
||||
FlagCategory += HealthcareRegistration
|
||||
New flags:
|
||||
- GMCNotFound / GMCRestricted / GMCLapsed
|
||||
@@ -338,7 +338,7 @@ MEDIUM VALUE + EASY │ MEDIUM VALUE + HARD
|
||||
- Detects employment after company dissolution
|
||||
- **Implementation:**
|
||||
```
|
||||
Extend: src/TrueCV.Infrastructure/ExternalApis/CompaniesHouseClient.cs
|
||||
Extend: src/RealCV.Infrastructure/ExternalApis/CompaniesHouseClient.cs
|
||||
Add: OfficerAppointmentsClient.GetDirectorAppointments(name, companyNumber)
|
||||
New Service: DirectorshipVerificationService
|
||||
FlagCategory += DirectorshipVerification
|
||||
@@ -360,8 +360,8 @@ MEDIUM VALUE + EASY │ MEDIUM VALUE + HARD
|
||||
- Regulatory appeal
|
||||
- **Implementation:**
|
||||
```
|
||||
src/TrueCV.Infrastructure/ExternalApis/ProfessionalBodyClient.cs
|
||||
src/TrueCV.Infrastructure/ExternalApis/Scrapers/
|
||||
src/RealCV.Infrastructure/ExternalApis/ProfessionalBodyClient.cs
|
||||
src/RealCV.Infrastructure/ExternalApis/Scrapers/
|
||||
- ICAEWMembershipVerifier.cs
|
||||
- SRALawverVerifier.cs
|
||||
- IETEngineerVerifier.cs
|
||||
@@ -569,7 +569,7 @@ public class DirectorshipVerificationService
|
||||
|
||||
## Competitive Advantage Summary
|
||||
|
||||
| Feature | TrueCV Advantage | Timeline |
|
||||
| Feature | RealCV Advantage | Timeline |
|
||||
|---|---|---|
|
||||
| **HEDD Integration** | Only dedicated CV tool with instant degree verification | Q1 2026 |
|
||||
| **Healthcare Register Targeting** | Only tool targeting healthcare recruitment niche | Q1 2026 |
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# TrueCV UK Market Strategy & Product Roadmap
|
||||
# RealCV UK Market Strategy & Product Roadmap
|
||||
|
||||
**Document Date:** January 2026
|
||||
**Focus:** UK CV verification market positioning
|
||||
@@ -37,7 +37,7 @@
|
||||
|
||||
### SAM (Serviceable Addressable Market)
|
||||
|
||||
**TrueCV Target Segment:**
|
||||
**RealCV Target Segment:**
|
||||
- Mid-market recruitment agencies (50-500 staff): ~800 companies
|
||||
- Corporate HR departments (100+ employees): ~15,000 companies
|
||||
- Specialist vertical recruiters (healthcare, finance, legal): ~2,500 companies
|
||||
@@ -65,9 +65,9 @@
|
||||
| **Verifile** | Pre-employment screening | Established relationships | Traditional manual process |
|
||||
| **Veriff** | Identity verification | Strong deepfake tech | Not employment-focused |
|
||||
|
||||
### TrueCV Differentiation
|
||||
### RealCV Differentiation
|
||||
|
||||
| Feature | TrueCV | Workable | Deel | Checkr | Verifile |
|
||||
| Feature | RealCV | Workable | Deel | Checkr | Verifile |
|
||||
|---|---|---|---|---|---|
|
||||
| **Degree Verification (HEDD)** | ✅ Q1 2026 | ❌ | ❌ | ❌ | ❌ |
|
||||
| **Healthcare Register Checks** | ✅ Q1 2026 | ❌ | ❌ | ❌ | ❌ |
|
||||
@@ -194,7 +194,7 @@
|
||||
|
||||
### Marketing Messaging
|
||||
|
||||
**Tagline:** "Hire with Confidence. Verify with TrueCV."
|
||||
**Tagline:** "Hire with Confidence. Verify with RealCV."
|
||||
|
||||
**Core Messages:**
|
||||
1. **For Recruiters:** "Catch 90% of degree fraud in seconds. One-click HEDD verification."
|
||||
@@ -279,7 +279,7 @@
|
||||
- **API Uptime:** 99.9%
|
||||
|
||||
### Market Metrics
|
||||
- **Brand Awareness:** 15%+ of recruitment agencies aware of TrueCV
|
||||
- **Brand Awareness:** 15%+ of recruitment agencies aware of RealCV
|
||||
- **Market Share:** 0.5-1% of addressable recruitment screening market
|
||||
- **Vertical Penetration:** 3%+ of healthcare recruiters, 2%+ financial recruiters
|
||||
|
||||
@@ -369,7 +369,7 @@
|
||||
|
||||
## Conclusion
|
||||
|
||||
TrueCV addresses a massive UK market problem (£4.2B+ annual cost from CV fraud) with a focused, integrated solution. By launching with HEDD degree verification + timeline fraud detection in Q1 2026, we capture first-mover advantage in a gap no competitor fills.
|
||||
RealCV addresses a massive UK market problem (£4.2B+ annual cost from CV fraud) with a focused, integrated solution. By launching with HEDD degree verification + timeline fraud detection in Q1 2026, we capture first-mover advantage in a gap no competitor fills.
|
||||
|
||||
**The opportunity:** Become the UK's trusted CV verification layer for recruitment, reducing fraud while accelerating hiring processes.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# TrueCV Deployment Guide
|
||||
# RealCV Deployment Guide
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -40,7 +40,7 @@ After DNS is configured and app is deployed:
|
||||
|
||||
```bash
|
||||
ssh user@your-server
|
||||
sudo certbot --nginx -d truecv.yourdomain.com
|
||||
sudo certbot --nginx -d realcv.yourdomain.com
|
||||
```
|
||||
|
||||
## Configuration
|
||||
@@ -54,7 +54,7 @@ The systemd service sets these environment variables:
|
||||
|
||||
To add more (like API keys), edit:
|
||||
```bash
|
||||
sudo systemctl edit truecv
|
||||
sudo systemctl edit realcv
|
||||
```
|
||||
|
||||
Add:
|
||||
@@ -65,12 +65,12 @@ Environment=OpenAI__ApiKey=your-key-here
|
||||
|
||||
### appsettings.Production.json
|
||||
|
||||
For sensitive settings, create `/var/www/truecv/appsettings.Production.json`:
|
||||
For sensitive settings, create `/var/www/realcv/appsettings.Production.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Server=127.0.0.1;Database=TrueCV;User Id=SA;Password=YourPassword;TrustServerCertificate=True"
|
||||
"DefaultConnection": "Server=127.0.0.1;Database=RealCV;User Id=SA;Password=YourPassword;TrustServerCertificate=True"
|
||||
},
|
||||
"OpenAI": {
|
||||
"ApiKey": "your-openai-key"
|
||||
@@ -83,41 +83,41 @@ For sensitive settings, create `/var/www/truecv/appsettings.Production.json`:
|
||||
### View Logs
|
||||
```bash
|
||||
# Application logs
|
||||
sudo journalctl -u truecv -f
|
||||
sudo journalctl -u realcv -f
|
||||
|
||||
# Nginx logs
|
||||
sudo tail -f /var/log/nginx/access.log
|
||||
sudo tail -f /var/log/nginx/error.log
|
||||
|
||||
# SQL Server logs
|
||||
docker logs truecv-sql -f
|
||||
docker logs realcv-sql -f
|
||||
```
|
||||
|
||||
### Restart Services
|
||||
```bash
|
||||
sudo systemctl restart truecv
|
||||
sudo systemctl restart realcv
|
||||
sudo systemctl restart nginx
|
||||
docker restart truecv-sql
|
||||
docker restart realcv-sql
|
||||
```
|
||||
|
||||
### Database Backup
|
||||
```bash
|
||||
# Backup
|
||||
docker exec truecv-sql /opt/mssql-tools18/bin/sqlcmd \
|
||||
docker exec realcv-sql /opt/mssql-tools18/bin/sqlcmd \
|
||||
-S localhost -U SA -P 'YourPassword' -C \
|
||||
-Q "BACKUP DATABASE TrueCV TO DISK='/var/opt/mssql/backup/truecv.bak'"
|
||||
-Q "BACKUP DATABASE RealCV TO DISK='/var/opt/mssql/backup/realcv.bak'"
|
||||
|
||||
# Copy backup from container
|
||||
docker cp truecv-sql:/var/opt/mssql/backup/truecv.bak ./truecv-backup.bak
|
||||
docker cp realcv-sql:/var/opt/mssql/backup/realcv.bak ./realcv-backup.bak
|
||||
```
|
||||
|
||||
### Rollback Deployment
|
||||
```bash
|
||||
# On server - restore previous version
|
||||
sudo systemctl stop truecv
|
||||
sudo rm -rf /var/www/truecv
|
||||
sudo mv /var/www/truecv.backup.YYYYMMDD_HHMMSS /var/www/truecv
|
||||
sudo systemctl start truecv
|
||||
sudo systemctl stop realcv
|
||||
sudo rm -rf /var/www/realcv
|
||||
sudo mv /var/www/realcv.backup.YYYYMMDD_HHMMSS /var/www/realcv
|
||||
sudo systemctl start realcv
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
@@ -125,23 +125,23 @@ sudo systemctl start truecv
|
||||
### App won't start
|
||||
```bash
|
||||
# Check status
|
||||
sudo systemctl status truecv
|
||||
sudo systemctl status realcv
|
||||
|
||||
# Check logs
|
||||
sudo journalctl -u truecv -n 100
|
||||
sudo journalctl -u realcv -n 100
|
||||
|
||||
# Test manually
|
||||
cd /var/www/truecv
|
||||
sudo -u www-data dotnet TrueCV.Web.dll
|
||||
cd /var/www/realcv
|
||||
sudo -u www-data dotnet RealCV.Web.dll
|
||||
```
|
||||
|
||||
### Database connection issues
|
||||
```bash
|
||||
# Check SQL Server is running
|
||||
docker ps | grep truecv-sql
|
||||
docker ps | grep realcv-sql
|
||||
|
||||
# Test connection
|
||||
docker exec -it truecv-sql /opt/mssql-tools18/bin/sqlcmd \
|
||||
docker exec -it realcv-sql /opt/mssql-tools18/bin/sqlcmd \
|
||||
-S localhost -U SA -P 'YourPassword' -C \
|
||||
-Q "SELECT name FROM sys.databases"
|
||||
```
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
# TrueCV Deployment Script
|
||||
# RealCV Deployment Script
|
||||
# Run this from your development machine to deploy to a Linux server
|
||||
|
||||
set -e
|
||||
@@ -7,8 +7,8 @@ set -e
|
||||
# Configuration - UPDATE THESE VALUES
|
||||
SERVER_USER="deploy"
|
||||
SERVER_HOST="your-server.com"
|
||||
SERVER_PATH="/var/www/truecv"
|
||||
DOMAIN="truecv.yourdomain.com"
|
||||
SERVER_PATH="/var/www/realcv"
|
||||
DOMAIN="realcv.yourdomain.com"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
@@ -16,7 +16,7 @@ GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${GREEN}=== TrueCV Deployment Script ===${NC}"
|
||||
echo -e "${GREEN}=== RealCV Deployment Script ===${NC}"
|
||||
|
||||
# Check if configuration is set
|
||||
if [[ "$SERVER_HOST" == "your-server.com" ]]; then
|
||||
@@ -27,15 +27,15 @@ fi
|
||||
# Step 1: Build and publish
|
||||
echo -e "${YELLOW}Step 1: Publishing application...${NC}"
|
||||
cd "$(dirname "$0")/.."
|
||||
dotnet publish src/TrueCV.Web -c Release -o ./publish --nologo
|
||||
dotnet publish src/RealCV.Web -c Release -o ./publish --nologo
|
||||
|
||||
# Step 2: Create deployment package
|
||||
echo -e "${YELLOW}Step 2: Creating deployment package...${NC}"
|
||||
tar -czf deploy/truecv-release.tar.gz -C publish .
|
||||
tar -czf deploy/realcv-release.tar.gz -C publish .
|
||||
|
||||
# Step 3: Transfer to server
|
||||
echo -e "${YELLOW}Step 3: Transferring to server...${NC}"
|
||||
scp deploy/truecv-release.tar.gz ${SERVER_USER}@${SERVER_HOST}:/tmp/
|
||||
scp deploy/realcv-release.tar.gz ${SERVER_USER}@${SERVER_HOST}:/tmp/
|
||||
|
||||
# Step 4: Deploy on server
|
||||
echo -e "${YELLOW}Step 4: Deploying on server...${NC}"
|
||||
@@ -43,23 +43,23 @@ ssh ${SERVER_USER}@${SERVER_HOST} << 'ENDSSH'
|
||||
set -e
|
||||
|
||||
# Stop the service if running
|
||||
sudo systemctl stop truecv 2>/dev/null || true
|
||||
sudo systemctl stop realcv 2>/dev/null || true
|
||||
|
||||
# Backup current deployment
|
||||
if [ -d "/var/www/truecv" ]; then
|
||||
sudo mv /var/www/truecv /var/www/truecv.backup.$(date +%Y%m%d_%H%M%S)
|
||||
if [ -d "/var/www/realcv" ]; then
|
||||
sudo mv /var/www/realcv /var/www/realcv.backup.$(date +%Y%m%d_%H%M%S)
|
||||
fi
|
||||
|
||||
# Create directory and extract
|
||||
sudo mkdir -p /var/www/truecv
|
||||
sudo tar -xzf /tmp/truecv-release.tar.gz -C /var/www/truecv
|
||||
sudo chown -R www-data:www-data /var/www/truecv
|
||||
sudo mkdir -p /var/www/realcv
|
||||
sudo tar -xzf /tmp/realcv-release.tar.gz -C /var/www/realcv
|
||||
sudo chown -R www-data:www-data /var/www/realcv
|
||||
|
||||
# Start the service
|
||||
sudo systemctl start truecv
|
||||
sudo systemctl start realcv
|
||||
|
||||
# Clean up
|
||||
rm /tmp/truecv-release.tar.gz
|
||||
rm /tmp/realcv-release.tar.gz
|
||||
|
||||
echo "Deployment complete on server"
|
||||
ENDSSH
|
||||
@@ -67,14 +67,14 @@ ENDSSH
|
||||
# Step 5: Verify deployment
|
||||
echo -e "${YELLOW}Step 5: Verifying deployment...${NC}"
|
||||
sleep 3
|
||||
if ssh ${SERVER_USER}@${SERVER_HOST} "sudo systemctl is-active truecv" | grep -q "active"; then
|
||||
if ssh ${SERVER_USER}@${SERVER_HOST} "sudo systemctl is-active realcv" | grep -q "active"; then
|
||||
echo -e "${GREEN}=== Deployment successful! ===${NC}"
|
||||
echo -e "Site should be available at: https://${DOMAIN}"
|
||||
else
|
||||
echo -e "${RED}Warning: Service may not be running. Check with: sudo systemctl status truecv${NC}"
|
||||
echo -e "${RED}Warning: Service may not be running. Check with: sudo systemctl status realcv${NC}"
|
||||
fi
|
||||
|
||||
# Cleanup local files
|
||||
rm -f deploy/truecv-release.tar.gz
|
||||
rm -f deploy/realcv-release.tar.gz
|
||||
|
||||
echo -e "${GREEN}Done!${NC}"
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#!/bin/bash
|
||||
# TrueCV Server Setup Script
|
||||
# RealCV Server Setup Script
|
||||
# Run this ONCE on a fresh Linux server (Ubuntu 22.04/24.04)
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration - UPDATE THESE VALUES
|
||||
DOMAIN="truecv.yourdomain.com"
|
||||
DOMAIN="realcv.yourdomain.com"
|
||||
DB_PASSWORD="YourStrong!Password123"
|
||||
ADMIN_EMAIL="admin@yourdomain.com"
|
||||
|
||||
@@ -15,7 +15,7 @@ GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo -e "${GREEN}=== TrueCV Server Setup ===${NC}"
|
||||
echo -e "${GREEN}=== RealCV Server Setup ===${NC}"
|
||||
|
||||
# Check if running as root
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
@@ -52,54 +52,54 @@ echo -e "${YELLOW}Step 5: Setting up SQL Server...${NC}"
|
||||
docker run -e 'ACCEPT_EULA=Y' \
|
||||
-e "SA_PASSWORD=${DB_PASSWORD}" \
|
||||
-p 127.0.0.1:1433:1433 \
|
||||
--name truecv-sql \
|
||||
--name realcv-sql \
|
||||
--restart unless-stopped \
|
||||
-v truecv-sqldata:/var/opt/mssql \
|
||||
-v realcv-sqldata:/var/opt/mssql \
|
||||
-d mcr.microsoft.com/mssql/server:2022-latest
|
||||
|
||||
echo "Waiting for SQL Server to start..."
|
||||
sleep 30
|
||||
|
||||
# Create the database
|
||||
docker exec truecv-sql /opt/mssql-tools18/bin/sqlcmd \
|
||||
docker exec realcv-sql /opt/mssql-tools18/bin/sqlcmd \
|
||||
-S localhost -U SA -P "${DB_PASSWORD}" -C \
|
||||
-Q "CREATE DATABASE TrueCV"
|
||||
-Q "CREATE DATABASE RealCV"
|
||||
|
||||
# Step 6: Create application directory
|
||||
echo -e "${YELLOW}Step 6: Creating application directory...${NC}"
|
||||
mkdir -p /var/www/truecv
|
||||
chown -R www-data:www-data /var/www/truecv
|
||||
mkdir -p /var/www/realcv
|
||||
chown -R www-data:www-data /var/www/realcv
|
||||
|
||||
# Step 7: Create systemd service
|
||||
echo -e "${YELLOW}Step 7: Creating systemd service...${NC}"
|
||||
cat > /etc/systemd/system/truecv.service << EOF
|
||||
cat > /etc/systemd/system/realcv.service << EOF
|
||||
[Unit]
|
||||
Description=TrueCV Web Application
|
||||
Description=RealCV Web Application
|
||||
After=network.target docker.service
|
||||
Requires=docker.service
|
||||
|
||||
[Service]
|
||||
WorkingDirectory=/var/www/truecv
|
||||
ExecStart=/usr/bin/dotnet /var/www/truecv/TrueCV.Web.dll
|
||||
WorkingDirectory=/var/www/realcv
|
||||
ExecStart=/usr/bin/dotnet /var/www/realcv/RealCV.Web.dll
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
KillSignal=SIGINT
|
||||
SyslogIdentifier=truecv
|
||||
SyslogIdentifier=realcv
|
||||
User=www-data
|
||||
Environment=ASPNETCORE_ENVIRONMENT=Production
|
||||
Environment=ASPNETCORE_URLS=http://localhost:5000
|
||||
Environment=ConnectionStrings__DefaultConnection=Server=127.0.0.1;Database=TrueCV;User Id=SA;Password=${DB_PASSWORD};TrustServerCertificate=True
|
||||
Environment=ConnectionStrings__DefaultConnection=Server=127.0.0.1;Database=RealCV;User Id=SA;Password=${DB_PASSWORD};TrustServerCertificate=True
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable truecv
|
||||
systemctl enable realcv
|
||||
|
||||
# Step 8: Configure Nginx
|
||||
echo -e "${YELLOW}Step 8: Configuring Nginx...${NC}"
|
||||
cat > /etc/nginx/sites-available/truecv << EOF
|
||||
cat > /etc/nginx/sites-available/realcv << EOF
|
||||
server {
|
||||
listen 80;
|
||||
server_name ${DOMAIN};
|
||||
@@ -122,7 +122,7 @@ server {
|
||||
}
|
||||
EOF
|
||||
|
||||
ln -sf /etc/nginx/sites-available/truecv /etc/nginx/sites-enabled/
|
||||
ln -sf /etc/nginx/sites-available/realcv /etc/nginx/sites-enabled/
|
||||
rm -f /etc/nginx/sites-enabled/default
|
||||
nginx -t
|
||||
systemctl reload nginx
|
||||
@@ -151,9 +151,9 @@ echo "2. Deploy the application using deploy.sh from your dev machine"
|
||||
echo "3. Run SSL setup: certbot --nginx -d ${DOMAIN}"
|
||||
echo ""
|
||||
echo "Useful commands:"
|
||||
echo " sudo systemctl status truecv - Check app status"
|
||||
echo " sudo journalctl -u truecv -f - View app logs"
|
||||
echo " docker logs truecv-sql - View SQL Server logs"
|
||||
echo " sudo systemctl status realcv - Check app status"
|
||||
echo " sudo journalctl -u realcv -f - View app logs"
|
||||
echo " docker logs realcv-sql - View SQL Server logs"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Database connection string:${NC}"
|
||||
echo " Server=127.0.0.1;Database=TrueCV;User Id=SA;Password=${DB_PASSWORD};TrustServerCertificate=True"
|
||||
echo " Server=127.0.0.1;Database=RealCV;User Id=SA;Password=${DB_PASSWORD};TrustServerCertificate=True"
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# TrueCV Web Application
|
||||
truecv-web:
|
||||
# RealCV Web Application
|
||||
realcv-web:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: truecv-web
|
||||
container_name: realcv-web
|
||||
ports:
|
||||
- "5000:8080"
|
||||
environment:
|
||||
- ASPNETCORE_ENVIRONMENT=Development
|
||||
- ConnectionStrings__DefaultConnection=Server=sqlserver;Database=TrueCV;User Id=sa;Password=TrueCV_P@ssw0rd!;TrustServerCertificate=True;
|
||||
- ConnectionStrings__HangfireConnection=Server=sqlserver;Database=TrueCV_Hangfire;User Id=sa;Password=TrueCV_P@ssw0rd!;TrustServerCertificate=True;
|
||||
- ConnectionStrings__DefaultConnection=Server=sqlserver;Database=RealCV;User Id=sa;Password=TrueCV_P@ssw0rd!;TrustServerCertificate=True;
|
||||
- ConnectionStrings__HangfireConnection=Server=sqlserver;Database=RealCV_Hangfire;User Id=sa;Password=TrueCV_P@ssw0rd!;TrustServerCertificate=True;
|
||||
- AzureBlob__ConnectionString=DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://azurite:10000/devstoreaccount1;
|
||||
- AzureBlob__ContainerName=cv-uploads
|
||||
- CompaniesHouse__BaseUrl=https://api.company-information.service.gov.uk
|
||||
@@ -24,13 +24,13 @@ services:
|
||||
azurite:
|
||||
condition: service_started
|
||||
networks:
|
||||
- truecv-network
|
||||
- realcv-network
|
||||
restart: unless-stopped
|
||||
|
||||
# SQL Server Database
|
||||
sqlserver:
|
||||
image: mcr.microsoft.com/mssql/server:2022-latest
|
||||
container_name: truecv-sqlserver
|
||||
container_name: realcv-sqlserver
|
||||
ports:
|
||||
- "1433:1433"
|
||||
environment:
|
||||
@@ -40,7 +40,7 @@ services:
|
||||
volumes:
|
||||
- sqlserver-data:/var/opt/mssql
|
||||
networks:
|
||||
- truecv-network
|
||||
- realcv-network
|
||||
healthcheck:
|
||||
test: /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "TrueCV_P@ssw0rd!" -C -Q "SELECT 1" || exit 1
|
||||
interval: 10s
|
||||
@@ -52,7 +52,7 @@ services:
|
||||
# Azure Storage Emulator (Azurite)
|
||||
azurite:
|
||||
image: mcr.microsoft.com/azure-storage/azurite:latest
|
||||
container_name: truecv-azurite
|
||||
container_name: realcv-azurite
|
||||
ports:
|
||||
- "10000:10000" # Blob service
|
||||
- "10001:10001" # Queue service
|
||||
@@ -61,7 +61,7 @@ services:
|
||||
- azurite-data:/data
|
||||
command: "azurite --blobHost 0.0.0.0 --queueHost 0.0.0.0 --tableHost 0.0.0.0 --location /data --debug /data/debug.log"
|
||||
networks:
|
||||
- truecv-network
|
||||
- realcv-network
|
||||
restart: unless-stopped
|
||||
|
||||
# Database initialization (runs migrations)
|
||||
@@ -69,18 +69,18 @@ services:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.migrations
|
||||
container_name: truecv-db-init
|
||||
container_name: realcv-db-init
|
||||
environment:
|
||||
- ConnectionStrings__DefaultConnection=Server=sqlserver;Database=TrueCV;User Id=sa;Password=TrueCV_P@ssw0rd!;TrustServerCertificate=True;
|
||||
- ConnectionStrings__DefaultConnection=Server=sqlserver;Database=RealCV;User Id=sa;Password=TrueCV_P@ssw0rd!;TrustServerCertificate=True;
|
||||
depends_on:
|
||||
sqlserver:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- truecv-network
|
||||
- realcv-network
|
||||
restart: "no"
|
||||
|
||||
networks:
|
||||
truecv-network:
|
||||
realcv-network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
|
||||
BIN
screenshots/homepage.png
Normal file
BIN
screenshots/homepage.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
BIN
screenshots/login.png
Normal file
BIN
screenshots/login.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 449 KiB |
BIN
screenshots/pricing.png
Normal file
BIN
screenshots/pricing.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 144 KiB |
BIN
screenshots/privacy.png
Normal file
BIN
screenshots/privacy.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 289 KiB |
BIN
screenshots/register.png
Normal file
BIN
screenshots/register.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 468 KiB |
@@ -1,9 +1,10 @@
|
||||
namespace TrueCV.Application.DTOs;
|
||||
namespace RealCV.Application.DTOs;
|
||||
|
||||
public sealed record CVCheckDto
|
||||
{
|
||||
public required Guid Id { get; init; }
|
||||
public required string OriginalFileName { get; init; }
|
||||
public string? CandidateName { get; init; }
|
||||
public required string Status { get; init; }
|
||||
public int? VeracityScore { get; init; }
|
||||
public string? ProcessingStage { get; init; }
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace TrueCV.Application.DTOs;
|
||||
namespace RealCV.Application.DTOs;
|
||||
|
||||
public sealed record CompanySearchResult
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace TrueCV.Application.Data;
|
||||
namespace RealCV.Application.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Known diploma mills and fake educational institutions.
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace TrueCV.Application.Data;
|
||||
namespace RealCV.Application.Data;
|
||||
|
||||
/// <summary>
|
||||
/// List of recognised UK higher education institutions.
|
||||
@@ -43,6 +43,8 @@ public static class UKInstitutions
|
||||
|
||||
// Other Major Universities
|
||||
"Aston University",
|
||||
"Leeds Beckett University",
|
||||
"Leeds Metropolitan University", // Former name of Leeds Beckett
|
||||
"University of Bath",
|
||||
"Birkbeck, University of London",
|
||||
"Bournemouth University",
|
||||
@@ -218,6 +220,9 @@ public static class UKInstitutions
|
||||
["Queen Mary"] = "Queen Mary University of London",
|
||||
["Royal Holloway University"] = "Royal Holloway, University of London",
|
||||
["RHUL"] = "Royal Holloway, University of London",
|
||||
["Leeds Beckett"] = "Leeds Beckett University",
|
||||
["Leeds Met"] = "Leeds Beckett University",
|
||||
["Leeds Metropolitan"] = "Leeds Beckett University",
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace TrueCV.Application.Helpers;
|
||||
namespace RealCV.Application.Helpers;
|
||||
|
||||
public static class DateHelpers
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace TrueCV.Application.Helpers;
|
||||
namespace RealCV.Application.Helpers;
|
||||
|
||||
public static class JsonDefaults
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace TrueCV.Application.Helpers;
|
||||
namespace RealCV.Application.Helpers;
|
||||
|
||||
public static class ScoreThresholds
|
||||
{
|
||||
@@ -0,0 +1,54 @@
|
||||
using RealCV.Application.Models;
|
||||
|
||||
namespace RealCV.Application.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Service for verifying academic researchers via ORCID
|
||||
/// </summary>
|
||||
public interface IAcademicVerifierService
|
||||
{
|
||||
/// <summary>
|
||||
/// Verify an academic researcher by ORCID ID
|
||||
/// </summary>
|
||||
Task<AcademicVerificationResult> VerifyByOrcidAsync(string orcidId);
|
||||
|
||||
/// <summary>
|
||||
/// Search for researchers and verify by name
|
||||
/// </summary>
|
||||
Task<AcademicVerificationResult> VerifyByNameAsync(
|
||||
string name,
|
||||
string? affiliation = null);
|
||||
|
||||
/// <summary>
|
||||
/// Search ORCID for researchers
|
||||
/// </summary>
|
||||
Task<List<OrcidSearchResult>> SearchResearchersAsync(
|
||||
string name,
|
||||
string? affiliation = null);
|
||||
|
||||
/// <summary>
|
||||
/// Verify claimed publications
|
||||
/// </summary>
|
||||
Task<List<PublicationVerificationResult>> VerifyPublicationsAsync(
|
||||
string orcidId,
|
||||
List<string> claimedPublications);
|
||||
}
|
||||
|
||||
public sealed record OrcidSearchResult
|
||||
{
|
||||
public required string OrcidId { get; init; }
|
||||
public required string Name { get; init; }
|
||||
public string? OrcidUrl { get; init; }
|
||||
public List<string>? Affiliations { get; init; }
|
||||
public int? PublicationCount { get; init; }
|
||||
}
|
||||
|
||||
public sealed record PublicationVerificationResult
|
||||
{
|
||||
public required string ClaimedTitle { get; init; }
|
||||
public required bool IsVerified { get; init; }
|
||||
public string? MatchedTitle { get; init; }
|
||||
public string? Doi { get; init; }
|
||||
public int? Year { get; init; }
|
||||
public string? Notes { get; init; }
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace TrueCV.Application.Interfaces;
|
||||
namespace RealCV.Application.Interfaces;
|
||||
|
||||
public interface IAuditService
|
||||
{
|
||||
@@ -1,7 +1,7 @@
|
||||
using TrueCV.Application.DTOs;
|
||||
using TrueCV.Application.Models;
|
||||
using RealCV.Application.DTOs;
|
||||
using RealCV.Application.Models;
|
||||
|
||||
namespace TrueCV.Application.Interfaces;
|
||||
namespace RealCV.Application.Interfaces;
|
||||
|
||||
public interface ICVCheckService
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using TrueCV.Application.Models;
|
||||
using RealCV.Application.Models;
|
||||
|
||||
namespace TrueCV.Application.Interfaces;
|
||||
namespace RealCV.Application.Interfaces;
|
||||
|
||||
public interface ICVParserService
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using TrueCV.Application.Models;
|
||||
using RealCV.Application.Models;
|
||||
|
||||
namespace TrueCV.Application.Interfaces;
|
||||
namespace RealCV.Application.Interfaces;
|
||||
|
||||
public interface ICompanyNameMatcherService
|
||||
{
|
||||
@@ -1,7 +1,7 @@
|
||||
using TrueCV.Application.DTOs;
|
||||
using TrueCV.Application.Models;
|
||||
using RealCV.Application.DTOs;
|
||||
using RealCV.Application.Models;
|
||||
|
||||
namespace TrueCV.Application.Interfaces;
|
||||
namespace RealCV.Application.Interfaces;
|
||||
|
||||
public interface ICompanyVerifierService
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using TrueCV.Application.Models;
|
||||
using RealCV.Application.Models;
|
||||
|
||||
namespace TrueCV.Application.Interfaces;
|
||||
namespace RealCV.Application.Interfaces;
|
||||
|
||||
public interface IEducationVerifierService
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace TrueCV.Application.Interfaces;
|
||||
namespace RealCV.Application.Interfaces;
|
||||
|
||||
public interface IFileStorageService
|
||||
{
|
||||
36
src/RealCV.Application/Interfaces/IGitHubVerifierService.cs
Normal file
36
src/RealCV.Application/Interfaces/IGitHubVerifierService.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using RealCV.Application.Models;
|
||||
|
||||
namespace RealCV.Application.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Service for verifying developer profiles and skills via GitHub
|
||||
/// </summary>
|
||||
public interface IGitHubVerifierService
|
||||
{
|
||||
/// <summary>
|
||||
/// Verify a GitHub profile and analyze activity
|
||||
/// </summary>
|
||||
Task<GitHubVerificationResult> VerifyProfileAsync(string username);
|
||||
|
||||
/// <summary>
|
||||
/// Verify claimed programming skills against GitHub activity
|
||||
/// </summary>
|
||||
Task<GitHubVerificationResult> VerifySkillsAsync(
|
||||
string username,
|
||||
List<string> claimedSkills);
|
||||
|
||||
/// <summary>
|
||||
/// Search for GitHub profiles matching a name
|
||||
/// </summary>
|
||||
Task<List<GitHubProfileSearchResult>> SearchProfilesAsync(string name);
|
||||
}
|
||||
|
||||
public sealed record GitHubProfileSearchResult
|
||||
{
|
||||
public required string Username { get; init; }
|
||||
public string? Name { get; init; }
|
||||
public string? AvatarUrl { get; init; }
|
||||
public string? Bio { get; init; }
|
||||
public int PublicRepos { get; init; }
|
||||
public int Followers { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using RealCV.Application.Models;
|
||||
|
||||
namespace RealCV.Application.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Service for verifying professional qualifications (FCA)
|
||||
/// </summary>
|
||||
public interface IProfessionalVerifierService
|
||||
{
|
||||
/// <summary>
|
||||
/// Verify if a person is registered with the FCA
|
||||
/// </summary>
|
||||
Task<ProfessionalVerificationResult> VerifyFcaRegistrationAsync(
|
||||
string name,
|
||||
string? firmName = null,
|
||||
string? referenceNumber = null);
|
||||
|
||||
/// <summary>
|
||||
/// Search FCA register for individuals
|
||||
/// </summary>
|
||||
Task<List<FcaIndividualSearchResult>> SearchFcaIndividualsAsync(string name);
|
||||
}
|
||||
|
||||
public sealed record FcaIndividualSearchResult
|
||||
{
|
||||
public required string Name { get; init; }
|
||||
public required string IndividualReferenceNumber { get; init; }
|
||||
public string? Status { get; init; }
|
||||
public List<string>? CurrentFirms { get; init; }
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using TrueCV.Application.Models;
|
||||
using RealCV.Application.Models;
|
||||
|
||||
namespace TrueCV.Application.Interfaces;
|
||||
namespace RealCV.Application.Interfaces;
|
||||
|
||||
public interface ITimelineAnalyserService
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace TrueCV.Application.Interfaces;
|
||||
namespace RealCV.Application.Interfaces;
|
||||
|
||||
public interface IUserContextService
|
||||
{
|
||||
62
src/RealCV.Application/Models/AcademicVerificationResult.cs
Normal file
62
src/RealCV.Application/Models/AcademicVerificationResult.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
namespace RealCV.Application.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Result of verifying an academic researcher via ORCID
|
||||
/// </summary>
|
||||
public sealed record AcademicVerificationResult
|
||||
{
|
||||
public required string ClaimedName { get; init; }
|
||||
public required bool IsVerified { get; init; }
|
||||
|
||||
// ORCID profile
|
||||
public string? OrcidId { get; init; }
|
||||
public string? MatchedName { get; init; }
|
||||
public string? OrcidUrl { get; init; }
|
||||
|
||||
// Academic affiliations
|
||||
public List<AcademicAffiliation> Affiliations { get; init; } = [];
|
||||
|
||||
// Publications
|
||||
public int TotalPublications { get; init; }
|
||||
public List<Publication> RecentPublications { get; init; } = [];
|
||||
|
||||
// Education from ORCID
|
||||
public List<AcademicEducation> Education { get; init; } = [];
|
||||
|
||||
public string? VerificationNotes { get; init; }
|
||||
public List<AcademicVerificationFlag> Flags { get; init; } = [];
|
||||
}
|
||||
|
||||
public sealed record AcademicAffiliation
|
||||
{
|
||||
public required string Organization { get; init; }
|
||||
public string? Department { get; init; }
|
||||
public string? Role { get; init; }
|
||||
public DateOnly? StartDate { get; init; }
|
||||
public DateOnly? EndDate { get; init; }
|
||||
}
|
||||
|
||||
public sealed record Publication
|
||||
{
|
||||
public required string Title { get; init; }
|
||||
public string? Journal { get; init; }
|
||||
public int? Year { get; init; }
|
||||
public string? Doi { get; init; }
|
||||
public string? Type { get; init; }
|
||||
}
|
||||
|
||||
public sealed record AcademicEducation
|
||||
{
|
||||
public required string Institution { get; init; }
|
||||
public string? Degree { get; init; }
|
||||
public string? Subject { get; init; }
|
||||
public int? Year { get; init; }
|
||||
}
|
||||
|
||||
public sealed record AcademicVerificationFlag
|
||||
{
|
||||
public required string Type { get; init; }
|
||||
public required string Severity { get; init; }
|
||||
public required string Message { get; init; }
|
||||
public int ScoreImpact { get; init; }
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace TrueCV.Application.Models;
|
||||
namespace RealCV.Application.Models;
|
||||
|
||||
public sealed record CVData
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace TrueCV.Application.Models;
|
||||
namespace RealCV.Application.Models;
|
||||
|
||||
public sealed record CompanyVerificationResult
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace TrueCV.Application.Models;
|
||||
namespace RealCV.Application.Models;
|
||||
|
||||
public sealed record EducationEntry
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace TrueCV.Application.Models;
|
||||
namespace RealCV.Application.Models;
|
||||
|
||||
public sealed record EducationVerificationResult
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace TrueCV.Application.Models;
|
||||
namespace RealCV.Application.Models;
|
||||
|
||||
public sealed record EmploymentEntry
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace TrueCV.Application.Models;
|
||||
namespace RealCV.Application.Models;
|
||||
|
||||
public sealed record FlagResult
|
||||
{
|
||||
52
src/RealCV.Application/Models/GitHubVerificationResult.cs
Normal file
52
src/RealCV.Application/Models/GitHubVerificationResult.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
namespace RealCV.Application.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Result of verifying a developer's GitHub profile
|
||||
/// </summary>
|
||||
public sealed record GitHubVerificationResult
|
||||
{
|
||||
public required string ClaimedUsername { get; init; }
|
||||
public required bool IsVerified { get; init; }
|
||||
|
||||
// Profile details
|
||||
public string? ProfileName { get; init; }
|
||||
public string? ProfileUrl { get; init; }
|
||||
public string? Bio { get; init; }
|
||||
public string? Company { get; init; }
|
||||
public string? Location { get; init; }
|
||||
public DateOnly? AccountCreated { get; init; }
|
||||
|
||||
// Activity metrics
|
||||
public int PublicRepos { get; init; }
|
||||
public int Followers { get; init; }
|
||||
public int Following { get; init; }
|
||||
public int TotalContributions { get; init; }
|
||||
|
||||
// Language breakdown
|
||||
public Dictionary<string, int> LanguageStats { get; init; } = new();
|
||||
|
||||
// Claimed skills verification
|
||||
public List<SkillVerification> SkillVerifications { get; init; } = [];
|
||||
|
||||
public string? VerificationNotes { get; init; }
|
||||
public List<GitHubVerificationFlag> Flags { get; init; } = [];
|
||||
}
|
||||
|
||||
public sealed record SkillVerification
|
||||
{
|
||||
public required string ClaimedSkill { get; init; }
|
||||
public required bool IsVerified { get; init; }
|
||||
public int RepoCount { get; init; }
|
||||
public int TotalLinesOfCode { get; init; }
|
||||
public DateOnly? FirstUsed { get; init; }
|
||||
public DateOnly? LastUsed { get; init; }
|
||||
public string? Notes { get; init; }
|
||||
}
|
||||
|
||||
public sealed record GitHubVerificationFlag
|
||||
{
|
||||
public required string Type { get; init; }
|
||||
public required string Severity { get; init; }
|
||||
public required string Message { get; init; }
|
||||
public int ScoreImpact { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
namespace RealCV.Application.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Result of verifying a professional qualification (FCA)
|
||||
/// </summary>
|
||||
public sealed record ProfessionalVerificationResult
|
||||
{
|
||||
public required string ClaimedName { get; init; }
|
||||
public required string ProfessionalBody { get; init; }
|
||||
public required bool IsVerified { get; init; }
|
||||
|
||||
// Matched professional details
|
||||
public string? MatchedName { get; init; }
|
||||
public string? RegistrationNumber { get; init; }
|
||||
public string? Status { get; init; }
|
||||
public string? CurrentEmployer { get; init; }
|
||||
public DateOnly? RegistrationDate { get; init; }
|
||||
|
||||
// For FCA
|
||||
public List<string>? ApprovedFunctions { get; init; }
|
||||
public List<string>? ControlledFunctions { get; init; }
|
||||
|
||||
public string? VerificationNotes { get; init; }
|
||||
public List<ProfessionalVerificationFlag> Flags { get; init; } = [];
|
||||
}
|
||||
|
||||
public sealed record ProfessionalVerificationFlag
|
||||
{
|
||||
public required string Type { get; init; }
|
||||
public required string Severity { get; init; }
|
||||
public required string Message { get; init; }
|
||||
public int ScoreImpact { get; init; }
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace TrueCV.Application.Models;
|
||||
namespace RealCV.Application.Models;
|
||||
|
||||
public record SemanticMatchResult
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace TrueCV.Application.Models;
|
||||
namespace RealCV.Application.Models;
|
||||
|
||||
public sealed record TimelineAnalysisResult
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace TrueCV.Application.Models;
|
||||
namespace RealCV.Application.Models;
|
||||
|
||||
public sealed record TimelineGap
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace TrueCV.Application.Models;
|
||||
namespace RealCV.Application.Models;
|
||||
|
||||
public sealed record TimelineOverlap
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace TrueCV.Application.Models;
|
||||
namespace RealCV.Application.Models;
|
||||
|
||||
public sealed record VeracityReport
|
||||
{
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\TrueCV.Domain\TrueCV.Domain.csproj" />
|
||||
<ProjectReference Include="..\RealCV.Domain\RealCV.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TrueCV.Domain.Entities;
|
||||
namespace RealCV.Domain.Entities;
|
||||
|
||||
public class AuditLog
|
||||
{
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using TrueCV.Domain.Enums;
|
||||
using RealCV.Domain.Enums;
|
||||
|
||||
namespace TrueCV.Domain.Entities;
|
||||
namespace RealCV.Domain.Entities;
|
||||
|
||||
public class CVCheck
|
||||
{
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using TrueCV.Domain.Enums;
|
||||
using RealCV.Domain.Enums;
|
||||
|
||||
namespace TrueCV.Domain.Entities;
|
||||
namespace RealCV.Domain.Entities;
|
||||
|
||||
public class CVFlag
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TrueCV.Domain.Entities;
|
||||
namespace RealCV.Domain.Entities;
|
||||
|
||||
public class CompanyCache
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace TrueCV.Domain.Enums;
|
||||
namespace RealCV.Domain.Enums;
|
||||
|
||||
public enum CheckStatus
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace TrueCV.Domain.Enums;
|
||||
namespace RealCV.Domain.Enums;
|
||||
|
||||
public enum FlagCategory
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace TrueCV.Domain.Enums;
|
||||
namespace RealCV.Domain.Enums;
|
||||
|
||||
public enum FlagSeverity
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace TrueCV.Domain.Enums;
|
||||
namespace RealCV.Domain.Enums;
|
||||
|
||||
public enum UserPlan
|
||||
{
|
||||
216
src/RealCV.Infrastructure/Clients/FcaRegisterClient.cs
Normal file
216
src/RealCV.Infrastructure/Clients/FcaRegisterClient.cs
Normal file
@@ -0,0 +1,216 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace RealCV.Infrastructure.Clients;
|
||||
|
||||
public sealed class FcaRegisterClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger<FcaRegisterClient> _logger;
|
||||
private readonly string _apiKey;
|
||||
|
||||
public FcaRegisterClient(
|
||||
HttpClient httpClient,
|
||||
IOptions<FcaOptions> options,
|
||||
ILogger<FcaRegisterClient> logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
_apiKey = options.Value.ApiKey;
|
||||
|
||||
_httpClient.BaseAddress = new Uri("https://register.fca.org.uk/services/V0.1/");
|
||||
_httpClient.DefaultRequestHeaders.Add("X-Auth-Email", options.Value.Email);
|
||||
_httpClient.DefaultRequestHeaders.Add("X-Auth-Key", _apiKey);
|
||||
}
|
||||
|
||||
public async Task<FcaIndividualResponse?> SearchIndividualsAsync(string name, int page = 1)
|
||||
{
|
||||
try
|
||||
{
|
||||
var encodedName = Uri.EscapeDataString(name);
|
||||
var url = $"Individuals?q={encodedName}&page={page}";
|
||||
|
||||
_logger.LogDebug("Searching FCA for individual: {Name}", name);
|
||||
|
||||
var response = await _httpClient.GetAsync(url);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning("FCA API returned {StatusCode} for search: {Name}",
|
||||
response.StatusCode, name);
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<FcaIndividualResponse>(JsonOptions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error searching FCA for individual: {Name}", name);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<FcaIndividualDetails?> GetIndividualAsync(string individualReferenceNumber)
|
||||
{
|
||||
try
|
||||
{
|
||||
var url = $"Individuals/{individualReferenceNumber}";
|
||||
|
||||
_logger.LogDebug("Getting FCA individual: {IRN}", individualReferenceNumber);
|
||||
|
||||
var response = await _httpClient.GetAsync(url);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning("FCA API returned {StatusCode} for IRN: {IRN}",
|
||||
response.StatusCode, individualReferenceNumber);
|
||||
return null;
|
||||
}
|
||||
|
||||
var wrapper = await response.Content.ReadFromJsonAsync<FcaIndividualDetailsWrapper>(JsonOptions);
|
||||
return wrapper?.Data?.FirstOrDefault();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting FCA individual: {IRN}", individualReferenceNumber);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<FcaFirmResponse?> SearchFirmsAsync(string name, int page = 1)
|
||||
{
|
||||
try
|
||||
{
|
||||
var encodedName = Uri.EscapeDataString(name);
|
||||
var url = $"Firms?q={encodedName}&page={page}";
|
||||
|
||||
var response = await _httpClient.GetAsync(url);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<FcaFirmResponse>(JsonOptions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error searching FCA for firm: {Name}", name);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
}
|
||||
|
||||
public class FcaOptions
|
||||
{
|
||||
public string ApiKey { get; set; } = string.Empty;
|
||||
public string Email { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
// Response models
|
||||
public class FcaIndividualResponse
|
||||
{
|
||||
public List<FcaIndividualSearchItem>? Data { get; set; }
|
||||
public FcaPagination? Pagination { get; set; }
|
||||
}
|
||||
|
||||
public class FcaIndividualSearchItem
|
||||
{
|
||||
[JsonPropertyName("Individual Reference Number")]
|
||||
public string? IndividualReferenceNumber { get; set; }
|
||||
|
||||
public string? Name { get; set; }
|
||||
public string? Status { get; set; }
|
||||
|
||||
[JsonPropertyName("Current Employer(s)")]
|
||||
public string? CurrentEmployers { get; set; }
|
||||
}
|
||||
|
||||
public class FcaIndividualDetailsWrapper
|
||||
{
|
||||
public List<FcaIndividualDetails>? Data { get; set; }
|
||||
}
|
||||
|
||||
public class FcaIndividualDetails
|
||||
{
|
||||
[JsonPropertyName("Individual Reference Number")]
|
||||
public string? IndividualReferenceNumber { get; set; }
|
||||
|
||||
public string? Name { get; set; }
|
||||
public string? Status { get; set; }
|
||||
|
||||
[JsonPropertyName("Effective Date")]
|
||||
public string? EffectiveDate { get; set; }
|
||||
|
||||
[JsonPropertyName("Controlled Functions")]
|
||||
public List<FcaControlledFunction>? ControlledFunctions { get; set; }
|
||||
|
||||
[JsonPropertyName("Previous Employments")]
|
||||
public List<FcaPreviousEmployment>? PreviousEmployments { get; set; }
|
||||
}
|
||||
|
||||
public class FcaControlledFunction
|
||||
{
|
||||
[JsonPropertyName("Controlled Function")]
|
||||
public string? ControlledFunction { get; set; }
|
||||
|
||||
[JsonPropertyName("Firm Name")]
|
||||
public string? FirmName { get; set; }
|
||||
|
||||
[JsonPropertyName("Firm Reference Number")]
|
||||
public string? FirmReferenceNumber { get; set; }
|
||||
|
||||
[JsonPropertyName("Status")]
|
||||
public string? Status { get; set; }
|
||||
|
||||
[JsonPropertyName("Effective From")]
|
||||
public string? EffectiveFrom { get; set; }
|
||||
}
|
||||
|
||||
public class FcaPreviousEmployment
|
||||
{
|
||||
[JsonPropertyName("Firm Name")]
|
||||
public string? FirmName { get; set; }
|
||||
|
||||
[JsonPropertyName("Firm Reference Number")]
|
||||
public string? FirmReferenceNumber { get; set; }
|
||||
|
||||
[JsonPropertyName("Start Date")]
|
||||
public string? StartDate { get; set; }
|
||||
|
||||
[JsonPropertyName("End Date")]
|
||||
public string? EndDate { get; set; }
|
||||
}
|
||||
|
||||
public class FcaFirmResponse
|
||||
{
|
||||
public List<FcaFirmSearchItem>? Data { get; set; }
|
||||
public FcaPagination? Pagination { get; set; }
|
||||
}
|
||||
|
||||
public class FcaFirmSearchItem
|
||||
{
|
||||
[JsonPropertyName("Firm Reference Number")]
|
||||
public string? FirmReferenceNumber { get; set; }
|
||||
|
||||
[JsonPropertyName("Firm Name")]
|
||||
public string? FirmName { get; set; }
|
||||
|
||||
public string? Status { get; set; }
|
||||
}
|
||||
|
||||
public class FcaPagination
|
||||
{
|
||||
public int Page { get; set; }
|
||||
public int TotalPages { get; set; }
|
||||
public int TotalItems { get; set; }
|
||||
}
|
||||
265
src/RealCV.Infrastructure/Clients/GitHubClient.cs
Normal file
265
src/RealCV.Infrastructure/Clients/GitHubClient.cs
Normal file
@@ -0,0 +1,265 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace RealCV.Infrastructure.Clients;
|
||||
|
||||
public sealed class GitHubApiClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger<GitHubApiClient> _logger;
|
||||
|
||||
public GitHubApiClient(
|
||||
HttpClient httpClient,
|
||||
IOptions<GitHubOptions> options,
|
||||
ILogger<GitHubApiClient> logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
|
||||
_httpClient.BaseAddress = new Uri("https://api.github.com/");
|
||||
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github+json"));
|
||||
_httpClient.DefaultRequestHeaders.Add("X-GitHub-Api-Version", "2022-11-28");
|
||||
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("RealCV/1.0");
|
||||
|
||||
if (!string.IsNullOrEmpty(options.Value.PersonalAccessToken))
|
||||
{
|
||||
_httpClient.DefaultRequestHeaders.Authorization =
|
||||
new AuthenticationHeaderValue("Bearer", options.Value.PersonalAccessToken);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<GitHubUser?> GetUserAsync(string username)
|
||||
{
|
||||
try
|
||||
{
|
||||
var url = $"users/{Uri.EscapeDataString(username)}";
|
||||
|
||||
_logger.LogDebug("Getting GitHub user: {Username}", username);
|
||||
|
||||
var response = await _httpClient.GetAsync(url);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning("GitHub API returned {StatusCode} for user: {Username}",
|
||||
response.StatusCode, username);
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<GitHubUser>(JsonOptions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting GitHub user: {Username}", username);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<GitHubRepo>> GetUserReposAsync(string username, int perPage = 100)
|
||||
{
|
||||
var repos = new List<GitHubRepo>();
|
||||
var page = 1;
|
||||
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var url = $"users/{Uri.EscapeDataString(username)}/repos?per_page={perPage}&page={page}&sort=updated";
|
||||
|
||||
var response = await _httpClient.GetAsync(url);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var pageRepos = await response.Content.ReadFromJsonAsync<List<GitHubRepo>>(JsonOptions);
|
||||
|
||||
if (pageRepos == null || pageRepos.Count == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
repos.AddRange(pageRepos);
|
||||
|
||||
if (pageRepos.Count < perPage)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
page++;
|
||||
|
||||
// Limit to avoid rate limiting
|
||||
if (page > 5)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting repos for user: {Username}", username);
|
||||
}
|
||||
|
||||
return repos;
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, int>?> GetRepoLanguagesAsync(string owner, string repo)
|
||||
{
|
||||
try
|
||||
{
|
||||
var url = $"repos/{Uri.EscapeDataString(owner)}/{Uri.EscapeDataString(repo)}/languages";
|
||||
|
||||
var response = await _httpClient.GetAsync(url);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<Dictionary<string, int>>(JsonOptions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting languages for repo: {Owner}/{Repo}", owner, repo);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<GitHubUserSearchResponse?> SearchUsersAsync(string query, int perPage = 30)
|
||||
{
|
||||
try
|
||||
{
|
||||
var encodedQuery = Uri.EscapeDataString(query);
|
||||
var url = $"search/users?q={encodedQuery}&per_page={perPage}";
|
||||
|
||||
var response = await _httpClient.GetAsync(url);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<GitHubUserSearchResponse>(JsonOptions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error searching GitHub users: {Query}", query);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
}
|
||||
|
||||
public class GitHubOptions
|
||||
{
|
||||
public string PersonalAccessToken { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
// Response models
|
||||
public class GitHubUser
|
||||
{
|
||||
public string? Login { get; set; }
|
||||
public int Id { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? Company { get; set; }
|
||||
public string? Blog { get; set; }
|
||||
public string? Location { get; set; }
|
||||
public string? Email { get; set; }
|
||||
public string? Bio { get; set; }
|
||||
|
||||
[JsonPropertyName("twitter_username")]
|
||||
public string? TwitterUsername { get; set; }
|
||||
|
||||
[JsonPropertyName("public_repos")]
|
||||
public int PublicRepos { get; set; }
|
||||
|
||||
[JsonPropertyName("public_gists")]
|
||||
public int PublicGists { get; set; }
|
||||
|
||||
public int Followers { get; set; }
|
||||
public int Following { get; set; }
|
||||
|
||||
[JsonPropertyName("created_at")]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
[JsonPropertyName("updated_at")]
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
|
||||
[JsonPropertyName("html_url")]
|
||||
public string? HtmlUrl { get; set; }
|
||||
|
||||
[JsonPropertyName("avatar_url")]
|
||||
public string? AvatarUrl { get; set; }
|
||||
}
|
||||
|
||||
public class GitHubRepo
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string? Name { get; set; }
|
||||
|
||||
[JsonPropertyName("full_name")]
|
||||
public string? FullName { get; set; }
|
||||
|
||||
public string? Description { get; set; }
|
||||
public string? Language { get; set; }
|
||||
|
||||
[JsonPropertyName("html_url")]
|
||||
public string? HtmlUrl { get; set; }
|
||||
|
||||
public bool Fork { get; set; }
|
||||
public bool Private { get; set; }
|
||||
|
||||
[JsonPropertyName("stargazers_count")]
|
||||
public int StargazersCount { get; set; }
|
||||
|
||||
[JsonPropertyName("watchers_count")]
|
||||
public int WatchersCount { get; set; }
|
||||
|
||||
[JsonPropertyName("forks_count")]
|
||||
public int ForksCount { get; set; }
|
||||
|
||||
public int Size { get; set; }
|
||||
|
||||
[JsonPropertyName("created_at")]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
[JsonPropertyName("updated_at")]
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
|
||||
[JsonPropertyName("pushed_at")]
|
||||
public DateTime? PushedAt { get; set; }
|
||||
}
|
||||
|
||||
public class GitHubUserSearchResponse
|
||||
{
|
||||
[JsonPropertyName("total_count")]
|
||||
public int TotalCount { get; set; }
|
||||
|
||||
[JsonPropertyName("incomplete_results")]
|
||||
public bool IncompleteResults { get; set; }
|
||||
|
||||
public List<GitHubUserSearchItem>? Items { get; set; }
|
||||
}
|
||||
|
||||
public class GitHubUserSearchItem
|
||||
{
|
||||
public string? Login { get; set; }
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("avatar_url")]
|
||||
public string? AvatarUrl { get; set; }
|
||||
|
||||
[JsonPropertyName("html_url")]
|
||||
public string? HtmlUrl { get; set; }
|
||||
|
||||
public double Score { get; set; }
|
||||
}
|
||||
342
src/RealCV.Infrastructure/Clients/OrcidClient.cs
Normal file
342
src/RealCV.Infrastructure/Clients/OrcidClient.cs
Normal file
@@ -0,0 +1,342 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace RealCV.Infrastructure.Clients;
|
||||
|
||||
public sealed class OrcidClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger<OrcidClient> _logger;
|
||||
|
||||
public OrcidClient(
|
||||
HttpClient httpClient,
|
||||
ILogger<OrcidClient> logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
|
||||
_httpClient.BaseAddress = new Uri("https://pub.orcid.org/v3.0/");
|
||||
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
}
|
||||
|
||||
public async Task<OrcidSearchResponse?> SearchResearchersAsync(string query, int start = 0, int rows = 20)
|
||||
{
|
||||
try
|
||||
{
|
||||
var encodedQuery = Uri.EscapeDataString(query);
|
||||
var url = $"search?q={encodedQuery}&start={start}&rows={rows}";
|
||||
|
||||
_logger.LogDebug("Searching ORCID: {Query}", query);
|
||||
|
||||
var response = await _httpClient.GetAsync(url);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning("ORCID API returned {StatusCode} for search: {Query}",
|
||||
response.StatusCode, query);
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<OrcidSearchResponse>(JsonOptions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error searching ORCID: {Query}", query);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<OrcidRecord?> GetRecordAsync(string orcidId)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Normalize ORCID ID format (remove URL prefix if present)
|
||||
orcidId = NormalizeOrcidId(orcidId);
|
||||
|
||||
var url = $"{orcidId}/record";
|
||||
|
||||
_logger.LogDebug("Getting ORCID record: {OrcidId}", orcidId);
|
||||
|
||||
var response = await _httpClient.GetAsync(url);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning("ORCID API returned {StatusCode} for ID: {OrcidId}",
|
||||
response.StatusCode, orcidId);
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<OrcidRecord>(JsonOptions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting ORCID record: {OrcidId}", orcidId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<OrcidWorks?> GetWorksAsync(string orcidId)
|
||||
{
|
||||
try
|
||||
{
|
||||
orcidId = NormalizeOrcidId(orcidId);
|
||||
var url = $"{orcidId}/works";
|
||||
|
||||
var response = await _httpClient.GetAsync(url);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<OrcidWorks>(JsonOptions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting ORCID works: {OrcidId}", orcidId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<OrcidEmployments?> GetEmploymentsAsync(string orcidId)
|
||||
{
|
||||
try
|
||||
{
|
||||
orcidId = NormalizeOrcidId(orcidId);
|
||||
var url = $"{orcidId}/employments";
|
||||
|
||||
var response = await _httpClient.GetAsync(url);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<OrcidEmployments>(JsonOptions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting ORCID employments: {OrcidId}", orcidId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<OrcidEducations?> GetEducationsAsync(string orcidId)
|
||||
{
|
||||
try
|
||||
{
|
||||
orcidId = NormalizeOrcidId(orcidId);
|
||||
var url = $"{orcidId}/educations";
|
||||
|
||||
var response = await _httpClient.GetAsync(url);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<OrcidEducations>(JsonOptions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting ORCID educations: {OrcidId}", orcidId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeOrcidId(string orcidId)
|
||||
{
|
||||
// Remove URL prefixes
|
||||
orcidId = orcidId.Replace("https://orcid.org/", "")
|
||||
.Replace("http://orcid.org/", "")
|
||||
.Trim();
|
||||
return orcidId;
|
||||
}
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
}
|
||||
|
||||
// Response models
|
||||
public class OrcidSearchResponse
|
||||
{
|
||||
[JsonPropertyName("num-found")]
|
||||
public int NumFound { get; set; }
|
||||
|
||||
public List<OrcidSearchResult>? Result { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidSearchResult
|
||||
{
|
||||
[JsonPropertyName("orcid-identifier")]
|
||||
public OrcidIdentifier? OrcidIdentifier { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidIdentifier
|
||||
{
|
||||
public string? Uri { get; set; }
|
||||
public string? Path { get; set; }
|
||||
public string? Host { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidRecord
|
||||
{
|
||||
[JsonPropertyName("orcid-identifier")]
|
||||
public OrcidIdentifier? OrcidIdentifier { get; set; }
|
||||
|
||||
public OrcidPerson? Person { get; set; }
|
||||
|
||||
[JsonPropertyName("activities-summary")]
|
||||
public OrcidActivitiesSummary? ActivitiesSummary { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidPerson
|
||||
{
|
||||
public OrcidName? Name { get; set; }
|
||||
public OrcidBiography? Biography { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidName
|
||||
{
|
||||
[JsonPropertyName("given-names")]
|
||||
public OrcidValue? GivenNames { get; set; }
|
||||
|
||||
[JsonPropertyName("family-name")]
|
||||
public OrcidValue? FamilyName { get; set; }
|
||||
|
||||
[JsonPropertyName("credit-name")]
|
||||
public OrcidValue? CreditName { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidValue
|
||||
{
|
||||
public string? Value { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidBiography
|
||||
{
|
||||
public string? Content { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidActivitiesSummary
|
||||
{
|
||||
public OrcidEmployments? Employments { get; set; }
|
||||
public OrcidEducations? Educations { get; set; }
|
||||
public OrcidWorks? Works { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidEmployments
|
||||
{
|
||||
[JsonPropertyName("affiliation-group")]
|
||||
public List<OrcidAffiliationGroup>? AffiliationGroup { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidEducations
|
||||
{
|
||||
[JsonPropertyName("affiliation-group")]
|
||||
public List<OrcidAffiliationGroup>? AffiliationGroup { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidAffiliationGroup
|
||||
{
|
||||
public List<OrcidAffiliationSummaryWrapper>? Summaries { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidAffiliationSummaryWrapper
|
||||
{
|
||||
[JsonPropertyName("employment-summary")]
|
||||
public OrcidAffiliationSummary? EmploymentSummary { get; set; }
|
||||
|
||||
[JsonPropertyName("education-summary")]
|
||||
public OrcidAffiliationSummary? EducationSummary { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidAffiliationSummary
|
||||
{
|
||||
[JsonPropertyName("department-name")]
|
||||
public string? DepartmentName { get; set; }
|
||||
|
||||
[JsonPropertyName("role-title")]
|
||||
public string? RoleTitle { get; set; }
|
||||
|
||||
[JsonPropertyName("start-date")]
|
||||
public OrcidDate? StartDate { get; set; }
|
||||
|
||||
[JsonPropertyName("end-date")]
|
||||
public OrcidDate? EndDate { get; set; }
|
||||
|
||||
public OrcidOrganization? Organization { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidDate
|
||||
{
|
||||
public OrcidValue? Year { get; set; }
|
||||
public OrcidValue? Month { get; set; }
|
||||
public OrcidValue? Day { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidOrganization
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public OrcidAddress? Address { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidAddress
|
||||
{
|
||||
public string? City { get; set; }
|
||||
public string? Region { get; set; }
|
||||
public string? Country { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidWorks
|
||||
{
|
||||
public List<OrcidWorkGroup>? Group { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidWorkGroup
|
||||
{
|
||||
[JsonPropertyName("work-summary")]
|
||||
public List<OrcidWorkSummary>? WorkSummary { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidWorkSummary
|
||||
{
|
||||
public string? Title { get; set; }
|
||||
public OrcidTitle? TitleObj { get; set; }
|
||||
public string? Type { get; set; }
|
||||
|
||||
[JsonPropertyName("publication-date")]
|
||||
public OrcidDate? PublicationDate { get; set; }
|
||||
|
||||
[JsonPropertyName("journal-title")]
|
||||
public OrcidValue? JournalTitle { get; set; }
|
||||
|
||||
[JsonPropertyName("external-ids")]
|
||||
public OrcidExternalIds? ExternalIds { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidTitle
|
||||
{
|
||||
public OrcidValue? Title { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidExternalIds
|
||||
{
|
||||
[JsonPropertyName("external-id")]
|
||||
public List<OrcidExternalId>? ExternalId { get; set; }
|
||||
}
|
||||
|
||||
public class OrcidExternalId
|
||||
{
|
||||
[JsonPropertyName("external-id-type")]
|
||||
public string? ExternalIdType { get; set; }
|
||||
|
||||
[JsonPropertyName("external-id-value")]
|
||||
public string? ExternalIdValue { get; set; }
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace TrueCV.Infrastructure.Configuration;
|
||||
namespace RealCV.Infrastructure.Configuration;
|
||||
|
||||
public sealed class AnthropicSettings
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace TrueCV.Infrastructure.Configuration;
|
||||
namespace RealCV.Infrastructure.Configuration;
|
||||
|
||||
public sealed class AzureBlobSettings
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace TrueCV.Infrastructure.Configuration;
|
||||
namespace RealCV.Infrastructure.Configuration;
|
||||
|
||||
public sealed class CompaniesHouseSettings
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace TrueCV.Infrastructure.Configuration;
|
||||
namespace RealCV.Infrastructure.Configuration;
|
||||
|
||||
public sealed class LocalStorageSettings
|
||||
{
|
||||
@@ -1,10 +1,10 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TrueCV.Domain.Entities;
|
||||
using TrueCV.Infrastructure.Identity;
|
||||
using RealCV.Domain.Entities;
|
||||
using RealCV.Infrastructure.Identity;
|
||||
|
||||
namespace TrueCV.Infrastructure.Data;
|
||||
namespace RealCV.Infrastructure.Data;
|
||||
|
||||
public class ApplicationDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid>, Guid>
|
||||
{
|
||||
@@ -5,11 +5,11 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using TrueCV.Infrastructure.Data;
|
||||
using RealCV.Infrastructure.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace TrueCV.Infrastructure.Data.Migrations
|
||||
namespace RealCV.Infrastructure.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20260118182916_InitialCreate")]
|
||||
@@ -156,7 +156,7 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrueCV.Domain.Entities.CVCheck", b =>
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CVCheck", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -211,7 +211,7 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
||||
b.ToTable("CVChecks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrueCV.Domain.Entities.CVFlag", b =>
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CVFlag", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -251,7 +251,7 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
||||
b.ToTable("CVFlags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrueCV.Domain.Entities.CompanyCache", b =>
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CompanyCache", b =>
|
||||
{
|
||||
b.Property<string>("CompanyNumber")
|
||||
.HasMaxLength(32)
|
||||
@@ -281,7 +281,7 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
||||
b.ToTable("CompanyCache");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrueCV.Domain.Entities.User", b =>
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -307,7 +307,7 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
||||
b.ToTable("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrueCV.Infrastructure.Identity.ApplicationUser", b =>
|
||||
modelBuilder.Entity("RealCV.Infrastructure.Identity.ApplicationUser", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -396,7 +396,7 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("TrueCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -405,7 +405,7 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("TrueCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -420,7 +420,7 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("TrueCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -429,29 +429,29 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("TrueCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrueCV.Domain.Entities.CVCheck", b =>
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CVCheck", b =>
|
||||
{
|
||||
b.HasOne("TrueCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany("CVChecks")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("TrueCV.Domain.Entities.User", null)
|
||||
b.HasOne("RealCV.Domain.Entities.User", null)
|
||||
.WithMany("CVChecks")
|
||||
.HasForeignKey("UserId1");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrueCV.Domain.Entities.CVFlag", b =>
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CVFlag", b =>
|
||||
{
|
||||
b.HasOne("TrueCV.Domain.Entities.CVCheck", "CVCheck")
|
||||
b.HasOne("RealCV.Domain.Entities.CVCheck", "CVCheck")
|
||||
.WithMany("Flags")
|
||||
.HasForeignKey("CVCheckId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -460,17 +460,17 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
||||
b.Navigation("CVCheck");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrueCV.Domain.Entities.CVCheck", b =>
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CVCheck", b =>
|
||||
{
|
||||
b.Navigation("Flags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrueCV.Domain.Entities.User", b =>
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.User", b =>
|
||||
{
|
||||
b.Navigation("CVChecks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrueCV.Infrastructure.Identity.ApplicationUser", b =>
|
||||
modelBuilder.Entity("RealCV.Infrastructure.Identity.ApplicationUser", b =>
|
||||
{
|
||||
b.Navigation("CVChecks");
|
||||
});
|
||||
@@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace TrueCV.Infrastructure.Data.Migrations
|
||||
namespace RealCV.Infrastructure.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialCreate : Migration
|
||||
@@ -5,11 +5,11 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using TrueCV.Infrastructure.Data;
|
||||
using RealCV.Infrastructure.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace TrueCV.Infrastructure.Data.Migrations
|
||||
namespace RealCV.Infrastructure.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20260120191035_AddProcessingStageToCV")]
|
||||
@@ -156,7 +156,7 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrueCV.Domain.Entities.CVCheck", b =>
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CVCheck", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -210,7 +210,7 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
||||
b.ToTable("CVChecks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrueCV.Domain.Entities.CVFlag", b =>
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CVFlag", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -250,7 +250,7 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
||||
b.ToTable("CVFlags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrueCV.Domain.Entities.CompanyCache", b =>
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CompanyCache", b =>
|
||||
{
|
||||
b.Property<string>("CompanyNumber")
|
||||
.HasMaxLength(32)
|
||||
@@ -292,7 +292,7 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
||||
b.ToTable("CompanyCache");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrueCV.Infrastructure.Identity.ApplicationUser", b =>
|
||||
modelBuilder.Entity("RealCV.Infrastructure.Identity.ApplicationUser", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -381,7 +381,7 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("TrueCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -390,7 +390,7 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("TrueCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -405,7 +405,7 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("TrueCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -414,25 +414,25 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("TrueCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrueCV.Domain.Entities.CVCheck", b =>
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CVCheck", b =>
|
||||
{
|
||||
b.HasOne("TrueCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany("CVChecks")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrueCV.Domain.Entities.CVFlag", b =>
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CVFlag", b =>
|
||||
{
|
||||
b.HasOne("TrueCV.Domain.Entities.CVCheck", "CVCheck")
|
||||
b.HasOne("RealCV.Domain.Entities.CVCheck", "CVCheck")
|
||||
.WithMany("Flags")
|
||||
.HasForeignKey("CVCheckId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -441,12 +441,12 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
||||
b.Navigation("CVCheck");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrueCV.Domain.Entities.CVCheck", b =>
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CVCheck", b =>
|
||||
{
|
||||
b.Navigation("Flags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrueCV.Infrastructure.Identity.ApplicationUser", b =>
|
||||
modelBuilder.Entity("RealCV.Infrastructure.Identity.ApplicationUser", b =>
|
||||
{
|
||||
b.Navigation("CVChecks");
|
||||
});
|
||||
@@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace TrueCV.Infrastructure.Data.Migrations
|
||||
namespace RealCV.Infrastructure.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddProcessingStageToCV : Migration
|
||||
@@ -5,11 +5,11 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using TrueCV.Infrastructure.Data;
|
||||
using RealCV.Infrastructure.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace TrueCV.Infrastructure.Data.Migrations
|
||||
namespace RealCV.Infrastructure.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20260120194532_AddAuditLogTable")]
|
||||
@@ -156,7 +156,7 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrueCV.Domain.Entities.AuditLog", b =>
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.AuditLog", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -202,7 +202,7 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
||||
b.ToTable("AuditLogs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrueCV.Domain.Entities.CVCheck", b =>
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CVCheck", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -256,7 +256,7 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
||||
b.ToTable("CVChecks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrueCV.Domain.Entities.CVFlag", b =>
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CVFlag", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -296,7 +296,7 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
||||
b.ToTable("CVFlags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrueCV.Domain.Entities.CompanyCache", b =>
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CompanyCache", b =>
|
||||
{
|
||||
b.Property<string>("CompanyNumber")
|
||||
.HasMaxLength(32)
|
||||
@@ -338,7 +338,7 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
||||
b.ToTable("CompanyCache");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrueCV.Infrastructure.Identity.ApplicationUser", b =>
|
||||
modelBuilder.Entity("RealCV.Infrastructure.Identity.ApplicationUser", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -427,7 +427,7 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("TrueCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -436,7 +436,7 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("TrueCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -451,7 +451,7 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("TrueCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -460,25 +460,25 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("TrueCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrueCV.Domain.Entities.CVCheck", b =>
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CVCheck", b =>
|
||||
{
|
||||
b.HasOne("TrueCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany("CVChecks")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrueCV.Domain.Entities.CVFlag", b =>
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CVFlag", b =>
|
||||
{
|
||||
b.HasOne("TrueCV.Domain.Entities.CVCheck", "CVCheck")
|
||||
b.HasOne("RealCV.Domain.Entities.CVCheck", "CVCheck")
|
||||
.WithMany("Flags")
|
||||
.HasForeignKey("CVCheckId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -487,12 +487,12 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
||||
b.Navigation("CVCheck");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrueCV.Domain.Entities.CVCheck", b =>
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CVCheck", b =>
|
||||
{
|
||||
b.Navigation("Flags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrueCV.Infrastructure.Identity.ApplicationUser", b =>
|
||||
modelBuilder.Entity("RealCV.Infrastructure.Identity.ApplicationUser", b =>
|
||||
{
|
||||
b.Navigation("CVChecks");
|
||||
});
|
||||
@@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace TrueCV.Infrastructure.Data.Migrations
|
||||
namespace RealCV.Infrastructure.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddAuditLogTable : Migration
|
||||
@@ -4,11 +4,11 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using TrueCV.Infrastructure.Data;
|
||||
using RealCV.Infrastructure.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace TrueCV.Infrastructure.Data.Migrations
|
||||
namespace RealCV.Infrastructure.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
|
||||
@@ -153,7 +153,7 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrueCV.Domain.Entities.AuditLog", b =>
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.AuditLog", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -199,7 +199,7 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
||||
b.ToTable("AuditLogs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrueCV.Domain.Entities.CVCheck", b =>
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CVCheck", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -253,7 +253,7 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
||||
b.ToTable("CVChecks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrueCV.Domain.Entities.CVFlag", b =>
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CVFlag", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -293,7 +293,7 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
||||
b.ToTable("CVFlags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrueCV.Domain.Entities.CompanyCache", b =>
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CompanyCache", b =>
|
||||
{
|
||||
b.Property<string>("CompanyNumber")
|
||||
.HasMaxLength(32)
|
||||
@@ -335,7 +335,7 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
||||
b.ToTable("CompanyCache");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrueCV.Infrastructure.Identity.ApplicationUser", b =>
|
||||
modelBuilder.Entity("RealCV.Infrastructure.Identity.ApplicationUser", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -424,7 +424,7 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("TrueCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -433,7 +433,7 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("TrueCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -448,7 +448,7 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("TrueCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -457,25 +457,25 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("TrueCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrueCV.Domain.Entities.CVCheck", b =>
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CVCheck", b =>
|
||||
{
|
||||
b.HasOne("TrueCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
b.HasOne("RealCV.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany("CVChecks")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrueCV.Domain.Entities.CVFlag", b =>
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CVFlag", b =>
|
||||
{
|
||||
b.HasOne("TrueCV.Domain.Entities.CVCheck", "CVCheck")
|
||||
b.HasOne("RealCV.Domain.Entities.CVCheck", "CVCheck")
|
||||
.WithMany("Flags")
|
||||
.HasForeignKey("CVCheckId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -484,12 +484,12 @@ namespace TrueCV.Infrastructure.Data.Migrations
|
||||
b.Navigation("CVCheck");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrueCV.Domain.Entities.CVCheck", b =>
|
||||
modelBuilder.Entity("RealCV.Domain.Entities.CVCheck", b =>
|
||||
{
|
||||
b.Navigation("Flags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TrueCV.Infrastructure.Identity.ApplicationUser", b =>
|
||||
modelBuilder.Entity("RealCV.Infrastructure.Identity.ApplicationUser", b =>
|
||||
{
|
||||
b.Navigation("CVChecks");
|
||||
});
|
||||
@@ -5,14 +5,15 @@ using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Polly;
|
||||
using Polly.Extensions.Http;
|
||||
using TrueCV.Application.Interfaces;
|
||||
using TrueCV.Infrastructure.Configuration;
|
||||
using TrueCV.Infrastructure.Data;
|
||||
using TrueCV.Infrastructure.ExternalApis;
|
||||
using TrueCV.Infrastructure.Jobs;
|
||||
using TrueCV.Infrastructure.Services;
|
||||
using RealCV.Application.Interfaces;
|
||||
using RealCV.Infrastructure.Clients;
|
||||
using RealCV.Infrastructure.Configuration;
|
||||
using RealCV.Infrastructure.Data;
|
||||
using RealCV.Infrastructure.ExternalApis;
|
||||
using RealCV.Infrastructure.Jobs;
|
||||
using RealCV.Infrastructure.Services;
|
||||
|
||||
namespace TrueCV.Infrastructure;
|
||||
namespace RealCV.Infrastructure;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
@@ -74,6 +75,13 @@ public static class DependencyInjection
|
||||
services.Configure<LocalStorageSettings>(
|
||||
configuration.GetSection(LocalStorageSettings.SectionName));
|
||||
|
||||
// Configure options for additional verification APIs
|
||||
services.Configure<FcaOptions>(
|
||||
configuration.GetSection("FcaRegister"));
|
||||
|
||||
services.Configure<GitHubOptions>(
|
||||
configuration.GetSection("GitHub"));
|
||||
|
||||
// Configure HttpClient for CompaniesHouseClient with retry policy
|
||||
services.AddHttpClient<CompaniesHouseClient>((serviceProvider, client) =>
|
||||
{
|
||||
@@ -88,6 +96,18 @@ public static class DependencyInjection
|
||||
})
|
||||
.AddPolicyHandler(GetRetryPolicy());
|
||||
|
||||
// Configure HttpClient for FCA Register API
|
||||
services.AddHttpClient<FcaRegisterClient>()
|
||||
.AddPolicyHandler(GetRetryPolicy());
|
||||
|
||||
// Configure HttpClient for GitHub API
|
||||
services.AddHttpClient<GitHubApiClient>()
|
||||
.AddPolicyHandler(GetRetryPolicy());
|
||||
|
||||
// Configure HttpClient for ORCID API
|
||||
services.AddHttpClient<OrcidClient>()
|
||||
.AddPolicyHandler(GetRetryPolicy());
|
||||
|
||||
// Register services
|
||||
services.AddScoped<ICVParserService, CVParserService>();
|
||||
services.AddScoped<ICompanyNameMatcherService, AICompanyNameMatcherService>();
|
||||
@@ -98,6 +118,11 @@ public static class DependencyInjection
|
||||
services.AddScoped<IUserContextService, UserContextService>();
|
||||
services.AddScoped<IAuditService, AuditService>();
|
||||
|
||||
// Register additional verification services
|
||||
services.AddScoped<IProfessionalVerifierService, ProfessionalVerifierService>();
|
||||
services.AddScoped<IGitHubVerifierService, GitHubVerifierService>();
|
||||
services.AddScoped<IAcademicVerifierService, AcademicVerifierService>();
|
||||
|
||||
// Register file storage - use local storage if configured, otherwise Azure
|
||||
var useLocalStorage = configuration.GetValue<bool>("UseLocalStorage");
|
||||
if (useLocalStorage)
|
||||
@@ -6,10 +6,10 @@ using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using TrueCV.Application.DTOs;
|
||||
using TrueCV.Infrastructure.Configuration;
|
||||
using RealCV.Application.DTOs;
|
||||
using RealCV.Infrastructure.Configuration;
|
||||
|
||||
namespace TrueCV.Infrastructure.ExternalApis;
|
||||
namespace RealCV.Infrastructure.ExternalApis;
|
||||
|
||||
public sealed class CompaniesHouseClient
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace TrueCV.Infrastructure.Helpers;
|
||||
namespace RealCV.Infrastructure.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Helper methods for processing AI/LLM JSON responses.
|
||||
@@ -1,8 +1,8 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using TrueCV.Domain.Entities;
|
||||
using TrueCV.Domain.Enums;
|
||||
using RealCV.Domain.Entities;
|
||||
using RealCV.Domain.Enums;
|
||||
|
||||
namespace TrueCV.Infrastructure.Identity;
|
||||
namespace RealCV.Infrastructure.Identity;
|
||||
|
||||
public class ApplicationUser : IdentityUser<Guid>
|
||||
{
|
||||
@@ -1,14 +1,14 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TrueCV.Application.Helpers;
|
||||
using TrueCV.Application.Interfaces;
|
||||
using TrueCV.Application.Models;
|
||||
using TrueCV.Domain.Entities;
|
||||
using TrueCV.Domain.Enums;
|
||||
using TrueCV.Infrastructure.Data;
|
||||
using RealCV.Application.Helpers;
|
||||
using RealCV.Application.Interfaces;
|
||||
using RealCV.Application.Models;
|
||||
using RealCV.Domain.Entities;
|
||||
using RealCV.Domain.Enums;
|
||||
using RealCV.Infrastructure.Data;
|
||||
|
||||
namespace TrueCV.Infrastructure.Jobs;
|
||||
namespace RealCV.Infrastructure.Jobs;
|
||||
|
||||
public sealed class ProcessCVCheckJob
|
||||
{
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\TrueCV.Application\TrueCV.Application.csproj" />
|
||||
<ProjectReference Include="..\RealCV.Application\RealCV.Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -3,13 +3,13 @@ using Anthropic.SDK;
|
||||
using Anthropic.SDK.Messaging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using TrueCV.Application.Helpers;
|
||||
using TrueCV.Application.Interfaces;
|
||||
using TrueCV.Application.Models;
|
||||
using TrueCV.Infrastructure.Configuration;
|
||||
using TrueCV.Infrastructure.Helpers;
|
||||
using RealCV.Application.Helpers;
|
||||
using RealCV.Application.Interfaces;
|
||||
using RealCV.Application.Models;
|
||||
using RealCV.Infrastructure.Configuration;
|
||||
using RealCV.Infrastructure.Helpers;
|
||||
|
||||
namespace TrueCV.Infrastructure.Services;
|
||||
namespace RealCV.Infrastructure.Services;
|
||||
|
||||
public sealed class AICompanyNameMatcherService : ICompanyNameMatcherService
|
||||
{
|
||||
509
src/RealCV.Infrastructure/Services/AcademicVerifierService.cs
Normal file
509
src/RealCV.Infrastructure/Services/AcademicVerifierService.cs
Normal file
@@ -0,0 +1,509 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using RealCV.Application.Interfaces;
|
||||
using RealCV.Application.Models;
|
||||
using RealCV.Infrastructure.Clients;
|
||||
|
||||
using InterfaceOrcidSearchResult = RealCV.Application.Interfaces.OrcidSearchResult;
|
||||
|
||||
namespace RealCV.Infrastructure.Services;
|
||||
|
||||
public sealed class AcademicVerifierService : IAcademicVerifierService
|
||||
{
|
||||
private readonly OrcidClient _orcidClient;
|
||||
private readonly ILogger<AcademicVerifierService> _logger;
|
||||
|
||||
public AcademicVerifierService(
|
||||
OrcidClient orcidClient,
|
||||
ILogger<AcademicVerifierService> logger)
|
||||
{
|
||||
_orcidClient = orcidClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<AcademicVerificationResult> VerifyByOrcidAsync(string orcidId)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Verifying ORCID: {OrcidId}", orcidId);
|
||||
|
||||
var record = await _orcidClient.GetRecordAsync(orcidId);
|
||||
|
||||
if (record == null)
|
||||
{
|
||||
return new AcademicVerificationResult
|
||||
{
|
||||
ClaimedName = orcidId,
|
||||
IsVerified = false,
|
||||
VerificationNotes = "ORCID record not found"
|
||||
};
|
||||
}
|
||||
|
||||
// Extract name
|
||||
string? name = null;
|
||||
if (record.Person?.Name != null)
|
||||
{
|
||||
var nameParts = new List<string>();
|
||||
if (!string.IsNullOrEmpty(record.Person.Name.GivenNames?.Value))
|
||||
nameParts.Add(record.Person.Name.GivenNames.Value);
|
||||
if (!string.IsNullOrEmpty(record.Person.Name.FamilyName?.Value))
|
||||
nameParts.Add(record.Person.Name.FamilyName.Value);
|
||||
name = string.Join(" ", nameParts);
|
||||
}
|
||||
|
||||
// Get detailed employment information
|
||||
var affiliations = new List<AcademicAffiliation>();
|
||||
var employments = await _orcidClient.GetEmploymentsAsync(orcidId);
|
||||
if (employments?.AffiliationGroup != null)
|
||||
{
|
||||
affiliations.AddRange(ExtractAffiliations(employments.AffiliationGroup, "employment"));
|
||||
}
|
||||
|
||||
// Get detailed education information
|
||||
var educationList = new List<AcademicEducation>();
|
||||
var educations = await _orcidClient.GetEducationsAsync(orcidId);
|
||||
if (educations?.AffiliationGroup != null)
|
||||
{
|
||||
educationList.AddRange(ExtractEducations(educations.AffiliationGroup));
|
||||
}
|
||||
|
||||
// Get works/publications
|
||||
var publications = new List<Publication>();
|
||||
var works = await _orcidClient.GetWorksAsync(orcidId);
|
||||
if (works?.Group != null)
|
||||
{
|
||||
publications.AddRange(ExtractPublications(works.Group));
|
||||
}
|
||||
|
||||
return new AcademicVerificationResult
|
||||
{
|
||||
ClaimedName = name ?? orcidId,
|
||||
IsVerified = true,
|
||||
OrcidId = orcidId,
|
||||
MatchedName = name,
|
||||
OrcidUrl = record.OrcidIdentifier?.Uri,
|
||||
Affiliations = affiliations,
|
||||
TotalPublications = publications.Count,
|
||||
RecentPublications = publications.Take(10).ToList(),
|
||||
Education = educationList,
|
||||
VerificationNotes = BuildVerificationSummary(affiliations, publications.Count, educationList.Count)
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error verifying ORCID: {OrcidId}", orcidId);
|
||||
return new AcademicVerificationResult
|
||||
{
|
||||
ClaimedName = orcidId,
|
||||
IsVerified = false,
|
||||
VerificationNotes = $"Error during verification: {ex.Message}"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<AcademicVerificationResult> VerifyByNameAsync(
|
||||
string name,
|
||||
string? affiliation = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Searching ORCID for: {Name} at {Affiliation}",
|
||||
name, affiliation ?? "any affiliation");
|
||||
|
||||
var query = name;
|
||||
if (!string.IsNullOrEmpty(affiliation))
|
||||
{
|
||||
query = $"{name} {affiliation}";
|
||||
}
|
||||
|
||||
var searchResponse = await _orcidClient.SearchResearchersAsync(query);
|
||||
|
||||
if (searchResponse?.Result == null || searchResponse.Result.Count == 0)
|
||||
{
|
||||
return new AcademicVerificationResult
|
||||
{
|
||||
ClaimedName = name,
|
||||
IsVerified = false,
|
||||
VerificationNotes = "No matching ORCID records found"
|
||||
};
|
||||
}
|
||||
|
||||
// Get the first match's ORCID ID
|
||||
var firstMatch = searchResponse.Result.First();
|
||||
var orcidId = firstMatch.OrcidIdentifier?.Path;
|
||||
|
||||
if (string.IsNullOrEmpty(orcidId))
|
||||
{
|
||||
return new AcademicVerificationResult
|
||||
{
|
||||
ClaimedName = name,
|
||||
IsVerified = false,
|
||||
VerificationNotes = "Search returned results but no ORCID ID found"
|
||||
};
|
||||
}
|
||||
|
||||
// Get full details
|
||||
var result = await VerifyByOrcidAsync(orcidId);
|
||||
|
||||
// Update claimed name to the search name
|
||||
return result with { ClaimedName = name };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error searching ORCID for: {Name}", name);
|
||||
return new AcademicVerificationResult
|
||||
{
|
||||
ClaimedName = name,
|
||||
IsVerified = false,
|
||||
VerificationNotes = $"Error during search: {ex.Message}"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<InterfaceOrcidSearchResult>> SearchResearchersAsync(
|
||||
string name,
|
||||
string? affiliation = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var query = name;
|
||||
if (!string.IsNullOrEmpty(affiliation))
|
||||
{
|
||||
query = $"{name} {affiliation}";
|
||||
}
|
||||
|
||||
var searchResponse = await _orcidClient.SearchResearchersAsync(query, 0, 20);
|
||||
|
||||
if (searchResponse?.Result == null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var results = new List<InterfaceOrcidSearchResult>();
|
||||
|
||||
foreach (var searchResult in searchResponse.Result.Take(10))
|
||||
{
|
||||
var orcidId = searchResult.OrcidIdentifier?.Path;
|
||||
if (string.IsNullOrEmpty(orcidId))
|
||||
continue;
|
||||
|
||||
var record = await _orcidClient.GetRecordAsync(orcidId);
|
||||
if (record == null)
|
||||
continue;
|
||||
|
||||
// Extract name
|
||||
string? researcherName = null;
|
||||
if (record.Person?.Name != null)
|
||||
{
|
||||
var nameParts = new List<string>();
|
||||
if (!string.IsNullOrEmpty(record.Person.Name.GivenNames?.Value))
|
||||
nameParts.Add(record.Person.Name.GivenNames.Value);
|
||||
if (!string.IsNullOrEmpty(record.Person.Name.FamilyName?.Value))
|
||||
nameParts.Add(record.Person.Name.FamilyName.Value);
|
||||
researcherName = string.Join(" ", nameParts);
|
||||
}
|
||||
|
||||
// Get affiliations
|
||||
var affiliations = new List<string>();
|
||||
if (record.ActivitiesSummary?.Employments?.AffiliationGroup != null)
|
||||
{
|
||||
affiliations = record.ActivitiesSummary.Employments.AffiliationGroup
|
||||
.SelectMany(g => g.Summaries ?? [])
|
||||
.Select(s => s.EmploymentSummary?.Organization?.Name)
|
||||
.Where(n => !string.IsNullOrEmpty(n))
|
||||
.Distinct()
|
||||
.Take(5)
|
||||
.ToList()!;
|
||||
}
|
||||
|
||||
// Get publication count
|
||||
var publicationCount = record.ActivitiesSummary?.Works?.Group?.Count ?? 0;
|
||||
|
||||
results.Add(new InterfaceOrcidSearchResult
|
||||
{
|
||||
OrcidId = orcidId,
|
||||
Name = researcherName ?? "Unknown",
|
||||
OrcidUrl = searchResult.OrcidIdentifier?.Uri,
|
||||
Affiliations = affiliations,
|
||||
PublicationCount = publicationCount
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error searching ORCID for: {Name}", name);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<PublicationVerificationResult>> VerifyPublicationsAsync(
|
||||
string orcidId,
|
||||
List<string> claimedPublications)
|
||||
{
|
||||
var results = new List<PublicationVerificationResult>();
|
||||
|
||||
try
|
||||
{
|
||||
var works = await _orcidClient.GetWorksAsync(orcidId);
|
||||
|
||||
if (works?.Group == null)
|
||||
{
|
||||
return claimedPublications.Select(title => new PublicationVerificationResult
|
||||
{
|
||||
ClaimedTitle = title,
|
||||
IsVerified = false,
|
||||
Notes = "Could not retrieve ORCID publications"
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
var orcidPublications = ExtractPublications(works.Group);
|
||||
|
||||
foreach (var claimedTitle in claimedPublications)
|
||||
{
|
||||
var match = FindBestPublicationMatch(claimedTitle, orcidPublications);
|
||||
|
||||
if (match != null)
|
||||
{
|
||||
results.Add(new PublicationVerificationResult
|
||||
{
|
||||
ClaimedTitle = claimedTitle,
|
||||
IsVerified = true,
|
||||
MatchedTitle = match.Title,
|
||||
Doi = match.Doi,
|
||||
Year = match.Year,
|
||||
Notes = "Publication found in ORCID record"
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
results.Add(new PublicationVerificationResult
|
||||
{
|
||||
ClaimedTitle = claimedTitle,
|
||||
IsVerified = false,
|
||||
Notes = "Publication not found in ORCID record"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error verifying publications for ORCID: {OrcidId}", orcidId);
|
||||
|
||||
return claimedPublications.Select(title => new PublicationVerificationResult
|
||||
{
|
||||
ClaimedTitle = title,
|
||||
IsVerified = false,
|
||||
Notes = $"Error during verification: {ex.Message}"
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static List<AcademicAffiliation> ExtractAffiliations(
|
||||
List<OrcidAffiliationGroup> groups,
|
||||
string type)
|
||||
{
|
||||
var affiliations = new List<AcademicAffiliation>();
|
||||
|
||||
foreach (var group in groups)
|
||||
{
|
||||
if (group.Summaries == null)
|
||||
continue;
|
||||
|
||||
foreach (var wrapper in group.Summaries)
|
||||
{
|
||||
var summary = type == "employment"
|
||||
? wrapper.EmploymentSummary
|
||||
: wrapper.EducationSummary;
|
||||
|
||||
if (summary?.Organization?.Name == null)
|
||||
continue;
|
||||
|
||||
affiliations.Add(new AcademicAffiliation
|
||||
{
|
||||
Organization = summary.Organization.Name,
|
||||
Department = summary.DepartmentName,
|
||||
Role = summary.RoleTitle,
|
||||
StartDate = ParseOrcidDate(summary.StartDate),
|
||||
EndDate = ParseOrcidDate(summary.EndDate)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by start date descending (most recent first)
|
||||
return affiliations
|
||||
.OrderByDescending(a => a.StartDate)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static List<AcademicEducation> ExtractEducations(List<OrcidAffiliationGroup> groups)
|
||||
{
|
||||
var educations = new List<AcademicEducation>();
|
||||
|
||||
foreach (var group in groups)
|
||||
{
|
||||
if (group.Summaries == null)
|
||||
continue;
|
||||
|
||||
foreach (var wrapper in group.Summaries)
|
||||
{
|
||||
var summary = wrapper.EducationSummary;
|
||||
if (summary?.Organization?.Name == null)
|
||||
continue;
|
||||
|
||||
var startYear = summary.StartDate?.Year?.Value;
|
||||
var endYear = summary.EndDate?.Year?.Value;
|
||||
|
||||
educations.Add(new AcademicEducation
|
||||
{
|
||||
Institution = summary.Organization.Name,
|
||||
Degree = summary.RoleTitle,
|
||||
Subject = summary.DepartmentName,
|
||||
Year = !string.IsNullOrEmpty(endYear)
|
||||
? int.TryParse(endYear, out var y) ? y : null
|
||||
: !string.IsNullOrEmpty(startYear)
|
||||
? int.TryParse(startYear, out var sy) ? sy : null
|
||||
: null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return educations
|
||||
.OrderByDescending(e => e.Year)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static List<Publication> ExtractPublications(List<OrcidWorkGroup> groups)
|
||||
{
|
||||
var publications = new List<Publication>();
|
||||
|
||||
foreach (var group in groups)
|
||||
{
|
||||
if (group.WorkSummary == null || group.WorkSummary.Count == 0)
|
||||
continue;
|
||||
|
||||
// Take the first work summary from each group (they're typically duplicates)
|
||||
var work = group.WorkSummary.First();
|
||||
|
||||
var title = work.TitleObj?.Title?.Value ?? work.Title;
|
||||
if (string.IsNullOrEmpty(title))
|
||||
continue;
|
||||
|
||||
// Extract DOI if available
|
||||
string? doi = null;
|
||||
if (work.ExternalIds?.ExternalId != null)
|
||||
{
|
||||
var doiEntry = work.ExternalIds.ExternalId
|
||||
.FirstOrDefault(e => e.ExternalIdType?.Equals("doi", StringComparison.OrdinalIgnoreCase) == true);
|
||||
doi = doiEntry?.ExternalIdValue;
|
||||
}
|
||||
|
||||
// Parse year
|
||||
int? year = null;
|
||||
if (!string.IsNullOrEmpty(work.PublicationDate?.Year?.Value) &&
|
||||
int.TryParse(work.PublicationDate.Year.Value, out var y))
|
||||
{
|
||||
year = y;
|
||||
}
|
||||
|
||||
publications.Add(new Publication
|
||||
{
|
||||
Title = title,
|
||||
Journal = work.JournalTitle?.Value,
|
||||
Year = year,
|
||||
Doi = doi,
|
||||
Type = work.Type
|
||||
});
|
||||
}
|
||||
|
||||
// Sort by publication year descending
|
||||
return publications
|
||||
.OrderByDescending(p => p.Year)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static DateOnly? ParseOrcidDate(OrcidDate? date)
|
||||
{
|
||||
if (date?.Year?.Value == null)
|
||||
return null;
|
||||
|
||||
if (!int.TryParse(date.Year.Value, out var year))
|
||||
return null;
|
||||
|
||||
var month = 1;
|
||||
if (!string.IsNullOrEmpty(date.Month?.Value) && int.TryParse(date.Month.Value, out var m))
|
||||
month = m;
|
||||
|
||||
var day = 1;
|
||||
if (!string.IsNullOrEmpty(date.Day?.Value) && int.TryParse(date.Day.Value, out var d))
|
||||
day = d;
|
||||
|
||||
try
|
||||
{
|
||||
return new DateOnly(year, month, day);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new DateOnly(year, 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
private static Publication? FindBestPublicationMatch(string claimedTitle, List<Publication> publications)
|
||||
{
|
||||
var normalizedClaimed = NormalizeTitle(claimedTitle);
|
||||
|
||||
// First try exact match
|
||||
var exactMatch = publications.FirstOrDefault(p =>
|
||||
NormalizeTitle(p.Title).Equals(normalizedClaimed, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (exactMatch != null)
|
||||
return exactMatch;
|
||||
|
||||
// Then try contains match
|
||||
var containsMatch = publications.FirstOrDefault(p =>
|
||||
NormalizeTitle(p.Title).Contains(normalizedClaimed, StringComparison.OrdinalIgnoreCase) ||
|
||||
normalizedClaimed.Contains(NormalizeTitle(p.Title), StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
return containsMatch;
|
||||
}
|
||||
|
||||
private static string NormalizeTitle(string title)
|
||||
{
|
||||
return title
|
||||
.ToLowerInvariant()
|
||||
.Replace(":", " ")
|
||||
.Replace("-", " ")
|
||||
.Replace(" ", " ")
|
||||
.Trim();
|
||||
}
|
||||
|
||||
private static string BuildVerificationSummary(List<AcademicAffiliation> affiliations, int publicationCount, int educationCount)
|
||||
{
|
||||
var parts = new List<string>();
|
||||
|
||||
var currentAffiliation = affiliations.FirstOrDefault(a => !a.EndDate.HasValue);
|
||||
if (currentAffiliation != null)
|
||||
{
|
||||
parts.Add($"Current: {currentAffiliation.Organization}");
|
||||
}
|
||||
|
||||
if (publicationCount > 0)
|
||||
{
|
||||
parts.Add($"Publications: {publicationCount}");
|
||||
}
|
||||
|
||||
if (affiliations.Count > 0)
|
||||
{
|
||||
parts.Add($"Positions: {affiliations.Count}");
|
||||
}
|
||||
|
||||
if (educationCount > 0)
|
||||
{
|
||||
parts.Add($"Education: {educationCount}");
|
||||
}
|
||||
|
||||
return parts.Count > 0 ? string.Join(" | ", parts) : "ORCID record verified";
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TrueCV.Application.Interfaces;
|
||||
using TrueCV.Domain.Entities;
|
||||
using TrueCV.Infrastructure.Data;
|
||||
using RealCV.Application.Interfaces;
|
||||
using RealCV.Domain.Entities;
|
||||
using RealCV.Infrastructure.Data;
|
||||
|
||||
namespace TrueCV.Infrastructure.Services;
|
||||
namespace RealCV.Infrastructure.Services;
|
||||
|
||||
public sealed class AuditService : IAuditService
|
||||
{
|
||||
@@ -2,16 +2,16 @@ using System.Text.Json;
|
||||
using Hangfire;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TrueCV.Application.DTOs;
|
||||
using TrueCV.Application.Helpers;
|
||||
using TrueCV.Application.Interfaces;
|
||||
using TrueCV.Application.Models;
|
||||
using TrueCV.Domain.Entities;
|
||||
using TrueCV.Domain.Enums;
|
||||
using TrueCV.Infrastructure.Data;
|
||||
using TrueCV.Infrastructure.Jobs;
|
||||
using RealCV.Application.DTOs;
|
||||
using RealCV.Application.Helpers;
|
||||
using RealCV.Application.Interfaces;
|
||||
using RealCV.Application.Models;
|
||||
using RealCV.Domain.Entities;
|
||||
using RealCV.Domain.Enums;
|
||||
using RealCV.Infrastructure.Data;
|
||||
using RealCV.Infrastructure.Jobs;
|
||||
|
||||
namespace TrueCV.Infrastructure.Services;
|
||||
namespace RealCV.Infrastructure.Services;
|
||||
|
||||
public sealed class CVCheckService : ICVCheckService
|
||||
{
|
||||
@@ -184,10 +184,38 @@ public sealed class CVCheckService : ICVCheckService
|
||||
|
||||
private static CVCheckDto MapToDto(CVCheck cvCheck)
|
||||
{
|
||||
string? candidateName = null;
|
||||
|
||||
// Try to get candidate name from ReportJson first (completed checks)
|
||||
if (!string.IsNullOrEmpty(cvCheck.ReportJson))
|
||||
{
|
||||
try
|
||||
{
|
||||
var report = JsonSerializer.Deserialize<VeracityReport>(cvCheck.ReportJson, JsonDefaults.CamelCase);
|
||||
candidateName = report?.CandidateName;
|
||||
}
|
||||
catch { /* Ignore deserialization errors */ }
|
||||
}
|
||||
|
||||
// Fallback to ExtractedDataJson if no name in report
|
||||
if (string.IsNullOrEmpty(candidateName) && !string.IsNullOrEmpty(cvCheck.ExtractedDataJson))
|
||||
{
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(cvCheck.ExtractedDataJson);
|
||||
if (doc.RootElement.TryGetProperty("fullName", out var nameElement))
|
||||
{
|
||||
candidateName = nameElement.GetString();
|
||||
}
|
||||
}
|
||||
catch { /* Ignore deserialization errors */ }
|
||||
}
|
||||
|
||||
return new CVCheckDto
|
||||
{
|
||||
Id = cvCheck.Id,
|
||||
OriginalFileName = cvCheck.OriginalFileName,
|
||||
CandidateName = candidateName,
|
||||
Status = cvCheck.Status.ToString(),
|
||||
VeracityScore = cvCheck.VeracityScore,
|
||||
ProcessingStage = cvCheck.ProcessingStage,
|
||||
@@ -1,19 +1,20 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Anthropic.SDK;
|
||||
using Anthropic.SDK.Messaging;
|
||||
using DocumentFormat.OpenXml.Packaging;
|
||||
using DocumentFormat.OpenXml.Wordprocessing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using TrueCV.Application.Helpers;
|
||||
using TrueCV.Application.Interfaces;
|
||||
using TrueCV.Application.Models;
|
||||
using TrueCV.Infrastructure.Configuration;
|
||||
using TrueCV.Infrastructure.Helpers;
|
||||
using RealCV.Application.Helpers;
|
||||
using RealCV.Application.Interfaces;
|
||||
using RealCV.Application.Models;
|
||||
using RealCV.Infrastructure.Configuration;
|
||||
using RealCV.Infrastructure.Helpers;
|
||||
using UglyToad.PdfPig;
|
||||
|
||||
namespace TrueCV.Infrastructure.Services;
|
||||
namespace RealCV.Infrastructure.Services;
|
||||
|
||||
public sealed class CVParserService : ICVParserService
|
||||
{
|
||||
@@ -83,6 +84,20 @@ public sealed class CVParserService : ICVParserService
|
||||
|
||||
_logger.LogDebug("Parsing CV file: {FileName}", fileName);
|
||||
|
||||
var extension = Path.GetExtension(fileName).ToLowerInvariant();
|
||||
|
||||
// Handle JSON files directly (debug/test format)
|
||||
if (extension == ".json")
|
||||
{
|
||||
var cvData = await ParseJsonFileAsync(fileStream, cancellationToken);
|
||||
_logger.LogInformation(
|
||||
"Successfully loaded JSON CV for {FullName} with {EmploymentCount} employment entries and {EducationCount} education entries",
|
||||
cvData.FullName,
|
||||
cvData.Employment.Count,
|
||||
cvData.Education.Count);
|
||||
return cvData;
|
||||
}
|
||||
|
||||
var text = await ExtractTextAsync(fileStream, fileName, cancellationToken);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
@@ -93,15 +108,15 @@ public sealed class CVParserService : ICVParserService
|
||||
|
||||
_logger.LogDebug("Extracted {CharCount} characters from {FileName}", text.Length, fileName);
|
||||
|
||||
var cvData = await ParseWithClaudeAsync(text, cancellationToken);
|
||||
var cvDataFromAI = await ParseWithClaudeAsync(text, cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Successfully parsed CV for {FullName} with {EmploymentCount} employment entries and {EducationCount} education entries",
|
||||
cvData.FullName,
|
||||
cvData.Employment.Count,
|
||||
cvData.Education.Count);
|
||||
cvDataFromAI.FullName,
|
||||
cvDataFromAI.Employment.Count,
|
||||
cvDataFromAI.Education.Count);
|
||||
|
||||
return cvData;
|
||||
return cvDataFromAI;
|
||||
}
|
||||
|
||||
private async Task<string> ExtractTextAsync(Stream fileStream, string fileName, CancellationToken cancellationToken)
|
||||
@@ -116,6 +131,60 @@ public sealed class CVParserService : ICVParserService
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<CVData> ParseJsonFileAsync(Stream fileStream, CancellationToken cancellationToken)
|
||||
{
|
||||
var testCv = await JsonSerializer.DeserializeAsync<TestCVData>(fileStream, TestJsonOptions, cancellationToken)
|
||||
?? throw new InvalidOperationException("Failed to deserialize JSON CV file");
|
||||
|
||||
return new CVData
|
||||
{
|
||||
FullName = testCv.Personal?.Name ?? "Unknown",
|
||||
Email = testCv.Personal?.Email,
|
||||
Phone = testCv.Personal?.Phone,
|
||||
Employment = testCv.Employment?.Select(e => new EmploymentEntry
|
||||
{
|
||||
CompanyName = e.Company ?? "Unknown",
|
||||
JobTitle = e.JobTitle ?? "Unknown",
|
||||
Location = e.Location,
|
||||
StartDate = ParseTestDate(e.StartDate),
|
||||
EndDate = ParseTestDate(e.EndDate),
|
||||
IsCurrent = e.EndDate == null,
|
||||
Description = e.Description
|
||||
}).ToList() ?? [],
|
||||
Education = testCv.Education?.Select(e => new EducationEntry
|
||||
{
|
||||
Institution = e.Institution ?? "Unknown",
|
||||
Qualification = e.Qualification,
|
||||
Subject = e.Subject,
|
||||
StartDate = ParseTestDate(e.StartDate),
|
||||
EndDate = ParseTestDate(e.EndDate)
|
||||
}).ToList() ?? [],
|
||||
Skills = testCv.Skills ?? []
|
||||
};
|
||||
}
|
||||
|
||||
private static DateOnly? ParseTestDate(string? dateStr)
|
||||
{
|
||||
if (string.IsNullOrEmpty(dateStr)) return null;
|
||||
|
||||
// Try parsing YYYY-MM format
|
||||
if (dateStr.Length == 7 && dateStr[4] == '-')
|
||||
{
|
||||
if (int.TryParse(dateStr[..4], out var year) && int.TryParse(dateStr[5..], out var month))
|
||||
{
|
||||
return new DateOnly(year, month, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Try standard parsing
|
||||
if (DateOnly.TryParse(dateStr, out var date))
|
||||
{
|
||||
return date;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<string> ExtractTextFromPdfAsync(Stream fileStream, CancellationToken cancellationToken)
|
||||
{
|
||||
// Copy stream to memory for PdfPig (requires seekable stream)
|
||||
@@ -244,6 +313,57 @@ public sealed class CVParserService : ICVParserService
|
||||
};
|
||||
}
|
||||
|
||||
// JSON options for test/debug CV format (snake_case)
|
||||
private static readonly JsonSerializerOptions TestJsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
};
|
||||
|
||||
// DTOs for test JSON format (snake_case with nested personal object)
|
||||
private sealed record TestCVData
|
||||
{
|
||||
public string? CvId { get; init; }
|
||||
public string? Category { get; init; }
|
||||
public List<string>? ExpectedFlags { get; init; }
|
||||
public TestPersonalData? Personal { get; init; }
|
||||
public string? Profile { get; init; }
|
||||
public List<TestEmploymentEntry>? Employment { get; init; }
|
||||
public List<TestEducationEntry>? Education { get; init; }
|
||||
public List<string>? Skills { get; init; }
|
||||
}
|
||||
|
||||
private sealed record TestPersonalData
|
||||
{
|
||||
public string? Name { get; init; }
|
||||
public string? Email { get; init; }
|
||||
public string? Phone { get; init; }
|
||||
public string? Address { get; init; }
|
||||
public string? LinkedIn { get; init; }
|
||||
}
|
||||
|
||||
private sealed record TestEmploymentEntry
|
||||
{
|
||||
public string? Company { get; init; }
|
||||
public string? JobTitle { get; init; }
|
||||
public string? StartDate { get; init; }
|
||||
public string? EndDate { get; init; }
|
||||
public string? Location { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public List<string>? Achievements { get; init; }
|
||||
}
|
||||
|
||||
private sealed record TestEducationEntry
|
||||
{
|
||||
public string? Institution { get; init; }
|
||||
public string? Qualification { get; init; }
|
||||
public string? Subject { get; init; }
|
||||
public string? Classification { get; init; }
|
||||
public string? StartDate { get; init; }
|
||||
public string? EndDate { get; init; }
|
||||
}
|
||||
|
||||
// Internal DTOs for Claude response parsing
|
||||
private sealed record ClaudeCVResponse
|
||||
{
|
||||
@@ -2,15 +2,15 @@ using System.Text.Json;
|
||||
using FuzzySharp;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TrueCV.Application.DTOs;
|
||||
using TrueCV.Application.Helpers;
|
||||
using TrueCV.Application.Interfaces;
|
||||
using TrueCV.Application.Models;
|
||||
using TrueCV.Domain.Entities;
|
||||
using TrueCV.Infrastructure.Data;
|
||||
using TrueCV.Infrastructure.ExternalApis;
|
||||
using RealCV.Application.DTOs;
|
||||
using RealCV.Application.Helpers;
|
||||
using RealCV.Application.Interfaces;
|
||||
using RealCV.Application.Models;
|
||||
using RealCV.Domain.Entities;
|
||||
using RealCV.Infrastructure.Data;
|
||||
using RealCV.Infrastructure.ExternalApis;
|
||||
|
||||
namespace TrueCV.Infrastructure.Services;
|
||||
namespace RealCV.Infrastructure.Services;
|
||||
|
||||
public sealed class CompanyVerifierService : ICompanyVerifierService
|
||||
{
|
||||
@@ -72,6 +72,154 @@ public sealed class CompanyVerifierService : ICompanyVerifierService
|
||||
"manufacturing", "operations", "trading"
|
||||
};
|
||||
|
||||
// Mapping of common trading names to their official Companies House registered names.
|
||||
// Many major UK companies trade under a different name than their registered name.
|
||||
private static readonly Dictionary<string, string[]> TradingNameAliases = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
// Retail
|
||||
["Boots"] = new[] { "BOOTS UK LIMITED", "THE BOOTS COMPANY PLC", "BOOTS OPTICIANS" },
|
||||
["Sainsbury's"] = new[] { "J SAINSBURY PLC", "SAINSBURY'S SUPERMARKETS LTD" },
|
||||
["Marks & Spencer"] = new[] { "MARKS AND SPENCER GROUP PLC", "MARKS AND SPENCER PLC" },
|
||||
["M&S"] = new[] { "MARKS AND SPENCER GROUP PLC", "MARKS AND SPENCER PLC" },
|
||||
["John Lewis"] = new[] { "JOHN LEWIS PLC", "JOHN LEWIS PARTNERSHIP PLC" },
|
||||
["John Lewis Partnership"] = new[] { "JOHN LEWIS PARTNERSHIP PLC", "JOHN LEWIS PLC" },
|
||||
["Waitrose"] = new[] { "WAITROSE LIMITED", "JOHN LEWIS PARTNERSHIP PLC" },
|
||||
["Tesco"] = new[] { "TESCO PLC", "TESCO STORES LIMITED" },
|
||||
["Asda"] = new[] { "ASDA STORES LIMITED", "ASDA GROUP LIMITED" },
|
||||
["Morrisons"] = new[] { "WM MORRISON SUPERMARKETS LIMITED" },
|
||||
["Lidl"] = new[] { "LIDL GREAT BRITAIN LIMITED" },
|
||||
["Aldi"] = new[] { "ALDI STORES LIMITED" },
|
||||
|
||||
// Banking & Finance
|
||||
["Lloyds Banking Group"] = new[] { "LLOYDS BANKING GROUP PLC", "LLOYDS BANK PLC" },
|
||||
["Lloyds Bank"] = new[] { "LLOYDS BANK PLC", "LLOYDS BANKING GROUP PLC" },
|
||||
["HSBC"] = new[] { "HSBC HOLDINGS PLC", "HSBC UK BANK PLC", "HSBC BANK PLC" },
|
||||
["HSBC Holdings PLC"] = new[] { "HSBC HOLDINGS PLC", "HSBC UK BANK PLC" },
|
||||
["HSBC UK"] = new[] { "HSBC UK BANK PLC", "HSBC HOLDINGS PLC" },
|
||||
["Barclays"] = new[] { "BARCLAYS PLC", "BARCLAYS BANK PLC" },
|
||||
["NatWest"] = new[] { "NATWEST GROUP PLC", "NATIONAL WESTMINSTER BANK PLC" },
|
||||
["NatWest Group"] = new[] { "NATWEST GROUP PLC", "NATIONAL WESTMINSTER BANK PLC" },
|
||||
["Santander UK"] = new[] { "SANTANDER UK PLC" },
|
||||
["Nationwide"] = new[] { "NATIONWIDE BUILDING SOCIETY" },
|
||||
|
||||
// Media & Broadcasting
|
||||
["BBC"] = new[] { "BRITISH BROADCASTING CORPORATION" },
|
||||
["ITV"] = new[] { "ITV PLC" },
|
||||
["Sky"] = new[] { "SKY LIMITED", "SKY UK LIMITED" },
|
||||
["Channel 4"] = new[] { "CHANNEL FOUR TELEVISION CORPORATION" },
|
||||
|
||||
// Technology
|
||||
["IBM UK"] = new[] { "IBM UNITED KINGDOM LIMITED", "INTERNATIONAL BUSINESS MACHINES" },
|
||||
["IBM"] = new[] { "IBM UNITED KINGDOM LIMITED", "INTERNATIONAL BUSINESS MACHINES" },
|
||||
["Google UK"] = new[] { "GOOGLE UK LIMITED", "GOOGLE LLC" },
|
||||
["Google"] = new[] { "GOOGLE UK LIMITED" },
|
||||
["Microsoft UK"] = new[] { "MICROSOFT LIMITED" },
|
||||
["Amazon UK"] = new[] { "AMAZON UK SERVICES LTD", "AMAZON.CO.UK LTD" },
|
||||
["Apple UK"] = new[] { "APPLE (UK) LIMITED", "APPLE RETAIL UK LIMITED" },
|
||||
|
||||
// Consulting & Professional Services
|
||||
["Accenture UK"] = new[] { "ACCENTURE (UK) LIMITED", "ACCENTURE PLC" },
|
||||
["Accenture"] = new[] { "ACCENTURE (UK) LIMITED", "ACCENTURE PLC" },
|
||||
["EY UK"] = new[] { "ERNST & YOUNG LLP", "EY LLP" },
|
||||
["EY"] = new[] { "ERNST & YOUNG LLP", "EY LLP" },
|
||||
["Ernst & Young"] = new[] { "ERNST & YOUNG LLP" },
|
||||
["Deloitte UK"] = new[] { "DELOITTE LLP" },
|
||||
["Deloitte"] = new[] { "DELOITTE LLP" },
|
||||
["KPMG UK"] = new[] { "KPMG LLP" },
|
||||
["KPMG"] = new[] { "KPMG LLP" },
|
||||
["PwC UK"] = new[] { "PRICEWATERHOUSECOOPERS LLP", "PWC (UK) LIMITED" },
|
||||
["PwC"] = new[] { "PRICEWATERHOUSECOOPERS LLP", "PWC (UK) LIMITED" },
|
||||
["McKinsey"] = new[] { "MCKINSEY & COMPANY, INC. UNITED KINGDOM" },
|
||||
["BCG"] = new[] { "THE BOSTON CONSULTING GROUP UK LLP" },
|
||||
["Bain"] = new[] { "BAIN & COMPANY UK LIMITED" },
|
||||
|
||||
// Advertising & Media
|
||||
["WPP"] = new[] { "WPP 2005 LIMITED", "WPP GROUP", "WIRE & PLASTIC PRODUCTS" },
|
||||
|
||||
// Fintech
|
||||
["Checkout.com"] = new[] { "CHECKOUT.COM LIMITED", "CHECKOUT LTD" },
|
||||
["Revolut"] = new[] { "REVOLUT LTD", "REVOLUT LIMITED" },
|
||||
["Monzo"] = new[] { "MONZO BANK LIMITED" },
|
||||
["Starling Bank"] = new[] { "STARLING BANK LIMITED" },
|
||||
["Deliveroo"] = new[] { "ROOFOODS LTD", "DELIVEROO HOLDINGS PLC" },
|
||||
|
||||
// Travel & Hospitality
|
||||
["Thomas Cook"] = new[] { "THOMAS COOK GROUP PLC", "THOMAS COOK UK LIMITED" },
|
||||
["TUI"] = new[] { "TUI UK LIMITED" },
|
||||
["British Airways"] = new[] { "BRITISH AIRWAYS PLC" },
|
||||
["EasyJet"] = new[] { "EASYJET PLC", "EASYJET AIRLINE COMPANY LIMITED" },
|
||||
["Ryanair"] = new[] { "RYANAIR UK LIMITED" },
|
||||
["Jamie's Italian"] = new[] { "JAMIE'S ITALIAN LIMITED", "JAMIE OLIVER HOLDINGS LIMITED" },
|
||||
|
||||
// Retail (Other)
|
||||
["Toys R Us"] = new[] { "TOYS R US LIMITED", "TOYS \"R\" US LIMITED" },
|
||||
["Toys R Us UK"] = new[] { "TOYS R US LIMITED" },
|
||||
["Debenhams"] = new[] { "DEBENHAMS PLC", "DEBENHAMS RETAIL LIMITED" },
|
||||
["House of Fraser"] = new[] { "HOUSE OF FRASER LIMITED" },
|
||||
["Next"] = new[] { "NEXT PLC", "NEXT RETAIL LIMITED" },
|
||||
["Primark"] = new[] { "PRIMARK STORES LIMITED" },
|
||||
["Sports Direct"] = new[] { "SPORTS DIRECT INTERNATIONAL PLC" },
|
||||
|
||||
// Telecoms
|
||||
["BT"] = new[] { "BT GROUP PLC", "BRITISH TELECOMMUNICATIONS PLC" },
|
||||
["BT Group"] = new[] { "BT GROUP PLC" },
|
||||
["Vodafone"] = new[] { "VODAFONE LIMITED", "VODAFONE GROUP PLC" },
|
||||
["O2"] = new[] { "TELEFONICA UK LIMITED" },
|
||||
["EE"] = new[] { "EE LIMITED" },
|
||||
["Three"] = new[] { "HUTCHISON 3G UK LIMITED" },
|
||||
["Virgin Media"] = new[] { "VIRGIN MEDIA LIMITED" },
|
||||
|
||||
// Energy
|
||||
["BP"] = new[] { "BP P.L.C.", "BP PLC" },
|
||||
["Shell UK"] = new[] { "SHELL U.K. LIMITED", "SHELL PLC" },
|
||||
["Shell"] = new[] { "SHELL PLC", "SHELL U.K. LIMITED" },
|
||||
["British Gas"] = new[] { "BRITISH GAS SERVICES LIMITED", "CENTRICA PLC" },
|
||||
["Centrica"] = new[] { "CENTRICA PLC" },
|
||||
["SSE"] = new[] { "SSE PLC" },
|
||||
["National Grid"] = new[] { "NATIONAL GRID PLC" },
|
||||
|
||||
// Automotive
|
||||
["Jaguar Land Rover"] = new[] { "JAGUAR LAND ROVER LIMITED" },
|
||||
["JLR"] = new[] { "JAGUAR LAND ROVER LIMITED" },
|
||||
["Rolls-Royce"] = new[] { "ROLLS-ROYCE PLC", "ROLLS-ROYCE HOLDINGS PLC" },
|
||||
["BMW UK"] = new[] { "BMW (UK) LIMITED", "BMW GROUP UK LIMITED" },
|
||||
["JCB"] = new[] { "J.C. BAMFORD EXCAVATORS LIMITED", "J. C. BAMFORD LIMITED" },
|
||||
|
||||
// Food & Beverage
|
||||
["Unilever"] = new[] { "UNILEVER PLC" },
|
||||
["Nestle UK"] = new[] { "NESTLE UK LTD" },
|
||||
["Coca-Cola UK"] = new[] { "COCA-COLA EUROPACIFIC PARTNERS PLC" },
|
||||
["PepsiCo UK"] = new[] { "PEPSICO UK LIMITED" },
|
||||
|
||||
// Pharmaceutical & Healthcare
|
||||
["GlaxoSmithKline"] = new[] { "GLAXOSMITHKLINE PLC", "GSK PLC" },
|
||||
["GSK"] = new[] { "GSK PLC", "GLAXOSMITHKLINE PLC" },
|
||||
["AstraZeneca"] = new[] { "ASTRAZENECA PLC" },
|
||||
["Pfizer UK"] = new[] { "PFIZER LIMITED" },
|
||||
|
||||
// Defence & Aerospace
|
||||
["BAE Systems"] = new[] { "BAE SYSTEMS PLC" },
|
||||
["BAE"] = new[] { "BAE SYSTEMS PLC" },
|
||||
|
||||
// Insurance
|
||||
["Aviva"] = new[] { "AVIVA PLC" },
|
||||
["Legal & General"] = new[] { "LEGAL & GENERAL GROUP PLC", "LEGAL AND GENERAL" },
|
||||
["Prudential"] = new[] { "PRUDENTIAL PLC", "PRUDENTIAL PUBLIC LIMITED COMPANY" },
|
||||
["Admiral"] = new[] { "ADMIRAL GROUP PLC" },
|
||||
|
||||
// Construction & Engineering
|
||||
["Balfour Beatty"] = new[] { "BALFOUR BEATTY PLC" },
|
||||
["Carillion"] = new[] { "CARILLION PLC" },
|
||||
["Kier"] = new[] { "KIER GROUP PLC" },
|
||||
["Taylor Wimpey"] = new[] { "TAYLOR WIMPEY PLC" },
|
||||
["Persimmon"] = new[] { "PERSIMMON PLC" },
|
||||
|
||||
// Outsourcing & Services
|
||||
["Serco"] = new[] { "SERCO GROUP PLC" },
|
||||
["Capita"] = new[] { "CAPITA PLC" },
|
||||
["G4S"] = new[] { "G4S PLC", "G4S LIMITED" },
|
||||
};
|
||||
|
||||
|
||||
public CompanyVerifierService(
|
||||
CompaniesHouseClient companiesHouseClient,
|
||||
@@ -162,6 +310,61 @@ public sealed class CompanyVerifierService : ICompanyVerifierService
|
||||
"Company name could not be verified against official records");
|
||||
}
|
||||
|
||||
// Check for direct alias match first - if we searched for a known alias and found it exactly, accept it
|
||||
// This handles cases where AI might reject valid matches (e.g., "WPP 2005 LIMITED" for "WPP")
|
||||
var directAliasMatch = FindDirectAliasMatch(companyName, allCandidates.Values.ToList(), startDate);
|
||||
if (directAliasMatch is not null)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Direct alias match for '{ClaimedName}' to '{MatchedName}' (known trading name alias)",
|
||||
companyName, directAliasMatch.Value.Item.Title);
|
||||
|
||||
var aliasDetails = await _companiesHouseClient.GetCompanyAsync(directAliasMatch.Value.Item.CompanyNumber);
|
||||
await CacheCompanyAsync(directAliasMatch.Value.Item, aliasDetails);
|
||||
|
||||
var aliasIncDate = DateHelpers.ParseDate(directAliasMatch.Value.Item.DateOfCreation);
|
||||
var aliasDissDate = DateHelpers.ParseDate(directAliasMatch.Value.Item.DateOfCessation);
|
||||
|
||||
CheckIncorporationDate(flags, startDate, aliasIncDate, directAliasMatch.Value.Item.Title);
|
||||
CheckDissolutionDate(flags, endDate, aliasDissDate, directAliasMatch.Value.Item.CompanyStatus, directAliasMatch.Value.Item.Title);
|
||||
CheckDormantCompany(flags, aliasDetails?.Accounts?.LastAccounts?.Type, jobTitle, directAliasMatch.Value.Item.Title);
|
||||
CheckCompanySizeVsRole(flags, aliasDetails?.Accounts?.LastAccounts?.Type, jobTitle, directAliasMatch.Value.Item.Title);
|
||||
|
||||
var (aliasJobPlausible, aliasJobNotes) = CheckJobTitlePlausibility(jobTitle, directAliasMatch.Value.Item.CompanyType);
|
||||
if (aliasJobPlausible == false)
|
||||
{
|
||||
flags.Add(new CompanyVerificationFlag
|
||||
{
|
||||
Type = "ImplausibleJobTitle",
|
||||
Severity = "Critical",
|
||||
Message = aliasJobNotes ?? "Job title requires verification",
|
||||
ScoreImpact = -15
|
||||
});
|
||||
}
|
||||
|
||||
return new CompanyVerificationResult
|
||||
{
|
||||
ClaimedCompany = companyName,
|
||||
MatchedCompanyName = directAliasMatch.Value.Item.Title,
|
||||
MatchedCompanyNumber = directAliasMatch.Value.Item.CompanyNumber,
|
||||
MatchScore = directAliasMatch.Value.Score,
|
||||
IsVerified = true,
|
||||
VerificationNotes = "Matched via known trading name alias",
|
||||
ClaimedStartDate = startDate,
|
||||
ClaimedEndDate = endDate,
|
||||
CompanyType = directAliasMatch.Value.Item.CompanyType,
|
||||
CompanyStatus = directAliasMatch.Value.Item.CompanyStatus,
|
||||
IncorporationDate = aliasIncDate,
|
||||
DissolutionDate = aliasDissDate,
|
||||
AccountsCategory = aliasDetails?.Accounts?.LastAccounts?.Type,
|
||||
SicCodes = aliasDetails?.SicCodes ?? directAliasMatch.Value.Item.SicCodes,
|
||||
ClaimedJobTitle = jobTitle,
|
||||
JobTitlePlausible = aliasJobPlausible,
|
||||
JobTitleNotes = aliasJobNotes,
|
||||
Flags = flags
|
||||
};
|
||||
}
|
||||
|
||||
// Use AI to find the best semantic match from all candidates
|
||||
_logger.LogDebug("Using AI to match '{CompanyName}' against {Count} candidates", companyName, allCandidates.Count);
|
||||
|
||||
@@ -624,6 +827,72 @@ public sealed class CompanyVerifierService : ICompanyVerifierService
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
/// <summary>
|
||||
/// Checks if any candidate directly matches a known trading name alias.
|
||||
/// This allows bypassing AI matching for known aliases where the AI might incorrectly reject.
|
||||
/// Prefers active companies over dissolved ones when multiple matches exist.
|
||||
/// </summary>
|
||||
private static (CompaniesHouseSearchItem Item, int Score)? FindDirectAliasMatch(
|
||||
string companyName,
|
||||
List<CompaniesHouseSearchItem> candidates,
|
||||
DateOnly? claimedStartDate)
|
||||
{
|
||||
var normalized = companyName.Trim();
|
||||
|
||||
// Check if this company name has known aliases
|
||||
if (!TradingNameAliases.TryGetValue(normalized, out var aliases))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Collect all matching candidates, then pick the best one
|
||||
var matchingCandidates = new List<(CompaniesHouseSearchItem Item, int AliasIndex)>();
|
||||
|
||||
for (var aliasIndex = 0; aliasIndex < aliases.Length; aliasIndex++)
|
||||
{
|
||||
var alias = aliases[aliasIndex];
|
||||
var aliasUpper = alias.ToUpperInvariant();
|
||||
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(candidate.Title))
|
||||
continue;
|
||||
|
||||
var titleUpper = candidate.Title.ToUpperInvariant();
|
||||
|
||||
// Check for exact match or very close match (fuzzy score >= 95)
|
||||
var fuzzyScore = Fuzz.Ratio(aliasUpper, titleUpper);
|
||||
if (fuzzyScore >= 95)
|
||||
{
|
||||
matchingCandidates.Add((candidate, aliasIndex));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matchingCandidates.Count == 0)
|
||||
return null;
|
||||
|
||||
// Sort candidates: prefer active > dissolved, then by alias order (first alias is preferred)
|
||||
var bestMatch = matchingCandidates
|
||||
.OrderBy(m => m.Item.CompanyStatus?.ToLowerInvariant() == "active" ? 0 : 1) // Active first
|
||||
.ThenBy(m => m.Item.CompanyStatus?.ToLowerInvariant() == "dissolved" ? 1 : 0) // Dissolved last
|
||||
.ThenBy(m => m.AliasIndex) // First alias is preferred
|
||||
.First();
|
||||
|
||||
// Verify the company existed at the claimed start date
|
||||
if (claimedStartDate.HasValue)
|
||||
{
|
||||
var incDate = DateHelpers.ParseDate(bestMatch.Item.DateOfCreation);
|
||||
if (incDate.HasValue && incDate.Value > claimedStartDate.Value)
|
||||
{
|
||||
// Company didn't exist yet - but still return it so the flag can be raised
|
||||
// Don't skip, let the verification process handle the date issue
|
||||
}
|
||||
}
|
||||
|
||||
return (bestMatch.Item, 100); // 100% match via known alias
|
||||
}
|
||||
|
||||
private async Task<CompanyCache?> FindCachedMatchAsync(string companyName)
|
||||
{
|
||||
var cutoffDate = DateTime.UtcNow.AddDays(-CacheExpirationDays);
|
||||
@@ -964,12 +1233,37 @@ public sealed class CompanyVerifierService : ICompanyVerifierService
|
||||
/// Generates alternative search queries to find companies that may be registered
|
||||
/// with slightly different names (e.g., "U.K." vs "UK", "Limited" vs "Ltd").
|
||||
/// Also handles "Brand (Parent Company)" format by extracting and prioritizing the parent.
|
||||
/// Uses TradingNameAliases to map common trading names to registered names.
|
||||
/// </summary>
|
||||
private static List<string> GenerateSearchQueries(string companyName)
|
||||
{
|
||||
var queries = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var normalized = companyName.Trim();
|
||||
|
||||
// Step 0: Check if this is a known trading name and add alias queries FIRST (highest priority)
|
||||
if (TradingNameAliases.TryGetValue(normalized, out var aliases))
|
||||
{
|
||||
foreach (var alias in aliases)
|
||||
{
|
||||
queries.Add(alias);
|
||||
}
|
||||
}
|
||||
|
||||
// Also check partial matches for trading names (e.g., "Boots UK" should match "Boots")
|
||||
foreach (var (tradingName, aliasNames) in TradingNameAliases)
|
||||
{
|
||||
// Check if the company name starts with or contains the trading name
|
||||
if (normalized.StartsWith(tradingName, StringComparison.OrdinalIgnoreCase) ||
|
||||
normalized.EndsWith(tradingName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
foreach (var alias in aliasNames)
|
||||
{
|
||||
queries.Add(alias);
|
||||
}
|
||||
break; // Only use first matching alias set
|
||||
}
|
||||
}
|
||||
|
||||
// Step 0a: Check for "Brand (Parent Company)" format and extract parent company
|
||||
// Parent company is more likely to be the registered name, so search it first
|
||||
var parentMatch = System.Text.RegularExpressions.Regex.Match(normalized, @"\(([^)]+)\)\s*$");
|
||||
@@ -1,8 +1,8 @@
|
||||
using TrueCV.Application.Data;
|
||||
using TrueCV.Application.Interfaces;
|
||||
using TrueCV.Application.Models;
|
||||
using RealCV.Application.Data;
|
||||
using RealCV.Application.Interfaces;
|
||||
using RealCV.Application.Models;
|
||||
|
||||
namespace TrueCV.Infrastructure.Services;
|
||||
namespace RealCV.Infrastructure.Services;
|
||||
|
||||
public sealed class EducationVerifierService : IEducationVerifierService
|
||||
{
|
||||
@@ -2,10 +2,10 @@ using Azure.Storage.Blobs;
|
||||
using Azure.Storage.Blobs.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using TrueCV.Application.Interfaces;
|
||||
using TrueCV.Infrastructure.Configuration;
|
||||
using RealCV.Application.Interfaces;
|
||||
using RealCV.Infrastructure.Configuration;
|
||||
|
||||
namespace TrueCV.Infrastructure.Services;
|
||||
namespace RealCV.Infrastructure.Services;
|
||||
|
||||
public sealed class FileStorageService : IFileStorageService
|
||||
{
|
||||
275
src/RealCV.Infrastructure/Services/GitHubVerifierService.cs
Normal file
275
src/RealCV.Infrastructure/Services/GitHubVerifierService.cs
Normal file
@@ -0,0 +1,275 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using RealCV.Application.Interfaces;
|
||||
using RealCV.Application.Models;
|
||||
using RealCV.Infrastructure.Clients;
|
||||
|
||||
namespace RealCV.Infrastructure.Services;
|
||||
|
||||
public sealed class GitHubVerifierService : IGitHubVerifierService
|
||||
{
|
||||
private readonly GitHubApiClient _gitHubClient;
|
||||
private readonly ILogger<GitHubVerifierService> _logger;
|
||||
|
||||
// Map common skill names to GitHub languages
|
||||
private static readonly Dictionary<string, string[]> SkillToLanguageMap = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["JavaScript"] = ["JavaScript", "TypeScript"],
|
||||
["TypeScript"] = ["TypeScript"],
|
||||
["Python"] = ["Python"],
|
||||
["Java"] = ["Java", "Kotlin"],
|
||||
["C#"] = ["C#"],
|
||||
[".NET"] = ["C#", "F#"],
|
||||
["React"] = ["JavaScript", "TypeScript"],
|
||||
["Angular"] = ["TypeScript", "JavaScript"],
|
||||
["Vue"] = ["JavaScript", "TypeScript", "Vue"],
|
||||
["Node.js"] = ["JavaScript", "TypeScript"],
|
||||
["Go"] = ["Go"],
|
||||
["Golang"] = ["Go"],
|
||||
["Rust"] = ["Rust"],
|
||||
["Ruby"] = ["Ruby"],
|
||||
["PHP"] = ["PHP"],
|
||||
["Swift"] = ["Swift"],
|
||||
["Kotlin"] = ["Kotlin"],
|
||||
["C++"] = ["C++", "C"],
|
||||
["C"] = ["C"],
|
||||
["Scala"] = ["Scala"],
|
||||
["R"] = ["R"],
|
||||
["SQL"] = ["PLSQL", "TSQL"],
|
||||
["Shell"] = ["Shell", "Bash", "PowerShell"],
|
||||
["DevOps"] = ["Shell", "Dockerfile", "HCL"],
|
||||
["Docker"] = ["Dockerfile"],
|
||||
["Terraform"] = ["HCL"],
|
||||
["Mobile"] = ["Swift", "Kotlin", "Dart", "Java"],
|
||||
["iOS"] = ["Swift", "Objective-C"],
|
||||
["Android"] = ["Kotlin", "Java"],
|
||||
["Flutter"] = ["Dart"],
|
||||
["Machine Learning"] = ["Python", "Jupyter Notebook", "R"],
|
||||
["Data Science"] = ["Python", "Jupyter Notebook", "R"],
|
||||
};
|
||||
|
||||
public GitHubVerifierService(
|
||||
GitHubApiClient gitHubClient,
|
||||
ILogger<GitHubVerifierService> logger)
|
||||
{
|
||||
_gitHubClient = gitHubClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<GitHubVerificationResult> VerifyProfileAsync(string username)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Verifying GitHub profile: {Username}", username);
|
||||
|
||||
var user = await _gitHubClient.GetUserAsync(username);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
return new GitHubVerificationResult
|
||||
{
|
||||
ClaimedUsername = username,
|
||||
IsVerified = false,
|
||||
VerificationNotes = "GitHub profile not found"
|
||||
};
|
||||
}
|
||||
|
||||
// Get repositories for language analysis
|
||||
var repos = await _gitHubClient.GetUserReposAsync(username);
|
||||
|
||||
// Analyze languages
|
||||
var languageStats = new Dictionary<string, int>();
|
||||
foreach (var repo in repos.Where(r => !r.Fork && !string.IsNullOrEmpty(r.Language)))
|
||||
{
|
||||
if (!languageStats.ContainsKey(repo.Language!))
|
||||
languageStats[repo.Language!] = 0;
|
||||
languageStats[repo.Language!]++;
|
||||
}
|
||||
|
||||
// Calculate flags
|
||||
var flags = new List<GitHubVerificationFlag>();
|
||||
|
||||
// Check account age
|
||||
var accountAge = DateTime.UtcNow - user.CreatedAt;
|
||||
if (accountAge.TotalDays < 90)
|
||||
{
|
||||
flags.Add(new GitHubVerificationFlag
|
||||
{
|
||||
Type = "NewAccount",
|
||||
Severity = "Info",
|
||||
Message = "Account created less than 90 days ago",
|
||||
ScoreImpact = -5
|
||||
});
|
||||
}
|
||||
|
||||
// Check for empty profile
|
||||
if (user.PublicRepos == 0)
|
||||
{
|
||||
flags.Add(new GitHubVerificationFlag
|
||||
{
|
||||
Type = "NoRepos",
|
||||
Severity = "Warning",
|
||||
Message = "No public repositories",
|
||||
ScoreImpact = -10
|
||||
});
|
||||
}
|
||||
|
||||
return new GitHubVerificationResult
|
||||
{
|
||||
ClaimedUsername = username,
|
||||
IsVerified = true,
|
||||
ProfileName = user.Name,
|
||||
ProfileUrl = user.HtmlUrl,
|
||||
Bio = user.Bio,
|
||||
Company = user.Company,
|
||||
Location = user.Location,
|
||||
AccountCreated = user.CreatedAt != default
|
||||
? DateOnly.FromDateTime(user.CreatedAt)
|
||||
: null,
|
||||
PublicRepos = user.PublicRepos,
|
||||
Followers = user.Followers,
|
||||
Following = user.Following,
|
||||
LanguageStats = languageStats,
|
||||
VerificationNotes = BuildVerificationSummary(user, repos),
|
||||
Flags = flags
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error verifying GitHub profile: {Username}", username);
|
||||
return new GitHubVerificationResult
|
||||
{
|
||||
ClaimedUsername = username,
|
||||
IsVerified = false,
|
||||
VerificationNotes = $"Error during verification: {ex.Message}"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<GitHubVerificationResult> VerifySkillsAsync(
|
||||
string username,
|
||||
List<string> claimedSkills)
|
||||
{
|
||||
var result = await VerifyProfileAsync(username);
|
||||
|
||||
if (!result.IsVerified)
|
||||
return result;
|
||||
|
||||
var skillVerifications = new List<SkillVerification>();
|
||||
|
||||
foreach (var skill in claimedSkills)
|
||||
{
|
||||
var verified = false;
|
||||
var repoCount = 0;
|
||||
|
||||
// Check if the skill matches a known language directly
|
||||
if (result.LanguageStats.TryGetValue(skill, out var count))
|
||||
{
|
||||
verified = true;
|
||||
repoCount = count;
|
||||
}
|
||||
else if (SkillToLanguageMap.TryGetValue(skill, out var mappedLanguages))
|
||||
{
|
||||
// Check if any mapped language exists in the user's repos
|
||||
foreach (var lang in mappedLanguages)
|
||||
{
|
||||
if (result.LanguageStats.TryGetValue(lang, out var langCount))
|
||||
{
|
||||
verified = true;
|
||||
repoCount += langCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
skillVerifications.Add(new SkillVerification
|
||||
{
|
||||
ClaimedSkill = skill,
|
||||
IsVerified = verified,
|
||||
RepoCount = repoCount,
|
||||
Notes = verified
|
||||
? $"Found in {repoCount} repositories"
|
||||
: "No repositories found using this skill"
|
||||
});
|
||||
}
|
||||
|
||||
var verifiedCount = skillVerifications.Count(sv => sv.IsVerified);
|
||||
var totalCount = skillVerifications.Count;
|
||||
var percentage = totalCount > 0 ? (verifiedCount * 100) / totalCount : 0;
|
||||
|
||||
return result with
|
||||
{
|
||||
SkillVerifications = skillVerifications,
|
||||
VerificationNotes = $"Skills verified: {verifiedCount}/{totalCount} ({percentage}%)"
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<List<GitHubProfileSearchResult>> SearchProfilesAsync(string name)
|
||||
{
|
||||
try
|
||||
{
|
||||
var searchResponse = await _gitHubClient.SearchUsersAsync(name);
|
||||
|
||||
if (searchResponse?.Items == null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var results = new List<GitHubProfileSearchResult>();
|
||||
|
||||
foreach (var item in searchResponse.Items.Take(10))
|
||||
{
|
||||
if (string.IsNullOrEmpty(item.Login))
|
||||
continue;
|
||||
|
||||
// Get full profile details
|
||||
var user = await _gitHubClient.GetUserAsync(item.Login);
|
||||
|
||||
results.Add(new GitHubProfileSearchResult
|
||||
{
|
||||
Username = item.Login,
|
||||
Name = user?.Name,
|
||||
AvatarUrl = item.AvatarUrl,
|
||||
Bio = user?.Bio,
|
||||
PublicRepos = user?.PublicRepos ?? 0,
|
||||
Followers = user?.Followers ?? 0
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error searching GitHub profiles: {Name}", name);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildVerificationSummary(GitHubUser user, List<GitHubRepo> repos)
|
||||
{
|
||||
var parts = new List<string>
|
||||
{
|
||||
$"Account created: {user.CreatedAt:yyyy-MM-dd}",
|
||||
$"Public repos: {user.PublicRepos}",
|
||||
$"Followers: {user.Followers}"
|
||||
};
|
||||
|
||||
var totalStars = repos.Sum(r => r.StargazersCount);
|
||||
if (totalStars > 0)
|
||||
{
|
||||
parts.Add($"Total stars: {totalStars}");
|
||||
}
|
||||
|
||||
var topLanguages = repos
|
||||
.Where(r => !r.Fork && !string.IsNullOrEmpty(r.Language))
|
||||
.GroupBy(r => r.Language)
|
||||
.OrderByDescending(g => g.Count())
|
||||
.Take(3)
|
||||
.Select(g => g.Key);
|
||||
|
||||
if (topLanguages.Any())
|
||||
{
|
||||
parts.Add($"Top languages: {string.Join(", ", topLanguages)}");
|
||||
}
|
||||
|
||||
return string.Join(" | ", parts);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using TrueCV.Application.Interfaces;
|
||||
using TrueCV.Infrastructure.Configuration;
|
||||
using RealCV.Application.Interfaces;
|
||||
using RealCV.Infrastructure.Configuration;
|
||||
|
||||
namespace TrueCV.Infrastructure.Services;
|
||||
namespace RealCV.Infrastructure.Services;
|
||||
|
||||
public sealed class LocalFileStorageService : IFileStorageService
|
||||
{
|
||||
@@ -0,0 +1,219 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using RealCV.Application.Interfaces;
|
||||
using RealCV.Application.Models;
|
||||
using RealCV.Infrastructure.Clients;
|
||||
|
||||
namespace RealCV.Infrastructure.Services;
|
||||
|
||||
public sealed class ProfessionalVerifierService : IProfessionalVerifierService
|
||||
{
|
||||
private readonly FcaRegisterClient _fcaClient;
|
||||
private readonly ILogger<ProfessionalVerifierService> _logger;
|
||||
|
||||
public ProfessionalVerifierService(
|
||||
FcaRegisterClient fcaClient,
|
||||
ILogger<ProfessionalVerifierService> logger)
|
||||
{
|
||||
_fcaClient = fcaClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<ProfessionalVerificationResult> VerifyFcaRegistrationAsync(
|
||||
string name,
|
||||
string? firmName = null,
|
||||
string? referenceNumber = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Verifying FCA registration for: {Name}", name);
|
||||
|
||||
// If we have a reference number, try to get directly
|
||||
if (!string.IsNullOrEmpty(referenceNumber))
|
||||
{
|
||||
var details = await _fcaClient.GetIndividualAsync(referenceNumber);
|
||||
if (details != null)
|
||||
{
|
||||
var isNameMatch = IsNameMatch(name, details.Name);
|
||||
return new ProfessionalVerificationResult
|
||||
{
|
||||
ClaimedName = name,
|
||||
ProfessionalBody = "FCA",
|
||||
IsVerified = isNameMatch,
|
||||
RegistrationNumber = details.IndividualReferenceNumber,
|
||||
MatchedName = details.Name,
|
||||
Status = details.Status,
|
||||
RegistrationDate = ParseDate(details.EffectiveDate),
|
||||
ControlledFunctions = details.ControlledFunctions?
|
||||
.Where(cf => cf.Status?.Equals("Active", StringComparison.OrdinalIgnoreCase) == true)
|
||||
.Select(cf => cf.ControlledFunction ?? "Unknown")
|
||||
.ToList(),
|
||||
VerificationNotes = isNameMatch
|
||||
? $"FCA Individual Reference Number: {details.IndividualReferenceNumber}"
|
||||
: "Reference number found but name does not match"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Search by name
|
||||
var searchResponse = await _fcaClient.SearchIndividualsAsync(name);
|
||||
|
||||
if (searchResponse?.Data == null || searchResponse.Data.Count == 0)
|
||||
{
|
||||
return new ProfessionalVerificationResult
|
||||
{
|
||||
ClaimedName = name,
|
||||
ProfessionalBody = "FCA",
|
||||
IsVerified = false,
|
||||
VerificationNotes = "No matching FCA registered individuals found"
|
||||
};
|
||||
}
|
||||
|
||||
// Find best match
|
||||
var matches = searchResponse.Data
|
||||
.Where(i => IsNameMatch(name, i.Name))
|
||||
.ToList();
|
||||
|
||||
if (matches.Count == 0)
|
||||
{
|
||||
return new ProfessionalVerificationResult
|
||||
{
|
||||
ClaimedName = name,
|
||||
ProfessionalBody = "FCA",
|
||||
IsVerified = false,
|
||||
VerificationNotes = $"Found {searchResponse.Data.Count} results but no close name matches"
|
||||
};
|
||||
}
|
||||
|
||||
// If firm specified, try to match on that too
|
||||
FcaIndividualSearchItem? bestMatch = null;
|
||||
if (!string.IsNullOrEmpty(firmName))
|
||||
{
|
||||
bestMatch = matches.FirstOrDefault(m =>
|
||||
m.CurrentEmployers?.Contains(firmName, StringComparison.OrdinalIgnoreCase) == true);
|
||||
}
|
||||
|
||||
bestMatch ??= matches.First();
|
||||
|
||||
// Get detailed information
|
||||
if (!string.IsNullOrEmpty(bestMatch.IndividualReferenceNumber))
|
||||
{
|
||||
var details = await _fcaClient.GetIndividualAsync(bestMatch.IndividualReferenceNumber);
|
||||
if (details != null)
|
||||
{
|
||||
return new ProfessionalVerificationResult
|
||||
{
|
||||
ClaimedName = name,
|
||||
ProfessionalBody = "FCA",
|
||||
IsVerified = true,
|
||||
RegistrationNumber = details.IndividualReferenceNumber,
|
||||
MatchedName = details.Name,
|
||||
Status = details.Status,
|
||||
RegistrationDate = ParseDate(details.EffectiveDate),
|
||||
CurrentEmployer = bestMatch.CurrentEmployers,
|
||||
ControlledFunctions = details.ControlledFunctions?
|
||||
.Where(cf => cf.Status?.Equals("Active", StringComparison.OrdinalIgnoreCase) == true)
|
||||
.Select(cf => cf.ControlledFunction ?? "Unknown")
|
||||
.ToList(),
|
||||
VerificationNotes = $"FCA Individual Reference Number: {details.IndividualReferenceNumber}"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Basic verification without details
|
||||
return new ProfessionalVerificationResult
|
||||
{
|
||||
ClaimedName = name,
|
||||
ProfessionalBody = "FCA",
|
||||
IsVerified = true,
|
||||
RegistrationNumber = bestMatch.IndividualReferenceNumber,
|
||||
MatchedName = bestMatch.Name,
|
||||
Status = bestMatch.Status,
|
||||
CurrentEmployer = bestMatch.CurrentEmployers,
|
||||
VerificationNotes = "Verified via FCA Register search"
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error verifying FCA registration for: {Name}", name);
|
||||
return new ProfessionalVerificationResult
|
||||
{
|
||||
ClaimedName = name,
|
||||
ProfessionalBody = "FCA",
|
||||
IsVerified = false,
|
||||
VerificationNotes = $"Error during verification: {ex.Message}"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<FcaIndividualSearchResult>> SearchFcaIndividualsAsync(string name)
|
||||
{
|
||||
try
|
||||
{
|
||||
var searchResponse = await _fcaClient.SearchIndividualsAsync(name);
|
||||
|
||||
if (searchResponse?.Data == null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return searchResponse.Data
|
||||
.Select(i => new FcaIndividualSearchResult
|
||||
{
|
||||
Name = i.Name ?? "Unknown",
|
||||
IndividualReferenceNumber = i.IndividualReferenceNumber ?? "Unknown",
|
||||
Status = i.Status,
|
||||
CurrentFirms = i.CurrentEmployers?.Split(',')
|
||||
.Select(f => f.Trim())
|
||||
.Where(f => !string.IsNullOrEmpty(f))
|
||||
.ToList()
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error searching FCA for: {Name}", name);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsNameMatch(string searchName, string? foundName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(foundName))
|
||||
return false;
|
||||
|
||||
var searchNormalized = NormalizeName(searchName);
|
||||
var foundNormalized = NormalizeName(foundName);
|
||||
|
||||
// Exact match
|
||||
if (searchNormalized.Equals(foundNormalized, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
|
||||
// Check if all parts of search name are in found name
|
||||
var searchParts = searchNormalized.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
var foundParts = foundNormalized.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
// All search parts must be found
|
||||
return searchParts.All(sp =>
|
||||
foundParts.Any(fp => fp.Equals(sp, StringComparison.OrdinalIgnoreCase)));
|
||||
}
|
||||
|
||||
private static string NormalizeName(string name)
|
||||
{
|
||||
return name
|
||||
.Replace(",", " ")
|
||||
.Replace(".", " ")
|
||||
.Replace("-", " ")
|
||||
.Trim();
|
||||
}
|
||||
|
||||
private static DateOnly? ParseDate(string? dateString)
|
||||
{
|
||||
if (string.IsNullOrEmpty(dateString))
|
||||
return null;
|
||||
|
||||
if (DateOnly.TryParse(dateString, out var date))
|
||||
return date;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TrueCV.Application.Interfaces;
|
||||
using TrueCV.Application.Models;
|
||||
using RealCV.Application.Interfaces;
|
||||
using RealCV.Application.Models;
|
||||
|
||||
namespace TrueCV.Infrastructure.Services;
|
||||
namespace RealCV.Infrastructure.Services;
|
||||
|
||||
public sealed class TimelineAnalyserService : ITimelineAnalyserService
|
||||
{
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using TrueCV.Application.Interfaces;
|
||||
using RealCV.Application.Interfaces;
|
||||
|
||||
namespace TrueCV.Infrastructure.Services;
|
||||
namespace RealCV.Infrastructure.Services;
|
||||
|
||||
public sealed class UserContextService : IUserContextService
|
||||
{
|
||||
@@ -7,7 +7,7 @@
|
||||
<base href="/" />
|
||||
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="app.css" />
|
||||
<link rel="stylesheet" href="TrueCV.Web.styles.css" />
|
||||
<link rel="stylesheet" href="RealCV.Web.styles.css" />
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<HeadOutlet @rendermode="InteractiveServer" />
|
||||
</head>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user