import { HttpErrorResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { SpinnerService } from '@core/services/spinner.service'; import { StatusService } from '@core/services/status.service'; import { TimeZoneService } from '@core/services/time-zone.service'; import { AddBatchForApi, BatchItem, BatchItemFromApi, CanRecallPaymentPayload, DisbursementPayload, DisbursementReportRow, EditBatchModalResponse, ExpeditePaymentForApi, PaymentForProcess, PaymentStatsFromApi, PaymentSummaryResponse, PaymentType, PaymentUpdateImport, ProcessingTypes, RecallPaymentPayload, SendToProcessingModalResponse, SimplePaymentStats, UpdatePaymentsApi, UpdatePaymentSpecialHandling } from '@core/typings/payment.typing'; import { BatchStatuses, PaymentStatus } from '@core/typings/status.typing'; import { ApplicantManagerService } from '@features/applicant/applicant-manager.service'; import { ApplicationActionService } from '@features/application-manager/services/application-actions/application-actions.service'; import { ClientSettingsService } from '@features/client-settings/client-settings.service'; import { NonprofitService } from '@features/nonprofit/nonprofit.service'; import { ProgramService } from '@features/programs/program.service'; import { DatahubService } from '@features/reporting/data-hub/data-hub.service'; import { SystemTagsService } from '@features/system-tags/system-tags.service'; import { ArrayHelpersService, AutoTableRepositoryFactory, CallMakerFactory, createValidator, DebounceFactory, FileService, IsAlphaNumeric, IsDate, IsNumber, IsOneOf, IsString, MoneyService, PaginationOptions, Required, TableDataDownloadFormat, Transform, TypeaheadSelectOption, Unique } from '@yourcause/common'; import { I18nService } from '@yourcause/common/i18n'; import { LogService } from '@yourcause/common/logging'; import { NotifierService } from '@yourcause/common/notifier'; import { AttachYCState, BaseYCService } from '@yourcause/common/state'; import moment from 'moment'; import { NotifyPayeesModalResponse } from './notify-payees-modal/notify-payees-modal.component'; import { PaymentProcessingResources } from './payment-processing.resources'; import { PaymentProcessingState } from './payment-processing.state'; @AttachYCState(PaymentProcessingState) @Injectable({ providedIn: 'root' }) export class PaymentProcessingService extends BaseYCService { constructor ( private logger: LogService, private fileService: FileService, private moneyService: MoneyService, private clientSettingsService: ClientSettingsService, private statusService: StatusService, private systemTagsService: SystemTagsService, private i18n: I18nService, private notifier: NotifierService, private arrayHelper: ArrayHelpersService, private spinnerService: SpinnerService, private autoTableFactory: AutoTableRepositoryFactory, private dataHubService: DatahubService, private applicantManagerService: ApplicantManagerService, private nonprofitService: NonprofitService, private paymentProcessingResources: PaymentProcessingResources, private programService: ProgramService, private applicationActionService: ApplicationActionService, private timezoneService: TimeZoneService ) { super(); } get isAvailable () { return location.pathname.includes('available'); } get isIncluded () { return location.pathname.includes('included'); } get processorType () { return this.clientSettingsService.clientSettings.clientProcessingType; } setAvailablePayments (items: BatchItem[]) { this.set('availablePayments', items); } setCurrentAvailableStats (stats: SimplePaymentStats) { this.set('currentAvailableStats', stats); } setCurrentIncludedOptions (options: PaginationOptions) { this.set('currentIncludedOptions', options); } setCurrentProgramOptions (options: TypeaheadSelectOption[]) { this.set('currentProgramOptions', options); } getStatusText (batch: BatchItemFromApi) { if ( batch.batchStatusLastUpdatedBy && batch.batchStatusLastUpdatedBy.firstName && batch.batchStatusLastUpdatedDate ) { const name = batch.batchStatusLastUpdatedBy.firstName + ' ' + batch.batchStatusLastUpdatedBy.lastName; const date = batch.batchStatusLastUpdatedDate; return this.i18n.translate( 'MANAGE:textUpdatedOnoBy', { date: moment(date).format('ll'), name }, '__date__ by __name__' ); } return ''; } missingValidAffiliateId () { this.notifier.error(this.i18n.translate( 'MANAGE:textPaymentsZoneNotIntegrated', {}, 'Payments cannot be sent to proccessing because the zone is not integrated' )); } async getProcessingStats () { const type = this.processorType; let statsArray: BatchItem[] = []; if (type !== ProcessingTypes.Both) { const stats = await this.paymentProcessingResources.getPaymentStats(type); statsArray.push(this.adaptStats(stats, type === ProcessingTypes.YourCause)); } else { const [ clientStats, yourCauseStats ] = await Promise.all([ this.paymentProcessingResources.getPaymentStats(ProcessingTypes.Client), this.paymentProcessingResources.getPaymentStats(ProcessingTypes.YourCause) ]); statsArray = [ this.adaptStats(clientStats, false), this.adaptStats(yourCauseStats, true) ]; } this.setAvailablePayments(statsArray); } getPaymentStatsByPrograms (type: ProcessingTypes, programIds: number[]) { return this.paymentProcessingResources.getPaymentStats( type, programIds ); } adaptStats (stats: PaymentStatsFromApi, isYourCause = true): BatchItem { return { id: null, name: this.i18n.translate( 'GLOBAL:hdrAvailablePayments' ), processorName: isYourCause ? 'YourCause' : stats.processorName, processorType: isYourCause ? ProcessingTypes.YourCause : ProcessingTypes.Client, paymentDates: stats.paymentStartDate && stats.paymentEndDate ? (this.timezoneService.returnMidnightUTCDateShort(stats.paymentStartDate) + ' - ' + this.timezoneService.returnMidnightUTCDateShort(stats.paymentEndDate)) : null, availableDetails: { includedPayments: { number: stats.includedPaymentsNumber, total: stats.includedPaymentsAmount }, excludedPayments: { number: stats.excludedPaymentsNumber, total: stats.excludedPaymentsAmount } }, footerMessage: stats.excludedPaymentsNumber ? this.i18n.translate( isYourCause ? 'MANAGE:textNumberOfPaymentsExcludedDueToHoldOrPayee3' : 'MANAGE:textNumberOfPaymentsExcludedDueToHoldOrPayee4', { number: stats.excludedPaymentsNumber }, isYourCause ? '__number__ payments will be excluded from processing from having a status of Hold, missing payee information, or having an alternate address request that has not been approved.' : '__number__ payments will be excluded from processing from having a status of Hold or missing payee information.' ) : '' }; } async getAndSetPaginatedStats ( paginationOptions: PaginationOptions, force = false ): Promise { if (force || this.isAvailable && this.isIncluded) { paginationOptions = this.systemTagsService.formatPaginationOptions( paginationOptions ); this.spinnerService.startSpinner(); const stats = await this.paymentProcessingResources.getPaymentStatsFromPagination( paginationOptions ); this.setCurrentIncludedOptions(paginationOptions); this.setCurrentAvailableStats(stats); this.spinnerService.stopSpinner(); } } async resetPaymentStats () { this.setCurrentIncludedOptions(undefined); await this.getProcessingStats(); } resetAllPaymentsRepo () { const allPayments = this.autoTableFactory.getRepository( 'ALL_PAYMENTS' ); if (allPayments) { allPayments.reset(); } } async resetAvailableRepos (isYourCause = true, clearFilters = false) { const includedRepo = this.autoTableFactory.getRepository( `AVAILABLE_INCLUDED_${ isYourCause ? 'YOURCAUSE' : 'CLIENT' }` ); const excludedRepo = this.autoTableFactory.getRepository( `AVAILABLE_EXCLUDED_${ isYourCause ? 'YOURCAUSE' : 'CLIENT' }` ); if (clearFilters && includedRepo) { includedRepo.clearAllFilters(); this.setCurrentIncludedOptions(undefined); this.setCurrentAvailableStats(undefined); await this.resetPaymentStats(); } if (includedRepo) { includedRepo.reset(); } if (excludedRepo) { excludedRepo.reset(); } this.resetAllPaymentsRepo(); } resetBatchRepos (id: number) { const included = this.autoTableFactory.getRepository( 'BATCH_INCLUDED_' + id ); const excluded = this.autoTableFactory.getRepository( 'BATCH_EXCLUDED_' + id ); if (included) { included.reset(); } if (excluded) { excluded.reset(); } this.resetBatchesRepo(); this.resetAllPaymentsRepo(); } resetBatchesRepo () { const repo = this.autoTableFactory.getRepository('BATCHES'); if (repo) { repo.reset(); } } getTableDataFactory ( batchId: number, isInKind = false ) { return DebounceFactory.createSimple( async (options: PaginationOptions) => { let result: any; if (!isInKind) { await this.getAndSetPaginatedStats( options ); result = await this.getAvailableOrBatchPayments( this.isAvailable, options, true, batchId, this.isIncluded ); } else { options = this.systemTagsService.formatPaginationOptions(options); result = await this.paymentProcessingResources.getInKindPayments( options ); } if (result.programFacets) { const programOptions = this.arrayHelper.sort(Object.keys(result.programFacets) .map(id => { const map = this.programService.programTranslationMap[id]; return { value: +id, label: map && map.Name ? map.Name : result.programFacets[id] }; }), 'label'); this.setCurrentProgramOptions( programOptions ); } result.records.forEach((record: PaymentForProcess) => { const map = this.programService.programTranslationMap[record.programId]; record.programName = map && map.Name ? map.Name : record.programName; this.setNonprofitAndApplicantRouterLinkMap(record); }); return { success: true, data: { recordCount: result.recordCount, records: result.records } }; } ); } resetInKindPaymentStats () { this.set('inKindPaymentStats', undefined); return this.setInKindPaymentStats(); } async setInKindPaymentStats () { if (!this.get('inKindPaymentStats')) { const stats = await this.paymentProcessingResources.getInKindPaymentStats(); this.set('inKindPaymentStats', stats); } } setNonprofitAndApplicantRouterLinkMap (row: PaymentForProcess) { if (row.insightsInfo) { this.applicantManagerService.setApplicantRouterLinkMap( row.insightsInfo.applicantId ); this.nonprofitService.setNonprofitRouterLinkMap( row.insightsInfo.organizationGuid || row.insightsInfo.organizationId ); } return row; } getAvailableOrBatchPayments ( isAvailable = true, paginationOptions: PaginationOptions, includeProgramFacet = true, batchId?: number, isIncluded?: boolean ) { paginationOptions = this.systemTagsService.formatPaginationOptions(paginationOptions); const options = { paginationOptions, batchId, includeProgramFacet }; return this.paymentProcessingResources.getAvailableOrBatchPayments( options, isAvailable, isIncluded ); } getAllPayments (paginationOptions: PaginationOptions) { paginationOptions = this.systemTagsService.formatPaginationOptions(paginationOptions); return this.paymentProcessingResources.getAllPayments( paginationOptions ); } async addBatch (data: AddBatchForApi) { const response = await this.paymentProcessingResources.addBatch(data); this.dataHubService.setBatchesList(undefined); return response; } async deleteBatch (batchId: number) { const response = await this.paymentProcessingResources.deleteBatch(batchId); this.dataHubService.setBatchesList(undefined); return response; } addPaymentsToBatchPaginated ( batchId: number, paginationOptions: PaginationOptions ) { paginationOptions = this.systemTagsService.formatPaginationOptions(paginationOptions); return this.paymentProcessingResources.addPaymentsToBatchPaginated( batchId, paginationOptions ); } async handleAddBatchModal ( data: AddBatchForApi, options: PaginationOptions ) { try { const { batchId } = await this.addBatch(data); await this.addPaymentsToBatchPaginated(batchId, options); this.notifier.success(this.i18n.translate( 'MANAGE:textSuccessfullyCreatedTheBatch', {}, 'Successfully created the batch' )); this.resetBatchesRepo(); return batchId; } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'MANAGE:textErrorCreatingBatch', {}, 'There was an error creating the batch' )); } } async handleUpdatePayments ( data: UpdatePaymentsApi ) { const single = data.paymentIds.length === 1; try { const response = await this.paymentProcessingResources.updatePayments( data, single ); if (response && response.automaticallyRouted) { this.applicationActionService.showAutomaticallyRoutedToaster(false); } this.notifier.success(this.i18n.translate( single ? 'MANAGE:textSuccessUpdatingPayment' : 'MANAGE:textSuccessUpdatingPayments', {}, `Successfully updated the payment${!single ? 's' : ''}` )); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( single ? 'MANAGE:textErrorUpdatingPayment' : 'MANAGE:textErrorUpdatingPayments', {}, `There was an error updating the payment${!single ? 's' : ''}` )); } } async handleHoldModal ( paymentId: number, toHold: boolean, notes: string ) { try { await this.paymentProcessingResources.updatePaymentHold( paymentId, toHold, notes ); await this.resetPaymentStats(); this.notifier.success(this.i18n.translate( toHold ? 'MANAGE:textSuccesHoldingPayment' : 'MANAGE:textSuccesUnHoldingPayment', {}, `Successfully ${ toHold ? 'placed' : 'removed' } the ${ toHold ? 'payment on hold' : 'the hold on the payment' }` )); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( toHold ? 'MANAGE:textErrorHoldingPayment' : 'MANAGE:textErrorUnHoldingPayment', {}, `There was an error ${ toHold ? 'placing the payment on hold' : 'removing the hold on the payment' }` )); } } async handleBatchUpdateModal ( response: EditBatchModalResponse, id: number ) { try { await this.paymentProcessingResources.updateBatch({ ...response, batchId: id }); this.notifier.success(this.i18n.translate( 'MANAGE:textSuccessUpdateBatch', {}, 'Successfully updated the batch' )); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'MANAGE:textErrorUpdatingBatch', {}, 'There was an error updating the batch' )); } } async handleSpecialHandlingModal (data: UpdatePaymentSpecialHandling) { try { await this.paymentProcessingResources.updateSpecialHandling(data); this.notifier.success(this.i18n.translate( 'MANAGE:textSuccessAlternateAddressRequest', {}, 'Successfully updated alternate address on the payment' )); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'MANAGE:textErrorAddingAlternateAddressRequest', {}, 'There was an updating alternate address on the payment' )); } } async handleExpediteModal (data: ExpeditePaymentForApi) { try { const { batchId } = await this.paymentProcessingResources.expeditePayment(data); // TODO: see if we need to add markAllPaymentsCleared to expedite await this.paymentProcessingResources.processBatch( batchId, data.batchNotes, null, null, null ); this.notifier.success(this.i18n.translate( 'MANAGE:textSuccessExpediteBatch', {}, 'Successfully expedited the batch' )); return batchId; } catch (err) { const e = err as HttpErrorResponse; this.logger.error(e); if (e.error && e.error.message === 'Client does not have a valid affiliate id.') { this.missingValidAffiliateId(); } else { this.notifier.error(this.i18n.translate( 'MANAGE:textErrorExpeditingBatch', {}, 'There was an error expediting the batch' )); } } } async updateBatchStatusModal ( item: BatchItem, notes: string, status: BatchStatuses ) { try { const paymentStatus = status === BatchStatuses.Processing ? PaymentStatus.Processing : undefined; await this.paymentProcessingResources.updateBatchStatus( item.id, status, notes, paymentStatus ); this.notifier.success(this.i18n.translate( 'MANAGE:textSuccessChangingStatus', {}, 'Successfully updated the status of the batch' )); this.resetBatchesRepo(); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'MANAGE:textErrorUpdatingStatus', {}, 'There was an error updating the status of the batch' )); } } async handleDeleteBatch (id: number) { try { await this.deleteBatch(id); this.notifier.success(this.i18n.translate( 'MANAGE:textSuccessfullyDeletedBatch', {}, 'Successfully deleted the batch' )); this.resetBatchesRepo(); await this.resetPaymentStats(); this.resetAllPaymentsRepo(); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'MANAGE:textErrorDeletingTheBatch', {}, 'There was an error deleting the batch' )); } } async downloadPaymentBatchReport ( batchId: number, batchName: string ) { try { const link = await this.paymentProcessingResources.downloadPaymentBatchReport(batchId); if (link) { await this.fileService.downloadUrlAs(link, batchName); } else { this.notifier.error( this.i18n.translate( 'MANAGE:textErrorDownloadingBatchReport', {}, 'There was an error fetching the batch report' ) ); } } catch (e) { this.logger.error(e); this.notifier.error( this.i18n.translate( 'MANAGE:textErrorDownloadingBatchReport', {}, 'There was an error fetching the batch report' ) ); } } async handleSendBatchToProcessing ( batchId: number, notes: string, status: PaymentStatus, issuedDate: moment.Moment ) { try { await this.paymentProcessingResources.processBatch( batchId, notes, false, status, issuedDate?.toISOString() ); this.notifier.success(this.i18n.translate( 'MANAGE:textSuccessSendBatchToProcessing', {}, 'Successfully sent the batch to processing' )); this.resetBatchRepos(batchId); this.resetAllPaymentsRepo(); this.resetBatchesRepo(); } catch (err) { const e = err as HttpErrorResponse; this.logger.error(e); if (e.error && e.error.message === 'Client does not have a valid affiliate id.') { this.missingValidAffiliateId(); } else { this.notifier.error(this.i18n.translate( 'MANAGE:textErrorSendingBatchToProcessing', {}, 'There was an error sending the batch to processing' )); } } } async handleSendAvailableToProcessing ( item: BatchItem, response: SendToProcessingModalResponse, useOptions = true ) { let id; try { if (useOptions) { const options = this.get('currentIncludedOptions'); const { batchId } = await this.addBatch({ name: response.name, notes: response.notes, processingTypeId: item.processorType }); id = batchId; await this.addPaymentsToBatchPaginated(id, options); } else { const { batchId } = await this.paymentProcessingResources.processAllAvailable({ batchName: response.name, batchNotes: response.notes, processingType: item.processorType }); id = batchId; } await this.paymentProcessingResources.processBatch( id, response.notes, false, response.paymentStatus, response.issueDate?.toISOString() ); this.notifier.success(this.i18n.translate( 'MANAGE:textSuccessSendBatchToProcessing', {}, 'Successfully sent the batch to processing' )); return id; } catch (err) { const e = err as HttpErrorResponse; this.logger.error(e); if (e.error && e.error.message === 'Client does not have a valid affiliate id.') { this.missingValidAffiliateId(); } else { this.notifier.error(this.i18n.translate( 'MANAGE:textErrorSendingBatchToProcessing', {}, 'There was an error sending the batch to processing' )); return id; } } } async getMissingPaymentIds ( ids: number[] ) { return this.paymentProcessingResources.validatePaymentIds(ids); } async handlePaymentImport (updates: PaymentUpdateImport[]) { try { await this.paymentProcessingResources.importApPayments(updates); this.notifier.success(this.i18n.translate( 'MANAGE:notificationSuccessUpdatePayments', {}, 'Successfully imported payment data' )); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'MANAGE:notificationFailedUpdatePayments', {}, 'Failed to import payment data' )); } } getDisbursementSummaryFactory () { return CallMakerFactory.create(async ({ startDate, endDate, programIds }) => { try { const result = await this.paymentProcessingResources.getDisbursementSummary( startDate.toISOString(), endDate.toISOString(), programIds ); return result; } catch (e) { this.logger.error(e); this.notifier.error( this.i18n.translate( 'MANAGE:notificationErrorGettingDisbursementSummary', {}, 'There was an error getting the disbursement summary' ) ); return null; } }); } getDisbursementReportFactory () { return CallMakerFactory.create( async ({ startDate, endDate, programIds }) => { try { const result = await this.paymentProcessingResources.getDisbursementRows( startDate.toISOString(), endDate.toISOString(), programIds ); return result.map(row => { return { ...row, $search: row.recipientFullName + '-' + row.checkNumber }; }); } catch (e) { this.logger.error(e); this.notifier.error( this.i18n.translate( 'MANAGE:notificationErrorGettingDisbursementSummary', {}, 'There was an error getting the disbursement summary' ) ); return null; } }, 0, false, true, false ); } downloadDisbursementReport ( records: DisbursementReportRow[], summary: PaymentSummaryResponse, format: TableDataDownloadFormat ) { const mappedRecords = records.map(record => { return { Type: this.translatePaymentType(record.paymentType), 'Check # / Payment #': record.checkNumber, Amount: record.totalAmount, Recipient: record.recipientFullName, Program: record.programName, Status: this.statusService.paymentStatusMap[record.paymentStatus]?.translated, 'Status Date': moment(record.statusDate).format('ll'), 'Prior Payment': this.getDollarAmountForReport(record.priorPaymentAmount), 'Current Payment': this.getDollarAmountForReport(record.currentPaymentAmount), Cleared: this.getDollarAmountForReport(record.clearedAmount * -1, true), Voided: this.getDollarAmountForReport(record.voidedAmount * -1, true), 'Current Outstanding': this.getDollarAmountForReport( record.currentOutstandingAmount ) }; }); const csv = this.fileService.convertObjectArrayToCSVString([ ...mappedRecords, { Type: '', 'Check # / Payment #': '', Amount: '', Recipient: '', Program: '', Status: '', 'Status Date': '', 'Prior Payment': this.getDollarAmountForReport(summary.outstandingPriorYear), 'Current Payment': this.getDollarAmountForReport( summary.newDisbursementsThisPeriod ), Cleared: this.getDollarAmountForReport(summary.clearedItems * -1, true), Voided: this.getDollarAmountForReport(summary.voidedItems * -1, true), 'Current Outstanding': this.getDollarAmountForReport( summary.outstandingEndOfPeriod ) } ]); return this.fileService.downloadByFormat(csv, format); } getDollarAmountForReport ( amount: number, isNegative = false ) { return amount ? (isNegative ? '-' : '') + this.moneyService.formatMoney( amount, this.clientSettingsService.defaultCurrency ) : ''; } async handleNotifyPayees ( response: NotifyPayeesModalResponse, paymentIds: number[] ) { try { await this.paymentProcessingResources.notifyPayeees(response, paymentIds); this.notifier.success(this.i18n.translate( 'MANAGE:textSuccessfullyNotifyPayeesOfStatus', {}, 'Successfully notified payees of status' )); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'MANAGE:textErrorNotifyingPayeesOfStatus', {}, 'There was an error notifying payees of status' )); } } getPaymentTypeFromImport (paymentType: string): PaymentType { switch (paymentType) { case 'check': return PaymentType.Check; case 'ach': return PaymentType.ACH; case 'wire': return PaymentType.WireTransfer; case 'other': return PaymentType.Other; default: return null; } } translatePaymentType (paymentType: PaymentType) { switch (paymentType) { case PaymentType.Check: return this.i18n.translate( 'GLOBAL:texCheck', {}, 'Check' ); case PaymentType.ACH: return this.i18n.translate( 'GLOBAL:textACH', {}, 'ACH' ); case PaymentType.WireTransfer: return this.i18n.translate( 'common:textWireTransfer', {}, 'Wire transfer' ); case PaymentType.Other: return this.i18n.translate( 'common:textOther', {}, 'Other' ); default: return ''; } } async handlePaymentIsEligibleForRecall (paymentId: number) { try { const eligiblity = await this.paymentProcessingResources.paymentIsEligibleForRecall(paymentId); return eligiblity; } catch(e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'common:PaymentEligibilityError', {}, 'Error finding payment recall eligibility' )); return false; } } async handleRecallPayment (payload: RecallPaymentPayload) { try { await this.paymentProcessingResources.recallPayment(payload); this.notifier.success(this.i18n.translate( 'common:recallPaymentSuccess', {}, 'The payment has been successfully removed from the batch' )); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'common:recallPaymentError', {}, 'Error recalling payment' )); } } canRecallPayment (payload: CanRecallPaymentPayload) { return payload.processingType === ProcessingTypes.YourCause && payload.batchStatus === BatchStatuses.Processing && payload.paymentStatus === PaymentStatus.Processing && payload.paymentInBatch; } } export const PaymentExistsInGC = createValidator(() => async (attr, { injector, group, context }) => { let call: Promise = context.call; if (!call) { const service = injector.get(PaymentProcessingService); call = context.call = service.getMissingPaymentIds(group.map((member: PaymentImportModel) => member['GC Payment ID'])); } const invalidIds = await call; return invalidIds.includes(+attr) ? { i18nKey: 'GLOBAL:textPaymentMustExistInGrantsConnect', defaultValue: 'Payment ID must exist in GrantsConnect' } : []; }); export const IsValidPaymentType = createValidator( () => (_, { ent }) => { const type = ent['Payment Type']; if (type) { const validTypes = ['check', 'ach', 'wire', 'other']; if (!validTypes.includes(type)) { return { i18nKey: 'common:textPleaseEnterValidPaymentType', defaultValue: 'Please enter a valid payment type. \"Check\", \"ACH\", \"Wire\", or \"Other\"' }; } } return []; } ); export class PaymentImportModel { @IsString() @Transform((val: string) => { return val ? val.toLowerCase() : val; }) @IsValidPaymentType() 'Payment Type': string; @IsNumber() @Required() 'Payment Number': number; @IsDate() @Required() 'Payment Date': string; @IsOneOf([ 'Outstanding', 'Cleared', 'Voided' ]) @Required() @Transform((val: string) => { if (val) { const lcVal = val.toLowerCase(); return lcVal[0].toUpperCase() + lcVal.slice(1); } return val; }) 'Payment Status': 'Outstanding' | 'Cleared' | 'Voided'; @IsDate() 'Status Date': string; @IsAlphaNumeric() 'AP System Payment ID': string; @Unique() @IsNumber() @Required() @PaymentExistsInGC() 'GC Payment ID': number; }