Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/adapters/postgres/postgres-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { AcademicYear } from 'src/academicyears/entities/academicyears-entity';
import { CohortAcademicYearService } from './cohortAcademicYear-adapter';
import { AuthUtils } from '@utils/auth-util';
import { ElasticsearchModule } from 'src/elasticsearch/elasticsearch.module';
import { ReferralsModule } from 'src/referrals/referrals.module';

@Module({
imports: [
Expand All @@ -44,6 +45,7 @@ import { ElasticsearchModule } from 'src/elasticsearch/elasticsearch.module';
AcademicYear,
]),
ElasticsearchModule,
ReferralsModule,
],
providers: [
PostgresUserService,
Expand Down
23 changes: 21 additions & 2 deletions src/adapters/postgres/user-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
import { UserElasticsearchService } from '../../elasticsearch/user-elasticsearch.service';
import { IUser } from '../../elasticsearch/interfaces/user.interface';
import { isElasticsearchEnabled } from 'src/common/utils/elasticsearch.util';
import { ReferralsService } from 'src/referrals/referrals.service';
import { buildReferLink } from 'src/referrals/utils/referral-slug.util';

interface UpdateField {
userId: string; // Required
Expand Down Expand Up @@ -96,7 +98,8 @@
private postgresAcademicYearService: PostgresAcademicYearService,
private readonly cohortAcademicYearService: CohortAcademicYearService,
private readonly authUtils: AuthUtils,
private readonly userElasticsearchService: UserElasticsearchService
private readonly userElasticsearchService: UserElasticsearchService,
private readonly referralsService: ReferralsService
) {
this.jwt_secret = this.configService.get<string>('RBAC_JWT_SECRET');
this.jwt_password_reset_expires_In = this.configService.get<string>(
Expand Down Expand Up @@ -1794,7 +1797,7 @@
}
}

async createUser(

Check failure on line 1800 in src/adapters/postgres/user-adapter.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 52 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=tekdi_user-microservice&issues=AZ4rlP7tXW3QDbPYtdIC&open=AZ4rlP7tXW3QDbPYtdIC&pullRequest=741
request: any,
userCreateDto: UserCreateDto,
academicYearId: string,
Expand Down Expand Up @@ -1991,6 +1994,21 @@
}
LoggerUtil.log(API_RESPONSES.USER_CREATE_SUCCESSFULLY, apiId);

// Referral attribution (first-touch provided by client; do not block registration)
let referLinkForElastic: string | null = null;
try {
const incomingSlug = (userCreateDto as any)?.referralSlug ?? null;
const attr = await this.referralsService.createUserAttribution({
userId: result.userId,
incomingSlug,
});
referLinkForElastic =
attr.referLink ??
(attr.resolvedSlug ? buildReferLink(attr.resolvedSlug) : null);
} catch (refError: any) {
LoggerUtil.error('Referral attribution failed', refError);
}
Comment thread
Tusharmahajan12 marked this conversation as resolved.

// Add Elasticsearch sync with custom fields
try {
if (isElasticsearchEnabled()) {
Expand Down Expand Up @@ -2029,6 +2047,7 @@
email: result.email || '',
mobile: result.mobile ? result.mobile.toString() : '',
mobile_country_code: result.mobile_country_code || '',
referLink: referLinkForElastic ?? undefined,
gender: result.gender,
dob:
result.dob instanceof Date
Expand All @@ -2039,7 +2058,7 @@
currentCountry: result.currentCountry || '',
status: result.status,
customFields: elasticCustomFields,
},
} as any,
applications: [],
courses: [],
createdAt: result.createdAt
Expand Down
2 changes: 2 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { PaymentsModule } from './payments/payments.module';
import { PathwaysModule } from './pathways/pathways.module';
import { CountriesModule } from './countries/countries.module';
import { ContentModule } from './content/content.module';
import { ReferralsModule } from './referrals/referrals.module';

/**
* Main Application Module
Expand Down Expand Up @@ -93,6 +94,7 @@ import { ContentModule } from './content/content.module';
PathwaysModule,
CountriesModule,
ContentModule,
ReferralsModule,
],
controllers: [AppController, HealthController],
providers: [
Expand Down
9 changes: 9 additions & 0 deletions src/common/utils/api-id.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ export const APIID = {
USER_DELETE: "api.user.delete",
USER_BULK_IMPORT: "api.user.bulkImport",

// Referral Management APIs
REFERRAL_CREATE: "api.referral.create",
REFERRAL_LIST: "api.referral.list",
REFERRAL_RESOLVE: "api.referral.resolve",
REFERRAL_UPDATE: "api.referral.update",
REFERRAL_IMPORT: "api.referral.import",
REFERRAL_BULK: "api.referral.bulk",
REFERRAL_REPORT: "api.referral.report",

// Role Management APIs
ROLE_GET: "api.role.get",
ROLE_CREATE: "api.role.create",
Expand Down
9 changes: 9 additions & 0 deletions src/common/utils/response.messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,4 +280,13 @@ export const API_RESPONSES = {

// Country Management Messages
COUNTRY_LIST_SUCCESS: "Countries retrieved successfully",

// Referral Management Messages
REFERRAL_CREATED_SUCCESSFULLY: "Referral created successfully",
REFERRAL_LIST_SUCCESS: "Referrals retrieved successfully",
REFERRAL_RESOLVED_SUCCESSFULLY: "Referral resolved successfully",
REFERRAL_UPDATED_SUCCESSFULLY: "Referral updated successfully",
REFERRAL_IMPORT_SUCCESS: "Referrals imported successfully",
REFERRAL_BULK_SUCCESS: "Bulk referrals processed successfully",
REFERRAL_REPORT_SUCCESS: "Referral report retrieved successfully",
};
1 change: 1 addition & 0 deletions src/elasticsearch/elasticsearch.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ export class ElasticsearchService {
email: source?.profile?.email || '',
mobile: source?.profile?.mobile || '',
mobile_country_code: source?.profile?.mobile_country_code || '',
...(source?.profile?.referLink ? { referLink: source.profile.referLink } : {}),
gender: source?.profile?.gender || '',
dob:source?.profile?.dob || '',
country:source?.profile?.country || '',
Expand Down
1 change: 1 addition & 0 deletions src/elasticsearch/interfaces/user.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export interface IProfile {
email: string;
mobile: string;
mobile_country_code: string;
referLink?: string;
gender: string;
dob: string;
country: string;
Expand Down
13 changes: 13 additions & 0 deletions src/elasticsearch/user-elasticsearch.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
email: { type: 'keyword' },
mobile: { type: 'keyword' },
mobile_country_code: { type: 'keyword' },
referLink: { type: 'keyword' },
gender: { type: 'keyword' },
dob: { type: 'date', null_value: null },
country: {
Expand Down Expand Up @@ -379,6 +380,7 @@
email: profile.email,
mobile: profile.mobile,
mobile_country_code: profile.mobile_country_code,
...(profile.referLink ? { referLink: profile.referLink } : {}),
gender: profile.gender,
dob: profile.dob,
country: this.normalizeCountryKeyword(profile.country),
Expand Down Expand Up @@ -759,7 +761,7 @@
if (query.filters && typeof query.filters === 'object') {
// Special handling for cohortId and cohortmemberstatus in applications
const appFilters: any = {};
Object.entries(query.filters).forEach(([field, value]) => {

Check failure on line 764 in src/elasticsearch/user-elasticsearch.service.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 43 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=tekdi_user-microservice&issues=AZ4rlP8uXW3QDbPYtdIE&open=AZ4rlP8uXW3QDbPYtdIE&pullRequest=741
if (value !== undefined && value !== null && value !== '') {
// Handle cohortId, cohortmemberstatus, and completionPercentage as nested application filters
if (
Expand All @@ -771,6 +773,17 @@
return;
}

// Handle referLink filtering — match by slug or full URL
if (field === 'referLink') {
const referLinkValue = typeof value === 'string' ? value : JSON.stringify(value);
searchQuery.bool.filter.push({
wildcard: {
'profile.referLink': `*${referLinkValue.toLowerCase()}*`,
},
});
return;
}

// Handle auto_tags filtering
if (field === 'auto_tags') {
const tags = Array.isArray(value) ? value : [value];
Expand Down
2 changes: 2 additions & 0 deletions src/forms/forms.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { HttpModule } from '@nestjs/axios';
import { ConfigModule } from '@nestjs/config';
import { NotificationRequest } from 'src/common/utils/notification.axios';
import { CohortModule } from 'src/cohort/cohort.module';
import { ReferralsModule } from 'src/referrals/referrals.module';
@Module({
controllers: [FormsController, FormSubmissionController],
imports: [
Expand All @@ -44,6 +45,7 @@ import { CohortModule } from 'src/cohort/cohort.module';
HttpModule,
ConfigModule,
forwardRef(() => CohortModule),
ReferralsModule,
],
providers: [
FormsService,
Expand Down
7 changes: 6 additions & 1 deletion src/forms/services/form-submission.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import { CohortMembers } from 'src/cohortMembers/entities/cohort-member.entity';
import { Cohort } from 'src/cohort/entities/cohort.entity';
import { FieldValueConverter } from 'src/utils/field-value-converter';
import { ReferralsService } from 'src/referrals/referrals.service';

interface DateRange {
start: string;
Expand Down Expand Up @@ -94,7 +95,8 @@
private readonly userElasticsearchService: UserElasticsearchService,
private readonly formsService: FormsService,
@Inject(forwardRef(() => PostgresCohortService))
private readonly postgresCohortService: PostgresCohortService
private readonly postgresCohortService: PostgresCohortService,
private readonly referralsService: ReferralsService
) {}

async create(
Expand Down Expand Up @@ -2140,7 +2142,7 @@
*
* Made public so it can be used as an upsert callback from other services (e.g., cohortMembers-adapter).
*/
public async buildUserDocumentForElasticsearch(

Check failure on line 2145 in src/forms/services/form-submission.service.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 762 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=tekdi_user-microservice&issues=AZ4rlP8KXW3QDbPYtdID&open=AZ4rlP8KXW3QDbPYtdID&pullRequest=741
userId: string
): Promise<IUser | null> {
// Fetch user profile from Users table
Expand Down Expand Up @@ -3158,6 +3160,8 @@
}
}
}
const referLink = await this.referralsService.getUserReferLink(user.userId);
Comment thread
Tusharmahajan12 marked this conversation as resolved.

// Build the IUser object
return {
userId: user.userId,
Expand All @@ -3170,6 +3174,7 @@
email: user.email || '',
mobile: user.mobile ? user.mobile.toString() : '',
mobile_country_code: user.mobile_country_code || '',
...(referLink ? { referLink } : {}),
gender: user.gender,
dob: user.dob instanceof Date ? user.dob.toISOString() : user.dob || '',
country: user.country,
Expand Down
69 changes: 69 additions & 0 deletions src/referrals/dto/create-referral-entity.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsArray, IsEmail, IsEnum, IsOptional, IsString, MaxLength } from 'class-validator';
import {
ReferralEntityStatus,
ReferralEntitySubType,
ReferralEntityType,
} from '../referrals.types';

export class CreateReferralEntityDto {
@ApiPropertyOptional({ description: 'Custom slug (any format accepted; will be normalized to lowercase a-z0-9_). If omitted, auto-generated.' })
@IsOptional()
@IsString()
@MaxLength(100)
slug?: string;

@ApiProperty({ description: 'First name (or full org/university name)' })
@IsString()
@MaxLength(255)
firstName: string;

@ApiPropertyOptional({ description: 'Last name (optional for org/university)' })
@IsOptional()
@IsString()
@MaxLength(255)
lastName?: string;

@ApiProperty({ enum: ReferralEntityType })
@IsEnum(ReferralEntityType)
type: ReferralEntityType;

@ApiProperty({ enum: ReferralEntitySubType })
@IsEnum(ReferralEntitySubType)
subType: ReferralEntitySubType;

@ApiPropertyOptional({ description: 'Region (optional)' })
@IsOptional()
@IsString()
@MaxLength(100)
region?: string;

@ApiPropertyOptional({ description: 'Linked internal entity UUID (optional)' })
@IsOptional()
@IsString()
linkedEntityId?: string;

@ApiPropertyOptional({ description: 'Primary contact email (optional)' })
@IsOptional()
@IsEmail()
@MaxLength(255)
contactEmail?: string;

@ApiPropertyOptional({ description: 'Additional emails (array of email strings)', type: [String] })
@IsOptional()
@IsArray()
@IsEmail({}, { each: true })
additionalEmails?: string[];

@ApiPropertyOptional({ description: 'Country (optional)' })
@IsOptional()
@IsString()
@MaxLength(100)
country?: string;

@ApiPropertyOptional({ enum: ReferralEntityStatus, default: ReferralEntityStatus.ACTIVE })
@IsOptional()
@IsEnum(ReferralEntityStatus)
status?: ReferralEntityStatus;
}
Comment thread
Tusharmahajan12 marked this conversation as resolved.

13 changes: 13 additions & 0 deletions src/referrals/dto/import-referrals.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';

export class ImportReferralsDto {
@ApiProperty({
description:
'CSV content with header: firstName,lastName,type,subType,region,contactEmail,country',
})
@IsString()
@IsNotEmpty()
csv: string;
}

Comment thread
Tusharmahajan12 marked this conversation as resolved.
55 changes: 55 additions & 0 deletions src/referrals/dto/list-referrals.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsArray, IsEnum, IsNumber, IsObject, IsOptional, IsString, Min } from 'class-validator';
import { ReferralEntitySubType, ReferralEntityType } from '../referrals.types';

export class ReferralFiltersDto {
@ApiPropertyOptional({ enum: ReferralEntityType, description: 'Filter by referral type' })
@IsOptional()
@IsEnum(ReferralEntityType)
type?: ReferralEntityType;

@ApiPropertyOptional({ enum: ReferralEntitySubType, description: 'Filter by referral sub-type' })
@IsOptional()
@IsEnum(ReferralEntitySubType)
subType?: ReferralEntitySubType;

@ApiPropertyOptional({ type: String, description: 'Search across firstName, lastName, contactEmail, and slug' })
@IsOptional()
@IsString()
search?: string;

@ApiPropertyOptional({ type: [String], description: 'Filter by regions' })
@IsOptional()
@IsArray()
@IsString({ each: true })
regions?: string[];

@ApiPropertyOptional({ type: [String], description: 'Filter by countries' })
@IsOptional()
@IsArray()
@IsString({ each: true })
countries?: string[];
}

export class ListReferralsDto {
@ApiPropertyOptional({ description: 'Number of records to return', default: 10 })
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
limit?: number;

@ApiPropertyOptional({ description: 'Number of records to skip', default: 0 })
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(0)
offset?: number;

@ApiPropertyOptional({ type: ReferralFiltersDto, description: 'Filters to apply' })
@IsOptional()
@IsObject()
@Type(() => ReferralFiltersDto)
filters?: ReferralFiltersDto;
Comment on lines +50 to +54
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add @ValidateNested() for nested validation.

The filters field uses @Type(() => ReferralFiltersDto) for transformation, but nested validation requires @ValidateNested() from class-validator to validate the transformed object against ReferralFiltersDto constraints.

🔧 Proposed fix
+import { IsArray, IsEnum, IsNumber, IsObject, IsOptional, IsString, Min, ValidateNested } from 'class-validator';

  `@ApiPropertyOptional`({ type: ReferralFiltersDto, description: 'Filters to apply' })
  `@IsOptional`()
+  `@ValidateNested`()
  `@IsObject`()
  `@Type`(() => ReferralFiltersDto)
  filters?: ReferralFiltersDto;

As per coding guidelines, the code should adhere to NestJS best practices for DTO validation.

🧰 Tools
🪛 ESLint

[error] 50-50: Replace ·type:·ReferralFiltersDto,·description:·'Filters·to·apply' with ⏎····type:·ReferralFiltersDto,⏎····description:·"Filters·to·apply",⏎·

(prettier/prettier)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/referrals/dto/list-referrals.dto.ts` around lines 50 - 54, The filters
property in ListReferralsDto currently uses `@Type`(() => ReferralFiltersDto) but
lacks nested validation; add `@ValidateNested`() (from class-validator) to the
filters property so the transformed object is validated against
ReferralFiltersDto constraints, ensuring the decorator is applied alongside
`@IsOptional`(), `@IsObject`(), `@Type`(() => ReferralFiltersDto) on the filters?:
ReferralFiltersDto field.

}
Loading