import { Component, Input, OnInit } from '@angular/core'; import { AbstractControl, Validators } from '@angular/forms'; import { CurrencyService } from '@core/services/currency.service'; import { SpecialHandlingService } from '@core/services/special-handling.service'; import { SpinnerService } from '@core/services/spinner.service'; import { TimeZoneService } from '@core/services/time-zone.service'; import { Budget, BudgetFundingSourceCombo, FundingSourceTypes, RemainingAmountBudgetMap } from '@core/typings/budget.typing'; import { Collaborator } from '@core/typings/collaboration.typing'; import { OrganizationForInfoPanel } from '@core/typings/organization.typing'; import { Payment, PaymentDisplayInfo, ProcessingTypes } from '@core/typings/payment.typing'; import { PaymentStatus } from '@core/typings/status.typing'; import { BudgetAssignmentsService } from '@features/budget-assignments/budget-assignments.service'; import { ApplicationBudgetInfo } from '@features/budget-assignments/budget-assignments.typing'; import { BudgetService } from '@features/budgets/budget.service'; import { ClientSettingsService } from '@features/client-settings/client-settings.service'; import { SpecialHandling } from '@features/formio/formio-components/standard-formio-components/gc-special-handling/gc-special-handling.component'; import { InKindService } from '@features/in-kind/in-kind.service'; import { InKindAwardedItemApi, InKindItemToAwardOrPay, InKindRequestedItem } from '@features/in-kind/in-kind.typing'; import { EmailService } from '@features/system-emails/email.service'; import { EmailNotificationType } from '@features/system-emails/email.typing'; import { SystemTagsService } from '@features/system-tags/system-tags.service'; import { SystemTags } from '@features/system-tags/typings/system-tags.typing'; import { EmailValidator, OrganizationEligibleForGivingStatus, SimpleStringMap, Tab, TextFriendlySpecialCharCleaner, TypeaheadSelectOption, TypeSafeFormBuilder, TypeSafeFormGroup, TypeToken } from '@yourcause/common'; import { AnalyticsService, EventType } from '@yourcause/common/analytics'; import { I18nService } from '@yourcause/common/i18n'; import { ExchangeRate } from '@yourcause/common/masking'; import { YCModalComponent } from '@yourcause/common/modals'; import moment from 'moment'; import { AwardControlGroup } from '../award-amount-control/award-amount-control.component'; import { AwardService } from '../award.service'; import { AwardModalResolver } from '../resolvers/award-modal.resolver'; import { Award, AwardModalResponse } from '../typings/award.typing'; interface AwardFormGroup { awardAmount: string; amountRequested: string; scheduledPayments: number|string; awardBalance: number|string; description: string; notifyApplicant: boolean; awardDate: string; clientEmailTemplateId: number; customMessage: string; cc: string[]; bcc: string[]; attachments: any[]; } interface PaymentFormGroup { budgetIdFundingSource: BudgetFundingSourceCombo; date: string; amount: string; conversion: string; id: number; paymentDesignation: string; notes: string; } interface TagsFormGroup { tags: number[]; newTagName: string; } @Component({ selector: 'gc-simple-award-modal', templateUrl: './simple-award-modal.component.html', styleUrls: ['./simple-award-modal.component.scss'] }) export class SimpleAwardModalComponent extends YCModalComponent implements OnInit { @Input() awardOnly = false; @Input() addPaymentOnly = false; @Input() currencyRequested = this.clientSettingsService.defaultCurrency; @Input() amountRequested: number; @Input() recommendedFundingAmount: number; @Input() requestedItems: InKindRequestedItem[] = []; @Input() currencyRequestedAmountEquivalent: number; @Input() originalAward: Award; @Input() originalPayment: Payment; @Input() existingAwardTypes: FundingSourceTypes[]; @Input() applicantCanReceiveEmails: boolean; @Input() appDesignation: string; @Input() programId: number; @Input() cycleId: number; @Input() applicationId: number; @Input() assignedBudgetId: number; @Input() assignedFsId: number; @Input() reservedFunds = false; @Input() specialHandling: SpecialHandling; @Input() organizationEligibleForGivingStatus: OrganizationEligibleForGivingStatus; @Input() organization: OrganizationForInfoPanel; @Input() applicant: Collaborator; @Input() isMasked = false; FundingSourceTypes = FundingSourceTypes; modalHeader = ''; cashString = this.i18n.translate('GLOBAL:textCash'); inKindString = this.i18n.translate('GLOBAL:textInKind'); PaymentStatuses = PaymentStatus; award: Award; payment: Payment; originalPayments: Payment[]; originalPaymentsSumInDefault = 0; // if edit, current payment filtered out originalPaymentsSumInRequested = 0; // ^ awardType: FundingSourceTypes; AwardTypes = FundingSourceTypes; isAwardTypeView = false; awardTypeOptions = this.awardService.awardTypeOptions; awardTypeFormGroup: TypeSafeFormGroup<{ awardType: FundingSourceTypes }>; awardFormGroup: TypeSafeFormGroup; paymentFormGroup: TypeSafeFormGroup; unitsFormGroup = this.formBuilder.group({}); tagsFormGroup: TypeSafeFormGroup; paymentDisplayInfo: PaymentDisplayInfo; potentialBudgets: Budget[] = []; finalBudgetList: Budget[]; budgetFundingSourceOptions: TypeaheadSelectOption[] = []; budgetFundingSourceOptionsForNewPayment: TypeaheadSelectOption[] = []; availableTags: TypeaheadSelectOption[] = []; tagType = SystemTags.Buckets.Payment; hasInternational = this.clientSettingsService.clientSettings.hasInternational; formattingData = this.currencyService.formattingData; defaultCurrency = this.clientSettingsService.defaultCurrency; hasOverage = this.clientSettingsService.clientSettings.allowBudgetOverages; defaultSymbol = this.formattingData[this.defaultCurrency].symbol; currencyRequestedSymbol: string; showConversions = false; exchangeRate: ExchangeRate; exchangeDate = moment().toString(); conversionMessage: string; currentEmailActive: boolean; negativeBalance = false; remainingAmountBudgetMap: RemainingAmountBudgetMap = {}; conversionText = this.i18n.translate( 'GLOBAL:textCurrencyConversionDynamic', { currency: this.defaultCurrency }, '__currency__ conversion' ); amountRequestedHelpText: string; awardBalanceHelpText: string; scheduledHelpText: string; amountRequestedBaseHelpText = this.i18n.translate( 'APPLY:textAmountApplicantIsRequesting', {}, 'The amount the applicant is requesting.' ); awardBalanceBaseHelpText = this.i18n.translate( 'AWARDS:textAwardBalanceHelp', {}, 'Remaining balance to award' ); scheduledPaymentsBaseHelpText = this.i18n.translate( 'AWARDS:textScheduledPaymentsHelp', {}, 'Sum of all payments against this award' ); paymentTabs: Tab[] = []; isPaymentView = true; showNotesAndTags = false; itemsToAward: InKindItemToAwardOrPay[] = []; itemsToPay: InKindItemToAwardOrPay[] = []; potentialItemsToPay: InKindItemToAwardOrPay[] = []; alreadyPaidItems: InKindAwardedItemApi[] = []; alreadyAwardedItems: InKindAwardedItemApi[] = []; editingAwardItems = false; editingPaidItems = false; awardAmount: number; paymentAmount: number; loaded = false; canMoveFundsText = ''; inactiveRequestedItems = ''; appReservedInfo: ApplicationBudgetInfo; createPaymentAlertHelper: string; hasSpecialHandling = false; recommendedText: string; addingNewTag = false; originalBudgetOrFsIsClosed = false; $awardGroup = new TypeToken>(); constructor ( private formBuilder: TypeSafeFormBuilder, private i18n: I18nService, private clientSettingsService: ClientSettingsService, private currencyService: CurrencyService, private spinnerService: SpinnerService, private emailService: EmailService, private budgetService: BudgetService, private tagsService: SystemTagsService, private awardService: AwardService, private inKindService: InKindService, private timeZoneService: TimeZoneService, private budgetAssignmentService: BudgetAssignmentsService, private specialHandlingService: SpecialHandlingService, private awardModalResolver: AwardModalResolver, private analyticsService: AnalyticsService ) { super(); } get isUnits () { return this.awardType === FundingSourceTypes.UNITS; } get currentEmailType () { return this.isUnits ? EmailNotificationType.ApplicationAwardedInKind : EmailNotificationType.ApplicationAwarded; } get rate () { return this.exchangeRate ? this.exchangeRate.rate : 1; } get precisionMap () { return this.currencyService.get('precisionMap'); } get onlyPaymentAddOrEdit () { return this.originalPayment || this.addPaymentOnly; } get allItemsMap () { return this.inKindService.allItemsMap; } get modalInvalid () { return (this.awardFormGroup && this.awardFormGroup.invalid) || (this.paymentFormGroup && this.paymentFormGroup.invalid) || this.editingAwardItems || this.editingPaidItems || this.negativeBalance || this.addingNewTag; } get budgetFundingSource (): BudgetFundingSourceCombo { return this.paymentFormGroup?.value?.budgetIdFundingSource ?? null; } get isYcProcessed () { return this.budgetFundingSource?.fundingSource?.processingTypeId === ProcessingTypes.YourCause; } get unallocatedSourceMap () { return this.budgetService.unallocatedSourceMap; } get budgetFsDisabled () { if (!!this.payment?.id) { return this.payment.statusId !== PaymentStatus.Pending || (this.isOriginalBudgetFsSelected && this.originalBudgetOrFsIsClosed); } return false; } get isUpdatingBudget () { if (!!this.payment?.id) { return this.budgetFundingSource?.budget.id !== this.originalPayment?.budget || this.budgetFundingSource?.fundingSource.fundingSourceId !== this.originalPayment?.fundingSource; } return false; } get isUpdatingBudgetFromClientToYc () { if (!!this.payment?.id) { const oldProcessorIsClient = this.originalPayment?.processor === ProcessingTypes.Client; const newProcessorIsYc = this.budgetFundingSource?.fundingSource?.processingTypeId === ProcessingTypes.YourCause; return this.isUpdatingBudget && oldProcessorIsClient && newProcessorIsYc; } return false; } get isOriginalBudgetFsSelected () { if (!!this.payment?.id) { return this.budgetFundingSource?.budget.id === this.originalPayment?.budget && this.budgetFundingSource?.fundingSource.fundingSourceId === this.originalPayment?.fundingSource; } return false; } async ngOnInit () { this.spinnerService.startSpinner(); const { availableTags } = await this.awardModalResolver.resolve(); this.availableTags = availableTags; this.hasSpecialHandling = this.specialHandlingService.hasSpecialHandling( this.specialHandling ); this.currencyRequested = this.currencyRequested || this.defaultCurrency; this.award = this.originalAward ? { ...this.originalAward } : null; this.payment = this.originalPayment ? { ...this.originalPayment } : null; this.originalPayments = this.award ? this.award.payments.map((p) => ({ ...p })) : []; this.originalPayments.filter((payment) => { return (payment.statusId !== PaymentStatus.Voided) && (!this.payment || (payment.id !== this.payment.id)); }).forEach((payment) => { this.originalPaymentsSumInDefault = this.originalPaymentsSumInDefault + +payment.amount; this.originalPaymentsSumInRequested = this.originalPaymentsSumInRequested + +payment.currencyRequestedAmountEquivalent; }); if (this.originalPayment) { this.showNotesAndTags = true; } await Promise.all([ this.getTagsForPayments(), this.getBudgetsAndOptions() ]); this.setModalHeader(); this.loaded = true; this.spinnerService.stopSpinner(); } async setCurrentEmailActive () { this.currentEmailActive = await this.emailService.isProgramEmailActive( this.currentEmailType, this.programId ); } setModalHeader () { if (this.awardOnly && this.originalAward) { this.modalHeader = this.i18n.translate('AWARDS:hdrEditAward'); } else if (this.originalPayment) { this.modalHeader = this.i18n.translate('AWARDS:hdrEditPayment'); } else if (this.addPaymentOnly) { this.modalHeader = this.i18n.translate('AWARDS:hdrAddNewPayment'); } else if (this.payment) { this.modalHeader = this.i18n.translate( 'AWARDS:hdrCreateAwardAndPayment', {}, 'Create Award and Payment' ); } else { this.modalHeader = this.i18n.translate('AWARDS:hdrCreateAward'); } } async getTagsForPayments () { if (this.payment && !this.payment.tags) { await this.tagsService.fetchTagsForRecord( SystemTags.Buckets.Payment, this.payment.id ); this.payment.tags = this.tagsService.getTagsForRecord( SystemTags.Buckets.Payment, this.payment.id ); } } async getBudgetsAndOptions () { const { allowCash, allowUnits, filteredBudgets } = await this.budgetService.getBudgetsFilteredByProgramAndProcessor( this.programId, this.organizationEligibleForGivingStatus === OrganizationEligibleForGivingStatus.ELIGIBLE, this.cycleId, this.originalPayment?.budget ); this.potentialBudgets = filteredBudgets; await this.setAwardType(allowCash, allowUnits); this.setFinalBudgetListAndOptions(); if (!this.isAwardTypeView) { await this.setCurrentEmailActive(); if (this.isUnits) { this.setInKindItemsFields(); } await this.setAwardFormGroup(); } } setInKindItemsFields () { this.amountRequested = this.requestedItems.reduce((acc, item) => { const map = this.allItemsMap[item.itemIdentification]; return acc + ((+item.count || 0) * +map.value); }, 0); this.currencyRequestedAmountEquivalent = this.amountRequested; this.currencyRequested = this.defaultCurrency; if (this.originalAward) { this.itemsToAward = this.getItemsToAwardOrPay( this.originalAward.inKindItems, true ); } else { this.itemsToAward = this.getItemsToAwardOrPay(this.requestedItems); this.inactiveRequestedItems = this.inKindService.getInactiveRequestedItemsString( this.requestedItems ); } this.setAlreadyPaidItems(); if (!this.awardOnly) { if (this.originalPayment) { this.itemsToPay = this.getItemsToAwardOrPay( this.originalPayment.inKindItems, true ); } else { this.itemsToPay = [ ...this.potentialItemsToPay ]; } } } getItemsToAwardOrPay ( items: (InKindAwardedItemApi|InKindRequestedItem)[], isOriginalAwardOrPayment = false ) { return items.map((item) => { const identification = (item as InKindAwardedItemApi).itemIdentification || (item as InKindRequestedItem).itemIdentification; const map = this.allItemsMap[identification]; return { identification, name: map.name, value: (item as InKindAwardedItemApi).value || map.value, unitsRemaining: map.unitsRemaining, categoryId: map.categoryId, unitsEntered: map.inUse || isOriginalAwardOrPayment ? item.count : 0, inUse: map.inUse }; }).filter((item) => { return item.inUse || isOriginalAwardOrPayment; }); } setFinalBudgetListAndOptions () { this.finalBudgetList = this.potentialBudgets.filter((budget) => { return this.isUnits ? !!(budget.fundingSourceType === FundingSourceTypes.UNITS) : !!(budget.fundingSourceType === FundingSourceTypes.DOLLARS); }); this.budgetFundingSourceOptions = this.budgetService.getBudgetFundingSourceComboOptions( this.finalBudgetList, [this.organizationEligibleForGivingStatus], this.originalPayment?.budget, this.originalPayment?.fundingSource ); this.budgetFundingSourceOptionsForNewPayment = this.budgetService.getBudgetFundingSourceComboOptions( this.finalBudgetList, [this.organizationEligibleForGivingStatus] ); } async setAwardType (allowCash: boolean, allowUnits: boolean) { if (this.originalAward) { this.awardType = this.originalAward.awardType; } else { const response = this.awardService.getAwardType( allowCash, allowUnits, this.originalAward, this.existingAwardTypes ); this.awardType = response.awardType; this.isAwardTypeView = response.isAwardTypeView; if (this.isAwardTypeView) { this.awardTypeFormGroup = this.formBuilder.group<{ awardType: FundingSourceTypes }>({ awardType: FundingSourceTypes.DOLLARS }); } } if (!this.isUnits && this.reservedFunds) { const amountReserved = await this.budgetAssignmentService.getReservedAmountForApplication( this.applicationId, this.amountRequested ); this.appReservedInfo = { budgetId: this.assignedBudgetId, fundingSourceId: this.assignedFsId, reservedFunds: this.reservedFunds, amountReserved }; } else { this.appReservedInfo = undefined; } } setAwardOrPaymentAmount (amount: number, type: 'awardAmount'|'paymentAmount') { this[type] = amount; } async setAwardFormGroup () { this.showConversions = !this.isUnits && this.currencyRequested && (this.currencyRequested !== this.defaultCurrency); this.currencyRequestedSymbol = this.formattingData[this.currencyRequested].symbol; if (this.showConversions) { if (!this.originalAward || !this.originalAward.exchangeRate) { await this.setCurrentExchangeRate(); } else { this.exchangeRate = { id: this.originalAward.exchangeRateId, base: this.defaultCurrency, code: this.currencyRequested, rate: this.originalAward.exchangeRate }; this.exchangeDate = this.originalAward.exchangeRateDateTime || moment().toString(); } this.setConversionMessage(); } let amount: string; let unformattedAmount: number; if (this.award) { unformattedAmount = this.showConversions ? this.award.currencyRequestedAmountEquivalent : this.award.amount; amount = this.getAwardAmountForFormGroup( unformattedAmount ); this.setAwardOrPaymentAmount(this.award.amount, 'awardAmount'); } else { unformattedAmount = this.showConversions ? this.currencyRequestedAmountEquivalent : this.amountRequested; if ( this.awardType === this.AwardTypes.DOLLARS && !!this.recommendedFundingAmount ) { unformattedAmount = this.showConversions ? this.recommendedFundingAmount / this.exchangeRate.rate : this.recommendedFundingAmount; this.recommendedText = this.i18n.translate( 'GLOBAL:textRecommended', {}, 'Recommended' ) + ': ' + this.currencyService.formatMoney( unformattedAmount, this.currencyRequested || this.defaultCurrency, true ); this.setAwardOrPaymentAmount(this.recommendedFundingAmount, 'awardAmount'); } else { this.setAwardOrPaymentAmount(this.amountRequested, 'awardAmount'); } amount = this.getAwardAmountForFormGroup( unformattedAmount ); } this.awardFormGroup = this.formBuilder.group({ awardAmount: [amount, this.awardMinimumValidator()], amountRequested: this.currencyRequestedAmountEquivalent ? this.currencyService.formatMoney( this.currencyRequestedAmountEquivalent, this.currencyRequested ) : this.i18n.translate('GLOBAL:textNA', {}, 'N/A'), scheduledPayments: 0, awardBalance: 0, description: this.award ? this.award.description : '', notifyApplicant: this.award ? false : this.applicantCanReceiveEmails, // this email is only valid on create (i.e. this.award shouldn't exist) awardDate: [ this.award && this.award.awardDate ? this.timeZoneService.returnDateForMgmtDisplay(this.award.awardDate, 0) : moment().format(), Validators.required ], clientEmailTemplateId: 0, customMessage: '', cc: [[], EmailValidator()], bcc: [[], EmailValidator()], attachments: [] }); if (!this.awardOnly) { await this.setUpPaymentFormGroup(); } else { this.calculateAwardBalance(); } this.setCurrencyHelpText(); } async setUpPaymentFormGroup () { if (!this.payment) { this.payment = await this.getDefaultPayment(); } this.setPaymentTabs(); const { conversion, amount } = this.getAmountsForPaymentControl(this.payment); const budgetFs = this.budgetFundingSourceOptions.find(option => { return option.value.budget.id === this.payment.budget && option.value.fundingSource.fundingSourceId === this.payment.fundingSource; }).value; if (budgetFs && !!this.originalPayment) { this.originalBudgetOrFsIsClosed = budgetFs.isClosed; } this.paymentFormGroup = this.formBuilder.group({ budgetIdFundingSource: [budgetFs, Validators.required], date: [ this.timeZoneService.returnDateForMgmtDisplay( this.payment.scheduledDate, 0 ), Validators.required ], amount: [amount, Validators.required], conversion, id: this.payment.id, paymentDesignation: [ TextFriendlySpecialCharCleaner( this.payment.paymentDesignation || this.appDesignation ), Validators.maxLength(300) ], notes: this.payment.notes }, { validator: [ this.sourceAvailableValidator(), this.paymentMinimumValidator() ] }); this.tagsFormGroup = this.formBuilder.group({ tags: [ this.payment.tags .map(tagId => this.availableTags.find(tag => tag.value === tagId)) .map((tag) => tag && tag.value) ], newTagName: '' }); this.setCreatePaymentAlertHelper(); this.setDisplayHelperMaps(); this.calculateAwardBalance(); } setPaymentTabs () { const sortedDates = this.originalPayments.sort((a, b) => { return new Date(a.createdDate).getTime() - new Date(b.createdDate).getTime(); }); const total = this.originalPayments.length + (!this.payment.id ? 1 : 0); let number = total; sortedDates.forEach((payment, idx) => { if (this.payment && (this.payment.id === payment.id)) { number = idx + 1; } }); this.paymentTabs = [{ label: this.i18n.translate( 'AWARDS:textPaymentNumberofTotal', { total, number }, 'Payment __number__ of __total__' ), active: true }, { label: this.i18n.translate( 'GLOBAL:textAdditionalOptions', {}, 'Additional options' ), active: false }]; } setDisplayHelperMaps () { this.setPaymentDisplayMap(); this.setRemainingAmountBudgetMap(); } setPaymentDisplayMap () { if (this.paymentFormGroup) { const paymentValue = this.paymentFormGroup.value; const standardReturn: Partial = { amountLabel: this.i18n.translate( 'AWARDS:lblPaymentAmount', {}, 'Payment amount' ), amountHelp: '', canEdit: false, showConversion: this.showConversions, currencyKey: this.currencyRequested, currencySymbol: this.currencyRequestedSymbol, isUnits: this.isUnits, editIcons: [{ icon: 'pencil', onIconClick: this.toggleEditConversion }], saveIcons: [{ icon: 'check', iconClass: 'text-success', onIconClick: this.saveConversion }, { icon: 'times', iconClass: 'text-danger', onIconClick: this.cancelConversion }] }; const budgetFundingSource = paymentValue.budgetIdFundingSource; const source = this.budgetService.getFundingSourceDetail( budgetFundingSource.fundingSource.fundingSourceId ); standardReturn.canEdit = this.budgetFundingSource && !this.budgetFundingSource.isClosed && (source.processingTypeId === ProcessingTypes.Client) && this.payment?.statusId === PaymentStatus.Pending; if (!standardReturn.canEdit) { standardReturn.isEditing = false; } this.paymentDisplayInfo = { ...this.paymentDisplayInfo, ...standardReturn }; } } setRemainingAmountBudgetMap () { const currentPaymentAmount = this.unmaskAndConvertFromRequestedToDefault( this.paymentFormGroup.value.amount ); this.remainingAmountBudgetMap = this.budgetService.getRemainingAmountBudgetMap( this.budgetFundingSourceOptions, this.budgetFundingSource, currentPaymentAmount, this.originalPayment ? this.originalPayment.amount : null, true, this.isUnits, false, !this.isUnits && this.appReservedInfo ? [this.appReservedInfo] : [], this.originalPayment?.budget, this.originalPayment?.fundingSource ); } setCurrencyHelpText () { const convertedString = this.i18n.translate( 'GLOBAL:textCurrencyConversionAtSubmissionDynamic', { currency: this.defaultCurrency, amount: this.currencyService.formatMoney(this.amountRequested) }, '__currency__ conversion: __amount__ at submission' ); this.amountRequestedHelpText = !this.showConversions ? this.amountRequestedBaseHelpText : `${this.amountRequestedBaseHelpText}
${convertedString}`; } setAwardHelpText () { if (this.showConversions) { const balance = this.getAwardBalanceInRequested(); const scheduled = this.getSumOfAllPayments(false); this.awardBalanceHelpText = `${this.awardBalanceBaseHelpText}
${ this.getConversionHelpText(balance * this.rate)}`; this.scheduledHelpText = `${this.scheduledPaymentsBaseHelpText}
${ this.getConversionHelpText(scheduled * this.rate)}`; } else { this.awardBalanceHelpText = this.awardBalanceBaseHelpText; this.scheduledHelpText = this.scheduledPaymentsBaseHelpText; } } setCreatePaymentAlertHelper () { const budgetFs = this.paymentFormGroup.value.budgetIdFundingSource; this.createPaymentAlertHelper = this.awardService.getCreatePaymentAlertHelper( budgetFs?.fundingSource?.processingTypeId === ProcessingTypes.YourCause, this.hasSpecialHandling ); } async setCurrentExchangeRate () { this.exchangeRate = await this.currencyService.getExchangeRate( this.currencyRequested, this.defaultCurrency ); this.exchangeDate = moment().toString(); } setConversionMessage () { this.conversionMessage = this.i18n.translate( 'AWARDS:textConversionRateHelpText', { date: moment(this.exchangeDate).format('ll') }, 'Estimated conversion rate based on standard rates on __date__. Your grantee will likely receive less than this award amount due to conversion rates and fees at the processing bank.' ); } getAmountsForPaymentControl (payment: Payment) { this.setAwardOrPaymentAmount(+payment.amount, 'paymentAmount'); const useEquivalent = !!(this.showConversions && payment.currencyRequestedAmountEquivalent); return this.awardService.getAmountsForPaymentControl( payment, useEquivalent, this.currencyRequested, this.isUnits, this.rate ); } async getDefaultPayment (): Promise { this.spinnerService.startSpinner(); this.spinnerService.stopSpinner(); const budgetFundingSource = await this.budgetService.getDefaultBudgetForNewPayment( this.budgetFundingSourceOptions, this.isUnits, this.programId, this.cycleId, this.assignedBudgetId, this.assignedFsId ); const budgetComboClosed = budgetFundingSource.isClosed; let amount: string|number = '0'; let currencyRequestedAmountEquivalent: string|number = '0'; if (!budgetComboClosed) { amount = this.getAwardBalanceInDefault(); amount = amount < 0 ? 0 : amount; currencyRequestedAmountEquivalent = this.getAwardBalanceInRequested(); } return { scheduledDate: moment().toString(), amount, paymentDesignation: TextFriendlySpecialCharCleaner(this.appDesignation), budget: budgetFundingSource.budget.id, fundingSource: budgetFundingSource.fundingSource.fundingSourceId, type: this.isUnits ? this.inKindString : this.cashString, typeId: budgetFundingSource ? budgetFundingSource.fundingSource.fundingSourceType : FundingSourceTypes.DOLLARS, status: this.i18n.translate('common:lblPending'), statusId: PaymentStatus.Pending, tags: [], currencyRequested: this.currencyRequested, currencyRequestedAmountEquivalent, differentThanConversion: false, substatus: null, reissuedForPaymentId: null, reissuedInPaymentId: null, organizationEligibleForGivingStatus: this.organizationEligibleForGivingStatus, batchName: '', paymentType: null, hidePaymentStatus: false, alternatePaymentStatusText: '' }; } updateEditablePaymentsToCurrentRate () { if (this.payment?.statusId === PaymentStatus.Pending) { const group = this.paymentFormGroup; const { conversion, amount } = this.getAmountsForPaymentControl(this.payment); group.get('amount').setValue(amount); group.get('conversion').setValue(conversion); } } toggleEditConversion = () => { const isEditing = !this.paymentDisplayInfo.isEditing; this.paymentDisplayInfo = { ...this.paymentDisplayInfo, isEditing }; }; saveConversion = () => { this.toggleEditConversion(); this.updateDifferentThanConversion(); }; cancelConversion = () => { this.setPaymentConversionValue(); this.toggleEditConversion(); }; async setPaymentConversionValue () { const converted = this.unmaskAndConvertFromRequestedToDefault( this.paymentFormGroup.value.amount ); this.paymentFormGroup.get('conversion').setValue( converted.toFixed(this.precisionMap[this.defaultCurrency]) ); this.payment.differentThanConversion = false; } updateDifferentThanConversion () { const shouldBe = this.unmaskAndConvertFromRequestedToDefault( this.paymentFormGroup.value.amount ); const actual = this.getUnmaskedAmount( this.paymentFormGroup.value.conversion, this.defaultCurrency ); const differentThanConversion = shouldBe !== actual; this.payment.differentThanConversion = differentThanConversion; if (differentThanConversion) { this.payment.amount = actual; } else { this.payment.amount = shouldBe; } } // ** Start Actions / Change Events ** // onAwardTypeChange () { this.awardType = this.awardTypeFormGroup.value.awardType; this.setFinalBudgetListAndOptions(); } async addPayment () { if (this.isUnits) { this.itemsToPay = [ ...this.potentialItemsToPay ]; } await this.setUpPaymentFormGroup(); this.awardOnly = false; this.setModalHeader(); } setAlreadyPaidItems () { this.alreadyPaidItems = this.inKindService.getAlreadyPaidItems( this.originalPayments.filter((pay) => { return !this.originalPayment || pay.id !== this.originalPayment.id; }) ); if (this.originalAward) { this.alreadyAwardedItems = [ ...this.originalAward.inKindItems ]; } this.setPotentialItemsToPay(); } setPotentialItemsToPay () { this.potentialItemsToPay = this.itemsToAward.map((item) => { const paid = this.alreadyPaidItems.find((i) => { return item.identification === i.itemIdentification; }); return { ...item, unitsEntered: paid ? item.unitsEntered - paid.count : item.unitsEntered }; }).filter((item) => { return item.inUse && item.unitsEntered > 0; }); } removePayment () { this.paymentFormGroup = null; this.awardOnly = true; this.payment = null; this.itemsToPay = []; this.setModalHeader(); this.setAlreadyPaidItems(); this.calculateAwardBalance(); } sourceChanged () { this.paymentDisplayInfo = { ...this.paymentDisplayInfo, isEditing: false }; this.paymentChanged(); this.setCreatePaymentAlertHelper(); this.paymentFormGroup.get('amount').updateValueAndValidity(); } async awardAmountChange () { this.setAwardOrPaymentAmount( this.getAmountInDefault( this.awardFormGroup.value.awardAmount ), 'awardAmount' ); if (this.originalAward) { if (this.showConversions) { await this.setCurrentExchangeRate(); this.setConversionMessage(); this.updateEditablePaymentsToCurrentRate(); } this.setPaymentDisplayMap(); } this.calculateAwardBalance(); } paymentChanged (setConversionValue = false) { const formValue = this.paymentFormGroup.value; const oldPayment = this.payment; const bfs: BudgetFundingSourceCombo = formValue.budgetIdFundingSource; const amountInRequested = this.getAmountInRequested(formValue.amount); if ( setConversionValue && oldPayment.currencyRequestedAmountEquivalent !== amountInRequested ) { this.setPaymentConversionValue(); } const differentThanConversion = this.payment.differentThanConversion; const amount = differentThanConversion ? this.getUnmaskedAmount( this.paymentFormGroup.get('conversion').value ) : this.getAmountInDefault(formValue.amount); this.setAwardOrPaymentAmount(amount, 'paymentAmount'); const newPayment: Payment = { ...oldPayment, tags: this.tagsFormGroup.value.tags || [], scheduledDate: formValue.date, amount, budget: +bfs.budget.id, budgetName: bfs.budget.name, fundingSource: +bfs.fundingSource.fundingSourceId, fundingSourceName: bfs.fundingSource.fundingSourceName, isUnits: this.isUnits, type: !this.isUnits ? this.cashString : this.inKindString, notes: formValue.notes, paymentDesignation: TextFriendlySpecialCharCleaner(formValue.paymentDesignation), typeId: bfs.fundingSource.fundingSourceType, currencyRequested: this.currencyRequested, currencyRequestedAmountEquivalent: amountInRequested }; Object.assign(this.payment, newPayment); this.setDisplayHelperMaps(); this.calculateAwardBalance(); } onTotalItemsChange (total: number, isAward = false) { if (isAward) { this.awardFormGroup.get('awardAmount').setValue( this.getAwardAmountForFormGroup(total) ); } else { this.paymentFormGroup.get('amount').setValue( this.getAwardAmountForFormGroup(total) ); } } onItemsChange (items: InKindItemToAwardOrPay[], isAward = false) { const attr = isAward ? 'itemsToAward' : 'itemsToPay'; this[attr] = [ ...items ]; if (isAward) { this.setPotentialItemsToPay(); } } async onPrimaryClick (moveFunds = false) { if (this.isAwardTypeView) { this.spinnerService.startSpinner(); if (this.isUnits) { this.setInKindItemsFields(); } await this.setCurrentEmailActive(); await this.setAwardFormGroup(); this.setModalHeader(); this.isAwardTypeView = false; this.spinnerService.stopSpinner(); } else { this.saveChanges(false, moveFunds); } this.analyticsService.emitEvent({ eventName: 'Simple award modal submit', eventType: EventType.Click, extras: null }); } getNewPaymentAmountFromMovingFunds ( budgetFs: BudgetFundingSourceCombo, remaining: number ) { const availToMove = this.unallocatedSourceMap[ budgetFs.fundingSource.fundingSourceId ]; const oldPaymentAmount = this.unmaskAndConvertFromRequestedToDefault( this.paymentFormGroup.value.amount ); const overBy = Math.abs(remaining); const difference = overBy - availToMove; return oldPaymentAmount - difference; } updatePaymentForMoveFunds () { const budgetFs = this.paymentFormGroup.get('budgetIdFundingSource').value; const availToMove = this.unallocatedSourceMap[ budgetFs.fundingSource.fundingSourceId ]; const remaining = this.getRemainingAmount(budgetFs); const canCoverAll = (remaining + availToMove) >= 0; if (!canCoverAll) { const newPaymentAmount = this.awardService.getNewPaymentAmountFromMovingFunds( budgetFs, remaining, this.paymentFormGroup.value.amount, this.currencyRequested, this.rate ); this.payment.amount = newPaymentAmount; const equiv = this.currencyService.fixNumberOfDecimals( newPaymentAmount / this.rate, this.currencyRequested ); this.payment.currencyRequestedAmountEquivalent = +equiv; } } saveChanges (addAnotherPayment = false, moveFunds = false) { if (moveFunds) { this.updatePaymentForMoveFunds(); } const currentPaymentId = this.payment ? this.payment.id : null; const originalPaymentId = this.originalPayment ? this.originalPayment.id : null; const paymentRemoved = !!(originalPaymentId && !currentPaymentId) || (originalPaymentId && originalPaymentId !== currentPaymentId); const removedPayment = paymentRemoved ? this.originalPayment : null; const addedPayment = this.payment && !this.payment.id ? this.payment : null; const updatedPayment = this.payment && (originalPaymentId === currentPaymentId) ? this.payment : null; const tagMap: SimpleStringMap<{ added: number[]; removed: number[] }> = {}; if (updatedPayment) { tagMap[originalPaymentId] = { added: this.payment.tags .filter(tag => !this.originalPayment.tags .some(originalTag => originalTag === tag)), removed: this.originalPayment.tags .filter(tag => !this.payment.tags .some(newTag => newTag === tag)) }; } const notifyApplicant = this.awardFormGroup.value.notifyApplicant; const returnValue: AwardModalResponse = { id: this.award ? this.award.id : null, amount: this.getAmountInDefault(), description: this.awardFormGroup.value.description, sendEmail: !this.award && notifyApplicant, awardDate: this.timeZoneService.returnMidnightUTCDate(this.awardFormGroup.value.awardDate), clientEmailTemplateId: notifyApplicant ? this.awardFormGroup.value.clientEmailTemplateId : null, currencyRequested: this.currencyRequested, amountEquivalent: this.getAmountInRequested( this.awardFormGroup.value.awardAmount ), currencyExchangeRate: this.exchangeRate, customMessage: !this.award && notifyApplicant ? this.awardFormGroup.value.customMessage : '', awardType: this.awardType, awardedItems: this.itemsToAward.filter((item) => { return +item.unitsEntered > 0; }), paidItems: this.itemsToPay.filter((item) => { return +item.unitsEntered > 0; }), addedPayment, updatedPayment, removedPayment, tagMap, addAnotherPayment, usedOverage: moveFunds, emailOptionsModel: { attachments: this.awardFormGroup.value.attachments || [], ccEmails: this.awardFormGroup.value.cc || [], bccEmails: this.awardFormGroup.value.bcc || [] } }; this.closeModal.emit(returnValue); } onSecondaryClick (moveFunds = false) { if (this.isAwardTypeView) { this.closeModal.emit(); } else { this.saveChanges(true, moveFunds); this.analyticsService.emitEvent({ eventName: 'Simple award modal submit', eventType: EventType.Click, extras: null }); } } // ** End Actions / Change Events ** // // ** Start Helpers * // fixNumberOfDecimals (amount: number, currency = this.currencyRequested): string { return this.currencyService.fixNumberOfDecimals(amount, currency); } getAwardAmountForFormGroup (amount: number) { return this.awardService.getAwardAmountForFormGroup(amount, this.currencyRequested); } getAmountInDefault (amount = this.awardFormGroup.value.awardAmount) { return this.unmaskAndConvertFromRequestedToDefault(amount); } getAmountInRequested (amount: string, currencyRequested = this.currencyRequested) { return this.awardService.getAmountInRequested(amount, currencyRequested); } getUnmaskedAmount (amount = '0', currency = this.currencyRequested) { return this.awardService.getUnmaskedAmount(amount, currency); } unmaskAndConvertFromRequestedToDefault (amount: string) { return this.awardService.unmaskAndConvertFromRequestedToDefault( amount, this.currencyRequested, this.rate ); } getAwardBalanceInDefault () { const amount = this.getAmountInDefault(); const sumPayments = this.getSumOfAllPayments(); const balance = amount - sumPayments; return balance; } getAwardBalanceInRequested () { return this.getAmountInRequested(this.awardFormGroup.value.awardAmount) - this.getSumOfAllPayments(false); } getConversionHelpText (convertedAmount: number) { return `${this.conversionText}: ${ this.currencyService.formatMoney(convertedAmount, this.defaultCurrency) }`; } getSumOfAllPayments (inDefault = true) { let currentPaymentAmount = this.paymentFormGroup ? this.paymentFormGroup.value.amount : '0'; if ( this.originalPayment && this.originalPayment.statusId === PaymentStatus.Voided ) { currentPaymentAmount = '0'; } return this.awardService.getSumOfAllPayments( currentPaymentAmount, this.currencyRequested, this.originalPaymentsSumInDefault, this.originalPaymentsSumInRequested, this.showConversions, inDefault, this.rate ); } getRemainingAmount (budgetFundingSource: BudgetFundingSourceCombo) { const currentPaymentAmount = this.unmaskAndConvertFromRequestedToDefault( this.paymentFormGroup.value.amount ); return this.budgetService.getRemainingAmountForBudgetFs( budgetFundingSource, budgetFundingSource.budget.id, budgetFundingSource.fundingSource.fundingSourceId, currentPaymentAmount, this.originalPayment?.amount, !this.isUnits && this.appReservedInfo ? [this.appReservedInfo] : [], this.originalPayment?.budget, this.originalPayment?.fundingSource ); } calculateAwardBalance () { const scheduledPayments = this.getSumOfAllPayments(false); const awardBalance = this.getAwardBalanceInRequested(); this.awardFormGroup.get('awardBalance').setValue( this.currencyService.formatMoney(awardBalance, this.currencyRequested) ); this.awardFormGroup.get('scheduledPayments').setValue( this.currencyService.formatMoney(scheduledPayments, this.currencyRequested) ); this.negativeBalance = awardBalance < 0; this.setAwardHelpText(); } // ** End Helpers * // // ** Start Validators * // awardMinimumValidator () { return (control: AbstractControl) => { const amount = this.getUnmaskedAmount(control.value); let min = 0; let invalid = amount < min; if (!this.paymentFormGroup) { if (this.awardType === FundingSourceTypes.DOLLARS) { min = this.getSumOfAllPayments(false); invalid = amount < min; } } if (invalid) { return { amountGreaterThanMin: { i18nKey: !this.paymentFormGroup ? 'APPLY:textPleaseEnterANumberGreaterThanEqualToMin' : 'common:textPleaseEnterANumberGreaterThanZero', context: { minimum: this.currencyService.formatMoney( min, this.currencyRequested ) }, defaultValue: !this.paymentFormGroup ? 'Amount must be greater than or equal to __minimum__.' : 'Amount must be greater than zero.' } }; } return null; }; } paymentMinimumValidator () { return (group: AbstractControl) => { let returnVal = null; const budgetFundingSource = group.value.budgetIdFundingSource; if (budgetFundingSource) { const invalidUnmaskedAmount = this.checkIfUnmaskedAmountValid(group); if (invalidUnmaskedAmount) { returnVal = { amount: { // control name minimum: { // type of error i18nKey: this.isUnits ? 'GLOBAL:textPleaseEnterUnitsGreaterThanZero' : 'GLOBAL:textPaymentAmountCannotBeZero' , defaultValue: this.isUnits ? 'Payment amount cannot be zero. Please select at least one item.' : 'Please enter a value greater than zero' } } }; } } return returnVal; }; } private checkIfUnmaskedAmountValid (group: AbstractControl) { // payments in dollars must be greater than zero, return TRUE if invalid if (this.awardType === FundingSourceTypes.DOLLARS) { return group.value.amount <= 0; } else { // payments in-kind can be 0 return (group.value.amount) < 0; } } sourceAvailableValidator () { return (group: AbstractControl) => { const value: BudgetFundingSourceCombo = group.get('budgetIdFundingSource').value; if (this.paymentFormGroup && value && value.fundingSource) { const remaining = this.getRemainingAmount(value); if (remaining < 0) { const skipClosedLogic = this.budgetService.getSkipClosedLogic( this.originalPayment?.amount, value.budget.id, value.fundingSource.fundingSourceId, this.originalPayment?.budget, this.originalPayment?.fundingSource ); const response = this.awardService.handlePaymentSourceValidation( value, this.paymentFormGroup.value.amount, remaining, this.currencyRequested, this.rate, this.isUnits, true, skipClosedLogic ); this.canMoveFundsText = response.canMoveFundsText; return response.error; } } this.canMoveFundsText = ''; return null; }; } // ** End Validators * // }