mirror of
https://github.com/HamzaSha1/zod-backend.git
synced 2026-03-10 20:51:44 +00:00
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:
@ -42,13 +42,11 @@ export class CardService {
|
|||||||
throw new BadRequestException('CUSTOMER.ALREADY_HAS_CARD');
|
throw new BadRequestException('CUSTOMER.ALREADY_HAS_CARD');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate required address fields for card creation
|
// Validate required fields for card creation
|
||||||
const missingFields = [];
|
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.nationalId) missingFields.push('nationalId');
|
||||||
if (!customer.dateOfBirth) missingFields.push('dateOfBirth');
|
if (!customer.dateOfBirth) missingFields.push('dateOfBirth');
|
||||||
|
if (!customer.nationalIdExpiry) missingFields.push('nationalIdExpiry');
|
||||||
|
|
||||||
if (missingFields.length > 0) {
|
if (missingFields.length > 0) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
|
|||||||
@ -93,24 +93,22 @@ export class NeoLeapService {
|
|||||||
incomeSource: dto.incomeSource,
|
incomeSource: dto.incomeSource,
|
||||||
jobCategory: dto.jobCategory,
|
jobCategory: dto.jobCategory,
|
||||||
incomeRange: dto.incomeRange,
|
incomeRange: dto.incomeRange,
|
||||||
// Map collected address fields to Neoleap's format
|
// Use default address values for Neoleap KYC
|
||||||
address: {
|
address: {
|
||||||
national: {
|
national: {
|
||||||
buildingNumber: dto.building || '',
|
buildingNumber: '1',
|
||||||
additionalNumber: '',
|
additionalNumber: '',
|
||||||
street: dto.street || '',
|
street: 'King Fahd Road',
|
||||||
streetEn: dto.street || '',
|
streetEn: 'King Fahd Road',
|
||||||
city: dto.city || '',
|
city: 'Riyadh',
|
||||||
cityEn: dto.city || '',
|
cityEn: 'Riyadh',
|
||||||
zipcode: '',
|
zipcode: '',
|
||||||
unitNumber: '',
|
unitNumber: '',
|
||||||
district: dto.neighborhood || '',
|
district: 'Al Olaya',
|
||||||
districtEn: dto.neighborhood || '',
|
districtEn: 'Al Olaya',
|
||||||
},
|
},
|
||||||
general: {
|
general: {
|
||||||
address: [dto.building, dto.street, dto.neighborhood, dto.city, dto.region]
|
address: '1, King Fahd Road, Al Olaya, Riyadh, Riyadh',
|
||||||
.filter(Boolean)
|
|
||||||
.join(', '),
|
|
||||||
website: '',
|
website: '',
|
||||||
email: dto.email || '',
|
email: dto.email || '',
|
||||||
telephone1: dto.mobileNumber || '',
|
telephone1: dto.mobileNumber || '',
|
||||||
@ -203,14 +201,14 @@ export class NeoLeapService {
|
|||||||
Title: customer.gender === Gender.MALE ? 'Mr' : 'Ms',
|
Title: customer.gender === Gender.MALE ? 'Mr' : 'Ms',
|
||||||
Gender: customer.gender === Gender.MALE ? 'M' : 'F',
|
Gender: customer.gender === Gender.MALE ? 'M' : 'F',
|
||||||
LocalizedDateOfBirth: moment(customer.dateOfBirth).format('YYYY-MM-DD'),
|
LocalizedDateOfBirth: moment(customer.dateOfBirth).format('YYYY-MM-DD'),
|
||||||
Nationality: CountriesNumericISO[customer.countryOfResidence],
|
Nationality: CountriesNumericISO[customer.countryOfResidence || 'SA'],
|
||||||
},
|
},
|
||||||
ApplicationAddress: {
|
ApplicationAddress: {
|
||||||
City: customer.city,
|
City: 'Riyadh',
|
||||||
Country: CountriesNumericISO[customer.country],
|
Country: CountriesNumericISO['SA'],
|
||||||
Region: customer.region,
|
Region: 'Riyadh',
|
||||||
AddressLine1: `${customer.street} ${customer.building}`,
|
AddressLine1: 'King Fahd Road 1',
|
||||||
AddressLine2: customer.neighborhood,
|
AddressLine2: 'Al Olaya',
|
||||||
AddressRole: 0,
|
AddressRole: 0,
|
||||||
Email: customer.user.email,
|
Email: customer.user.email,
|
||||||
Phone1: customer.user.phoneNumber,
|
Phone1: customer.user.phoneNumber,
|
||||||
@ -279,14 +277,14 @@ export class NeoLeapService {
|
|||||||
Title: parent.gender === Gender.MALE ? 'Mr' : 'Ms',
|
Title: parent.gender === Gender.MALE ? 'Mr' : 'Ms',
|
||||||
Gender: parent.gender === Gender.MALE ? 'M' : 'F',
|
Gender: parent.gender === Gender.MALE ? 'M' : 'F',
|
||||||
LocalizedDateOfBirth: moment(parent.dateOfBirth).format('YYYY-MM-DD'),
|
LocalizedDateOfBirth: moment(parent.dateOfBirth).format('YYYY-MM-DD'),
|
||||||
Nationality: CountriesNumericISO[parent.countryOfResidence],
|
Nationality: CountriesNumericISO[parent.countryOfResidence || 'SA'],
|
||||||
},
|
},
|
||||||
ApplicationAddress: {
|
ApplicationAddress: {
|
||||||
City: parent.city,
|
City: 'Riyadh',
|
||||||
Country: CountriesNumericISO[parent.country],
|
Country: CountriesNumericISO['SA'],
|
||||||
Region: parent.region,
|
Region: 'Riyadh',
|
||||||
AddressLine1: `${parent.street} ${parent.building}`,
|
AddressLine1: 'King Fahd Road 1',
|
||||||
AddressLine2: parent.neighborhood,
|
AddressLine2: 'Al Olaya',
|
||||||
AddressRole: 0,
|
AddressRole: 0,
|
||||||
Email: child.user.email,
|
Email: child.user.email,
|
||||||
Phone1: child.user.phoneNumber,
|
Phone1: child.user.phoneNumber,
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsDateString, IsEmail, IsEnum, IsOptional, IsString, Matches } from 'class-validator';
|
import { IsDateString, IsEmail, IsEnum, IsOptional, IsString, Matches } from 'class-validator';
|
||||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||||
import { CountryIso } from '~/common/enums';
|
import { Gender, IncomeRange, IncomeSource, JobCategory, JobSector, PoiType } from '~/customer/enums';
|
||||||
import { IncomeRange, IncomeSource, JobCategory, JobSector, PoiType } from '~/customer/enums';
|
|
||||||
|
|
||||||
export class InitiateKycRequestDto {
|
export class InitiateKycRequestDto {
|
||||||
@ApiProperty({ example: '2586234623', description: 'Saudi National ID or Iqama number' })
|
@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' }) })
|
@IsDateString({}, { message: i18n('validation.IsDateString', { path: 'general', property: 'customer.dateOfBirth' }) })
|
||||||
dateOfBirth!: string;
|
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 })
|
@ApiProperty({ enum: JobSector, example: JobSector.PRIVATE_SECTOR })
|
||||||
@IsEnum(JobSector, { message: i18n('validation.IsEnum', { path: 'general', property: 'customer.jobSector' }) })
|
@IsEnum(JobSector, { message: i18n('validation.IsEnum', { path: 'general', property: 'customer.jobSector' }) })
|
||||||
jobSector!: JobSector;
|
jobSector!: JobSector;
|
||||||
@ -45,37 +52,4 @@ export class InitiateKycRequestDto {
|
|||||||
@ApiProperty({ enum: IncomeRange, example: IncomeRange.RANGE_10000_20000 })
|
@ApiProperty({ enum: IncomeRange, example: IncomeRange.RANGE_10000_20000 })
|
||||||
@IsEnum(IncomeRange, { message: i18n('validation.IsEnum', { path: 'general', property: 'customer.incomeRange' }) })
|
@IsEnum(IncomeRange, { message: i18n('validation.IsEnum', { path: 'general', property: 'customer.incomeRange' }) })
|
||||||
incomeRange!: 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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -49,24 +49,6 @@ export class CustomerResponseDto {
|
|||||||
@ApiProperty({ example: 12345 })
|
@ApiProperty({ example: 12345 })
|
||||||
waitingNumber!: number;
|
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 })
|
@ApiPropertyOptional({ type: DocumentMetaResponseDto })
|
||||||
profilePicture!: DocumentMetaResponseDto | null;
|
profilePicture!: DocumentMetaResponseDto | null;
|
||||||
|
|
||||||
@ -86,11 +68,5 @@ export class CustomerResponseDto {
|
|||||||
this.isJunior = customer.isJunior;
|
this.isJunior = customer.isJunior;
|
||||||
this.isGuardian = customer.isGuardian;
|
this.isGuardian = customer.isGuardian;
|
||||||
this.waitingNumber = customer.applicationNumber;
|
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -68,24 +68,6 @@ export class Customer extends BaseEntity {
|
|||||||
@Column('varchar', { name: 'user_id' })
|
@Column('varchar', { name: 'user_id' })
|
||||||
userId!: string;
|
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
|
// KYC-specific fields
|
||||||
@Column('varchar', { length: 255, nullable: true, name: 'neoleap_external_customer_id' })
|
@Column('varchar', { length: 255, nullable: true, name: 'neoleap_external_customer_id' })
|
||||||
neoleapExternalCustomerId!: string | null;
|
neoleapExternalCustomerId!: string | null;
|
||||||
|
|||||||
@ -30,13 +30,6 @@ export class CustomerRepository {
|
|||||||
dateOfBirth: body.dateOfBirth,
|
dateOfBirth: body.dateOfBirth,
|
||||||
countryOfResidence: body.countryOfResidence,
|
countryOfResidence: body.countryOfResidence,
|
||||||
gender: body.gender,
|
gender: body.gender,
|
||||||
// Address fields
|
|
||||||
country: body.country,
|
|
||||||
region: body.region,
|
|
||||||
city: body.city,
|
|
||||||
neighborhood: body.neighborhood,
|
|
||||||
street: body.street,
|
|
||||||
building: body.building,
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -73,10 +73,13 @@ export class CustomerService {
|
|||||||
throw new ConflictException('KYC verification already in progress for this National ID');
|
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, {
|
await this.customerRepository.updateCustomer(customerId, {
|
||||||
nationalId: body.poiNumber,
|
nationalId: body.poiNumber,
|
||||||
dateOfBirth: new Date(body.dateOfBirth),
|
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,
|
mobileNumber: body.mobileNumber,
|
||||||
jobSector: body.jobSector,
|
jobSector: body.jobSector,
|
||||||
employer: body.employer,
|
employer: body.employer,
|
||||||
@ -84,13 +87,6 @@ export class CustomerService {
|
|||||||
jobCategory: body.jobCategory,
|
jobCategory: body.jobCategory,
|
||||||
incomeRange: body.incomeRange,
|
incomeRange: body.incomeRange,
|
||||||
kycStatus: KycStatus.PENDING,
|
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
|
// Call Neoleap KYC API
|
||||||
@ -184,12 +180,6 @@ export class CustomerService {
|
|||||||
nationalId: '1089055972',
|
nationalId: '1089055972',
|
||||||
nationalIdExpiry: moment('2031-09-17').toDate(),
|
nationalIdExpiry: moment('2031-09-17').toDate(),
|
||||||
countryOfResidence: CountryIso.SAUDI_ARABIA,
|
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, {
|
await User.update(userId, {
|
||||||
|
|||||||
26
src/db/migrations/1765975126402-RemoveAddressColumns.ts
Normal file
26
src/db/migrations/1765975126402-RemoveAddressColumns.ts
Normal 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)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -9,3 +9,4 @@ export * from './1761032305682-AddUniqueConstraintToUserEmail';
|
|||||||
export * from './1765804942393-AddKycFieldsAndTransactions';
|
export * from './1765804942393-AddKycFieldsAndTransactions';
|
||||||
export * from './1765877128065-AddNationalIdToKycTransactions';
|
export * from './1765877128065-AddNationalIdToKycTransactions';
|
||||||
export * from './1765891028260-RemoveOldCustomerColumns';
|
export * from './1765891028260-RemoveOldCustomerColumns';
|
||||||
|
export * from './1765975126402-RemoveAddressColumns';
|
||||||
@ -65,13 +65,6 @@ export class UserService {
|
|||||||
firstName: body.firstName,
|
firstName: body.firstName,
|
||||||
lastName: body.lastName,
|
lastName: body.lastName,
|
||||||
countryOfResidence: body.countryOfResidence,
|
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, {
|
this.userRepository.update(userId, {
|
||||||
isPhoneVerified: true,
|
isPhoneVerified: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user