Skip to content
Open
Show file tree
Hide file tree
Changes from all 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