diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..6d37fb6 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,13 @@ +{ + "arrowParens": "always", + "embeddedLanguageFormatting": "off", + "endOfLine": "auto", + "jsxBracketSameLine": false, + "jsxSingleQuote": true, + "printWidth": 100, + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "all", + "useTabs": false +} diff --git a/src/Dtos/address.dto.ts b/src/Dtos/address.dto.ts index 7c83c70..afdfe89 100644 --- a/src/Dtos/address.dto.ts +++ b/src/Dtos/address.dto.ts @@ -1,16 +1,21 @@ -import { ApiProperty } from "@nestjs/swagger"; +import { ApiProperty } from '@nestjs/swagger'; export class AddressDto { - @ApiProperty() - address1: string; - @ApiProperty() - address2: string; - @ApiProperty() - city: string; - @ApiProperty() - state: string; - @ApiProperty() - zip: string; - @ApiProperty() - country: string; + @ApiProperty() + address1: string; + + @ApiProperty() + address2?: string; + + @ApiProperty() + city: string; + + @ApiProperty() + country: string; + + @ApiProperty() + state: string; + + @ApiProperty() + zip: string; } diff --git a/src/Dtos/customer.dto.ts b/src/Dtos/customer.dto.ts index 57a6112..404cd3a 100644 --- a/src/Dtos/customer.dto.ts +++ b/src/Dtos/customer.dto.ts @@ -1,38 +1,53 @@ -import { ApiProperty } from "@nestjs/swagger"; +import { ApiProperty } from '@nestjs/swagger'; import { DriverLicenseDto } from './driveLicense.dto'; import { AddressDto } from './address.dto'; export class CustomerDto { - @ApiProperty() - customerId: string; - @ApiProperty() - externalId: string; - @ApiProperty() - merchantId: string; - @ApiProperty() - name: string; - @ApiProperty() - vip: string; - @ApiProperty() - taxId: string; - @ApiProperty() - driverLicense: DriverLicenseDto; - @ApiProperty() - address: AddressDto; - @ApiProperty() - phone: string; - @ApiProperty() - email: string; - @ApiProperty() - balance: any; - @ApiProperty() - currency: any; - @ApiProperty() - enrollDate: number; - @ApiProperty() - dateOfBirth: number; - @ApiProperty() - createdAt: number; - @ApiProperty() - updatedAt: number; + @ApiProperty() + address: AddressDto; + + @ApiProperty() + balance?: string; + + @ApiProperty() + createdAt: number; + + @ApiProperty() + currency?: string; + + @ApiProperty() + customerId: string; + + @ApiProperty() + dateOfBirth: string; + + @ApiProperty() + driverLicense?: DriverLicenseDto; + + @ApiProperty() + email: string; + + @ApiProperty() + enrollDate: number; // Unix Timestamp + + @ApiProperty() + externalId: string; + + @ApiProperty() + merchantId: string; + + @ApiProperty() + name: string; + + @ApiProperty() + phone: string; + + @ApiProperty() + taxId: string; + + @ApiProperty() + updatedAt: number; + + @ApiProperty() + vip?: string; } diff --git a/src/Dtos/establish.dto.ts b/src/Dtos/establish.dto.ts index c46df36..5708f38 100644 --- a/src/Dtos/establish.dto.ts +++ b/src/Dtos/establish.dto.ts @@ -1,4 +1,4 @@ -import { ApiProperty } from "@nestjs/swagger"; +import { ApiProperty } from '@nestjs/swagger'; import { AccountDto } from './account.dto'; import { MetaDataDto } from './meta.dto'; import { RecurrenceDto } from './recurrence.dto'; @@ -6,48 +6,67 @@ import { VerificationDto } from './verification.dto'; import { CustomerDto } from './customer.dto'; export class EstablishDto { - @ApiProperty() - accessId: any; - @ApiProperty() - merchantId: any; - @ApiProperty() - description: any; - @ApiProperty() - currency: any; - @ApiProperty() - amount: any; - @ApiProperty() - displayAmount: any; - @ApiProperty() - minimumBalance: any; - @ApiProperty() - merchantReference: any; - @ApiProperty() - paymentType: string; - @ApiProperty() - timeZone: any; - @ApiProperty() - returnUrl: any; - @ApiProperty() - cancelUrl: any; - @ApiProperty() - env: any; - @ApiProperty() - localUrl: any; - @ApiProperty() - metadata: MetaDataDto; - @ApiProperty() - recurrence: RecurrenceDto; - @ApiProperty() - verification: VerificationDto; - @ApiProperty() - customer: CustomerDto; - @ApiProperty() - account: AccountDto; - @ApiProperty() - transactionId: any; + @ApiProperty() + accessId: string; + + @ApiProperty() + account: AccountDto; + + @ApiProperty() + amount: string; + + @ApiProperty() + cancelUrl: string; + + @ApiProperty() + currency: string; + + @ApiProperty() + customer: CustomerDto; + + @ApiProperty() + description: string; + + @ApiProperty() + displayAmount: string; + + @ApiProperty() + env: string; + + @ApiProperty() + localUrl: string; + + @ApiProperty() + merchantId: string; + + @ApiProperty() + merchantReference: string; + + @ApiProperty() + metadata: MetaDataDto; + + @ApiProperty() + minimumBalance: number; + + @ApiProperty() + paymentType: string; + + @ApiProperty() + recurrence: RecurrenceDto; + + @ApiProperty() + returnUrl: string; + + @ApiProperty() + timeZone: any; + + @ApiProperty() + transactionId: any; + + @ApiProperty() + verification: VerificationDto; } export interface EstablishDto { - [key: string]: string | object; + [key: string]: number | object | string; } diff --git a/src/Dtos/recurrence.dto.ts b/src/Dtos/recurrence.dto.ts index 5ede82b..67de028 100644 --- a/src/Dtos/recurrence.dto.ts +++ b/src/Dtos/recurrence.dto.ts @@ -1,18 +1,24 @@ -import { ApiProperty } from "@nestjs/swagger"; +import { ApiProperty } from '@nestjs/swagger'; export class RecurrenceDto { - @ApiProperty() - startDate: any; - @ApiProperty() - endDate: any; - @ApiProperty() - frequency: any; - @ApiProperty() - frequencyUnit: any; - @ApiProperty() - frequencyUnitType: any; - @ApiProperty() - recurringAmount: any; - @ApiProperty() - automaticCapture: any; + @ApiProperty() + automaticCapture: boolean; + + @ApiProperty() + endDate?: number; // Unix Timestamp + + @ApiProperty() + frequency?: number; + + @ApiProperty() + frequencyUnit: number; + + @ApiProperty() + frequencyUnitType: number; + + @ApiProperty() + recurringAmount: string; + + @ApiProperty() + startDate?: number; // Unix Timestamp } diff --git a/src/RequestSignature/request-signature.utils.spec.ts b/src/RequestSignature/request-signature.utils.spec.ts index 49eefb5..b3a66bc 100644 --- a/src/RequestSignature/request-signature.utils.spec.ts +++ b/src/RequestSignature/request-signature.utils.spec.ts @@ -1,5 +1,180 @@ import { EstablishDto } from '../Dtos/establish.dto'; -import { normalizeEstablishData } from './request-signature.utils'; + +import { + createSignatureQueryString, + generateSignature, + normalizeEstablishData, +} from './request-signature.utils'; + +describe('generateSignature', () => { + const accessId = '123456'; + const establishData: EstablishDto = { + accessId: '1234', + account: { + accountNumber: '', + routingNumber: '', + type: 1, + // Application specific fields + country: '', + name: '', + nameOnAccount: '', + paymentProviderSubtype: 0, + profile: 0, + source: 0, + token: '', + verification: { + hasEnoughFunds: false, + verificationDate: 0, + verified: false, + type: 0, + }, + verified: false, + }, + amount: '1.00', + cancelUrl: '', + currency: 'USD', + customer: { + address: { + address1: '123 Main St', + city: 'San Francisco', + country: 'US', + state: 'CA', + zip: '94111', + }, + dateOfBirth: '1990-01-01', + driverLicense: { + number: '1010', + state: 'CA', + }, + email: 'john@ca.us', + enrollDate: 1234567890, + externalId: '1234', + name: 'John Doe', + phone: '123-456-7890', + taxId: '123-45-6789', + // Application specific fields + createdAt: 0, + customerId: '', + merchantId: '', + updatedAt: 0, + }, + description: 'text', + displayAmount: '1.00', + localUrl: 'mySite', + merchantId: '1234', + merchantReference: 'ref123', + metadata: { + urlScheme: '', + }, + paymentType: 'Recurring', + recurrence: { + automaticCapture: false, + frequencyUnit: 1, + frequencyUnitType: 3, + recurringAmount: '1.00', + }, + returnUrl: '', + transactionId: '', + verification: { + verifyCustomer: false, + // Application specific fields + status: '', + verified: false, + }, + // Application specific fields + env: '', + minimumBalance: 0, + timeZone: '', + }; + + it('should generate proper signature', () => { + const expectedQueryString = + 'accessId=1234&merchantId=1234&description=text¤cy=USD&amount=1.00&displayAmount=1.00&merchantReference=ref123&paymentType=Recurring&recurrence.frequencyUnit=1&recurrence.frequencyUnitType=3&recurrence.recurringAmount=1.00&customer.externalId=1234&customer.name=John Doe&customer.taxId=123-45-6789&customer.driverLicense.number=1010&customer.driverLicense.state=CA&customer.address.address1=123 Main St&customer.address.city=San Francisco&customer.address.state=CA&customer.address.zip=94111&customer.address.country=US&customer.phone=123-456-7890&customer.email=john@ca.us&customer.enrollDate=1234567890&customer.dateOfBirth=1990-01-01&account.type=1'; + const expectedSignature = 'HrGQpg/NsPXmh+P9u37R121Rot0='; + + const queryString = createSignatureQueryString(establishData); + const signature = generateSignature(establishData, accessId); + + expect(queryString).toEqual(expectedQueryString); + expect(signature).toBe(expectedSignature); + }); + + it('should generate proper signature ignoring empty strings', () => { + const establishDataWithEmptyStrings = { + ...establishData, + displayAmount: '', + }; + const expectedSignature = 'L/tBy40/WKaWNH0dcdp9j09cMqs='; + + const queryString = createSignatureQueryString(establishDataWithEmptyStrings); + const signature = generateSignature(establishDataWithEmptyStrings, accessId); + + expect(queryString).not.toContain('transactionId='); + expect(signature).toBe(expectedSignature); + }); + + it('should generate proper signature ignoring not intended data', () => { + const establishDataWithNotIntendedData = { + ...establishData, + notIntendedData: 'notIntendedData', + }; + const expectedSignature = 'HrGQpg/NsPXmh+P9u37R121Rot0='; + + const queryString = createSignatureQueryString(establishDataWithNotIntendedData); + const signature = generateSignature(establishDataWithNotIntendedData, accessId); + + expect(queryString).not.toContain('notIntendedData='); + expect(signature).toBe(expectedSignature); + }); + + it('should generate proper signature ignoring false booleans', () => { + const establishDataWithFalseBooleans = { + ...establishData, + recurrence: { + ...establishData.recurrence, + automaticCapture: false, + }, + }; + const expectedSignature = 'HrGQpg/NsPXmh+P9u37R121Rot0='; + + const queryString = createSignatureQueryString(establishDataWithFalseBooleans); + const signature = generateSignature(establishDataWithFalseBooleans, accessId); + + expect(queryString).not.toContain('recurrence.automaticCapture='); + expect(signature).toBe(expectedSignature); + }); + + it('should create proper query string with first-level nested objects', () => { + const establishDataWithNestedObjects = { + ...establishData, + customer: { + ...establishData.customer, + dateOfBirth: '2000-01-01', + }, + }; + + const queryString = createSignatureQueryString(establishDataWithNestedObjects); + + expect(queryString).toContain('customer.dateOfBirth=2000-01-01'); + }); + + it('should create proper query string with deep nested objects', () => { + const establishDataWithNestedObjects = { + ...establishData, + customer: { + ...establishData.customer, + address: { + ...establishData.customer.address, + country: 'BR', + }, + }, + }; + + const queryString = createSignatureQueryString(establishDataWithNestedObjects); + + expect(queryString).toContain('customer.address.country=BR'); + }); +}); describe('normalizeEstablishData', () => { test('converts dot notation into nested object', () => { diff --git a/src/RequestSignature/request-signature.utils.ts b/src/RequestSignature/request-signature.utils.ts index 7dbe78f..69fa63d 100644 --- a/src/RequestSignature/request-signature.utils.ts +++ b/src/RequestSignature/request-signature.utils.ts @@ -1,84 +1,96 @@ import { EstablishDto } from '../Dtos/establish.dto'; import { convertDotNotationIntoNestedObject } from '../utils/normalize'; -const CryptoSignature = require('crypto'); +const crypto = require('crypto'); -export const generateSignature = (establishData: EstablishDto, accessKey: string) => { - let query = ''; - query += `accessId=${establishData.accessId}`; - query += `&merchantId=${establishData.merchantId}`; - query += `&description=${establishData.description}`; - query += `¤cy=${establishData.currency}`; - query += `&amount=${establishData.amount}`; +const signatureKeysOrdered = [ + 'accessId', + 'merchantId', + 'description', + 'currency', + 'amount', + 'displayAmount', + 'minimumBalance', + 'merchantReference', + 'paymentType', + 'timeZone', + 'recurrence.startDate', + 'recurrence.endDate', + 'recurrence.frequency', + 'recurrence.frequencyUnit', + 'recurrence.frequencyUnitType', + 'recurrence.recurringAmount', + 'recurrence.automaticCapture', + 'verification.status', + 'verification.verifyCustomer', + 'customer.customerId', + 'customer.externalId', + 'customer.name', + 'customer.vip', + 'customer.taxId', + 'customer.driverLicense.number', + 'customer.driverLicense.state', + 'customer.address.address1', + 'customer.address.address2', + 'customer.address.city', + 'customer.address.state', + 'customer.address.zip', + 'customer.address.country', + 'customer.phone', + 'customer.email', + 'customer.balance', + 'customer.currency', + 'customer.enrollDate', + 'customer.externalTier', + 'customer.dateOfBirth', + 'account.nameOnAccount', + 'account.name', + 'account.type', + 'account.profile', + 'account.accountNumber', + 'account.routingNumber', + 'transactionId', +]; - if (establishData.displayAmount) query += `&displayAmount=${establishData.displayAmount}`; - if (establishData.minimumBalance) query += `&minimumBalance=${establishData.minimumBalance}`; - - query += `&merchantReference=${establishData.merchantReference}`; - query += `&paymentType=${establishData.paymentType}`; - - if (establishData.timeZone) query += `&timeZone=${establishData.timeZone}`; +export const createSignatureQueryString = (establishData: EstablishDto) => { + const searchParams: string[] = []; - if (establishData.paymentType === 'Recurring' && establishData.recurrence) { - if (establishData.recurrence.startDate) query += `&recurrence.startDate=${establishData.recurrence.startDate}`; - if (establishData.recurrence.endDate) query += `&recurrence.endDate=${establishData.recurrence.endDate}`; - if (establishData.recurrence.frequency) query += `&recurrence.frequency=${establishData.recurrence.frequency}`; - if (establishData.recurrence.frequencyUnit) query += `&recurrence.frequencyUnit=${establishData.recurrence.frequencyUnit}`; - if (establishData.recurrence.frequencyUnitType) query += `&recurrence.frequencyUnitType=${establishData.recurrence.frequencyUnitType}`; - if (establishData.recurrence.recurringAmount) query += `&recurrence.recurringAmount=${establishData.recurrence.recurringAmount}`; - if (establishData.recurrence.automaticCapture) query += `&recurrence.automaticCapture=${establishData.recurrence.automaticCapture}`; - } + const appendSearchParam = (key: string, value: any) => { + value && searchParams.push(`${key}=${value.toString()}`); + }; - if (establishData.verification) { - if (establishData.verification.status) query += `&verification.status=${establishData.verification.status}`; - if (establishData.verification.verifyCustomer) query += `&verification.verifyCustomer=${establishData.verification.verifyCustomer}`; + for (const key of signatureKeysOrdered) { + if (!key.includes('.')) { + appendSearchParam(key, establishData[key]); + continue; } - if (establishData.customer) { - if (establishData.customer.customerId) query += `&customer.customerId=${establishData.customer.customerId}`; - if (establishData.customer.externalId) query += `&customer.externalId=${establishData.customer.externalId}`; - if (establishData.customer.name) query += `&customer.name=${establishData.customer.name}`; - if (establishData.customer.vip !== undefined) query += `&customer.vip=${establishData.customer.vip}`; - if (establishData.customer.taxId) query += `&customer.taxId=${establishData.customer.taxId}`; - if (establishData.customer.driverLicense) { - if (establishData.customer.driverLicense.number) query += `&customer.driverLicense.number=${establishData.customer.driverLicense.number}`; - if (establishData.customer.driverLicense.state) query += `&customer.driverLicense.state=${establishData.customer.driverLicense.state}`; - } - if (establishData.customer.address) { - if (establishData.customer.address.address1) query += `&customer.address.address1=${establishData.customer.address.address1}`; - if (establishData.customer.address.address2) query += `&customer.address.address2=${establishData.customer.address.address2}`; - if (establishData.customer.address.city) query += `&customer.address.city=${establishData.customer.address.city}`; - if (establishData.customer.address.state) query += `&customer.address.state=${establishData.customer.address.state}`; - if (establishData.customer.address.zip) query += `&customer.address.zip=${establishData.customer.address.zip}`; - if (establishData.customer.address.country) query += `&customer.address.country=${establishData.customer.address.country}`; - } - if (establishData.customer.phone) query += `&customer.phone=${establishData.customer.phone}`; - if (establishData.customer.email) query += `&customer.email=${establishData.customer.email}`; - if (establishData.customer.balance) query += `&customer.balance=${establishData.customer.balance}`; - if (establishData.customer.currency) query += `&customer.currency=${establishData.customer.currency}`; - if (establishData.customer.enrollDate) query += `&customer.enrollDate=${establishData.customer.enrollDate}`; - if (establishData.customer.dateOfBirth) query += `&customer.dateOfBirth=${establishData.customer.dateOfBirth}`; - } + const subKeys = key.split('.'); + let data: EstablishDto = establishData; + + for (const subKey of subKeys) { + const innerValue = data[subKey]; - if (establishData.account) { - if (establishData.account.nameOnAccount) query += `&account.nameOnAccount=${establishData.account.nameOnAccount}`; - if (establishData.account.name) query += `&account.name=${establishData.account.name}`; - if (establishData.account.type) query += `&account.type=${establishData.account.type}`; - if (establishData.account.profile) query += `&account.profile=${establishData.account.profile}`; - if (establishData.account.accountNumber) query += `&account.accountNumber=${establishData.account.accountNumber}`; - if (establishData.account.routingNumber) query += `&account.routingNumber=${establishData.account.routingNumber}`; + if (typeof innerValue === 'object') { + data = innerValue as EstablishDto; + } else { + appendSearchParam(key, innerValue); + break; + } } + } + + return searchParams.join('&'); +}; - if (establishData.transactionId) query += `&transactionId=${establishData.transactionId}`; +export const generateSignature = (establishData: EstablishDto, accessKey: string) => { + const query = createSignatureQueryString(establishData); + const requestSignature = crypto.createHmac('sha1', accessKey).update(query).digest('base64'); - const requestSignature = CryptoSignature.createHmac('sha1', accessKey).update(query).digest('base64'); - return requestSignature; + return requestSignature; }; -export const normalizeEstablishData = ( - establish: EstablishDto, - rawBody: Buffer -) => { +export const normalizeEstablishData = (establish: EstablishDto, rawBody: Buffer) => { // Remove dot notations for (const key in establish) { if (key.includes('.')) delete establish[key]; diff --git a/src/main.ts b/src/main.ts index 07e95da..2e85c11 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,7 +10,7 @@ async function bootstrap() { }); const config = new DocumentBuilder() - .setTitle('Alpha Merchante Backend Demonstration ') + .setTitle('Alpha Merchant Backend Demonstration ') .setDescription('Simple backend demonstrate how to integrate with Trustly') .setVersion('1.0') .build();