This repository was archived by the owner on May 12, 2026. It is now read-only.
update staging - #61
Merged
Merged
Conversation
New endpoint replaces the legacy onboarding flow for the FE-ONB-EMP-001 UI:
Fields accepted:
- employerType: Founder | Recruiter | Agency (422 on invalid)
- companyName: required string
- companySize: 1-10 | 11-50 | 51-200 | 201-500 | 500+ (422 on invalid)
- companyWebsite: optional, URL-validated (422 on bad format)
- hiringRoles: string[] min 1, custom values accepted (422 if empty)
- hiringLocations: string[] min 1, validated enum (422 on invalid/empty)
Returns 201 { status: "success", message: "Profile saved" }.
Upserts employer_profiles and sets onboarding_complete = true.
Validation uses endpoint-level pipe with errorHttpStatusCode 422.
Migration adds employer_type, hiring_roles, hiring_locations columns.
Legacy POST /employer/onboarding kept for backward compatibility.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New endpoints (all role-guarded to TALENT, 422 for validation errors):
POST /talent/onboarding/goal — Step 1: save single goal; 422 on missing/invalid
POST /talent/onboarding/track — Step 2: save single track; 422 on missing/invalid
POST /talent/profile — Step 3: multipart photo (required) + optional
region/educationLevel/linkedinProfile; returns 201
Sets onboarding_complete=true; profile_verified=true
only when all optional fields are also provided
POST /talent/onboarding/personalise — Step 4: reads saved data, logs personalisation
event (userId, timestamp, available data points);
422 if track missing; assessments generated from
track; recommendations skipped if goal/region/
education missing
Migration adds track (varchar) and profile_verified (bool) to talent_profiles.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rding Feat/talent progressive onboarding
leothatguy
approved these changes
May 12, 2026
There was a problem hiding this comment.
Pull request overview
This PR introduces new multi-step onboarding endpoints for talent (goal → track → profile upload → personalisation) and a new employer profile onboarding endpoint, alongside the required message additions and database schema changes.
Changes:
- Added new talent onboarding step endpoints and corresponding service logic, including a profile-photo upload flow and a personalisation trigger.
- Added a new employer
POST /employer/profileonboarding completion endpoint with new profile fields/constants. - Added DB migrations + entity updates for
talent_profiles.track/profile_verifiedand newemployer_profilesfields, plus updated shared success/error messages and adjusted an e2e mock.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| test/onboarding.e2e-spec.ts | Updates UploadService mock to return a URL for avatar uploads. |
| src/shared/messages/success.messages.ts | Adds new onboarding success message keys for the new endpoints. |
| src/shared/messages/error.messages.ts | Adds new onboarding error message keys for photo validation and personalisation. |
| src/modules/talent/talent.service.ts | Implements new talent onboarding step methods, photo/profile save, and personalisation logging. |
| src/modules/talent/talent.controller.ts | Adds new talent onboarding routes (goal/track/profile/personalise) and multipart handling. |
| src/modules/talent/entities/talent-profile.entity.ts | Adds track and profile_verified fields to the TalentProfile entity. |
| src/modules/talent/dto/save-goal.dto.ts | Adds DTO + validation for single-goal selection. |
| src/modules/talent/dto/save-track.dto.ts | Adds DTO + validation for single-track selection. |
| src/modules/talent/dto/save-talent-profile.dto.ts | Adds DTO + validation for optional profile fields in the new step flow. |
| src/modules/employer/entities/employer-profile.entity.ts | Adds new employer profile fields for the new onboarding endpoint. |
| src/modules/employer/employer.service.ts | Implements employer profile save + onboarding completion transaction flow. |
| src/modules/employer/employer.controller.ts | Adds POST /employer/profile endpoint with validation pipe configuration. |
| src/modules/employer/employer.constants.ts | Adds constants for employer types, company sizes, and hiring locations. |
| src/modules/employer/dto/save-employer-profile.dto.ts | Adds DTO + validation for the new employer profile onboarding endpoint. |
| src/database/migrations/1779100000000-AddTalentTrackAndProfileVerified.ts | Adds track and profile_verified columns to talent_profiles. |
| src/database/migrations/1779000000000-AddEmployerProfileNewFields.ts | Adds employer_type, hiring_roles, and hiring_locations columns to employer_profiles. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+168
to
+173
| FileInterceptor('photo', { | ||
| storage: memoryStorage(), | ||
| fileFilter: (_req, file, cb) => { | ||
| if (ALLOWED_MIME_TYPES.includes(file.mimetype)) { | ||
| cb(null, true); | ||
| } else { |
Comment on lines
+141
to
+143
| @ApiOperation({ summary: 'Step 1 — save career goal (BE-ONB-TAL-001)' }) | ||
| @UsePipes(new ValidationPipe({ whitelist: true, transform: true, errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY })) | ||
| async saveGoalStep( |
Comment on lines
+153
to
+155
| @ApiOperation({ summary: 'Step 2 — save track (BE-ONB-TAL-002)' }) | ||
| @UsePipes(new ValidationPipe({ whitelist: true, transform: true, errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY })) | ||
| async saveTrackStep( |
| }, | ||
| }), | ||
| ) | ||
| @UsePipes(new ValidationPipe({ whitelist: true, transform: true, errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY })) |
Comment on lines
+85
to
+95
| /** BE-ONB-TAL-001 — save single goal, 422 on invalid. */ | ||
| async saveGoalStep( | ||
| userId: string, | ||
| dto: SaveGoalDto, | ||
| ): Promise<{ status: string; message: string }> { | ||
| const profile = await this.findOrCreateProfile(userId); | ||
| profile.goal = dto.goal; | ||
| if (profile.onboarding_step < 1) profile.onboarding_step = 1; | ||
| await this.talentProfileRepository.save(profile); | ||
| return { status: 'success', message: SuccessMessages.ONBOARDING.GOAL_SAVED }; | ||
| } |
Comment on lines
+39
to
+45
| @UsePipes( | ||
| new ValidationPipe({ | ||
| whitelist: true, | ||
| transform: true, | ||
| errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, | ||
| }), | ||
| ) |
Comment on lines
+27
to
+31
| @ApiProperty({ example: 'Acme Labs' }) | ||
| @IsString() | ||
| @MinLength(1, { message: 'companyName is required' }) | ||
| @MaxLength(255) | ||
| companyName: string; |
| @Column({ type: 'varchar', length: 100, nullable: true }) | ||
| track: string | null; | ||
|
|
||
| @ApiProperty({ default: false, description: 'True when all profile fields including optional ones are complete' }) |
Comment on lines
+138
to
+203
| /** BE-ONB-TAL-001 — save goal (single select, 422 on missing/invalid). */ | ||
| @Post('onboarding/goal') | ||
| @HttpCode(HttpStatus.OK) | ||
| @ApiOperation({ summary: 'Step 1 — save career goal (BE-ONB-TAL-001)' }) | ||
| @UsePipes(new ValidationPipe({ whitelist: true, transform: true, errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY })) | ||
| async saveGoalStep( | ||
| @CurrentUser('sub') userId: string, | ||
| @Body() dto: SaveGoalDto, | ||
| ) { | ||
| return this.talentService.saveGoalStep(userId, dto); | ||
| } | ||
|
|
||
| /** BE-ONB-TAL-002 — save single track (422 on missing/invalid). */ | ||
| @Post('onboarding/track') | ||
| @HttpCode(HttpStatus.OK) | ||
| @ApiOperation({ summary: 'Step 2 — save track (BE-ONB-TAL-002)' }) | ||
| @UsePipes(new ValidationPipe({ whitelist: true, transform: true, errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY })) | ||
| async saveTrackStep( | ||
| @CurrentUser('sub') userId: string, | ||
| @Body() dto: SaveTrackDto, | ||
| ) { | ||
| return this.talentService.saveTrackStep(userId, dto); | ||
| } | ||
|
|
||
| /** BE-ONB-TAL-003 — save profile; photo required, optional fields optional. */ | ||
| @Post('profile') | ||
| @HttpCode(HttpStatus.CREATED) | ||
| @ApiOperation({ summary: 'Step 3 — save talent profile with photo upload (BE-ONB-TAL-003)' }) | ||
| @ApiConsumes('multipart/form-data') | ||
| @UseInterceptors( | ||
| FileInterceptor('photo', { | ||
| storage: memoryStorage(), | ||
| fileFilter: (_req, file, cb) => { | ||
| if (ALLOWED_MIME_TYPES.includes(file.mimetype)) { | ||
| cb(null, true); | ||
| } else { | ||
| cb( | ||
| new UnprocessableEntityException(ErrorMessages.ONBOARDING.INVALID_PHOTO_TYPE), | ||
| false, | ||
| ); | ||
| } | ||
| }, | ||
| }), | ||
| ) | ||
| @UsePipes(new ValidationPipe({ whitelist: true, transform: true, errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY })) | ||
| async saveTalentProfile( | ||
| @CurrentUser('sub') userId: string, | ||
| @UploadedFile() photo: Express.Multer.File | undefined, | ||
| @Body() dto: SaveTalentProfileDto, | ||
| ) { | ||
| if (!photo) { | ||
| throw new UnprocessableEntityException(ErrorMessages.ONBOARDING.PHOTO_REQUIRED); | ||
| } | ||
| if (photo.size > MAX_FILE_BYTES) { | ||
| throw new UnprocessableEntityException(ErrorMessages.ONBOARDING.PHOTO_TOO_LARGE); | ||
| } | ||
| return this.talentService.saveTalentProfile(userId, photo, dto); | ||
| } | ||
|
|
||
| /** BE-ONB-TAL-004 — trigger personalisation; no body needed. */ | ||
| @Post('onboarding/personalise') | ||
| @HttpCode(HttpStatus.OK) | ||
| @ApiOperation({ summary: 'Step 4 — personalise dashboard from saved onboarding data (BE-ONB-TAL-004)' }) | ||
| async personalise(@CurrentUser('sub') userId: string) { | ||
| return this.talentService.personalise(userId); | ||
| } |
Comment on lines
+34
to
+51
| @Post('profile') | ||
| @HttpCode(HttpStatus.CREATED) | ||
| @ApiOperation({ summary: 'Save employer profile and complete onboarding (BE-ONB-EMP-001)' }) | ||
| @ApiUnprocessableEntityResponse({ description: 'Validation failed — field-specific error messages' }) | ||
| @ApiForbiddenResponse({ description: 'Onboarding already completed or wrong role' }) | ||
| @UsePipes( | ||
| new ValidationPipe({ | ||
| whitelist: true, | ||
| transform: true, | ||
| errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, | ||
| }), | ||
| ) | ||
| async saveProfile( | ||
| @CurrentUser('sub') userId: string, | ||
| @Body() dto: SaveEmployerProfileDto, | ||
| ) { | ||
| return this.employerService.saveProfile(userId, dto); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Related Issue (Link to Github issue)
Motivation and Context
How Has This Been Tested?
Screenshots (if appropriate - Postman, etc):
Types of changes
Checklist: