From d2a8c049c33ffc8a04adb77dcec0286de13acc3c Mon Sep 17 00:00:00 2001 From: Dan Gowans Date: Wed, 31 Jul 2024 13:50:58 -0400 Subject: [PATCH] validation warnings in object helps with testing and debugging --- formats/cpa005.d.ts | 5 +- formats/cpa005.js | 90 ++++++++++++++++++++----------- formats/cpa005.ts | 129 +++++++++++++++++++++++++++----------------- test/test.js | 34 +++++++++--- test/test.ts | 47 +++++++++++++--- types.d.ts | 12 +++++ types.ts | 17 ++++++ 7 files changed, 238 insertions(+), 96 deletions(-) diff --git a/formats/cpa005.d.ts b/formats/cpa005.d.ts index 57375be..a1b4191 100644 --- a/formats/cpa005.d.ts +++ b/formats/cpa005.d.ts @@ -1,4 +1,5 @@ -import { type EFTGenerator } from '../index.js'; +import type { EFTGenerator } from '../index.js'; +import type { ValidationWarning } from '../types.js'; export declare const NEWLINE = "\r\n"; -export declare function validateCPA005(eftGenerator: EFTGenerator): number; +export declare function validateCPA005(eftGenerator: EFTGenerator): ValidationWarning[]; export declare function formatToCPA005(eftGenerator: EFTGenerator): string; diff --git a/formats/cpa005.js b/formats/cpa005.js index 699f97c..c7686ac 100644 --- a/formats/cpa005.js +++ b/formats/cpa005.js @@ -7,7 +7,7 @@ function toJulianDate(date) { return ('0' + toShortModernJulianDate(date)); } function validateConfig(eftConfig) { - let warningCount = 0; + const validationWarnings = []; if (eftConfig.originatorId.length > 10) { throw new Error(`originatorId length exceeds 10: ${eftConfig.originatorId}`); } @@ -18,54 +18,72 @@ function validateConfig(eftConfig) { throw new Error(`destinationDataCentre should be 1 to 5 digits: ${eftConfig.destinationDataCentre}`); } if (eftConfig.originatorShortName === undefined) { - debug('originatorShortName not defined, using originatorLongName.'); - warningCount += 1; + validationWarnings.push({ + warningField: 'originatorShortName', + warning: 'originatorShortName not defined, using originatorLongName.' + }); eftConfig.originatorShortName = eftConfig.originatorLongName; } if (eftConfig.originatorShortName.length > 15) { - debug(`originatorShortName will be truncated: ${eftConfig.originatorShortName}`); - warningCount += 1; + validationWarnings.push({ + warningField: 'originatorShortName', + warning: `originatorShortName will be truncated to 15 characters: ${eftConfig.originatorShortName}` + }); } if (eftConfig.originatorLongName.length > 30) { - debug(`originatorLongName will be truncated: ${eftConfig.originatorLongName}`); - warningCount += 1; + validationWarnings.push({ + warningField: 'originatorLongName', + warning: `originatorLongName will be truncated to 30 characters: ${eftConfig.originatorLongName}` + }); } if (!['', 'CAD', 'USD'].includes(eftConfig.destinationCurrency ?? '')) { throw new Error(`Unsupported destinationCurrency: ${eftConfig.destinationCurrency}`); } - return warningCount; + return validationWarnings; } function validateTransactions(eftTransactions) { - let warningCount = 0; + const validationWarnings = []; if (eftTransactions.length === 0) { - debug('There are no transactions to include in the file.'); - warningCount += 1; + validationWarnings.push({ + warningField: 'transactions', + warning: 'There are no transactions to include in the file.' + }); } else if (eftTransactions.length > 999_999_999) { throw new Error('Transaction count exceeds 999,999,999.'); } const crossReferenceNumbers = new Set(); - for (const transaction of eftTransactions) { + for (const [transactionIndex, transaction] of eftTransactions.entries()) { if (transaction.segments.length === 0) { - debug('Transaction record has no segments, will be ignored.'); - warningCount += 1; + validationWarnings.push({ + transactionIndex, + warningField: 'segments', + warning: 'Transaction record has no segments, will be ignored.' + }); } - if (transaction.segments.length > 6) { - debug('Transaction record has more than 6 segments, will be split into multiple transactions.'); - warningCount += 1; + else if (transaction.segments.length > 6) { + validationWarnings.push({ + transactionIndex, + warningField: 'segments', + warning: 'Transaction record has more than 6 segments, will be split into multiple transactions.' + }); } if (!['C', 'D'].includes(transaction.recordType)) { throw new Error(`Unsupported recordType: ${transaction.recordType}`); } - for (const segment of transaction.segments) { + for (const [transactionSegmentIndex, segment] of transaction.segments.entries()) { if (!isCPATransactionCode(segment.cpaCode)) { - debug(`Unknown CPA code: ${segment.cpaCode}`); - warningCount += 1; + validationWarnings.push({ + transactionIndex, + transactionSegmentIndex, + warningField: 'cpaCode', + warning: `Unknown CPA code: ${segment.cpaCode}` + }); } if (segment.amount <= 0) { throw new Error(`Segment amount cannot be less than or equal to zero: ${segment.amount}`); } - if (segment.amount >= 100_000_000) { + else if (segment.amount >= 100_000_000) { throw new Error(`Segment amount cannot exceed $100,000,000: ${segment.amount}`); } if (!/^\d{1,3}$/.test(segment.bankInstitutionNumber)) { @@ -78,23 +96,32 @@ function validateTransactions(eftTransactions) { throw new Error(`bankAccountNumber should be 1 to 12 digits: ${segment.bankAccountNumber}`); } if (segment.payeeName.length > 30) { - debug(`payeeName will be truncated: ${segment.payeeName}`); - warningCount += 1; + validationWarnings.push({ + transactionIndex, + transactionSegmentIndex, + warningField: 'payeeName', + warning: `payeeName will be truncated to 30 characters: ${segment.payeeName}` + }); } if (segment.crossReferenceNumber !== undefined) { if (crossReferenceNumbers.has(segment.crossReferenceNumber)) { - debug(`crossReferenceNumber should be unique: ${segment.crossReferenceNumber}`); - warningCount += 1; + validationWarnings.push({ + transactionIndex, + transactionSegmentIndex, + warningField: 'crossReferenceNumber', + warning: `crossReferenceNumber should be unique: ${segment.crossReferenceNumber}` + }); } crossReferenceNumbers.add(segment.crossReferenceNumber); } } } - return warningCount; + return validationWarnings; } export function validateCPA005(eftGenerator) { - return (validateConfig(eftGenerator.getConfiguration()) + - validateTransactions(eftGenerator.getTransactions())); + const validationWarnings = validateConfig(eftGenerator.getConfiguration()); + validationWarnings.push(...validateTransactions(eftGenerator.getTransactions())); + return validationWarnings; } function formatHeader(eftConfig) { const fileCreationJulianDate = toJulianDate(eftConfig.fileCreationDate ?? new Date()); @@ -117,9 +144,10 @@ function formatHeader(eftConfig) { ''.padEnd(1406, ' ')); } export function formatToCPA005(eftGenerator) { - const warningCount = validateCPA005(eftGenerator); - if (warningCount > 0) { - debug(`Proceeding with ${warningCount} warnings.`); + const validationWarnings = validateCPA005(eftGenerator); + if (validationWarnings.length > 0) { + debug(`Proceeding with ${validationWarnings.length} warnings.`); + debug(validationWarnings); } const eftConfig = eftGenerator.getConfiguration(); const outputLines = []; diff --git a/formats/cpa005.ts b/formats/cpa005.ts index f16c576..f5bf470 100644 --- a/formats/cpa005.ts +++ b/formats/cpa005.ts @@ -2,8 +2,12 @@ import { isCPATransactionCode } from '@cityssm/cpa-codes' import { toShortModernJulianDate } from '@cityssm/modern-julian-date' import Debug from 'debug' -import { type EFTGenerator } from '../index.js' -import type { EFTConfiguration, EFTTransaction } from '../types.js' +import type { EFTGenerator } from '../index.js' +import type { + EFTConfiguration, + EFTTransaction, + ValidationWarning +} from '../types.js' const debug = Debug('eft-generator:cpa005') @@ -13,8 +17,8 @@ function toJulianDate(date: Date): `0${string}` { return ('0' + toShortModernJulianDate(date)) as `0${string}` } -function validateConfig(eftConfig: EFTConfiguration): number { - let warningCount = 0 +function validateConfig(eftConfig: EFTConfiguration): ValidationWarning[] { + const validationWarnings: ValidationWarning[] = [] if (eftConfig.originatorId.length > 10) { throw new Error(`originatorId length exceeds 10: ${eftConfig.originatorId}`) @@ -33,23 +37,26 @@ function validateConfig(eftConfig: EFTConfiguration): number { } if (eftConfig.originatorShortName === undefined) { - debug('originatorShortName not defined, using originatorLongName.') - warningCount += 1 + validationWarnings.push({ + warningField: 'originatorShortName', + warning: 'originatorShortName not defined, using originatorLongName.' + }) + eftConfig.originatorShortName = eftConfig.originatorLongName } if (eftConfig.originatorShortName.length > 15) { - debug( - `originatorShortName will be truncated: ${eftConfig.originatorShortName}` - ) - warningCount += 1 + validationWarnings.push({ + warningField: 'originatorShortName', + warning: `originatorShortName will be truncated to 15 characters: ${eftConfig.originatorShortName}` + }) } if (eftConfig.originatorLongName.length > 30) { - debug( - `originatorLongName will be truncated: ${eftConfig.originatorLongName}` - ) - warningCount += 1 + validationWarnings.push({ + warningField: 'originatorLongName', + warning: `originatorLongName will be truncated to 30 characters: ${eftConfig.originatorLongName}` + }) } if (!['', 'CAD', 'USD'].includes(eftConfig.destinationCurrency ?? '')) { @@ -58,52 +65,64 @@ function validateConfig(eftConfig: EFTConfiguration): number { ) } - return warningCount + return validationWarnings } // eslint-disable-next-line sonarjs/cognitive-complexity -function validateTransactions(eftTransactions: EFTTransaction[]): number { - let warningCount = 0 +function validateTransactions( + eftTransactions: EFTTransaction[] +): ValidationWarning[] { + const validationWarnings: ValidationWarning[] = [] if (eftTransactions.length === 0) { - debug('There are no transactions to include in the file.') - warningCount += 1 + validationWarnings.push({ + warningField: 'transactions', + warning: 'There are no transactions to include in the file.' + }) } else if (eftTransactions.length > 999_999_999) { throw new Error('Transaction count exceeds 999,999,999.') } const crossReferenceNumbers = new Set() - for (const transaction of eftTransactions) { + for (const [transactionIndex, transaction] of eftTransactions.entries()) { if (transaction.segments.length === 0) { - debug('Transaction record has no segments, will be ignored.') - warningCount += 1 - } - - if (transaction.segments.length > 6) { - debug( - 'Transaction record has more than 6 segments, will be split into multiple transactions.' - ) - warningCount += 1 + validationWarnings.push({ + transactionIndex, + warningField: 'segments', + warning: 'Transaction record has no segments, will be ignored.' + }) + } else if (transaction.segments.length > 6) { + validationWarnings.push({ + transactionIndex, + warningField: 'segments', + warning: + 'Transaction record has more than 6 segments, will be split into multiple transactions.' + }) } if (!['C', 'D'].includes(transaction.recordType)) { throw new Error(`Unsupported recordType: ${transaction.recordType}`) } - for (const segment of transaction.segments) { + for (const [ + transactionSegmentIndex, + segment + ] of transaction.segments.entries()) { if (!isCPATransactionCode(segment.cpaCode)) { - debug(`Unknown CPA code: ${segment.cpaCode}`) - warningCount += 1 + validationWarnings.push({ + transactionIndex, + transactionSegmentIndex, + warningField: 'cpaCode', + warning: `Unknown CPA code: ${segment.cpaCode}` + }) } if (segment.amount <= 0) { throw new Error( `Segment amount cannot be less than or equal to zero: ${segment.amount}` ) - } - - if (segment.amount >= 100_000_000) { + } else if (segment.amount >= 100_000_000) { throw new Error( `Segment amount cannot exceed $100,000,000: ${segment.amount}` ) @@ -128,30 +147,41 @@ function validateTransactions(eftTransactions: EFTTransaction[]): number { } if (segment.payeeName.length > 30) { - debug(`payeeName will be truncated: ${segment.payeeName}`) - warningCount += 1 + validationWarnings.push({ + transactionIndex, + transactionSegmentIndex, + warningField: 'payeeName', + warning: `payeeName will be truncated to 30 characters: ${segment.payeeName}` + }) } if (segment.crossReferenceNumber !== undefined) { if (crossReferenceNumbers.has(segment.crossReferenceNumber)) { - debug( - `crossReferenceNumber should be unique: ${segment.crossReferenceNumber}` - ) - warningCount += 1 + validationWarnings.push({ + transactionIndex, + transactionSegmentIndex, + warningField: 'crossReferenceNumber', + warning: `crossReferenceNumber should be unique: ${segment.crossReferenceNumber}` + }) } crossReferenceNumbers.add(segment.crossReferenceNumber) } } } - return warningCount + return validationWarnings } -export function validateCPA005(eftGenerator: EFTGenerator): number { - return ( - validateConfig(eftGenerator.getConfiguration()) + - validateTransactions(eftGenerator.getTransactions()) +export function validateCPA005( + eftGenerator: EFTGenerator +): ValidationWarning[] { + const validationWarnings = validateConfig(eftGenerator.getConfiguration()) + + validationWarnings.push( + ...validateTransactions(eftGenerator.getTransactions()) ) + + return validationWarnings } function formatHeader(eftConfig: EFTConfiguration): string { @@ -192,10 +222,11 @@ function formatHeader(eftConfig: EFTConfiguration): string { } export function formatToCPA005(eftGenerator: EFTGenerator): string { - const warningCount = validateCPA005(eftGenerator) + const validationWarnings = validateCPA005(eftGenerator) - if (warningCount > 0) { - debug(`Proceeding with ${warningCount} warnings.`) + if (validationWarnings.length > 0) { + debug(`Proceeding with ${validationWarnings.length} warnings.`) + debug(validationWarnings) } const eftConfig = eftGenerator.getConfiguration() diff --git a/test/test.js b/test/test.js index 29854e7..8995021 100644 --- a/test/test.js +++ b/test/test.js @@ -1,7 +1,7 @@ import assert from 'node:assert'; import fs from 'node:fs'; import { describe, it } from 'node:test'; -import { NEWLINE as cpa005_newline } from '../formats/cpa005.js'; +import { NEWLINE as cpa005_newline, validateCPA005 } from '../formats/cpa005.js'; import { EFTGenerator } from '../index.js'; const config = { originatorId: '0123456789', @@ -140,7 +140,9 @@ await describe('eft-generator - CPA-005', async () => { originatorLongName: 'This name exceeds the 30 character limit and will be truncated.', fileCreationNumber: '0001' }); - assert.ok(eftGenerator.validateCPA005()); + assert(validateCPA005(eftGenerator).some((validationWarning) => { + return validationWarning.warningField === 'originatorLongName'; + })); }); }); await describe('Transaction errors and warnings', async () => { @@ -173,7 +175,10 @@ await describe('eft-generator - CPA-005', async () => { recordType: 'D', segments: [] }); - assert.ok(eftGenerator.validateCPA005()); + const validationWarnings = validateCPA005(eftGenerator); + assert(validationWarnings.some((validationWarning) => { + return validationWarning.warningField === 'segments'; + })); const output = eftGenerator.toCPA005(); assert.ok(output.length > 0); }); @@ -240,7 +245,12 @@ await describe('eft-generator - CPA-005', async () => { } ] }); - assert.ok(eftGenerator.validateCPA005()); + const validationWarnings = validateCPA005(eftGenerator); + assert(validationWarnings.some((validationWarning) => { + return validationWarning.warningField === 'segments'; + })); + const output = eftGenerator.toCPA005(); + assert.ok(output.length > 0); }); await it('Throws error when a transaction has a negative amount.', () => { const eftGenerator = new EFTGenerator(config); @@ -252,7 +262,13 @@ await describe('eft-generator - CPA-005', async () => { bankAccountNumber: '1', cpaCode: '1' }); - assert.ok(!eftGenerator.validateCPA005()); + try { + eftGenerator.toCPA005(); + assert.fail(); + } + catch { + assert.ok(true); + } }); await it('Throws error when a transaction has too large of an amount', () => { const eftGenerator = new EFTGenerator(config); @@ -336,7 +352,9 @@ await describe('eft-generator - CPA-005', async () => { bankAccountNumber: '1', cpaCode: cpaCodePropertyTaxes }); - assert.ok(eftGenerator.validateCPA005()); + assert(validateCPA005(eftGenerator).some((validationWarning) => { + return validationWarning.warningField === 'payeeName'; + })); }); await it('Warns when the crossReferenceNumber is duplicated.', () => { const eftGenerator = new EFTGenerator(config); @@ -358,7 +376,9 @@ await describe('eft-generator - CPA-005', async () => { bankAccountNumber: '1', cpaCode: cpaCodePropertyTaxes }); - assert.ok(eftGenerator.validateCPA005()); + assert(validateCPA005(eftGenerator).some((validationWarning) => { + return validationWarning.warningField === 'crossReferenceNumber'; + })); }); }); }); diff --git a/test/test.ts b/test/test.ts index a337601..b36fce9 100644 --- a/test/test.ts +++ b/test/test.ts @@ -2,7 +2,7 @@ import assert from 'node:assert' import fs from 'node:fs' import { describe, it } from 'node:test' -import { NEWLINE as cpa005_newline } from '../formats/cpa005.js' +import { NEWLINE as cpa005_newline, validateCPA005 } from '../formats/cpa005.js' import { EFTGenerator } from '../index.js' import type { EFTConfiguration } from '../types.js' @@ -162,7 +162,11 @@ await describe('eft-generator - CPA-005', async () => { fileCreationNumber: '0001' }) - assert.ok(eftGenerator.validateCPA005()) + assert( + validateCPA005(eftGenerator).some((validationWarning) => { + return validationWarning.warningField === 'originatorLongName' + }) + ) }) }) @@ -200,7 +204,13 @@ await describe('eft-generator - CPA-005', async () => { segments: [] }) - assert.ok(eftGenerator.validateCPA005()) + const validationWarnings = validateCPA005(eftGenerator) + + assert( + validationWarnings.some((validationWarning) => { + return validationWarning.warningField === 'segments' + }) + ) const output = eftGenerator.toCPA005() @@ -272,7 +282,17 @@ await describe('eft-generator - CPA-005', async () => { ] }) - assert.ok(eftGenerator.validateCPA005()) + const validationWarnings = validateCPA005(eftGenerator) + + assert( + validationWarnings.some((validationWarning) => { + return validationWarning.warningField === 'segments' + }) + ) + + const output = eftGenerator.toCPA005() + + assert.ok(output.length > 0) }) await it('Throws error when a transaction has a negative amount.', () => { @@ -287,7 +307,12 @@ await describe('eft-generator - CPA-005', async () => { cpaCode: '1' }) - assert.ok(!eftGenerator.validateCPA005()) + try { + eftGenerator.toCPA005() + assert.fail() + } catch { + assert.ok(true) + } }) await it('Throws error when a transaction has too large of an amount', () => { @@ -382,7 +407,11 @@ await describe('eft-generator - CPA-005', async () => { cpaCode: cpaCodePropertyTaxes }) - assert.ok(eftGenerator.validateCPA005()) + assert( + validateCPA005(eftGenerator).some((validationWarning) => { + return validationWarning.warningField === 'payeeName' + }) + ) }) await it('Warns when the crossReferenceNumber is duplicated.', () => { @@ -408,7 +437,11 @@ await describe('eft-generator - CPA-005', async () => { cpaCode: cpaCodePropertyTaxes }) - assert.ok(eftGenerator.validateCPA005()) + assert( + validateCPA005(eftGenerator).some((validationWarning) => { + return validationWarning.warningField === 'crossReferenceNumber' + }) + ) }) }) }) diff --git a/types.d.ts b/types.d.ts index 030af80..4c96a64 100644 --- a/types.d.ts +++ b/types.d.ts @@ -21,3 +21,15 @@ export interface EFTTransactionSegment { payeeName: string; crossReferenceNumber?: string; } +export type ValidationWarning = { + warning: string; +} & ({ + warningField: keyof EFTConfiguration | 'transactions'; +} | { + transactionIndex: number; + warningField: keyof EFTTransaction; +} | { + transactionIndex: number; + transactionSegmentIndex: number; + warningField: keyof EFTTransactionSegment; +}); diff --git a/types.ts b/types.ts index 5f1eaa4..9847aed 100644 --- a/types.ts +++ b/types.ts @@ -72,3 +72,20 @@ export interface EFTTransactionSegment { crossReferenceNumber?: string } + +export type ValidationWarning = { + warning: string +} & ( + | { + warningField: keyof EFTConfiguration | 'transactions' + } + | { + transactionIndex: number + warningField: keyof EFTTransaction + } + | { + transactionIndex: number + transactionSegmentIndex: number + warningField: keyof EFTTransactionSegment + } +)