refactor: remove address fields from customer entity and related services

- Removed address-related fields from Customer entity, DTOs, and services to streamline KYC process.
- Updated KYC initiation and customer update logic to default to Saudi Arabia for country and use fixed address values.
- Added migration to drop address columns from the database.
This commit is contained in:
Abdalhamid Alhamad
2025-12-18 12:35:32 +03:00
parent 24bcb10d76
commit 110a6fb0ee
10 changed files with 64 additions and 133 deletions

View File

@ -42,13 +42,11 @@ export class CardService {
throw new BadRequestException('CUSTOMER.ALREADY_HAS_CARD');
}
// Validate required address fields for card creation
// Validate required fields for card creation
const missingFields = [];
if (!customer.country) missingFields.push('country');
if (!customer.city) missingFields.push('city');
if (!customer.region) missingFields.push('region');
if (!customer.nationalId) missingFields.push('nationalId');
if (!customer.dateOfBirth) missingFields.push('dateOfBirth');
if (!customer.nationalIdExpiry) missingFields.push('nationalIdExpiry');
if (missingFields.length > 0) {
throw new BadRequestException(

View File

@ -93,24 +93,22 @@ export class NeoLeapService {
incomeSource: dto.incomeSource,
jobCategory: dto.jobCategory,
incomeRange: dto.incomeRange,
// Map collected address fields to Neoleap's format
// Use default address values for Neoleap KYC
address: {
national: {
buildingNumber: dto.building || '',
buildingNumber: '1',
additionalNumber: '',
street: dto.street || '',
streetEn: dto.street || '',
city: dto.city || '',
cityEn: dto.city || '',
street: 'King Fahd Road',
streetEn: 'King Fahd Road',
city: 'Riyadh',
cityEn: 'Riyadh',
zipcode: '',
unitNumber: '',
district: dto.neighborhood || '',
districtEn: dto.neighborhood || '',
district: 'Al Olaya',
districtEn: 'Al Olaya',
},
general: {
address: [dto.building, dto.street, dto.neighborhood, dto.city, dto.region]
.filter(Boolean)
.join(', '),
address: '1, King Fahd Road, Al Olaya, Riyadh, Riyadh',
website: '',
email: dto.email || '',
telephone1: dto.mobileNumber || '',
@ -203,14 +201,14 @@ export class NeoLeapService {
Title: customer.gender === Gender.MALE ? 'Mr' : 'Ms',
Gender: customer.gender === Gender.MALE ? 'M' : 'F',
LocalizedDateOfBirth: moment(customer.dateOfBirth).format('YYYY-MM-DD'),
Nationality: CountriesNumericISO[customer.countryOfResidence],
Nationality: CountriesNumericISO[customer.countryOfResidence || 'SA'],
},
ApplicationAddress: {
City: customer.city,
Country: CountriesNumericISO[customer.country],
Region: customer.region,
AddressLine1: `${customer.street} ${customer.building}`,
AddressLine2: customer.neighborhood,
City: 'Riyadh',
Country: CountriesNumericISO['SA'],
Region: 'Riyadh',
AddressLine1: 'King Fahd Road 1',
AddressLine2: 'Al Olaya',
AddressRole: 0,
Email: customer.user.email,
Phone1: customer.user.phoneNumber,
@ -279,14 +277,14 @@ export class NeoLeapService {
Title: parent.gender === Gender.MALE ? 'Mr' : 'Ms',
Gender: parent.gender === Gender.MALE ? 'M' : 'F',
LocalizedDateOfBirth: moment(parent.dateOfBirth).format('YYYY-MM-DD'),
Nationality: CountriesNumericISO[parent.countryOfResidence],
Nationality: CountriesNumericISO[parent.countryOfResidence || 'SA'],
},
ApplicationAddress: {
City: parent.city,
Country: CountriesNumericISO[parent.country],
Region: parent.region,
AddressLine1: `${parent.street} ${parent.building}`,
AddressLine2: parent.neighborhood,
City: 'Riyadh',
Country: CountriesNumericISO['SA'],
Region: 'Riyadh',
AddressLine1: 'King Fahd Road 1',
AddressLine2: 'Al Olaya',
AddressRole: 0,
Email: child.user.email,
Phone1: child.user.phoneNumber,

View File

@ -1,8 +1,7 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsDateString, IsEmail, IsEnum, IsOptional, IsString, Matches } from 'class-validator';
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
import { CountryIso } from '~/common/enums';
import { IncomeRange, IncomeSource, JobCategory, JobSector, PoiType } from '~/customer/enums';
import { Gender, IncomeRange, IncomeSource, JobCategory, JobSector, PoiType } from '~/customer/enums';
export class InitiateKycRequestDto {
@ApiProperty({ example: '2586234623', description: 'Saudi National ID or Iqama number' })
@ -26,6 +25,14 @@ export class InitiateKycRequestDto {
@IsDateString({}, { message: i18n('validation.IsDateString', { path: 'general', property: 'customer.dateOfBirth' }) })
dateOfBirth!: string;
@ApiProperty({ example: '2030-12-31', format: 'date', description: 'National ID expiry date' })
@IsDateString({}, { message: i18n('validation.IsDateString', { path: 'general', property: 'customer.nationalIdExpiry' }) })
nationalIdExpiry!: string;
@ApiProperty({ enum: Gender, example: Gender.MALE })
@IsEnum(Gender, { message: i18n('validation.IsEnum', { path: 'general', property: 'customer.gender' }) })
gender!: Gender;
@ApiProperty({ enum: JobSector, example: JobSector.PRIVATE_SECTOR })
@IsEnum(JobSector, { message: i18n('validation.IsEnum', { path: 'general', property: 'customer.jobSector' }) })
jobSector!: JobSector;
@ -45,37 +52,4 @@ export class InitiateKycRequestDto {
@ApiProperty({ enum: IncomeRange, example: IncomeRange.RANGE_10000_20000 })
@IsEnum(IncomeRange, { message: i18n('validation.IsEnum', { path: 'general', property: 'customer.incomeRange' }) })
incomeRange!: IncomeRange;
// Address fields (optional - can be provided here or during registration)
@ApiProperty({ example: 'SA', description: 'Country code', required: false })
@IsEnum(CountryIso, {
message: i18n('validation.IsEnum', { path: 'general', property: 'customer.country' }),
})
@IsOptional()
country?: CountryIso;
@ApiProperty({ example: 'Riyadh', description: 'Region/Province', required: false })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'customer.region' }) })
@IsOptional()
region?: string;
@ApiProperty({ example: 'Riyadh', description: 'City', required: false })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'customer.city' }) })
@IsOptional()
city?: string;
@ApiProperty({ example: 'Al Olaya', description: 'Neighborhood/District', required: false })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'customer.neighborhood' }) })
@IsOptional()
neighborhood?: string;
@ApiProperty({ example: 'King Fahd Road', description: 'Street name', required: false })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'customer.street' }) })
@IsOptional()
street?: string;
@ApiProperty({ example: '123', description: 'Building number', required: false })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'customer.building' }) })
@IsOptional()
building?: string;
}

View File

@ -49,24 +49,6 @@ export class CustomerResponseDto {
@ApiProperty({ example: 12345 })
waitingNumber!: number;
@ApiProperty({ example: 'SA' })
country!: string | null;
@ApiProperty({ example: 'Riyadh' })
region!: string | null;
@ApiProperty({ example: 'Riyadh City' })
city!: string | null;
@ApiProperty({ example: 'Al-Masif' })
neighborhood!: string | null;
@ApiProperty({ example: 'King Fahd Road' })
street!: string | null;
@ApiProperty({ example: '123' })
building!: string | null;
@ApiPropertyOptional({ type: DocumentMetaResponseDto })
profilePicture!: DocumentMetaResponseDto | null;
@ -86,11 +68,5 @@ export class CustomerResponseDto {
this.isJunior = customer.isJunior;
this.isGuardian = customer.isGuardian;
this.waitingNumber = customer.applicationNumber;
this.country = customer.country;
this.region = customer.region;
this.city = customer.city;
this.neighborhood = customer.neighborhood;
this.street = customer.street;
this.building = customer.building;
}
}

View File

@ -68,24 +68,6 @@ export class Customer extends BaseEntity {
@Column('varchar', { name: 'user_id' })
userId!: string;
@Column('varchar', { name: 'country', length: 255, nullable: true })
country!: CountryIso;
@Column('varchar', { name: 'region', length: 255, nullable: true })
region!: string;
@Column('varchar', { name: 'city', length: 255, nullable: true })
city!: string;
@Column('varchar', { name: 'neighborhood', length: 255, nullable: true })
neighborhood!: string;
@Column('varchar', { name: 'street', length: 255, nullable: true })
street!: string;
@Column('varchar', { name: 'building', length: 255, nullable: true })
building!: string;
// KYC-specific fields
@Column('varchar', { length: 255, nullable: true, name: 'neoleap_external_customer_id' })
neoleapExternalCustomerId!: string | null;

View File

@ -30,13 +30,6 @@ export class CustomerRepository {
dateOfBirth: body.dateOfBirth,
countryOfResidence: body.countryOfResidence,
gender: body.gender,
// Address fields
country: body.country,
region: body.region,
city: body.city,
neighborhood: body.neighborhood,
street: body.street,
building: body.building,
}),
);
}

View File

@ -73,10 +73,13 @@ export class CustomerService {
throw new ConflictException('KYC verification already in progress for this National ID');
}
// Update customer with KYC data (including address if provided)
// Update customer with KYC data
await this.customerRepository.updateCustomer(customerId, {
nationalId: body.poiNumber,
dateOfBirth: new Date(body.dateOfBirth),
nationalIdExpiry: new Date(body.nationalIdExpiry),
gender: body.gender,
countryOfResidence: CountryIso.SAUDI_ARABIA, // Always default to Saudi Arabia
mobileNumber: body.mobileNumber,
jobSector: body.jobSector,
employer: body.employer,
@ -84,13 +87,6 @@ export class CustomerService {
jobCategory: body.jobCategory,
incomeRange: body.incomeRange,
kycStatus: KycStatus.PENDING,
// Address fields (only update if provided)
...(body.country && { country: body.country }),
...(body.region && { region: body.region }),
...(body.city && { city: body.city }),
...(body.neighborhood && { neighborhood: body.neighborhood }),
...(body.street && { street: body.street }),
...(body.building && { building: body.building }),
});
// Call Neoleap KYC API
@ -184,12 +180,6 @@ export class CustomerService {
nationalId: '1089055972',
nationalIdExpiry: moment('2031-09-17').toDate(),
countryOfResidence: CountryIso.SAUDI_ARABIA,
country: CountryIso.SAUDI_ARABIA,
region: 'Mecca',
city: 'AT Taif',
neighborhood: 'Al Faisaliah',
street: 'Al Faisaliah Street',
building: '4',
});
await User.update(userId, {

View File

@ -0,0 +1,26 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class RemoveAddressColumns1765975126402 implements MigrationInterface {
name = 'RemoveAddressColumns1765975126402';
public async up(queryRunner: QueryRunner): Promise<void> {
// Drop address columns from customers table
await queryRunner.query(`ALTER TABLE "customers" DROP COLUMN IF EXISTS "country"`);
await queryRunner.query(`ALTER TABLE "customers" DROP COLUMN IF EXISTS "region"`);
await queryRunner.query(`ALTER TABLE "customers" DROP COLUMN IF EXISTS "city"`);
await queryRunner.query(`ALTER TABLE "customers" DROP COLUMN IF EXISTS "neighborhood"`);
await queryRunner.query(`ALTER TABLE "customers" DROP COLUMN IF EXISTS "street"`);
await queryRunner.query(`ALTER TABLE "customers" DROP COLUMN IF EXISTS "building"`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
// Re-add address columns in case of rollback
await queryRunner.query(`ALTER TABLE "customers" ADD "country" varchar(255)`);
await queryRunner.query(`ALTER TABLE "customers" ADD "region" varchar(255)`);
await queryRunner.query(`ALTER TABLE "customers" ADD "city" varchar(255)`);
await queryRunner.query(`ALTER TABLE "customers" ADD "neighborhood" varchar(255)`);
await queryRunner.query(`ALTER TABLE "customers" ADD "street" varchar(255)`);
await queryRunner.query(`ALTER TABLE "customers" ADD "building" varchar(255)`);
}
}

View File

@ -9,3 +9,4 @@ export * from './1761032305682-AddUniqueConstraintToUserEmail';
export * from './1765804942393-AddKycFieldsAndTransactions';
export * from './1765877128065-AddNationalIdToKycTransactions';
export * from './1765891028260-RemoveOldCustomerColumns';
export * from './1765975126402-RemoveAddressColumns';

View File

@ -65,13 +65,6 @@ export class UserService {
firstName: body.firstName,
lastName: body.lastName,
countryOfResidence: body.countryOfResidence,
// Address fields (optional during registration)
country: body.country,
region: body.region,
city: body.city,
neighborhood: body.neighborhood,
street: body.street,
building: body.building,
}),
this.userRepository.update(userId, {
isPhoneVerified: true,