Skip to content

Task#00000 Referral Tracking implementation#741

Open
Tusharmahajan12 wants to merge 1 commit into
tekdi:aspire-leadersfrom
Tusharmahajan12:new_asp_apr24
Open

Task#00000 Referral Tracking implementation#741
Tusharmahajan12 wants to merge 1 commit into
tekdi:aspire-leadersfrom
Tusharmahajan12:new_asp_apr24

Conversation

@Tusharmahajan12
Copy link
Copy Markdown
Collaborator

@Tusharmahajan12 Tusharmahajan12 commented May 15, 2026

Summary by CodeRabbit

Release Notes

  • New Features
    • Complete referral management system supporting creation, listing, filtering, and tracking of referral entities
    • Referral slug resolution with historical tracking of slug changes
    • CSV import and bulk insert capabilities for managing referrals
    • User attribution system that tracks referral sources and generates unique referral links
    • Referral links displayed in user profiles and search results for enhanced visibility

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 15, 2026

Walkthrough

This PR introduces a complete referral management system allowing organizations and individuals to be registered as referral entities with unique slugs and refer-links. The system tracks user attribution to referrals, supports CSV import and bulk operations, integrates with user creation workflows, and adds Elasticsearch search indexing for referral links.

Changes

Referral Management System

Layer / File(s) Summary
Type contracts and DTOs
src/referrals/referrals.types.ts, src/referrals/dto/*
Enums for referral entity type (EXTERNAL/INTERNAL), subtype (ORGANISATION/UNIVERSITY/ALUMNI), status (ACTIVE/INACTIVE); five DTOs for create, list, update, import operations with full validation including string length limits, email validation, enum membership, and array-of-emails support.
Database entities and schema
src/referrals/entities/referral-entity.entity.ts, src/referrals/entities/referral-slug-history.entity.ts, src/referrals/entities/user-attribution.entity.ts
Three TypeORM entities: ReferralEntity with unique slug and categorization fields, ReferralSlugHistory tracking old→new slug transitions, UserAttribution recording which referral entity each user was attributed to via their registration referral slug.
Slug generation and normalization utilities
src/referrals/utils/referral-slug.util.ts
Utilities for generating collision-resistant unique slugs (name-based for external entities, random base36 for internal alumni), normalizing slug input, building refer-links from frontend URLs, and validating slug format compliance.
Referral service core logic
src/referrals/referrals.service.ts
Complete CRUD lifecycle: create entities with auto-generated or provided slugs with uniqueness enforcement, list with dynamic filtering (type/subType/search/regions/countries) and pagination, resolve incoming slugs against active and historical records, update with full field editing and slug change history, record user attribution, fetch user refer-links, import referrals from CSV with per-row error collection, and bulk insert with configurable batch concurrency.
Module definition and API endpoints
src/referrals/referrals.module.ts, src/referrals/referrals.controller.ts
ReferralsModule wires TypeORM repositories and exports the service; ReferralsController defines six authenticated endpoints (POST create, POST list, GET resolve, PATCH update by ID, POST import, POST bulk) extracting user ID from JWT and returning standardized responses.
Elasticsearch indexing and search support
src/elasticsearch/interfaces/user.interface.ts, src/elasticsearch/user-elasticsearch.service.ts, src/elasticsearch/elasticsearch.service.ts
Adds referLink to user IProfile interface, includes it in index mappings as a keyword field, normalizes it during profile indexing, supports wildcard search filtering on refer-link values, and includes it in search result transformation.
User creation and referral attribution
src/adapters/postgres/user-adapter.ts, src/user/dto/user-create.dto.ts
Extends PostgresUserService to invoke referral attribution after user DB creation using optional referralSlug, derives and includes refer-link in Elasticsearch profile; adds referralSlug field to UserCreateDto with validation.
Form submission service referral enrichment
src/forms/services/form-submission.service.ts
Integrates ReferralsService to fetch and conditionally include user refer-link in Elasticsearch IUser profile document during form submission.
Module imports and API constants
src/adapters/postgres/postgres-module.ts, src/app.module.ts, src/forms/forms.module.ts, src/common/utils/api-id.config.ts, src/common/utils/response.messages.ts
Registers ReferralsModule across PostgreSQL adapter, app root, and forms modules; adds six referral-related API ID constants and six success message constants for create/list/resolve/update/import/bulk operations.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • tekdi/user-microservice#305: Overlaps in src/adapters/postgres/user-adapter.ts user creation flow, main PR adds referral attribution wiring while retrieved PR refactors SSO user creation in the same path.
  • tekdi/user-microservice#333: Main PR extends ElasticsearchService.search() hit-to-response mapping introduced in retrieved PR to additionally map profile.referLink.
  • tekdi/user-microservice#464: Both PRs modify PostgresUserService user-creation flow in src/adapters/postgres/user-adapter.ts, likely interaction points at the same code path.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title references a task identifier (Task#00000) and uses a vague, generic term 'Referral Tracking implementation' that doesn't clearly convey the specific technical changes. While 'Referral Tracking' is related to the changeset, the term 'implementation' is overly broad and doesn't highlight the main architectural or feature details. Replace the vague task reference and generic term with a more specific description. Consider: 'Add referral entity management system with user attribution tracking' or 'Implement referral module with slug-based tracking and CSV import'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a comprehensive referral management system, including modules for tracking referral entities, slug history, and user attribution. Key features include automated slug generation, CSV import capabilities, and integration with Elasticsearch for filtering users by referral links. Feedback focused on improving data consistency through transactions during slug updates, adopting a more robust CSV parsing strategy, and refining code quality by removing unnecessary type casts and correcting misleading error messages.

Comment on lines +222 to +231
await this.historyRepo.save(
this.historyRepo.create({
referralEntityId: entity.id,
oldSlug: entity.slug,
newSlug,
changedBy: changedBy ?? null,
})
);
entity.slug = newSlug;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The update of the referral slug and the recording of its history should be performed within a database transaction. Currently, if historyRepo.save succeeds but referralRepo.save fails, the system will have an inconsistent state where a slug change is recorded in history but not applied to the entity.

const csv = String(dto.csv || '').trim();
if (!csv) throw new BadRequestException('CSV is empty');

const lines = csv.split(/\r?\n/).filter((l) => l.trim().length > 0);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Splitting CSV content using a simple regex like split(/\r?\n/) and split(',') is fragile. It will fail if any field contains a newline or a comma, even if properly quoted according to CSV standards. Consider using a robust CSV parsing library like csv-parse or papaparse.

...dto,
lastName: dto.lastName ?? null,
region: dto.region ?? null,
linkedEntityId: (dto as any).linkedEntityId ?? null,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The cast to any is unnecessary here as linkedEntityId is already defined as an optional property in CreateReferralEntityDto.

Suggested change
linkedEntityId: (dto as any).linkedEntityId ?? null,
linkedEntityId: dto.linkedEntityId ?? null,

if (dto.contactEmail) {
const existingEmail = await this.referralRepo.findOne({ where: { contactEmail: dto.contactEmail } });
if (existingEmail) {
throw new ConflictException(`Referral slug already exists for email ${dto.contactEmail}`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The error message is slightly misleading. It mentions 'Referral slug' while the check is actually validating the uniqueness of the contactEmail field.

Suggested change
throw new ConflictException(`Referral slug already exists for email ${dto.contactEmail}`);
throw new ConflictException(`Referral entity already exists for email ${dto.contactEmail}`);

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 17

🧹 Nitpick comments (9)
src/forms/services/form-submission.service.ts (1)

99-99: 💤 Low value

Consider adding a trailing comma for consistency.

Adding a trailing comma after the last parameter improves diff cleanliness when parameters are added later and maintains consistency with common TypeScript/JavaScript style guides.

✨ Suggested change
-    private readonly referralsService: ReferralsService
+    private readonly referralsService: ReferralsService,
🤖 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/forms/services/form-submission.service.ts` at line 99, The constructor
parameter list ending with "private readonly referralsService: ReferralsService"
should include a trailing comma for style consistency; update the constructor
(the parameter declaration containing referralsService) to add a comma after
that parameter so future diffs remain cleaner and align with TypeScript/JS style
guides.
src/referrals/entities/referral-slug-history.entity.ts (3)

17-19: ⚡ Quick win

Remove redundant unique constraint.

The @Index({ unique: true }) decorator already creates a unique constraint, so specifying unique: true in the @Column decorator is redundant.

♻️ Proposed fix
  `@Index`({ unique: true })
-  `@Column`({ type: 'varchar', length: 100, unique: true })
+  `@Column`({ type: 'varchar', length: 100 })
  oldSlug: string;

As per coding guidelines, the code should adhere to best practices for TypeORM.

🤖 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/entities/referral-slug-history.entity.ts` around lines 17 - 19,
Remove the redundant unique constraint on the oldSlug column: keep the `@Index`({
unique: true }) decorator and remove the unique: true option from the `@Column`
decorator on the oldSlug property in the ReferralSlugHistory entity (look for
oldSlug, `@Index` and `@Column` in the file) so the unique constraint is defined
only once via the index.

27-28: ⚡ Quick win

Remove redundant default function from timestamp column.

The default: () => 'now()' is redundant with @CreateDateColumn, which automatically handles the timestamp default.

♻️ Proposed fix
-  `@CreateDateColumn`({ type: 'timestamp', default: () => 'now()' })
+  `@CreateDateColumn`({ type: 'timestamp' })
  changedAt: Date;

As per coding guidelines, the code should adhere to best practices for TypeORM.

🤖 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/entities/referral-slug-history.entity.ts` around lines 27 - 28,
The `@CreateDateColumn` decorator on the changedAt property currently includes a
redundant default: () => 'now()'; remove that default option so the decorator is
simply `@CreateDateColumn`({ type: 'timestamp' }) (or just `@CreateDateColumn`())
and leave the changedAt: Date; property intact to let TypeORM manage the
timestamp automatically.

14-15: ⚡ Quick win

Consider adding foreign key relationship for referralEntityId.

The referralEntityId column lacks a @ManyToOne relationship to ReferralEntity, which would enable TypeORM to manage referential integrity and allow easier querying with joins.

🔗 Proposed enhancement
+import { ManyToOne, JoinColumn } from 'typeorm';
+import { ReferralEntity } from './referral-entity.entity';

+  `@ManyToOne`(() => ReferralEntity, { onDelete: 'CASCADE' })
+  `@JoinColumn`({ name: 'referralEntityId' })
+  referralEntity: ReferralEntity;
+
  `@Column`({ type: 'uuid' })
  referralEntityId: string;

As per coding guidelines, the code should adhere to best practices for TypeORM and database integrity.

🤖 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/entities/referral-slug-history.entity.ts` around lines 14 - 15,
The referralEntityId column in ReferralSlugHistory should be modeled as a
foreign key relation to ReferralEntity: add a `@ManyToOne`(() => ReferralEntity, {
nullable: false }) relation property (e.g., referralEntity) on the
ReferralSlugHistory class and annotate it with `@JoinColumn`({ name:
'referralEntityId' }), keeping or adding the referralEntityId `@Column`({ type:
'uuid' }) if you want the explicit FK field; this lets TypeORM enforce
referential integrity and enables join queries using the
ReferralSlugHistory.referralEntity and the ReferralEntity entity.
src/referrals/entities/referral-entity.entity.ts (3)

59-63: ⚡ Quick win

Remove redundant default functions from timestamp columns.

The default: () => 'now()' is redundant with @CreateDateColumn and @UpdateDateColumn, which automatically handle timestamps.

♻️ Proposed fix
-  `@CreateDateColumn`({ type: 'timestamp', default: () => 'now()' })
+  `@CreateDateColumn`({ type: 'timestamp' })
  createdAt: Date;

-  `@UpdateDateColumn`({ type: 'timestamp', default: () => 'now()' })
+  `@UpdateDateColumn`({ type: 'timestamp' })
  updatedAt: Date;

As per coding guidelines, the code should adhere to best practices for TypeORM.

🤖 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/entities/referral-entity.entity.ts` around lines 59 - 63,
Remove the redundant default functions on the timestamp columns: the
`@CreateDateColumn` and `@UpdateDateColumn` decorators already manage timestamps, so
update the referral entity by removing the "default: () => 'now()'" option from
the `@CreateDateColumn` and `@UpdateDateColumn` decorators for createdAt and
updatedAt (keep the decorators and the createdAt/updatedAt fields intact).

28-30: ⚡ Quick win

Remove redundant unique constraint.

The @Index({ unique: true }) decorator already creates a unique constraint, so specifying unique: true in the @Column decorator is redundant.

♻️ Proposed fix
  `@Index`({ unique: true })
-  `@Column`({ type: 'varchar', length: 100, unique: true })
+  `@Column`({ type: 'varchar', length: 100 })
  slug: string;

As per coding guidelines, the code should adhere to best practices for TypeORM.

🤖 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/entities/referral-entity.entity.ts` around lines 28 - 30, The
Column decorator on the slug field redundantly declares unique: true while there
is already an `@Index`({ unique: true }) on the same field; remove the unique:
true option from the Column decorator so the unique constraint is defined only
by `@Index`, leaving slug: string and `@Index`({ unique: true }) intact (update any
related tests/migrations if they rely on the duplicate option).

47-48: ⚡ Quick win

Consider using JSON or array column type for additionalEmails.

Storing an email array as a varchar(500) string requires manual serialization/deserialization. TypeORM supports JSON columns or Postgres array types (text[]) which handle this automatically and provide better type safety.

💡 Alternative approaches

Option 1: JSON column (database-agnostic)

-  `@Column`({ type: 'varchar', length: 500, nullable: true })
-  additionalEmails: string | null;
+  `@Column`({ type: 'json', nullable: true })
+  additionalEmails: string[] | null;

Option 2: Postgres array (if using PostgreSQL)

-  `@Column`({ type: 'varchar', length: 500, nullable: true })
-  additionalEmails: string | null;
+  `@Column`({ type: 'text', array: true, nullable: true })
+  additionalEmails: string[] | null;

As per coding guidelines, the code should adhere to best practices for performance and type safety.

🤖 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/entities/referral-entity.entity.ts` around lines 47 - 48, The
additionalEmails column currently uses a varchar string which requires manual
serialization; update the Referral entity's additionalEmails property to use a
typed array/JSON column instead: change the Column decorator on additionalEmails
to either a JSON column (e.g., type: 'json', property type string[] | null) for
a DB-agnostic solution, or to a Postgres array (e.g., type: 'text', array: true,
property type string[] | null) if you are on PostgreSQL; ensure any repository
code that reads/writes additionalEmails treats it as an array and remove manual
split/join serialization.
src/referrals/entities/user-attribution.entity.ts (2)

22-23: ⚡ Quick win

Remove redundant default function from timestamp column.

The default: () => 'now()' is redundant with @CreateDateColumn, which automatically handles the timestamp default.

♻️ Proposed fix
-  `@CreateDateColumn`({ type: 'timestamp', default: () => 'now()' })
+  `@CreateDateColumn`({ type: 'timestamp' })
  createdAt: Date;

As per coding guidelines, the code should adhere to best practices for TypeORM.

🤖 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/entities/user-attribution.entity.ts` around lines 22 - 23, The
CreateDateColumn on the createdAt property in the UserAttribution entity
redundantly specifies default: () => 'now()'; remove the default option so the
decorator is used as intended—locate the createdAt field annotated with
`@CreateDateColumn` in user-attribution.entity (the createdAt property) and delete
the default: () => 'now()' configuration, leaving simply `@CreateDateColumn`({
type: 'timestamp' }) createdAt: Date; to rely on TypeORM's built-in behavior.

8-14: ⚡ Quick win

Consider adding foreign key relationships.

Both userId and referralEntityId lack @ManyToOne relationships to their respective entities. Adding these would enable TypeORM to manage referential integrity and support cascading operations.

🔗 Proposed enhancement
+import { ManyToOne, JoinColumn } from 'typeorm';
+import { User } from '../../user/user.entity'; // Adjust path as needed
+import { ReferralEntity } from './referral-entity.entity';

+  `@ManyToOne`(() => User, { onDelete: 'CASCADE' })
+  `@JoinColumn`({ name: 'userId' })
+  user: User;
+
  `@Index`()
  `@Column`({ type: 'uuid' })
  userId: string;

+  `@ManyToOne`(() => ReferralEntity, { onDelete: 'SET NULL', nullable: true })
+  `@JoinColumn`({ name: 'referralEntityId' })
+  referralEntity: ReferralEntity;
+
  `@Index`()
  `@Column`({ type: 'uuid', nullable: true })
  referralEntityId: string | null;

As per coding guidelines, the code should adhere to best practices for TypeORM and database integrity.

🤖 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/entities/user-attribution.entity.ts` around lines 8 - 14, Add
proper TypeORM relations for the userId and referralEntityId columns: replace or
augment the raw columns with `@ManyToOne` relations (e.g., add properties user:
User and referralEntity: ReferralEntity) decorated with `@ManyToOne`(() => User, u
=> u.<backRef> , { onDelete: 'CASCADE' }) and `@JoinColumn`({ name: 'userId' })
for user, and similarly `@ManyToOne`(() => ReferralEntity, r => r.<backRef> , {
onDelete: 'SET NULL' }) with `@JoinColumn`({ name: 'referralEntityId' }) for
referralEntity; also import User and ReferralEntity and remove
duplicate/conflicting column metadata or ensure the numeric/uuid `@Column`
definitions remain in sync with the relation metadata (use nullable on
referralEntityId).
🤖 Prompt for all review comments with 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.

Inline comments:
In `@src/adapters/postgres/user-adapter.ts`:
- Around line 1997-2010: The code only computes referLinkForElastic during
initial creation (referLinkForElastic, referralsService.createUserAttribution,
buildReferLink, result.userId), so ES upsert/rebuild paths miss it; extract this
logic into a reusable helper (e.g., getReferLinkForElastic(userId,
incomingSlug?)) that tries referralsService.createUserAttribution (or an
equivalent service call to fetch existing attribution) and falls back to
buildReferLink when resolvedSlug exists, and call that helper from both the
initial create flow and any profile upsert/rebuild code paths so referLink is
always populated for Elasticsearch documents.

In `@src/forms/services/form-submission.service.ts`:
- Line 3163: Wrap the call to
this.referralsService.getUserReferLink(user.userId) in a try-catch to prevent
exceptions from bubbling up and breaking the Elasticsearch user document build;
if the call fails assign a safe fallback (e.g., null or empty string) to
referLink and log the error (use the existing logger instance, e.g.,
this.logger.error) with contextual info including user.userId and the caught
error. Ensure you only change the area around the existing const referLink =
await this.referralsService.getUserReferLink(user.userId); statement so other
logic that consumes referLink continues to work with the fallback.

In `@src/referrals/dto/create-referral-entity.dto.ts`:
- Around line 1-68: The file CreateReferralEntityDto uses single quotes across
imports, decorators and string literals which violates the project's
Prettier/formatting rules; run the project's Prettier formatter (or your
configured formatter) on src/referrals/dto/create-referral-entity.dto.ts to
convert all single quotes to double quotes and normalize spacing for imports,
decorator metadata (ApiProperty/ApiPropertyOptional), and string literals (e.g.,
descriptions and type arrays) so formatting matches the repository standard and
linter/prettier checks pass.

In `@src/referrals/dto/import-referrals.dto.ts`:
- Around line 1-13: The file ImportReferralsDto uses single quotes for string
literals and should be formatted with double quotes per project Prettier
settings; update all string literals (e.g., the ApiProperty description, import
specifiers if necessary) to use double quotes and then run Prettier (or your
editor formatter) to apply the project’s formatting rules so ImportReferralsDto,
its csv property, and decorators (`@ApiProperty`, `@IsString`, `@IsNotEmpty`) follow
the configured style.

In `@src/referrals/dto/list-referrals.dto.ts`:
- Around line 1-55: The file uses single quotes for imports and decorators;
update all string literals to use double quotes to match project Prettier
settings (e.g., in ReferralFiltersDto and ListReferralsDto usage:
ApiPropertyOptional type strings, enum references, and import statements). Run
the project's Prettier formatter (or replace single quotes with double quotes
across the file) and ensure linting passes for symbols ReferralFiltersDto,
ListReferralsDto, ReferralEntityType, and ReferralEntitySubType.
- Around line 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.

In `@src/referrals/dto/update-referral-slug.dto.ts`:
- Around line 1-62: The file UpdateReferralSlugDto uses single quotes for
imports and strings; update all string literals and import quote marks to double
quotes and run the project's Prettier/formatting script (or your editor's
formatter) to apply project-wide rules so the file conforms to the Google style
and Prettier config; ensure you re-save UpdateReferralSlugDto and commit only
the formatting changes (no logic changes).

In `@src/referrals/entities/referral-entity.entity.ts`:
- Around line 1-64: The file uses single quotes instead of the project's
double-quote style; update all string literals and import/ decorator metadata to
use double quotes (e.g., the import lines, Entity name string, Column type
values, default values, and any other quoted strings found in the ReferralEntity
class and its decorators) and then run Prettier to reformat — target symbols:
the top-level imports, the `@Entity` decorator, all
`@Column/`@CreateDateColumn/@UpdateDateColumn options, and the ReferralEntity
class declaration to ensure consistent double-quote formatting throughout.

In `@src/referrals/entities/referral-slug-history.entity.ts`:
- Around line 1-30: Replace all single-quoted string literals with double quotes
in this entity (e.g., decorator arguments like Entity("ReferralSlugHistory"),
PrimaryGeneratedColumn("uuid"), Column({ type: "uuid" }), Index({ unique: true
}) and Column length/type values) and then run Prettier to apply project
formatting; ensure exported class ReferralSlugHistory and its properties (id,
referralEntityId, oldSlug, newSlug, changedBy, changedAt) retain their types and
decorators exactly as before while only changing quote style and running the
formatter.

In `@src/referrals/entities/user-attribution.entity.ts`:
- Around line 1-24: The file uses single quotes for string literals which
violates Prettier; update all string literals to use double quotes (including
import module specifiers, Entity name, Column type strings,
PrimaryGeneratedColumn argument, and CreateDateColumn default SQL string) in the
UserAttribution class and its decorators (see imports and decorators like
Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, Index) and then run
Prettier to auto-format the file to ensure it matches the project's formatting
rules.

In `@src/referrals/referrals.controller.ts`:
- Around line 109-111: The bulkInsert handler currently uses `@Body`() with
ValidationPipe which won't validate each array element; update the `@Body`
decorator to use ParseArrayPipe so each item is validated: replace `@Body`() dtos:
CreateReferralEntityDto[] with `@Body`(new ParseArrayPipe({ items:
CreateReferralEntityDto })) dtos: CreateReferralEntityDto[] (keep the existing
ValidationPipe on the method), referencing the bulkInsert method and
CreateReferralEntityDto to enforce per-item validation.

In `@src/referrals/referrals.service.ts`:
- Around line 277-297: The CSV parsing currently uses naive split(',') in the
header and row loops (header = lines[0].split(',') and cols =
lines[rowIdx].split(',')), which breaks when fields are quoted or contain
escaped commas; replace this with a proper CSV parse step (e.g., use a CSV
parser library or a quote-aware parser function) before computing header/indexes
(idx, iFirst, iLast, etc.) and before iterating rows so header and cols are
constructed from parsed records rather than string.split, preserving quoted
fields and escapes; ensure the rest of the logic (references to iFirst, iType,
iSubType, firstName, etc.) consumes the parsed record arrays/objects.
- Around line 36-37: Normalize dto.contactEmail (trim + toLowerCase) before any
uniqueness checks and before persisting: create a normalizedEmail =
dto.contactEmail?.trim().toLowerCase() and use that when calling
this.referralRepo.findOne and when assigning to the entity/DTO saved to the DB;
apply the same normalization in the update path (the code around the update
handler and the block that currently queries in lines handling updates) so both
the create and update flows use the normalized value and prevent case-variant
duplicates. Ensure you also update dto.contactEmail or the entity field to the
normalizedEmail prior to saving.
- Around line 182-185: When updating referral fields (the dto → entity block
handling dto.firstName/lastName/type/subType/contactEmail), ensure you compute
the effective values (let effectiveType = dto.type ?? entity.type and let
effectiveContactEmail = dto.contactEmail ?? entity.contactEmail) and, if
effectiveType === 'INTERNAL', run the existing "internal contact existence"
validation against effectiveContactEmail before persisting changes; apply the
same fix to the similar block around the 196-207 logic so that changing type to
INTERNAL without supplying contactEmail still validates the stored
entity.contactEmail.
- Around line 340-345: The parsed batchSize from
this.configService.get('REFERRAL_BULK_BATCH_SIZE') must be validated before the
for loop that uses i += batchSize; ensure the value assigned to batchSize is a
positive integer (e.g., parseInt(...,10), check for NaN, <= 0, or non-finite)
and if invalid replace it with a safe default (like 100) before iterating over
dtos; update the variable used in the loop (batchSize) so the loop cannot be
non-terminating and add a brief comment referencing batchSize and dtos to
clarify the guard.

In `@src/referrals/referrals.types.ts`:
- Around line 1-16: The enum string literals use single quotes and must be
formatted with double quotes to satisfy Prettier; update the string values in
ReferralEntityType, ReferralEntitySubType, and ReferralEntityStatus to use
double quotes (or run the project's Prettier/formatter) so entries like EXTERNAL
= "external", ORGANISATION = "organisation", and ACTIVE = "active" follow the
project's formatting rules.

In `@src/referrals/utils/referral-slug.util.ts`:
- Around line 4-10: DEFAULT_REFERRAL_BASE_URL currently returns a
possibly-undefined FRONTEND_URL string and buildReferLink concatenates it
directly causing malformed links; change DEFAULT_REFERRAL_BASE_URL to throw /
return error fast when process.env.FRONTEND_URL is not set, and rewrite
buildReferLink to construct the link using the standard URL and URLSearchParams
APIs (use new URL(base) to normalize slashes and preserve existing path/query,
then append or set the "refer" search param via url.searchParams.set('refer',
encodeURIComponent(slug.trim())), and return url.toString()); update references
to DEFAULT_REFERRAL_BASE_URL() and buildReferLink(slug, baseUrl?) accordingly so
missing base URL fails fast and existing query strings are preserved.

---

Nitpick comments:
In `@src/forms/services/form-submission.service.ts`:
- Line 99: The constructor parameter list ending with "private readonly
referralsService: ReferralsService" should include a trailing comma for style
consistency; update the constructor (the parameter declaration containing
referralsService) to add a comma after that parameter so future diffs remain
cleaner and align with TypeScript/JS style guides.

In `@src/referrals/entities/referral-entity.entity.ts`:
- Around line 59-63: Remove the redundant default functions on the timestamp
columns: the `@CreateDateColumn` and `@UpdateDateColumn` decorators already manage
timestamps, so update the referral entity by removing the "default: () =>
'now()'" option from the `@CreateDateColumn` and `@UpdateDateColumn` decorators for
createdAt and updatedAt (keep the decorators and the createdAt/updatedAt fields
intact).
- Around line 28-30: The Column decorator on the slug field redundantly declares
unique: true while there is already an `@Index`({ unique: true }) on the same
field; remove the unique: true option from the Column decorator so the unique
constraint is defined only by `@Index`, leaving slug: string and `@Index`({ unique:
true }) intact (update any related tests/migrations if they rely on the
duplicate option).
- Around line 47-48: The additionalEmails column currently uses a varchar string
which requires manual serialization; update the Referral entity's
additionalEmails property to use a typed array/JSON column instead: change the
Column decorator on additionalEmails to either a JSON column (e.g., type:
'json', property type string[] | null) for a DB-agnostic solution, or to a
Postgres array (e.g., type: 'text', array: true, property type string[] | null)
if you are on PostgreSQL; ensure any repository code that reads/writes
additionalEmails treats it as an array and remove manual split/join
serialization.

In `@src/referrals/entities/referral-slug-history.entity.ts`:
- Around line 17-19: Remove the redundant unique constraint on the oldSlug
column: keep the `@Index`({ unique: true }) decorator and remove the unique: true
option from the `@Column` decorator on the oldSlug property in the
ReferralSlugHistory entity (look for oldSlug, `@Index` and `@Column` in the file) so
the unique constraint is defined only once via the index.
- Around line 27-28: The `@CreateDateColumn` decorator on the changedAt property
currently includes a redundant default: () => 'now()'; remove that default
option so the decorator is simply `@CreateDateColumn`({ type: 'timestamp' }) (or
just `@CreateDateColumn`()) and leave the changedAt: Date; property intact to let
TypeORM manage the timestamp automatically.
- Around line 14-15: The referralEntityId column in ReferralSlugHistory should
be modeled as a foreign key relation to ReferralEntity: add a `@ManyToOne`(() =>
ReferralEntity, { nullable: false }) relation property (e.g., referralEntity) on
the ReferralSlugHistory class and annotate it with `@JoinColumn`({ name:
'referralEntityId' }), keeping or adding the referralEntityId `@Column`({ type:
'uuid' }) if you want the explicit FK field; this lets TypeORM enforce
referential integrity and enables join queries using the
ReferralSlugHistory.referralEntity and the ReferralEntity entity.

In `@src/referrals/entities/user-attribution.entity.ts`:
- Around line 22-23: The CreateDateColumn on the createdAt property in the
UserAttribution entity redundantly specifies default: () => 'now()'; remove the
default option so the decorator is used as intended—locate the createdAt field
annotated with `@CreateDateColumn` in user-attribution.entity (the createdAt
property) and delete the default: () => 'now()' configuration, leaving simply
`@CreateDateColumn`({ type: 'timestamp' }) createdAt: Date; to rely on TypeORM's
built-in behavior.
- Around line 8-14: Add proper TypeORM relations for the userId and
referralEntityId columns: replace or augment the raw columns with `@ManyToOne`
relations (e.g., add properties user: User and referralEntity: ReferralEntity)
decorated with `@ManyToOne`(() => User, u => u.<backRef> , { onDelete: 'CASCADE'
}) and `@JoinColumn`({ name: 'userId' }) for user, and similarly `@ManyToOne`(() =>
ReferralEntity, r => r.<backRef> , { onDelete: 'SET NULL' }) with `@JoinColumn`({
name: 'referralEntityId' }) for referralEntity; also import User and
ReferralEntity and remove duplicate/conflicting column metadata or ensure the
numeric/uuid `@Column` definitions remain in sync with the relation metadata (use
nullable on referralEntityId).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b8c6c8ce-08e1-4006-a964-06f66fec9a76

📥 Commits

Reviewing files that changed from the base of the PR and between faa92d5 and 72dbb26.

📒 Files selected for processing (23)
  • src/adapters/postgres/postgres-module.ts
  • src/adapters/postgres/user-adapter.ts
  • src/app.module.ts
  • src/common/utils/api-id.config.ts
  • src/common/utils/response.messages.ts
  • src/elasticsearch/elasticsearch.service.ts
  • src/elasticsearch/interfaces/user.interface.ts
  • src/elasticsearch/user-elasticsearch.service.ts
  • src/forms/forms.module.ts
  • src/forms/services/form-submission.service.ts
  • src/referrals/dto/create-referral-entity.dto.ts
  • src/referrals/dto/import-referrals.dto.ts
  • src/referrals/dto/list-referrals.dto.ts
  • src/referrals/dto/update-referral-slug.dto.ts
  • src/referrals/entities/referral-entity.entity.ts
  • src/referrals/entities/referral-slug-history.entity.ts
  • src/referrals/entities/user-attribution.entity.ts
  • src/referrals/referrals.controller.ts
  • src/referrals/referrals.module.ts
  • src/referrals/referrals.service.ts
  • src/referrals/referrals.types.ts
  • src/referrals/utils/referral-slug.util.ts
  • src/user/dto/user-create.dto.ts

Comment on lines +1997 to +2010
// 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);
}
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

Preserve referLink in all Elasticsearch profile rebuild paths.

referLink is computed only during initial create, but profile upsert/rebuild paths can recreate ES docs without it. This causes referral filter/search regressions after document-missing recovery or reindex-style rebuilds.

Suggested fix
 private async syncUserToElasticsearch(user: User) {
   try {
-    const customFields = await this.getFilteredCustomFields(user.userId);
+    const [customFields, referLink] = await Promise.all([
+      this.getFilteredCustomFields(user.userId),
+      this.referralsService.getUserReferLink(user.userId),
+    ]);

     let formattedDob: string | null = null;
@@
       const profile = {
         userId: user.userId,
         username: user.username,
         firstName: user.firstName,
         lastName: user.lastName,
         middleName: user.middleName || '',
         email: user.email || '',
         mobile: user.mobile?.toString() || '',
         mobile_country_code: user.mobile_country_code || '',
+        referLink: referLink ?? undefined,
         dob: formattedDob,
         country: user.country,
@@
           async (userId: string) => {
             const dbUser = await this.usersRepository.findOne({
               where: { userId },
             });
             if (!dbUser) return null;
-            const customFields = await this.getFilteredCustomFields(userId);
+            const [customFields, referLink] = await Promise.all([
+              this.getFilteredCustomFields(userId),
+              this.referralsService.getUserReferLink(userId),
+            ]);
             let formattedDob: string | null = null;
@@
               profile: {
                 userId: dbUser.userId,
                 username: dbUser.username,
                 firstName: dbUser.firstName,
                 lastName: dbUser.lastName,
                 middleName: dbUser.middleName || '',
                 email: dbUser.email || '',
                 mobile: dbUser.mobile?.toString() || '',
                 mobile_country_code: dbUser.mobile_country_code || '',
+                referLink: referLink ?? undefined,
                 dob: formattedDob,
                 gender: dbUser.gender,
🧰 Tools
🪛 ESLint

[error] 2009-2009: Replace 'Referral·attribution·failed' with "Referral·attribution·failed"

(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/adapters/postgres/user-adapter.ts` around lines 1997 - 2010, The code
only computes referLinkForElastic during initial creation (referLinkForElastic,
referralsService.createUserAttribution, buildReferLink, result.userId), so ES
upsert/rebuild paths miss it; extract this logic into a reusable helper (e.g.,
getReferLinkForElastic(userId, incomingSlug?)) that tries
referralsService.createUserAttribution (or an equivalent service call to fetch
existing attribution) and falls back to buildReferLink when resolvedSlug exists,
and call that helper from both the initial create flow and any profile
upsert/rebuild code paths so referLink is always populated for Elasticsearch
documents.

}
}
}
const referLink = await this.referralsService.getUserReferLink(user.userId);
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 error handling around referrals service call.

The call to getUserReferLink lacks error handling. If this service call throws, it will break the entire Elasticsearch user document build. Other external service calls in this method use try-catch blocks for resilience.

🛡️ Proposed fix to add error handling
-    const referLink = await this.referralsService.getUserReferLink(user.userId);
+    let referLink = null;
+    try {
+      referLink = await this.referralsService.getUserReferLink(user.userId);
+    } catch (error) {
+      LoggerUtil.warn('Failed to fetch user refer link:', error);
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const referLink = await this.referralsService.getUserReferLink(user.userId);
let referLink = null;
try {
referLink = await this.referralsService.getUserReferLink(user.userId);
} catch (error) {
LoggerUtil.warn('Failed to fetch user refer link:', error);
}
🤖 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/forms/services/form-submission.service.ts` at line 3163, Wrap the call to
this.referralsService.getUserReferLink(user.userId) in a try-catch to prevent
exceptions from bubbling up and breaking the Elasticsearch user document build;
if the call fails assign a safe fallback (e.g., null or empty string) to
referLink and log the error (use the existing logger instance, e.g.,
this.logger.error) with contextual info including user.userId and the caught
error. Ensure you only change the area around the existing const referLink =
await this.referralsService.getUserReferLink(user.userId); statement so other
logic that consumes referLink continues to work with the fallback.

Comment on lines +1 to +68
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;
}
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 | 🟡 Minor | ⚡ Quick win

Fix formatting to use double quotes.

The file uses single quotes throughout, which violates the project's Prettier configuration. Run Prettier to auto-format all imports, decorators, and string literals.

As per coding guidelines, the code should adhere to the Google JavaScript style guide and project formatting standards.

🧰 Tools
🪛 ESLint

[error] 1-1: Replace '@nestjs/swagger' with "@nestjs/swagger"

(prettier/prettier)


[error] 2-2: Replace ·IsArray,·IsEmail,·IsEnum,·IsOptional,·IsString,·MaxLength·}·from·'class-validator' with ⏎··IsArray,⏎··IsEmail,⏎··IsEnum,⏎··IsOptional,⏎··IsString,⏎··MaxLength,⏎}·from·"class-validator"

(prettier/prettier)


[error] 7-7: Replace '../referrals.types' with "../referrals.types"

(prettier/prettier)


[error] 10-10: Replace ·description:·'Custom·slug·(any·format·accepted;·will·be·normalized·to·lowercase·a-z0-9_).·If·omitted,·auto-generated.' with ⏎····description:⏎······"Custom·slug·(any·format·accepted;·will·be·normalized·to·lowercase·a-z0-9_).·If·omitted,·auto-generated.",⏎·

(prettier/prettier)


[error] 16-16: Replace 'First·name·(or·full·org/university·name)' with "First·name·(or·full·org/university·name)"

(prettier/prettier)


[error] 21-21: Replace ·description:·'Last·name·(optional·for·org/university)' with ⏎····description:·"Last·name·(optional·for·org/university)",⏎·

(prettier/prettier)


[error] 35-35: Replace 'Region·(optional)' with "Region·(optional)"

(prettier/prettier)


[error] 41-41: Replace ·description:·'Linked·internal·entity·UUID·(optional)' with ⏎····description:·"Linked·internal·entity·UUID·(optional)",⏎·

(prettier/prettier)


[error] 46-46: Replace 'Primary·contact·email·(optional)' with "Primary·contact·email·(optional)"

(prettier/prettier)


[error] 52-52: Replace ·description:·'Additional·emails·(array·of·email·strings)',·type:·[String] with ⏎····description:·"Additional·emails·(array·of·email·strings)",⏎····type:·[String],⏎·

(prettier/prettier)


[error] 58-58: Replace 'Country·(optional)' with "Country·(optional)"

(prettier/prettier)


[error] 64-64: Replace ·enum:·ReferralEntityStatus,·default:·ReferralEntityStatus.ACTIVE with ⏎····enum:·ReferralEntityStatus,⏎····default:·ReferralEntityStatus.ACTIVE,⏎·

(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/create-referral-entity.dto.ts` around lines 1 - 68, The
file CreateReferralEntityDto uses single quotes across imports, decorators and
string literals which violates the project's Prettier/formatting rules; run the
project's Prettier formatter (or your configured formatter) on
src/referrals/dto/create-referral-entity.dto.ts to convert all single quotes to
double quotes and normalize spacing for imports, decorator metadata
(ApiProperty/ApiPropertyOptional), and string literals (e.g., descriptions and
type arrays) so formatting matches the repository standard and linter/prettier
checks pass.

Comment on lines +1 to +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;
}

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 | 🟡 Minor | ⚡ Quick win

Fix formatting to use double quotes.

The file uses single quotes throughout, which violates the project's Prettier configuration. Run Prettier to auto-format.

As per coding guidelines, the code should adhere to the Google JavaScript style guide and project formatting standards.

🧰 Tools
🪛 ESLint

[error] 1-1: Replace '@nestjs/swagger' with "@nestjs/swagger"

(prettier/prettier)


[error] 2-2: Replace 'class-validator' with "class-validator"

(prettier/prettier)


[error] 7-7: Replace 'CSV·content·with·header:·firstName,lastName,type,subType,region,contactEmail,country' with "CSV·content·with·header:·firstName,lastName,type,subType,region,contactEmail,country"

(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/import-referrals.dto.ts` around lines 1 - 13, The file
ImportReferralsDto uses single quotes for string literals and should be
formatted with double quotes per project Prettier settings; update all string
literals (e.g., the ApiProperty description, import specifiers if necessary) to
use double quotes and then run Prettier (or your editor formatter) to apply the
project’s formatting rules so ImportReferralsDto, its csv property, and
decorators (`@ApiProperty`, `@IsString`, `@IsNotEmpty`) follow the configured style.

Comment on lines +1 to +55
import { ApiProperty, 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;
}
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 | 🟡 Minor | ⚡ Quick win

Fix formatting to use double quotes.

The file uses single quotes throughout, which violates the project's Prettier configuration. Run Prettier to auto-format.

As per coding guidelines, the code should adhere to the Google JavaScript style guide and project formatting standards.

🧰 Tools
🪛 ESLint

[error] 1-1: Replace '@nestjs/swagger' with "@nestjs/swagger"

(prettier/prettier)


[error] 2-2: Replace 'class-transformer' with "class-transformer"

(prettier/prettier)


[error] 3-3: Replace ·IsArray,·IsEnum,·IsNumber,·IsObject,·IsOptional,·IsString,·Min·}·from·'class-validator' with ⏎··IsArray,⏎··IsEnum,⏎··IsNumber,⏎··IsObject,⏎··IsOptional,⏎··IsString,⏎··Min,⏎}·from·"class-validator"

(prettier/prettier)


[error] 4-4: Replace '../referrals.types' with "../referrals.types"

(prettier/prettier)


[error] 7-7: Replace ·enum:·ReferralEntityType,·description:·'Filter·by·referral·type' with ⏎····enum:·ReferralEntityType,⏎····description:·"Filter·by·referral·type",⏎·

(prettier/prettier)


[error] 12-12: Replace ·enum:·ReferralEntitySubType,·description:·'Filter·by·referral·sub-type' with ⏎····enum:·ReferralEntitySubType,⏎····description:·"Filter·by·referral·sub-type",⏎·

(prettier/prettier)


[error] 17-17: Replace ·type:·String,·description:·'Search·across·firstName,·lastName,·contactEmail,·and·slug' with ⏎····type:·String,⏎····description:·"Search·across·firstName,·lastName,·contactEmail,·and·slug",⏎·

(prettier/prettier)


[error] 22-22: Replace 'Filter·by·regions' with "Filter·by·regions"

(prettier/prettier)


[error] 28-28: Replace 'Filter·by·countries' with "Filter·by·countries"

(prettier/prettier)


[error] 36-36: Replace ·description:·'Number·of·records·to·return',·default:·10 with ⏎····description:·"Number·of·records·to·return",⏎····default:·10,⏎·

(prettier/prettier)


[error] 43-43: Replace 'Number·of·records·to·skip' with "Number·of·records·to·skip"

(prettier/prettier)


[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 1 - 55, The file uses
single quotes for imports and decorators; update all string literals to use
double quotes to match project Prettier settings (e.g., in ReferralFiltersDto
and ListReferralsDto usage: ApiPropertyOptional type strings, enum references,
and import statements). Run the project's Prettier formatter (or replace single
quotes with double quotes across the file) and ensure linting passes for symbols
ReferralFiltersDto, ListReferralsDto, ReferralEntityType, and
ReferralEntitySubType.

Comment on lines +182 to +185
if (dto.firstName !== undefined) entity.firstName = dto.firstName;
if (dto.lastName !== undefined) entity.lastName = dto.lastName ?? null;
if (dto.type !== undefined) entity.type = dto.type;
if (dto.subType !== undefined) entity.subType = dto.subType;
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

Internal-email validation is skipped when type changes without contactEmail.

If dto.type becomes INTERNAL and dto.contactEmail is omitted, no existence check runs for entity.contactEmail. Validate the effective contact email whenever effective type is INTERNAL.

Suggested fix
+    const effectiveType = dto.type ?? entity.type;
+    const effectiveContactEmail = (dto.contactEmail ?? entity.contactEmail)?.toLowerCase().trim();

+    if (effectiveType === ReferralEntityType.INTERNAL) {
+      if (!effectiveContactEmail) {
+        throw new BadRequestException("Internal referral must have contactEmail");
+      }
+      const existingUser = await this.userRepo.findOne({
+        where: { email: effectiveContactEmail },
+      });
+      if (!existingUser) {
+        throw new BadRequestException(
+          `Internal user email ${effectiveContactEmail} does not exist in the system`
+        );
+      }
+    }

Also applies to: 196-207

🤖 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/referrals.service.ts` around lines 182 - 185, When updating
referral fields (the dto → entity block handling
dto.firstName/lastName/type/subType/contactEmail), ensure you compute the
effective values (let effectiveType = dto.type ?? entity.type and let
effectiveContactEmail = dto.contactEmail ?? entity.contactEmail) and, if
effectiveType === 'INTERNAL', run the existing "internal contact existence"
validation against effectiveContactEmail before persisting changes; apply the
same fix to the similar block around the 196-207 logic so that changing type to
INTERNAL without supplying contactEmail still validates the stored
entity.contactEmail.

Comment on lines +277 to +297
const header = lines[0].split(',').map((h) => h.trim());
const idx = (name: string) => header.findIndex((h) => h.toLowerCase() === name.toLowerCase());

const iFirst = idx('firstName');
const iLast = idx('lastName');
const iType = idx('type');
const iSubType = idx('subType');
const iRegion = idx('region');
const iEmail = idx('contactEmail');
const iCountry = idx('country');

if (iFirst < 0 || iType < 0 || iSubType < 0) {
throw new BadRequestException('CSV header must include at least: firstName,type,subType');
}

const created: any[] = [];
const errors: any[] = [];

for (let rowIdx = 1; rowIdx < lines.length; rowIdx++) {
const cols = lines[rowIdx].split(',').map((c) => c.trim());
const firstName = cols[iFirst] ?? '';
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 | 🏗️ Heavy lift

CSV parsing is incorrect for quoted fields.

split(",") breaks valid CSV rows containing quoted commas, causing field shifts and wrong attribution/import failures. This path needs a real CSV parser behavior (quote + escape aware).

🧰 Tools
🪛 ESLint

[error] 277-277: Replace ',' with ","

(prettier/prettier)


[error] 278-278: Insert ⏎·····

(prettier/prettier)


[error] 280-280: Replace 'firstName' with "firstName"

(prettier/prettier)


[error] 281-281: Replace 'lastName' with "lastName"

(prettier/prettier)


[error] 282-282: Replace 'type' with "type"

(prettier/prettier)


[error] 283-283: Replace 'subType' with "subType"

(prettier/prettier)


[error] 284-284: Replace 'region' with "region"

(prettier/prettier)


[error] 285-285: Replace 'contactEmail' with "contactEmail"

(prettier/prettier)


[error] 286-286: Replace 'country' with "country"

(prettier/prettier)


[error] 289-289: Replace 'CSV·header·must·include·at·least:·firstName,type,subType' with ⏎········"CSV·header·must·include·at·least:·firstName,type,subType"⏎······

(prettier/prettier)


[error] 296-296: Replace ',' with ","

(prettier/prettier)


[error] 297-297: Replace '' with ""

(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/referrals.service.ts` around lines 277 - 297, The CSV parsing
currently uses naive split(',') in the header and row loops (header =
lines[0].split(',') and cols = lines[rowIdx].split(',')), which breaks when
fields are quoted or contain escaped commas; replace this with a proper CSV
parse step (e.g., use a CSV parser library or a quote-aware parser function)
before computing header/indexes (idx, iFirst, iLast, etc.) and before iterating
rows so header and cols are constructed from parsed records rather than
string.split, preserving quoted fields and escapes; ensure the rest of the logic
(references to iFirst, iType, iSubType, firstName, etc.) consumes the parsed
record arrays/objects.

Comment on lines +340 to +345
const batchSize = parseInt(this.configService.get('REFERRAL_BULK_BATCH_SIZE') || '100', 10);
const created: any[] = [];
const errors: any[] = [];

for (let i = 0; i < dtos.length; i += batchSize) {
const batch = dtos.slice(i, i + batchSize);
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 | 🔴 Critical | ⚡ Quick win

Guard REFERRAL_BULK_BATCH_SIZE against invalid values.

If parsed batch size is 0, negative, or NaN, the loop can become non-terminating (i += batchSize). Enforce a positive integer fallback before entering the loop.

Suggested fix
-    const batchSize = parseInt(this.configService.get('REFERRAL_BULK_BATCH_SIZE') || '100', 10);
+    const parsedBatchSize = Number.parseInt(
+      this.configService.get("REFERRAL_BULK_BATCH_SIZE") || "100",
+      10
+    );
+    const batchSize =
+      Number.isInteger(parsedBatchSize) && parsedBatchSize > 0
+        ? parsedBatchSize
+        : 100;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const batchSize = parseInt(this.configService.get('REFERRAL_BULK_BATCH_SIZE') || '100', 10);
const created: any[] = [];
const errors: any[] = [];
for (let i = 0; i < dtos.length; i += batchSize) {
const batch = dtos.slice(i, i + batchSize);
const parsedBatchSize = Number.parseInt(
this.configService.get("REFERRAL_BULK_BATCH_SIZE") || "100",
10
);
const batchSize =
Number.isInteger(parsedBatchSize) && parsedBatchSize > 0
? parsedBatchSize
: 100;
const created: any[] = [];
const errors: any[] = [];
for (let i = 0; i < dtos.length; i += batchSize) {
const batch = dtos.slice(i, i + batchSize);
🧰 Tools
🪛 ESLint

[error] 340-340: Replace this.configService.get('REFERRAL_BULK_BATCH_SIZE')·||·'100',·10 with ⏎······this.configService.get("REFERRAL_BULK_BATCH_SIZE")·||·"100",⏎······10⏎····

(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/referrals.service.ts` around lines 340 - 345, The parsed
batchSize from this.configService.get('REFERRAL_BULK_BATCH_SIZE') must be
validated before the for loop that uses i += batchSize; ensure the value
assigned to batchSize is a positive integer (e.g., parseInt(...,10), check for
NaN, <= 0, or non-finite) and if invalid replace it with a safe default (like
100) before iterating over dtos; update the variable used in the loop
(batchSize) so the loop cannot be non-terminating and add a brief comment
referencing batchSize and dtos to clarify the guard.

Comment on lines +1 to +16
export enum ReferralEntityType {
EXTERNAL = 'external',
INTERNAL = 'internal',
}

export enum ReferralEntitySubType {
ORGANISATION = 'organisation',
UNIVERSITY = 'university',
ALUMNI = 'alumni',
}

export enum ReferralEntityStatus {
ACTIVE = 'active',
INACTIVE = 'inactive',
}

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 | 🟡 Minor | ⚡ Quick win

Fix formatting to use double quotes.

The code uses single quotes throughout, which violates the project's Prettier configuration. Run Prettier to auto-format.

🎨 Proposed fix
 export enum ReferralEntityType {
-  EXTERNAL = 'external',
-  INTERNAL = 'internal',
+  EXTERNAL = "external",
+  INTERNAL = "internal",
 }
 
 export enum ReferralEntitySubType {
-  ORGANISATION = 'organisation',
-  UNIVERSITY = 'university',
-  ALUMNI = 'alumni',
+  ORGANISATION = "organisation",
+  UNIVERSITY = "university",
+  ALUMNI = "alumni",
 }
 
 export enum ReferralEntityStatus {
-  ACTIVE = 'active',
-  INACTIVE = 'inactive',
+  ACTIVE = "active",
+  INACTIVE = "inactive",
 }

As per coding guidelines, the code should adhere to the Google JavaScript style guide and project formatting standards.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export enum ReferralEntityType {
EXTERNAL = 'external',
INTERNAL = 'internal',
}
export enum ReferralEntitySubType {
ORGANISATION = 'organisation',
UNIVERSITY = 'university',
ALUMNI = 'alumni',
}
export enum ReferralEntityStatus {
ACTIVE = 'active',
INACTIVE = 'inactive',
}
export enum ReferralEntityType {
EXTERNAL = "external",
INTERNAL = "internal",
}
export enum ReferralEntitySubType {
ORGANISATION = "organisation",
UNIVERSITY = "university",
ALUMNI = "alumni",
}
export enum ReferralEntityStatus {
ACTIVE = "active",
INACTIVE = "inactive",
}
🧰 Tools
🪛 ESLint

[error] 2-2: Replace 'external' with "external"

(prettier/prettier)


[error] 3-3: Replace 'internal' with "internal"

(prettier/prettier)


[error] 7-7: Replace 'organisation' with "organisation"

(prettier/prettier)


[error] 8-8: Replace 'university' with "university"

(prettier/prettier)


[error] 9-9: Replace 'alumni' with "alumni"

(prettier/prettier)


[error] 13-13: Replace 'active' with "active"

(prettier/prettier)


[error] 14-14: Replace 'inactive' with "inactive"

(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/referrals.types.ts` around lines 1 - 16, The enum string
literals use single quotes and must be formatted with double quotes to satisfy
Prettier; update the string values in ReferralEntityType, ReferralEntitySubType,
and ReferralEntityStatus to use double quotes (or run the project's
Prettier/formatter) so entries like EXTERNAL = "external", ORGANISATION =
"organisation", and ACTIVE = "active" follow the project's formatting rules.

Comment on lines +4 to +10
export const DEFAULT_REFERRAL_BASE_URL = () =>
`${process.env.FRONTEND_URL}registration`;

export function buildReferLink(slug: string, baseUrl?: string) {
const base = baseUrl ?? DEFAULT_REFERRAL_BASE_URL();
const s = String(slug || '').trim();
return `${base}?refer=${encodeURIComponent(s)}`;
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

Harden refer-link URL construction to avoid malformed links.

The current concatenation can produce invalid links (e.g., missing FRONTEND_URL, missing slash, existing query string). Build the URL via URL/URLSearchParams and fail fast if base URL is unset.

Suggested fix
-export const DEFAULT_REFERRAL_BASE_URL = () =>
-  `${process.env.FRONTEND_URL}registration`;
+export const DEFAULT_REFERRAL_BASE_URL = () => {
+  const frontendUrl = process.env.FRONTEND_URL;
+  if (!frontendUrl) {
+    throw new Error("FRONTEND_URL is not configured");
+  }
+  return new URL("registration", frontendUrl).toString();
+};

 export function buildReferLink(slug: string, baseUrl?: string) {
-  const base = baseUrl ?? DEFAULT_REFERRAL_BASE_URL();
-  const s = String(slug || "").trim();
-  return `${base}?refer=${encodeURIComponent(s)}`;
+  const base = new URL(baseUrl ?? DEFAULT_REFERRAL_BASE_URL());
+  const s = String(slug || "").trim();
+  base.searchParams.set("refer", s);
+  return base.toString();
 }
🧰 Tools
🪛 ESLint

[error] 9-9: Replace '' with ""

(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/utils/referral-slug.util.ts` around lines 4 - 10,
DEFAULT_REFERRAL_BASE_URL currently returns a possibly-undefined FRONTEND_URL
string and buildReferLink concatenates it directly causing malformed links;
change DEFAULT_REFERRAL_BASE_URL to throw / return error fast when
process.env.FRONTEND_URL is not set, and rewrite buildReferLink to construct the
link using the standard URL and URLSearchParams APIs (use new URL(base) to
normalize slashes and preserve existing path/query, then append or set the
"refer" search param via url.searchParams.set('refer',
encodeURIComponent(slug.trim())), and return url.toString()); update references
to DEFAULT_REFERRAL_BASE_URL() and buildReferLink(slug, baseUrl?) accordingly so
missing base URL fails fast and existing query strings are preserved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant