diff --git a/backend/src/migrations/1725362133806-OrgViewUpdateAdminEmail.ts b/backend/src/migrations/1725362133806-OrgViewUpdateAdminEmail.ts new file mode 100644 index 00000000..f99493ce --- /dev/null +++ b/backend/src/migrations/1725362133806-OrgViewUpdateAdminEmail.ts @@ -0,0 +1,135 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class OrgViewUpdateAdminEmail1725362133806 + implements MigrationInterface +{ + name = 'OrgViewUpdateAdminEmail1725362133806'; + + public async up(queryRunner: QueryRunner): Promise { + // Drop the existing view and its metadata + await queryRunner.query( + `DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`, + ['VIEW', 'OrganizationView', 'public'], + ); + await queryRunner.query(`DROP VIEW "OrganizationView"`); + + // Create the updated view + // Changes: + // - "adminEmail" now uses "organization_general".contact_person->>'email' instead of "organization_general".email because we are interested in "Contact person in relation with ONGHub" + await queryRunner.query(`CREATE VIEW "OrganizationView" AS + SELECT + "organization".id AS "id", + "organization".status AS "status", + "organization".created_on AS "createdOn", + CASE + WHEN COUNT(DISTINCT CASE WHEN "organization_financial".status != 'Completed' AND "organization_financial".status IS NOT NULL THEN 1 END) > 0 + OR COUNT(DISTINCT CASE WHEN "report".status != 'Completed' AND "report".status IS NOT NULL THEN 1 END) > 0 + OR COUNT(DISTINCT CASE WHEN "partner".status != 'Completed' AND "partner".status IS NOT NULL THEN 1 END) > 0 + OR COUNT(DISTINCT CASE WHEN "investor".status != 'Completed' AND "investor".status IS NOT NULL THEN 1 END) > 0 + THEN 'Not Completed' + ELSE 'Completed' + END AS "completionStatus", + "organization_general".name AS "name", + "organization_general".alias AS "alias", + "organization_general".contact_person->>'email' AS "adminEmail", + COUNT(DISTINCT "user".id) AS "userCount", + "organization_general".logo AS "logo", + CASE + WHEN MAX(GREATEST(COALESCE("organization_financial".updated_on, '1970-01-01'), COALESCE("report".updated_on, '1970-01-01'), COALESCE("partner".updated_on, '1970-01-01'), COALESCE("investor".updated_on, '1970-01-01'))) = '1970-01-01' + THEN NULL + ELSE MAX(GREATEST(COALESCE("organization_financial".updated_on, '1970-01-01'), COALESCE("report".updated_on, '1970-01-01'), COALESCE("partner".updated_on, '1970-01-01'), COALESCE("investor".updated_on, '1970-01-01')))::text + END AS "updatedOn" + FROM + "organization" "organization" + LEFT JOIN "organization_general" "organization_general" ON "organization".organization_general_id = "organization_general".id + LEFT JOIN "user" "user" ON "user".organization_id = "organization".id + AND "user".deleted_on IS NULL + AND "user".ROLE = 'employee' + AND "user".status != 'pending' + LEFT JOIN "organization_financial" "organization_financial" ON "organization".id = "organization_financial"."organizationId" + LEFT JOIN "organization_report" "organization_report" ON "organization".organization_report_id = "organization_report".id + LEFT JOIN "report" "report" ON "organization_report".id = "report"."organizationReportId" + LEFT JOIN "partner" "partner" ON "organization_report".id = "partner"."organizationReportId" + LEFT JOIN "investor" "investor" ON "organization_report".id = "investor"."organizationReportId" + WHERE + "organization".status != 'pending' + GROUP BY + "organization".id, + "organization_general".id + `); + + // Insert metadata for the new view + await queryRunner.query( + `INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`, + [ + 'public', + 'VIEW', + 'OrganizationView', + 'SELECT\n "organization".id AS "id",\n "organization".status AS "status",\n "organization".created_on AS "createdOn",\n \tCASE \n WHEN COUNT(DISTINCT CASE WHEN "organization_financial".status != \'Completed\' AND "organization_financial".status IS NOT NULL THEN 1 END) > 0\n OR COUNT(DISTINCT CASE WHEN "report".status != \'Completed\' AND "report".status IS NOT NULL THEN 1 END) > 0\n OR COUNT(DISTINCT CASE WHEN "partner".status != \'Completed\' AND "partner".status IS NOT NULL THEN 1 END) > 0\n OR COUNT(DISTINCT CASE WHEN "investor".status != \'Completed\' AND "investor".status IS NOT NULL THEN 1 END) > 0\n THEN \'Not Completed\'\n ELSE \'Completed\'\n \tEND AS "completionStatus",\n "organization_general".name AS "name",\n "organization_general".alias AS "alias",\n "organization_general".contact_person->>\'email\' AS "adminEmail",\n COUNT(DISTINCT "user".id) AS "userCount",\n "organization_general".logo AS "logo",\n CASE\n WHEN MAX(GREATEST(COALESCE("organization_financial".updated_on, \'1970-01-01\'), COALESCE("report".updated_on, \'1970-01-01\'), COALESCE("partner".updated_on, \'1970-01-01\'), COALESCE("investor".updated_on, \'1970-01-01\'))) = \'1970-01-01\'\n THEN NULL\n ELSE MAX(GREATEST(COALESCE("organization_financial".updated_on, \'1970-01-01\'), COALESCE("report".updated_on, \'1970-01-01\'), COALESCE("partner".updated_on, \'1970-01-01\'), COALESCE("investor".updated_on, \'1970-01-01\')))::text\n END AS "updatedOn"\n FROM\n "organization" "organization"\n LEFT JOIN "organization_general" "organization_general" ON "organization".organization_general_id = "organization_general".id\n LEFT JOIN "user" "user" ON "user".organization_id = "organization".id\n AND "user".deleted_on IS NULL\n AND "user".ROLE = \'employee\'\n AND "user".status != \'pending\'\n LEFT JOIN "organization_financial" "organization_financial" ON "organization".id = "organization_financial"."organizationId"\n LEFT JOIN "organization_report" "organization_report" ON "organization".organization_report_id = "organization_report".id\n LEFT JOIN "report" "report" ON "organization_report".id = "report"."organizationReportId"\n LEFT JOIN "partner" "partner" ON "organization_report".id = "partner"."organizationReportId"\n LEFT JOIN "investor" "investor" ON "organization_report".id = "investor"."organizationReportId"\n WHERE\n "organization".status != \'pending\'\n GROUP BY\n "organization".id,\n "organization_general".id', + ], + ); + } + + public async down(queryRunner: QueryRunner): Promise { + // Drop the updated view and its metadata + await queryRunner.query( + `DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`, + ['VIEW', 'OrganizationView', 'public'], + ); + await queryRunner.query(`DROP VIEW "OrganizationView"`); + + // Recreate the original view + // Changes: + // - "adminEmail" now uses "organization_general".email instead of "organization_general".contact_person->>'email' + await queryRunner.query(`CREATE VIEW "OrganizationView" AS SELECT + "organization".id AS "id", + "organization".status AS "status", + "organization".created_on AS "createdOn", + CASE + WHEN COUNT(DISTINCT CASE WHEN "organization_financial".status != 'Completed' AND "organization_financial".status IS NOT NULL THEN 1 END) > 0 + OR COUNT(DISTINCT CASE WHEN "report".status != 'Completed' AND "report".status IS NOT NULL THEN 1 END) > 0 + OR COUNT(DISTINCT CASE WHEN "partner".status != 'Completed' AND "partner".status IS NOT NULL THEN 1 END) > 0 + OR COUNT(DISTINCT CASE WHEN "investor".status != 'Completed' AND "investor".status IS NOT NULL THEN 1 END) > 0 + THEN 'Not Completed' + ELSE 'Completed' + END AS "completionStatus", + "organization_general".name AS "name", + "organization_general".alias AS "alias", + "organization_general".email AS "adminEmail", + COUNT(DISTINCT "user".id) AS "userCount", + "organization_general".logo AS "logo", + CASE + WHEN MAX(GREATEST(COALESCE("organization_financial".updated_on, '1970-01-01'), COALESCE("report".updated_on, '1970-01-01'), COALESCE("partner".updated_on, '1970-01-01'), COALESCE("investor".updated_on, '1970-01-01'))) = '1970-01-01' + THEN NULL + ELSE MAX(GREATEST(COALESCE("organization_financial".updated_on, '1970-01-01'), COALESCE("report".updated_on, '1970-01-01'), COALESCE("partner".updated_on, '1970-01-01'), COALESCE("investor".updated_on, '1970-01-01')))::text + END AS "updatedOn" + FROM + "organization" "organization" + LEFT JOIN "organization_general" "organization_general" ON "organization".organization_general_id = "organization_general".id + LEFT JOIN "user" "user" ON "user".organization_id = "organization".id + AND "user".deleted_on IS NULL + AND "user".ROLE = 'employee' + AND "user".status != 'pending' + LEFT JOIN "organization_financial" "organization_financial" ON "organization".id = "organization_financial"."organizationId" + LEFT JOIN "organization_report" "organization_report" ON "organization".organization_report_id = "organization_report".id + LEFT JOIN "report" "report" ON "organization_report".id = "report"."organizationReportId" + LEFT JOIN "partner" "partner" ON "organization_report".id = "partner"."organizationReportId" + LEFT JOIN "investor" "investor" ON "organization_report".id = "investor"."organizationReportId" + WHERE + "organization".status != 'pending' + GROUP BY + "organization".id, + "organization_general".id`); + + // Insert metadata for the original view + await queryRunner.query( + `INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`, + [ + 'public', + 'VIEW', + 'OrganizationView', + 'SELECT\n "organization".id AS "id",\n "organization".status AS "status",\n "organization".created_on AS "createdOn",\n \tCASE \n WHEN COUNT(DISTINCT CASE WHEN "organization_financial".status != \'Completed\' AND "organization_financial".status IS NOT NULL THEN 1 END) > 0\n OR COUNT(DISTINCT CASE WHEN "report".status != \'Completed\' AND "report".status IS NOT NULL THEN 1 END) > 0\n OR COUNT(DISTINCT CASE WHEN "partner".status != \'Completed\' AND "partner".status IS NOT NULL THEN 1 END) > 0\n OR COUNT(DISTINCT CASE WHEN "investor".status != \'Completed\' AND "investor".status IS NOT NULL THEN 1 END) > 0\n THEN \'Not Completed\'\n ELSE \'Completed\'\n \tEND AS "completionStatus",\n "organization_general".name AS "name",\n "organization_general".alias AS "alias",\n "organization_general".email AS "adminEmail",\n COUNT(DISTINCT "user".id) AS "userCount",\n "organization_general".logo AS "logo",\n CASE\n WHEN MAX(GREATEST(COALESCE("organization_financial".updated_on, \'1970-01-01\'), COALESCE("report".updated_on, \'1970-01-01\'), COALESCE("partner".updated_on, \'1970-01-01\'), COALESCE("investor".updated_on, \'1970-01-01\'))) = \'1970-01-01\'\n THEN NULL\n ELSE MAX(GREATEST(COALESCE("organization_financial".updated_on, \'1970-01-01\'), COALESCE("report".updated_on, \'1970-01-01\'), COALESCE("partner".updated_on, \'1970-01-01\'), COALESCE("investor".updated_on, \'1970-01-01\')))::text\n END AS "updatedOn"\n FROM\n "organization" "organization"\n LEFT JOIN "organization_general" "organization_general" ON "organization".organization_general_id = "organization_general".id\n LEFT JOIN "user" "user" ON "user".organization_id = "organization".id\n AND "user".deleted_on IS NULL\n AND "user".ROLE = \'employee\'\n AND "user".status != \'pending\'\n LEFT JOIN "organization_financial" "organization_financial" ON "organization".id = "organization_financial"."organizationId"\n LEFT JOIN "organization_report" "organization_report" ON "organization".organization_report_id = "organization_report".id\n LEFT JOIN "report" "report" ON "organization_report".id = "report"."organizationReportId"\n LEFT JOIN "partner" "partner" ON "organization_report".id = "partner"."organizationReportId"\n LEFT JOIN "investor" "investor" ON "organization_report".id = "investor"."organizationReportId"\n WHERE\n "organization".status != \'pending\'\n GROUP BY\n "organization".id,\n "organization_general".id', + ], + ); + } +} diff --git a/backend/src/modules/organization/entities/organization-view.entity.ts b/backend/src/modules/organization/entities/organization-view.entity.ts index f6edf791..04324632 100644 --- a/backend/src/modules/organization/entities/organization-view.entity.ts +++ b/backend/src/modules/organization/entities/organization-view.entity.ts @@ -18,7 +18,7 @@ import { OrganizationStatus } from '../enums/organization-status.enum'; END AS "completionStatus", "organization_general".name AS "name", "organization_general".alias AS "alias", - "organization_general".email AS "adminEmail", + "organization_general".contact_person->>'email' AS "adminEmail", COUNT(DISTINCT "user".id) AS "userCount", "organization_general".logo AS "logo", CASE diff --git a/backend/src/modules/organization/services/organization-financial.service.ts b/backend/src/modules/organization/services/organization-financial.service.ts index 57696a9b..a6bee3e4 100644 --- a/backend/src/modules/organization/services/organization-financial.service.ts +++ b/backend/src/modules/organization/services/organization-financial.service.ts @@ -184,6 +184,35 @@ export class OrganizationFinancialService { return obj.indicator === 'I46'; }); + // If any of the required indicators are undefined, it likely means + // the CUI is not for an NGO or the ANAF service structure has changed + if ( + income === undefined || + expense === undefined || + employees === undefined + ) { + const missingIndicators = [ + income === undefined ? 'I38 (income)' : null, + expense === undefined ? 'I40 (expense)' : null, + employees === undefined ? 'I46 (employees)' : null, + ] + .filter(Boolean) + .join(', '); + + const sentryMessage = `ANAF data missing required indicators (${missingIndicators}) for CUI: ${cui}, Year: ${year}`; + Sentry.captureMessage(sentryMessage, { + level: 'warning', + extra: { + cui, + year, + anafData, + missingIndicators, + }, + }); + this.logger.warn(sentryMessage); + return null; + } + return { numberOfEmployees: employees?.val_indicator, totalExpense: expense?.val_indicator, diff --git a/backend/src/modules/organization/services/organization-report.service.ts b/backend/src/modules/organization/services/organization-report.service.ts index e75a33e8..bc953513 100644 --- a/backend/src/modules/organization/services/organization-report.service.ts +++ b/backend/src/modules/organization/services/organization-report.service.ts @@ -237,16 +237,27 @@ export class OrganizationReportService { organization: Organization; year: number; }): Promise { - const organizationReport = organization.organizationReport; + let organizationReport = organization.organizationReport; + + if (!organizationReport) { + const newOrganizationReport = + await this.organizationReportRepository.save({ + organization: { id: organization.id }, + reports: [], + partners: [], + investors: [], + }); + organizationReport = newOrganizationReport; + } // Check if the given organizationId has already reports for the given year to avoid duplicating them - const hasReport = organizationReport.reports.find( + const hasReport = organizationReport?.reports.find( (report) => report.year === year, ); - const hasPartners = organizationReport.partners.find( + const hasPartners = organizationReport?.partners.find( (partner) => partner.year === year, ); - const hasInvestors = organizationReport.investors.find( + const hasInvestors = organizationReport?.investors.find( (investor) => investor.year === year, ); @@ -272,6 +283,7 @@ export class OrganizationReportService { : {}), }); } catch (err) { + console.log(err); Sentry.captureException(err, { extra: { organization, diff --git a/backend/src/modules/organization/services/organization.service.ts b/backend/src/modules/organization/services/organization.service.ts index f36c94b4..5dfd3bc0 100644 --- a/backend/src/modules/organization/services/organization.service.ts +++ b/backend/src/modules/organization/services/organization.service.ts @@ -241,15 +241,24 @@ export class OrganizationService { ), } : {}), + // Initialize organizationReport based on whether the organization existed last year ...(organizationExistedLastYear ? { + // If the organization existed last year, create initial reports, partners, and investors for that year organizationReport: { reports: [{ year: lastYear }], partners: [{ year: lastYear }], investors: [{ year: lastYear }], }, } - : {}), + : { + // If the organization is new, initialize empty arrays for reports, partners, and investors + organizationReport: { + reports: [], + partners: [], + investors: [], + }, + }), }); // upload logo