import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { SpecialHandlingModalComponent } from '@core/components/special-handling-modal/special-handling-modal.component'; import { SpecialHandlingService } from '@core/services/special-handling.service'; import { SpinnerService } from '@core/services/spinner.service'; import { StatusService } from '@core/services/status.service'; import { RouterLinkAttr } from '@core/typings/applicant.typing'; import { FundingSourceTypes } from '@core/typings/budget.typing'; import { ApplicantInfo, ExpeditePaymentForApi, OrganizationInfo, PaymentForProcess, ProcessingTypes } from '@core/typings/payment.typing'; import { BatchStatuses, PaymentStatus } from '@core/typings/status.typing'; import { ApplicantManagerService } from '@features/applicant/applicant-manager.service'; import { AwardResources } from '@features/awards/award.resources'; import { BudgetService } from '@features/budgets/budget.service'; import { ClientSettingsService } from '@features/client-settings/client-settings.service'; import { CyclesService } from '@features/cycles/cycles.service'; import { NonprofitService } from '@features/nonprofit/nonprofit.service'; import { DesignationModalComponent } from '@features/payment-processing/designation-modal/designation-modal.component'; import { ExpeditePaymentModalComponent } from '@features/payment-processing/expedite-payment-modal/expedite-payment-modal.component'; import { HoldPaymentModalComponent } from '@features/payment-processing/hold-payment-modal/hold-payment-modal.component'; import { PaymentDetailsModalComponent } from '@features/payment-processing/payment-details-modal/payment-details-modal.component'; import { ProgramService } from '@features/programs/program.service'; import { SystemTagsService } from '@features/system-tags/system-tags.service'; import { SystemTags } from '@features/system-tags/typings/system-tags.typing'; import { WorkflowService } from '@features/workflow/workflow.service'; import { AddressFormatterService, ArrayHelpersService, AutoTableRepositoryFactory, DebounceFactory, PaginationOptions, SimpleUpdateModalComponent, TableDataFactory, TextFriendlySpecialCharCleaner, TopLevelFilter, TypeaheadSelectOption } from '@yourcause/common'; import { I18nService } from '@yourcause/common/i18n'; import { ModalFactory } from '@yourcause/common/modals'; import { uniqBy } from 'lodash'; import { from as observableFrom, map } from 'rxjs'; import { PaymentProcessingService } from '../payment-processing.service'; @Component({ selector: 'gc-payments-page', templateUrl: './payments-page.component.html', styleUrls: ['./payments-page.component.scss'] }) export class PaymentsPageComponent implements OnInit { tableDataFactory: TableDataFactory;; fromRoute: RouterLinkAttr; paymentStatusMap = this.statusService.paymentStatusMap; topLevelFilters: TopLevelFilter[] = []; PaymentStatuses = PaymentStatus; BatchStatuses = BatchStatuses; ProcessingTypes = ProcessingTypes; clientSettings = this.clientSettingsService.get('clientSettings'); programOptions: TypeaheadSelectOption[] = []; tagOptions = uniqBy( this.systemTagsService.getTagsForBucket(SystemTags.Buckets.Payment, true) .concat(this.systemTagsService.getTagsForBucket(SystemTags.Buckets.Application, true)) .concat(this.systemTagsService.getTagsForBucket(SystemTags.Buckets.NonprofitProfile, true)), 'id' ).map(tag => { return { label: tag.name, value: tag.id }; } ); budgetOptions = this.budgetService.cashBudgetOptions; sourceOptions = this.budgetService.cashSourceOptions; paymentStatusOptions: TypeaheadSelectOption[] = []; cycleOptions = this.cyclesService.allMyNonFutureCycleSelectOptions; workflowOptions = this.workflowService.myWorkflowOptions; workflowLevelOptions = this.workflowService.myWorkflowLevelOptions; showSpecialHandling = this.clientSettings.specialHandling; ProcessorTypes = ProcessingTypes; FundingSourceTypes = FundingSourceTypes; constructor ( private systemTagsService: SystemTagsService, private paymentProcessingService: PaymentProcessingService, private statusService: StatusService, private i18n: I18nService, private modalFactory: ModalFactory, private addressFormatter: AddressFormatterService, private clientSettingsService: ClientSettingsService, private awardResources: AwardResources, private spinnerService: SpinnerService, private arrayHelper: ArrayHelpersService, private autoTableFactory: AutoTableRepositoryFactory, private router: Router, private budgetService: BudgetService, private applicantManagerService: ApplicantManagerService, private nonprofitService: NonprofitService, private programService: ProgramService, private cyclesService: CyclesService, private workflowService: WorkflowService, private specialHandlingService: SpecialHandlingService ) { } get applicantRouterLink () { return this.applicantManagerService.get('applicantProfileRouterLink'); } get nonprofitRouterLink () { return this.nonprofitService.get('nonprofitProfileRouterLink'); } ngOnInit () { const statusOptions = this.statusService.paymentStatusOptions; this.paymentStatusOptions = statusOptions.filter((option) => { return ![ PaymentStatus.Fulfilled ].includes(option.value); }); this.fromRoute = this.applicantManagerService.getFromRouteForProfile(); this.topLevelFilters = [ new TopLevelFilter( 'text', 'applicantInfo.fullName', '', this.i18n.translate( 'MANAGE:textSearchByApplicantOrgNameAppID', {}, 'Search by applicant, organization name, or application ID' ), undefined, undefined, [{ column: 'applicantInfo.fullName', filterType: 'cn' }, { column: 'organizationInfo.name', filterType: 'cn' }, { column: 'applicationId', filterType: 'eq' }] ), new TopLevelFilter( 'dateRange', 'scheduledDate', '', this.i18n.translate( 'GLOBAL:textAvailableToProcess', {}, 'Available to process' ) ) ]; this.tableDataFactory = DebounceFactory.createSimple( (options: PaginationOptions) => { return observableFrom( this.paymentProcessingService.getAllPayments( options ) ).pipe(map(result => { if (result.programFacets) { this.programOptions = this.arrayHelper.sort(Object.keys(result.programFacets) .map(id => { const translationMap = this.programService.programTranslationMap[id]; return { value: +id, label: translationMap && translationMap.Name ? translationMap.Name : result.programFacets[id] }; }), 'label'); } return result; }), map(result => { result.records.forEach((record: PaymentForProcess) => { const translationMap = this.programService.programTranslationMap[record.programId]; record.programName = translationMap && translationMap.Name ? translationMap.Name : record.programName; this.setNonprofitAndApplicantRouterLinkMap(record); }); return { success: true, data: { recordCount: result.recordCount, records: result.records } }; })); } ); } setNonprofitAndApplicantRouterLinkMap (row: PaymentForProcess) { if (row.insightsInfo) { this.applicantManagerService.setApplicantRouterLinkMap( row.insightsInfo.applicantId ); this.nonprofitService.setNonprofitRouterLinkMap( row.insightsInfo.organizationGuid || row.insightsInfo.organizationId ); } return row; } formatAddress (info: ApplicantInfo|OrganizationInfo) { return this.addressFormatter.format({ address1: info.address, address2: info.address2, city: info.city, postalCode: info.postalCode, countryCode: info.country, stateProvRegCode: info.state, stateProvRegName: info.state }, true); } async paymentDetails (payment: PaymentForProcess) { const proceed = await this.modalFactory.open( PaymentDetailsModalComponent, { payment, batchStatus: payment.batchInfo?.batchStatusId } ); if (proceed) { this.resetRepo(); } } async expedite (row: PaymentForProcess) { const response = await this.modalFactory.open( ExpeditePaymentModalComponent, { paymentId: row.paymentId } ); if (response) { this.spinnerService.startSpinner(); const data: ExpeditePaymentForApi = { batchName: response.name, batchNotes: response.notes, paymentId: row.paymentId, batchProcessingTypeId: ProcessingTypes.YourCause }; const batchId = await this.paymentProcessingService.handleExpediteModal( data ); this.resetRepo(); this.spinnerService.stopSpinner(); if (batchId) { this.router.navigate([ `/management/payment-processing/manage-batch/${batchId}/included` ]); } } } async designation (row: PaymentForProcess) { const response = await this.modalFactory.open( DesignationModalComponent, { paymentId: row.paymentId, applicationId: row.applicationId, paymentDesignation: TextFriendlySpecialCharCleaner(row.paymentDesignation) } ); if (response) { this.spinnerService.startSpinner(); await this.awardResources.adjustPayment( row.applicationId, row.awardId, row.paymentId, { scheduledDate: row.scheduledDate, value: row.totalAmount, notes: row.notes, paymentDesignation: TextFriendlySpecialCharCleaner(response.paymentDesignation), currencyRequested: row.currencyRequested, valueEquivalent: row.totalAmountEquivalent, currencyExchangeRate: row.currencyExchangeRate, differentThanConversion: row.differentThanConversion, inKindPaymentItemsRequested: row.inKindItems, careOf: row.careOf, budgetId: row.budgetId, fundingSourceId: row.fundingSourceId } ); this.resetRepo(); this.spinnerService.stopSpinner(); } } async attention (row: PaymentForProcess) { const attentionText = this.i18n.translate( 'FORMS:textAttention', {}, 'Attention' ); const paymentIDText = this.i18n.translate( 'MANAGE:hdrPaymentIdDynamic', { paymentId: row.paymentId }, 'Payment ID: __paymentId__' ); const attentionDescriptionText = this.i18n.translate( 'FORMS:textAttentionDescription', {}, 'Attention allows payments to go to a specific person or department at the above address' ); const modalResponse = await this.modalFactory.open(SimpleUpdateModalComponent, { modalHeader: attentionText, modalSubHeader: paymentIDText, existingValue: row.careOf, inputLabel: attentionText, inputDescription: attentionDescriptionText }); if (modalResponse) { this.spinnerService.startSpinner(); await this.awardResources.adjustPayment( row.applicationId, row.awardId, row.paymentId, { scheduledDate: row.scheduledDate, value: row.totalAmount, notes: row.notes, paymentDesignation: TextFriendlySpecialCharCleaner(row.paymentDesignation), currencyRequested: row.currencyRequested, valueEquivalent: row.totalAmountEquivalent, currencyExchangeRate: row.currencyExchangeRate, differentThanConversion: row.differentThanConversion, inKindPaymentItemsRequested: row.inKindItems, careOf: modalResponse.payload, budgetId: row.budgetId, fundingSourceId: row.fundingSourceId } ); this.resetRepo(); this.spinnerService.stopSpinner(); } } async hold (row: PaymentForProcess) { const response = await this.modalFactory.open( HoldPaymentModalComponent, { paymentId: row.paymentId, toHold: !row.onHold, isAllPayments: false } ); if (response) { this.spinnerService.startSpinner(); const toHold = !row.onHold; await this.paymentProcessingService.handleHoldModal( row.paymentId, toHold, response.notes ); this.resetRepo(); this.spinnerService.stopSpinner(); } } resetRepo () { const repo = this.autoTableFactory.getRepository('ALL_PAYMENTS'); if (repo) { repo.reset(); } } async specialHandling (row: PaymentForProcess) { this.spinnerService.startSpinner(); const { defaultAddress } = await this.specialHandlingService.getDefaultSpecialHandling( row.organizationInfo?.organizationId, row.organizationInfo?.nonprofitGuid, row.organizationInfo?.name, row.applicantInfo?.applicantId, row.applicantInfo?.firstName, row.applicantInfo?.lastName, row.applicantInfo?.email, row.applicantInfo?.phoneNumber ); this.spinnerService.stopSpinner(); const response = await this.modalFactory.open( SpecialHandlingModalComponent, { paymentId: row.paymentId, payeeOverride: row.payeeOverrideInfo, name: defaultAddress.name || row.organizationInfo?.name || row.applicantInfo?.fullName, defaultAddress, applicationId: row.applicationId, isClientProcessed: row.paymentProcessingType === ProcessingTypes.Client } ); if (response) { this.spinnerService.startSpinner(); await this.paymentProcessingService.handleSpecialHandlingModal( response ); this.resetRepo(); this.spinnerService.stopSpinner(); } } }