import { EventEmitter, Injectable } from '@angular/core'; import { PortalDeterminationService } from '@core/services/portal-determination.service'; import { LocationState } from '@core/states/location.state'; import { VettingRequestStatusId } from '@core/typings/application.typing'; import { AdHocVettingStatus, OrgUnion } from '@core/typings/organization.typing'; import { ProcessingTypes } from '@core/typings/payment.typing'; import { AddOrgUI } from '@core/typings/ui/add-org.typing'; import { ClientSettingsService } from '@features/client-settings/client-settings.service'; import { NonprofitService } from '@features/nonprofit/nonprofit.service'; import { AddressFormatterService, OrganizationEligibleForGivingStatus, OrganizationSearchResponse, PaginationOptions, SearchResult, TypeaheadSelectOption } from '@yourcause/common'; import { I18nService } from '@yourcause/common/i18n'; import { AttachYCState, BaseYCService } from '@yourcause/common/state'; import { map, Observable } from 'rxjs'; import { AddOrganizationState } from './add-organization.state'; @Injectable({ providedIn: 'root' }) @AttachYCState(AddOrganizationState) export class AddOrganizationService extends BaseYCService { closeModal: EventEmitter<{ selectionType: AddOrgUI.SelectionType; searchResult: SearchResult; isInvalidOrg: boolean; }>; addOrgModalHeader = this.i18n.translate( 'common:textAddOrganization', {}, 'Add Organization' ); vettingRequiredHeader = this.i18n.translate( 'common:textVettingRequired', {}, 'Vetting Required' ); continueToApplicationHeader = this.i18n.translate( 'common:hdrContinueToApplication', {}, 'Continue to Application' ); confirmationText = this.i18n.translate( 'addOrg:textContinueWithVettedOrg2', {}, 'The identification number you entered matches the nonprofit below. To proceed with your application using this nonprofit click \'Next\'. If you wish to return to search click \'Cancel\'.' ); vettingRequiredConfirmationText = this.i18n.translate( 'addOrg:textContinueAndVetFoundOrg2', {}, 'The identification number you entered matches the nonprofit below. This organization has not been vetted. Vetting is a process of determining if an organization is eligible to receive donations and grants. The organization must exist for 5 years to be ED vetted and completing vetting is not a commitment of any funding. Once you submit your application we will begin the vetting process. We do our best to expedite this process. A majority of these vetting requests are initiated in 7-10 days. If we cannot establish communication or the organization does not have the proper documentation readily available, this process can take much longer. To proceed with your application using this nonprofit click \'Next\'. If you wish to return to search click \'Cancel\'.' ); vettingRequiredForNewOrgConfirmationText = this.i18n.translate( 'addOrg:textContinueAndVetNewOrg3', {}, 'The organization you have selected has not been vetted. Vetting is a process of determining if an organization is eligible to receive donations and grants. The organization must exist for 5 years to be ED vetted and completing vetting is not a commitment of any funding. Once you submit your application, we will begin the vetting process. We do our best to expedite this process. A majority of these vetting requests are initiated in 7-10 days. If we cannot establish communication or the organization does not have the proper documentation readily available, this process can take much longer. To proceed with your application, please provide contact information for the organization below and click \'Next\'. If you wish to return to search click \'Cancel\'.' ); vettingRequiredForDomesticOrgConfirmText = this.i18n.translate( 'addOrg:textVettingForDomesticOrgConfirm2', {}, 'The organization you have selected has not been vetted. Vetting is a process of determining if an organization is eligible to receive donations and grants. In order to begin the vetting process, a member of the organization must request to become an administrator through YourCause NPOconnect Nonprofit Platform. Once their request has been approved, they will be required to upload specific documentation. Please provide contact information below and we will send an email to the individual instructing them on how to start the vetting process.' ); clientProcessingNewOrgText = this.i18n.translate( 'addOrg:textClientProcessingNewOrg4', {}, 'To proceed with your application using this nonprofit, click \'Next.\' If you wish to return to search click \'Cancel\'' ); isGrantManager = this.portalDeterminationService.isManager; countrySelects$: Observable[]> = this.locationState .changesTo$('countries') .pipe(map(countries => { return countries .filter((country) => !!country.code) .map(country => ({ label: country.name, value: country.code })); })); internationalCountrySelects$: Observable[]> = this.locationState .changesTo$('countries') .pipe(map(countries => { return countries .filter((country) => !!country.code) .filter((country) => country.code !== 'US') .map(country => ({ label: country.name, value: country.code })); })); constructor ( private locationState: LocationState, private clientSettingsService: ClientSettingsService, private i18n: I18nService, private addressFormatter: AddressFormatterService, private portalDeterminationService: PortalDeterminationService, private nonprofitService: NonprofitService ) { super(); } get isApplicant (): boolean { return this.portalDeterminationService.isApply; } get addOrgPayload (): AddOrgUI.AddOrgPayload { return this.get('addOrgPayload'); } get selectionType () { return this.get('selectionType'); } get primaryDisabled () { return this.get('primaryDisabled'); } get originalSearchResponse () { return this.get('originalSearchResponse'); } get currentPage () { return this.get('currentPage'); } get isTestClient () { return this.clientSettingsService.clientSettings.isTestClient; } get processorType () { return this.get('processorType'); } getAdHocVettingStatusOptions () { return [{ label: this.i18n.translate( 'classification:textUnknown', {}, 'Unknown' ), value: AdHocVettingStatus.Unknown }, { label: this.i18n.translate( 'common:lblPending', {}, 'Pending' ), value: AdHocVettingStatus.Pending }, { label: this.i18n.translate( 'GLOBAL:textApproved', {}, 'Approved' ), value: AdHocVettingStatus.Approved }, { label: this.i18n.translate( 'GLOBAL:textRejected', {}, 'Rejected' ), value: AdHocVettingStatus.Rejected }]; } setOriginalSearchResponse (payload: OrgUnion) { return this.set('originalSearchResponse', payload); } setCurrentPage (page: AddOrgUI.AddOrgModalPages) { this.set('currentPage', page); } setShowContactForm (showContact: boolean) { this.set('showContact', showContact); } setClientId (clientId: number) { this.set('clientId', clientId); } setAddOrgAttr ( attr: K, data: AddOrgUI.AddOrgPayload[K] ) { this.set('addOrgPayload', { ...this.addOrgPayload, [attr]: data }); } setValidation (valid: boolean) { return this.set('primaryDisabled', !valid); } setProcessorType (processorType: ProcessingTypes) { this.set('processorType', processorType); } setSearchResult (searchResult: SearchResult) { this.set('searchResult', searchResult); } setSelectionType (selectionType: AddOrgUI.SelectionType) { this.set('selectionType', selectionType); } setConfirmTextToVettingRequiredForNewOrg () { this.set('confirmationText', this.vettingRequiredForNewOrgConfirmationText); } setConfirmTextToDomesticOrg () { this.set('confirmationText', this.vettingRequiredForDomesticOrgConfirmText); } setModalHeaderToVettingRequired () { this.set('modalHeader', this.vettingRequiredHeader); } setModalHeader () { switch (this.currentPage) { case AddOrgUI.AddOrgModalPages.ID_LOOKUP: case AddOrgUI.AddOrgModalPages.DETAILS: case AddOrgUI.AddOrgModalPages.ADDRESS: return this.set('modalHeader', this.addOrgModalHeader); case AddOrgUI.AddOrgModalPages.CONFIRMATION: return this.set('modalHeader', this.continueToApplicationHeader); default: return this.set('modalHeader', this.addOrgModalHeader); } } handlePrimaryButtonClick () { switch (this.currentPage) { case AddOrgUI.AddOrgModalPages.ID_LOOKUP: return this.handleIDLookup(); case AddOrgUI.AddOrgModalPages.DETAILS: return this.handleDetails(); case AddOrgUI.AddOrgModalPages.ADDRESS: return this.handleAddress(); case AddOrgUI.AddOrgModalPages.CONFIRMATION: return this.handleConfirmation(); default: return this.handleIDLookup(); } } handleTertiaryButtonClick () { switch (this.currentPage) { case AddOrgUI.AddOrgModalPages.ID_LOOKUP: return this.handleReloadSearchModal(); case AddOrgUI.AddOrgModalPages.DETAILS: return this.handleBackButton(AddOrgUI.AddOrgModalPages.ID_LOOKUP); case AddOrgUI.AddOrgModalPages.ADDRESS: return this.handleBackButton(AddOrgUI.AddOrgModalPages.DETAILS); case AddOrgUI.AddOrgModalPages.CONFIRMATION: if (this.selectionType !== AddOrgUI.SelectionType.NEW_ORG) { return this.handleBackButton(AddOrgUI.AddOrgModalPages.ID_LOOKUP); } else { return this.handleBackButton(AddOrgUI.AddOrgModalPages.ADDRESS); } default: return this.handleReloadSearchModal(); } } handleReloadSearchModal () { this.closeModal.emit(); } handleCancelClick () { this.closeModal.emit(); } cleanUserRegIDInput (input: number) { const cleanedUp = ('' + input).match(/[A-Z0-9a-z]/g).join(''); return cleanedUp; } async handleIDLookup () { this.setShowContactForm(false); let searchResult: any; const gcPaginationOptions: PaginationOptions = { retrieveTotalRecordCount: true, filterColumns: [ { columnName: 'orgIdentification', filters: [ { filterType: 'eq', filterValue: this.addOrgPayload.id } ] } ], sortColumns: [], returnAll: false, pageNumber: 1, rowsPerPage: 15 }; const regAuthorities = await this.getRegAuthoritiesPerCountry( this.addOrgPayload.country ); const regAuthFilters = ([ ...regAuthorities.domesticRegistrationAuthorities ]).map((regAuth) => { return { filterName: 'RegistrationAuthority', filterValue: regAuth.name }; }); const nppSearchResponse = await this.nonprofitService.searchPublicNonprofits( '' + this.addOrgPayload.id, regAuthFilters, 1, 5 ); const nppSearchResults = nppSearchResponse?.results?.map((result) => { return SearchResult.construct(result); }); // check to see if any results match the registrationId // (false hits dues to fuzzy search) // clean up the user input for this exact matching const cleanedInput = this.cleanUserRegIDInput(this.addOrgPayload.id); const nppResult = nppSearchResults.find((result) => { return (result.document.registrationId).toLowerCase() === (cleanedInput).toLowerCase(); }); if ((!nppSearchResults[0]) || !nppResult) { const gcSearchResponse = await this.nonprofitService.searchPrivateOrgs( this.get('clientId'), gcPaginationOptions ); if (!gcSearchResponse.records[0]) { /// set selection type and adapt if (this.processorType !== ProcessingTypes.YourCause) { this.set('confirmationText', this.clientProcessingNewOrgText); this.set('modalHeader', this.continueToApplicationHeader); } else { if (this.addOrgPayload.country === 'US') { this.setModalHeader(); } else { this.setModalHeaderToVettingRequired(); } this.setConfirmTextToVettingRequiredForNewOrg(); } this.setSelectionType(AddOrgUI.SelectionType.NEW_ORG); return this.setCurrentPage(AddOrgUI.AddOrgModalPages.DETAILS); } else { /// set selection type and adapt if (this.processorType !== ProcessingTypes.YourCause) { this.set('confirmationText', this.confirmationText); this.set('modalHeader', this.continueToApplicationHeader); } else { this.set('confirmationText', this.vettingRequiredConfirmationText); } this.setSelectionType(AddOrgUI.SelectionType.PRIVATE_ORG); searchResult = this.adaptClientOrgSearchResult(gcSearchResponse.records[0]); } } else { // check npp org vetting here searchResult = nppSearchResults[0]; this.set('originalSearchResponse', nppSearchResults[0]); const eligibleForGivingStatus = nppResult.document.eligibleForGivingStatusId; if ( eligibleForGivingStatus === OrganizationEligibleForGivingStatus.ELIGIBLE || this.processorType !== ProcessingTypes.YourCause ) { this.set('confirmationText', this.confirmationText); this.set('modalHeader', this.continueToApplicationHeader); this.setSelectionType(AddOrgUI.SelectionType.VETTED_ORG); } else { const vettingStatus = await this.nonprofitService.getVettingStatus( searchResult.document.nonprofitGuid ); // If allowCreateVettingRequest is false, // this means vetting has been declined, // so they should not be able to select this org if (!vettingStatus.allowCreateVettingRequest) { return this.closeModal.emit({ selectionType: null, searchResult, isInvalidOrg: true }); } else { this.set('confirmationText', this.vettingRequiredConfirmationText); this.setModalHeaderToVettingRequired(); this.setSelectionType(AddOrgUI.SelectionType.UNVETTED_ORG); } } } this.setCurrentPage(AddOrgUI.AddOrgModalPages.CONFIRMATION); this.set('searchResult', searchResult); return searchResult; } handleDetails () { this.setCurrentPage(AddOrgUI.AddOrgModalPages.ADDRESS); this.setModalHeader(); } handleAddress () { this.set('searchResult', this.adaptAddNonprofitResult(this.addOrgPayload)); this.setCurrentPage(AddOrgUI.AddOrgModalPages.CONFIRMATION); this.setModalHeader(); } removeQueryParams (url: string) { return url.split('?')[0]; } getUnionInfo (searchResult: OrgUnion) { if ('document' in searchResult) { return { id: searchResult.document.id, nonprofitGuid: searchResult.document.nonprofitGuid, country: searchResult.document.country }; } else if ('clientId' in searchResult) { return { id: searchResult.id, nonprofitGuid: searchResult.nonprofitGuid, country: searchResult.country }; } else if ('uniqueGuid' in searchResult) { return { id: searchResult.id, nonprofitGuid: searchResult.uniqueGuid, country: searchResult.location.countryCode }; } else { return { id: searchResult.id, nonprofitGuid: searchResult.nonprofitGuid, country: searchResult.country }; } } async handleVettingAfterUpdateProgram ( organizationId: number, nonprofitContactName: string, nonprofitContactEmail: string, nonprofitContactWebsite: string, cycleId: number, applicationId: number ) { await this.nonprofitService.addOrVetOrganization({ organizationId, nonprofitContactName, nonprofitContactEmail, nonprofitContactWebsite, grantProgramCycleId: cycleId, isApplicant: false, applicationId }); } async handleOrgForApplication ( cycleId: number, cycleClientOrgProcType: ProcessingTypes, organizationId: number, applicationId: number, organizationEligibleForGivingStatus: OrganizationEligibleForGivingStatus, latestVettingRequestStatusForOrg: VettingRequestStatusId, clientId: number, nonprofitContactName?: string, nonprofitContactEmail?: string, nonprofitContactWebsite?: string ) { const addOrgPayload = this.get('addOrgPayload'); nonprofitContactName = nonprofitContactName || addOrgPayload?.contactFullName; nonprofitContactEmail = nonprofitContactEmail || addOrgPayload?.contactEmail; nonprofitContactWebsite = nonprofitContactWebsite || addOrgPayload?.contactWebsite; // first we check the processor and the type of org they have chosen const eligible = this.selectionType === AddOrgUI.SelectionType.VETTED_ORG || organizationEligibleForGivingStatus === OrganizationEligibleForGivingStatus.ELIGIBLE; const existingOrgAndClient = cycleClientOrgProcType === ProcessingTypes.Client && this.selectionType !== AddOrgUI.SelectionType.NEW_ORG; // then we check the vetting status if we need to const vettingRequested = !!( latestVettingRequestStatusForOrg && latestVettingRequestStatusForOrg !== VettingRequestStatusId.DECLINED_ED ); if ( !eligible && !existingOrgAndClient && !vettingRequested && nonprofitContactName && nonprofitContactEmail ) { if (organizationId) { if (applicationId) { await this.nonprofitService.addOrVetOrganization({ organizationId, nonprofitContactName, nonprofitContactEmail, nonprofitContactWebsite, grantProgramCycleId: cycleId, clientId, isApplicant: this.isApplicant, applicationId }); this.set('addOrgPayload', null); } } else { const addNonprofitModel = { ...addOrgPayload, allowDonations: true, allowVolunteering: true }; const addIconResponse = await this.nonprofitService.uploadIcon( addNonprofitModel, addOrgPayload.image ); organizationId = await this.nonprofitService.addOrVetOrganization({ addNonprofitModel: { name: addOrgPayload.name, website: addOrgPayload.website, address: addOrgPayload.address, registrationId: '' + addOrgPayload.id, imageName: addIconResponse.imageName, imageUrl: addIconResponse.imageUrl, allowDonations: true, allowVolunteering: true }, updateNonprofitClassificationModel: { classificationOneId: null, classificationTwoId: null, classificationThreeId: null }, nonprofitContactName, nonprofitContactEmail, nonprofitContactWebsite, grantProgramCycleId: cycleId, clientId, isApplicant: this.isApplicant, applicationId }); this.set('addOrgPayload', null); } } return organizationId; } async handleConfirmation () { const searchResult = this.get('searchResult'); const selectionType = this.selectionType; this.closeModal.emit({ selectionType, searchResult, isInvalidOrg: false }); } handleBackButton (destination: AddOrgUI.AddOrgModalPages) { this.setCurrentPage(destination); this.setModalHeader(); } async getRegAuthoritiesPerCountry (country: string) { const regAuthorities = await this.clientSettingsService.getRegAuthoritiesPerCountry( country ); return regAuthorities; } registerModal (closeModal: EventEmitter<{ selectionType: AddOrgUI.SelectionType; searchResult: SearchResult; isInvalidOrg: boolean; }>) { this.closeModal = closeModal; } adaptClientOrgSearchResult (org: OrganizationSearchResponse) { return SearchResult.construct({ text: org.name, document: { id: '' + org.id, addressString: this.addressFormatter.format({ address1: org.address, address2: org.address2, city: org.city, stateProvRegCode: org.state, country: org.country, postalCode: org.postalCode }, true), iconURL: '', name: org.name, displayName: org.name, address1: org.address, address2: org.address2, city: org.city, postalCode: org.postalCode, country: org.country, stateProvRegCode: org.state, registrationId: org.orgIdentification } }); } adaptAddNonprofitResult (org: AddOrgUI.AddOrgPayload) { return SearchResult.construct({ text: org.name, document: { id: null, addressString: this.addressFormatter.format({ address1: org.address.address1, address2: org.address.address2, city: org.address.city, stateProvRegCode: org.address.stateProvRegCode, country: org.address.countryCode, postalCode: org.address.postalCode }, true), iconURL: org.imageUrl, name: org.name, website: org.website, displayName: org.name, address1: org.address.address1, address2: org.address.address2, city: org.address.city, postalCode: org.address.postalCode, country: org.address.countryCode, stateProvRegCode: org.address.stateProvRegCode, stateProvRegId: org.address.stateProvRegId, registrationId: '' + org.id, icon: org.image } }); } }