Skip to content

Commit

Permalink
[Booster] Milestone 3: Address Verification
Browse files Browse the repository at this point in the history
  • Loading branch information
javiertoledo committed May 3, 2023
1 parent bec8b97 commit 727d3ce
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 16 deletions.
50 changes: 50 additions & 0 deletions kyc-booster/src/commands/process-address-verification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Booster, Command } from '@boostercloud/framework-core'
import { Register, UUID } from '@boostercloud/framework-types'
import { AddressVerificationSuccess } from '../events/address-verification-success'
import { AddressVerificationRejected } from '../events/address-verification-rejected'
import { isValidTransition } from '../common/state-validation'
import { Profile } from '../entities/profile'

@Command({
authorize: 'all',
})
export class ProcessAddressVerification {
public constructor(
readonly userId: UUID,
readonly verificationId: UUID,
readonly result: 'success' | 'rejected',
readonly timestamp: string
) {}

public static async handle(command: ProcessAddressVerification, register: Register): Promise<void> {
const profile = await Booster.entity(Profile, command.userId)

// TODO: Ideally, the command payload should include some kind of signature to ensure that it comes
// from the address verification service. This is not implemented in this example, but it's a good
// practice to avoid malicious users sending fake verification results.

// Reject verification confirmations for unknown profiles
if (!profile) {
throw new Error(`Profile with ID ${command.userId} not found`)
}

// Ensure that the verification result is valid
if (command.result !== 'success' && command.result !== 'rejected') {
throw new Error(`Invalid address verification result: ${command.result}`)
}

// Emit the corresponding event depending on the result, making sure that the transition is valid
if (command.result === 'success' && isValidTransition(profile.kycStatus, 'KYCAddressVerified')) {
register.events(new AddressVerificationSuccess(command.userId, command.verificationId, command.timestamp))
} else if (command.result === 'rejected' && isValidTransition(profile.kycStatus, 'KYCAddressRejected')) {
register.events(new AddressVerificationRejected(command.userId, command.verificationId, command.timestamp))
} else {
// Handle invalid state transitions
throw new Error(
`Invalid transition from ${profile.kycStatus} to ${
command.result === 'success' ? 'KYCAddressVerified' : 'KYCAddressRejected'
}`
)
}
}
}
23 changes: 16 additions & 7 deletions kyc-booster/src/common/state-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,20 @@ export function isValidTransition(
currentState: KYCStatus,
newState: KYCStatus,
): boolean {
const allowedTransitions: Record<KYCStatus, Array<KYCStatus>> = {
KYCPending: ['KYCIDVerified', 'KYCIDRejected'],
KYCIDVerified: [],
KYCIDRejected: [],
};

return allowedTransitions[currentState]?.includes(newState)
return allowedTransitions(currentState).includes(newState);
}

function allowedTransitions(currentState: KYCStatus): KYCStatus[] {
switch (currentState) {
case 'KYCPending':
return ['KYCIDVerified', 'KYCIDRejected'];
case 'KYCIDVerified':
return ['KYCAddressVerified', 'KYCAddressRejected'];
case 'KYCIDRejected':
return [];
case 'KYCAddressVerified':
return [];
case 'KYCAddressRejected':
return [];
}
}
2 changes: 1 addition & 1 deletion kyc-booster/src/common/types.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export type KYCStatus = 'KYCPending' | 'KYCIDVerified' | 'KYCIDRejected'
export type KYCStatus = 'KYCPending' | 'KYCIDVerified' | 'KYCIDRejected' | 'KYCAddressVerified' | 'KYCAddressRejected'
45 changes: 40 additions & 5 deletions kyc-booster/src/entities/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { ProfileCreated } from '../events/profile-created'
import { KYCStatus } from '../common/types'
import { IDVerificationSuccess } from '../events/id-verification-success'
import { IDVerificationRejected } from '../events/id-verification-rejected'
import { AddressVerificationSuccess } from '../events/address-verification-success'
import { AddressVerificationRejected } from '../events/address-verification-rejected'

@Entity
export class Profile {
Expand All @@ -21,9 +23,12 @@ export class Profile {
readonly kycStatus: KYCStatus,
readonly ssn?: string,
readonly tin?: string,
readonly verificationId?: UUID,
readonly idVerificationId?: UUID,
readonly idVerifiedAt?: string,
readonly idRejectedAt?: string
readonly idRejectedAt?: string,
readonly addressVerificationId?: UUID,
readonly addressVerifiedAt?: string,
readonly addressRejectedAt?: string
) {}

@Reduces(ProfileCreated)
Expand All @@ -33,18 +38,48 @@ export class Profile {

@Reduces(IDVerificationSuccess)
public static onIDVerificationSuccess(event: IDVerificationSuccess, currentProfile?: Profile): Profile {
return Profile.nextProfile({ kycStatus: 'KYCIDVerified', verificationId: event.verificationId, idVerifiedAt: event.timestamp }, currentProfile)
return Profile.nextProfile({ kycStatus: 'KYCIDVerified', idVerificationId: event.verificationId, idVerifiedAt: event.timestamp }, currentProfile)
}

@Reduces(IDVerificationRejected)
public static onIDVerificationRejected(event: IDVerificationRejected, currentProfile?: Profile): Profile {
return Profile.nextProfile({ kycStatus: 'KYCIDRejected', idRejectedAt: event.timestamp }, currentProfile)
}

@Reduces(AddressVerificationSuccess)
public static onAddressVerificationSuccess(event: AddressVerificationSuccess, currentProfile?: Profile): Profile {
return Profile.nextProfile({ kycStatus: 'KYCAddressVerified', addressVerificationId: event.verificationId, addressVerifiedAt: event.timestamp }, currentProfile)
}

@Reduces(AddressVerificationRejected)
public static onAddressVerificationRejected(event: AddressVerificationRejected, currentProfile?: Profile): Profile {
return Profile.nextProfile({ kycStatus: 'KYCAddressRejected', addressRejectedAt: event.timestamp }, currentProfile)
}

private static nextProfile(fields: Partial<Profile>, currentProfile?: Profile): Profile {
if (!currentProfile) {
throw new Error('Cannot reduce an event over an non-existing profile')
throw new Error('Cannot reduce an event over a non-existing profile')
}
return new Profile(currentProfile.id, fields.firstName || currentProfile.firstName, fields.lastName || currentProfile.lastName, fields.address || currentProfile.address, fields.city || currentProfile.city, fields.state || currentProfile.state, fields.zipCode || currentProfile.zipCode, fields.dateOfBirth || currentProfile.dateOfBirth, fields.phoneNumber || currentProfile.phoneNumber, fields.email || currentProfile.email, fields.kycStatus || currentProfile.kycStatus, fields.ssn || currentProfile.ssn, fields.tin || currentProfile.tin)
return new Profile(
currentProfile.id,
fields.firstName || currentProfile.firstName,
fields.lastName || currentProfile.lastName,
fields.address || currentProfile.address,
fields.city || currentProfile.city,
fields.state || currentProfile.state,
fields.zipCode || currentProfile.zipCode,
fields.dateOfBirth || currentProfile.dateOfBirth,
fields.phoneNumber || currentProfile.phoneNumber,
fields.email || currentProfile.email,
fields.kycStatus || currentProfile.kycStatus,
fields.ssn || currentProfile.ssn,
fields.tin || currentProfile.tin,
fields.idVerificationId || currentProfile.idVerificationId,
fields.idVerifiedAt || currentProfile.idVerifiedAt,
fields.idRejectedAt || currentProfile.idRejectedAt,
fields.addressVerificationId || currentProfile.addressVerificationId,
fields.addressVerifiedAt || currentProfile.addressVerifiedAt,
fields.addressRejectedAt || currentProfile.addressRejectedAt
)
}
}
11 changes: 11 additions & 0 deletions kyc-booster/src/events/address-verification-rejected.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Event } from '@boostercloud/framework-core'
import { UUID } from '@boostercloud/framework-types'

@Event
export class AddressVerificationRejected {
public constructor(readonly profileId: UUID, readonly verificationId: UUID, readonly timestamp: string) {}

public entityID(): UUID {
return this.profileId
}
}
11 changes: 11 additions & 0 deletions kyc-booster/src/events/address-verification-success.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Event } from '@boostercloud/framework-core'
import { UUID } from '@boostercloud/framework-types'

@Event
export class AddressVerificationSuccess {
public constructor(readonly profileId: UUID, readonly verificationId: UUID, readonly timestamp: string) {}

public entityID(): UUID {
return this.profileId
}
}
30 changes: 27 additions & 3 deletions kyc-booster/src/read-models/ProfileReadModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,36 @@ export class ProfileReadModel {
readonly kycStatus: KYCStatus,
readonly ssn?: string,
readonly tin?: string,
readonly verificationId?: UUID,
readonly verifiedAt?: string,
readonly idVerificationId?: UUID,
readonly idVerifiedAt?: string,
readonly idRejectedAt?: string,
readonly addressVerificationId?: UUID,
readonly addressVerifiedAt?: string,
readonly addressRejectedAt?: string
) {}

@Projects(Profile, 'id')
public static projectProfile(entity: Profile): ProjectionResult<ProfileReadModel> {
return new ProfileReadModel(entity.id, entity.firstName, entity.lastName, entity.address, entity.city, entity.state, entity.zipCode, entity.dateOfBirth, entity.phoneNumber, entity.email, entity.kycStatus, entity.ssn, entity.tin, entity.verificationId, entity.idVerifiedAt)
return new ProfileReadModel(
entity.id,
entity.firstName,
entity.lastName,
entity.address,
entity.city,
entity.state,
entity.zipCode,
entity.dateOfBirth,
entity.phoneNumber,
entity.email,
entity.kycStatus,
entity.ssn,
entity.tin,
entity.idVerificationId,
entity.idVerifiedAt,
entity.idRejectedAt,
entity.addressVerificationId,
entity.addressVerifiedAt,
entity.addressRejectedAt
)
}
}

0 comments on commit 727d3ce

Please sign in to comment.