Skip to content

Commit

Permalink
rework script to suggest establishment list after 6 months with no up…
Browse files Browse the repository at this point in the history
…dates
  • Loading branch information
JeromeBu committed Jun 15, 2023
1 parent 0abc3c5 commit c3ba7fb
Show file tree
Hide file tree
Showing 16 changed files with 425 additions and 374 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ cypress/cypress/downloads
cypress/cypress/videos
cypress/cypress/screenshots
.turbo
storage
storage
logs
11 changes: 11 additions & 0 deletions back/src/_testBuilders/EstablishmentAggregateBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@ export class EstablishmentAggregateBuilder
});
}

public withEstablishmentUpdatedAt(updatedAt: Date) {
return new EstablishmentAggregateBuilder({
...this.aggregate,
establishment: new EstablishmentEntityBuilder(
this.aggregate.establishment,
)
.withUpdatedAt(updatedAt)
.build(),
});
}

public withImmersionOffers(immersionOffers: ImmersionOfferEntityV2[]) {
return new EstablishmentAggregateBuilder({
...this.aggregate,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ handleEndOfScriptNotification(
`Total of immersion ending tomorrow : ${numberOfSuccess}`,
`Number of successfully sent Assessments : ${numberOfSuccess}`,
`Number of failures : ${numberOfFailures}`,
...(numberOfFailures > 0 ? [`Failures : ${errorsAsString}`] : [""]),
...(numberOfFailures > 0 ? [`Failures : ${errorsAsString}`] : []),
].join("\n");
},
logger,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,133 +1,60 @@
import { addMonths } from "date-fns";
import { Pool } from "pg";
import { immersionFacileContactEmail, SiretDto } from "shared";
import { getTestPgPool } from "../../../_testBuilders/getTestPgPool";
import { SiretDto } from "shared";
import { makeGenerateJwtES256 } from "../../../domain/auth/jwt";
import { makeCreateNewEvent } from "../../../domain/core/eventBus/EventBus";
import { SuggestEditFormEstablishment } from "../../../domain/immersionOffer/useCases/SuggestEditFormEstablishment";
import { createLogger } from "../../../utils/logger";
import { makeSaveNotificationAndRelatedEvent } from "../../../domain/generic/notifications/entities/Notification";
import { SuggestEditEstablishment } from "../../../domain/immersionOffer/useCases/SuggestEditEstablishment";
import { SuggestEditEstablishmentsScript } from "../../../domain/immersionOffer/useCases/SuggestEditEstablishmentsScript";
import { RealTimeGateway } from "../../secondary/core/TimeGateway/RealTimeGateway";
import { UuidV4Generator } from "../../secondary/core/UuidGeneratorImplementations";
import { BrevoNotificationGateway } from "../../secondary/notificationGateway/BrevoNotificationGateway";
import { brevoNotificationGatewayTargets } from "../../secondary/notificationGateway/BrevoNotificationGateway.targets";
import { InMemoryNotificationGateway } from "../../secondary/notificationGateway/InMemoryNotificationGateway";
import { PgUowPerformer } from "../../secondary/pg/PgUowPerformer";
import { AppConfig, makeEmailAllowListPredicate } from "../config/appConfig";
import { configureCreateHttpClientForExternalApi } from "../config/createHttpClientForExternalApi";
import { AppConfig } from "../config/appConfig";
import { makeGenerateEditFormEstablishmentUrl } from "../config/magicLinkUrl";
import { createPgUow } from "../config/uowConfig";
import { handleEndOfScriptNotification } from "./handleEndOfScriptNotification";

const NB_MONTHS_BEFORE_SUGGEST = 6;

const logger = createLogger(__filename);

const config = AppConfig.createFromEnv();

type Report = {
numberOfEstablishmentsToContact: number;
errors?: Record<SiretDto, any>;
};

const triggerSuggestEditFormEstablishmentEvery6Months =
async (): Promise<Report> => {
logger.info(
`[triggerSuggestEditFormEstablishmentEvery6Months] Script started.`,
);

const dbUrl = config.pgImmersionDbUrl;
const pool = new Pool({
connectionString: dbUrl,
});
const client = await pool.connect();
const timeGateway = new RealTimeGateway();

const since = addMonths(timeGateway.now(), -NB_MONTHS_BEFORE_SUGGEST);

const establishmentsToContact = (
await client.query(
`SELECT DISTINCT siret FROM establishments WHERE update_date < $1
AND siret NOT IN (
SELECT payload ->> 'siret' as siret FROM outbox WHERE topic='FormEstablishmentEditLinkSent'
AND occurred_at > $2)`,
[since, since],
)
).rows.map(({ siret }) => siret);

if (establishmentsToContact.length === 0)
return { numberOfEstablishmentsToContact: 0 };

logger.info(
`[triggerSuggestEditFormEstablishmentEvery6Months] Found ${
establishmentsToContact.length
} establishments not updated since ${since} to contact, with siret : ${establishmentsToContact.join(
", ",
)}`,
);

const testPool = getTestPgPool();
const pgUowPerformer = new PgUowPerformer(testPool, createPgUow);

const notificationGateway =
config.notificationGateway === "BREVO"
? new BrevoNotificationGateway(
configureCreateHttpClientForExternalApi()(
brevoNotificationGatewayTargets,
),
makeEmailAllowListPredicate({
skipEmailAllowList: config.skipEmailAllowlist,
emailAllowList: config.emailAllowList,
}),
config.apiKeyBrevo,
{
name: "Immersion Facilitée",
email: immersionFacileContactEmail,
},
)
: new InMemoryNotificationGateway(timeGateway);

const generateEditEstablishmentJwt =
makeGenerateJwtES256<"editEstablishment">(
config.jwtPrivateKey,
3600 * 24,
);
const suggestEditFormEstablishment = new SuggestEditFormEstablishment(
pgUowPerformer,
notificationGateway,
timeGateway,
makeGenerateEditFormEstablishmentUrl(
config,
generateEditEstablishmentJwt,
),
makeCreateNewEvent({
timeGateway,
uuidGenerator: new UuidV4Generator(),
}),
);

const errors: Record<SiretDto, any> = {};

await Promise.all(
establishmentsToContact.map(async (siret) => {
await suggestEditFormEstablishment
.execute(siret)
.catch((error: any) => {
errors[siret] = error;
});
}),
);

return {
numberOfEstablishmentsToContact: establishmentsToContact.length,
errors,
};
};
const startScript = async (): Promise<Report> => {
const timeGateway = new RealTimeGateway();
const uuidGenerator = new UuidV4Generator();
const pool = new Pool({
connectionString: config.pgImmersionDbUrl,
});
const uowPerformer = new PgUowPerformer(pool, createPgUow);

const generateEditEstablishmentJwt =
makeGenerateJwtES256<"editEstablishment">(config.jwtPrivateKey, 3600 * 24);

const saveNotificationAndRelatedEvent = makeSaveNotificationAndRelatedEvent(
uuidGenerator,
timeGateway,
);

const suggestEditEstablishment = new SuggestEditEstablishment(
uowPerformer,
saveNotificationAndRelatedEvent,
timeGateway,
makeGenerateEditFormEstablishmentUrl(config, generateEditEstablishmentJwt),
);

const suggestEditEstablishmentsScript = new SuggestEditEstablishmentsScript(
uowPerformer,
suggestEditEstablishment,
timeGateway,
);
return suggestEditEstablishmentsScript.execute();
};

/* eslint-disable @typescript-eslint/no-floating-promises */
handleEndOfScriptNotification(
"triggerSuggestEditFormEstablishmentEvery6Months",
config,
triggerSuggestEditFormEstablishmentEvery6Months,
startScript,
({ numberOfEstablishmentsToContact, errors = {} }) => {
const nSiretFailed = Object.keys(errors).length;
const nSiretSuccess = numberOfEstablishmentsToContact - nSiretFailed;
Expand Down
10 changes: 0 additions & 10 deletions back/src/adapters/secondary/core/InMemoryOutboxQueries.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { EstablishmentJwtPayload } from "shared";
import {
DomainEvent,
eventsToDebugInfo,
Expand Down Expand Up @@ -44,13 +43,4 @@ export class InMemoryOutboxQueries implements OutboxQueries {
return lastPublication.failures.length > 0;
});
}
public async getLastPayloadOfFormEstablishmentEditLinkSentWithSiret(
siret: string,
): Promise<EstablishmentJwtPayload | undefined> {
return this.outboxRepository.events.find((event) => {
if (event.topic !== "FormEstablishmentEditLinkSent") return false;
const payload = event.payload as EstablishmentJwtPayload;
return payload.siret === siret;
})?.payload as EstablishmentJwtPayload;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ export class InMemoryEstablishmentAggregateRepository
private _establishmentAggregates: EstablishmentAggregate[] = [],
) {}

getSiretOfEstablishmentsToSuggestUpdate(): Promise<SiretDto[]> {
throw new Error(
"Method not implemented : getSiretOfEstablishmentsToSuggestUpdate, you can use PG implementation instead",
);
}
async markEstablishmentAsSearchableWhenRecentDiscussionAreUnderMaxContactPerWeek(): Promise<number> {
// not implemented because this method is used only in a script,
// and the use case consists only in a PG query
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { addDays } from "date-fns";
import subDays from "date-fns/subDays";
import { Pool, PoolClient } from "pg";
import { expectToEqual } from "shared";
import { EstablishmentAggregateBuilder } from "../../../_testBuilders/EstablishmentAggregateBuilder";
import { getTestPgPool } from "../../../_testBuilders/getTestPgPool";
import { PgEstablishmentAggregateRepository } from "./PgEstablishmentAggregateRepository";
import { PgNotificationRepository } from "./PgNotificationRepository";
import { PgOutboxRepository } from "./PgOutboxRepository";

describe("PgScriptsQueries", () => {
let pool: Pool;
let client: PoolClient;
let pgEstablishmentAggregateRepository: PgEstablishmentAggregateRepository;
let pgOutboxRepository: PgOutboxRepository;
let pgNotificationRepository: PgNotificationRepository;

beforeAll(async () => {
pool = getTestPgPool();
client = await pool.connect();
});

beforeEach(async () => {
await client.query("DELETE FROM immersion_contacts");
await client.query("DELETE FROM establishments");
await client.query("DELETE FROM outbox_failures");
await client.query("DELETE FROM outbox_publications");
await client.query("DELETE FROM outbox");
await client.query("DELETE FROM notifications_email_recipients");
await client.query("DELETE FROM notifications_email");
pgOutboxRepository = new PgOutboxRepository(client);
pgNotificationRepository = new PgNotificationRepository(client);
pgEstablishmentAggregateRepository = new PgEstablishmentAggregateRepository(
client,
);
});

afterAll(async () => {
client.release();
await pool.end();
});

describe("getSiretOfEstablishmentsToSuggestUpdate", () => {
it("gets only the establishment that before since and that have not received the suggest email recently", async () => {
const before = new Date("2023-07-01");
const toUpdateDate = subDays(before, 5);
const establishmentToUpdate = new EstablishmentAggregateBuilder()
.withEstablishmentSiret("11110000111100")
.withEstablishmentUpdatedAt(toUpdateDate)
.withContactId("11111111-1111-4000-1111-111111111111")
.build();

// <<<<<----------- this is the legacy behavior, we keep it until we reach the 6 months.
// We can remove this part of the code, and the FormEstablishmentEditLinkSent events in january 2024

const establishmentWithLinkSentEvent = new EstablishmentAggregateBuilder()
.withEstablishmentSiret("22220000222200")
.withEstablishmentUpdatedAt(toUpdateDate)
.withContactId("22222222-2222-4000-2222-222222222222")
.build();

await pgOutboxRepository.save({
id: "22222222-2222-4000-2222-000000000000",
topic: "FormEstablishmentEditLinkSent",
payload: {
siret: establishmentWithLinkSentEvent.establishment.siret,
} as any,
occurredAt: addDays(before, 1).toISOString(),
publications: [],
wasQuarantined: false,
});

// end of legacy ----------->>>>>>

const eventWithNotificationSavedButLongAgo =
new EstablishmentAggregateBuilder()
.withEstablishmentSiret("33330000333300")
.withEstablishmentUpdatedAt(toUpdateDate)
.withContactId("33333333-3333-4000-3333-333333333333")
.build();

await pgNotificationRepository.save({
id: "33333333-3333-4000-3333-000000000000",
followedIds: {
establishmentSiret:
eventWithNotificationSavedButLongAgo.establishment.siret,
},
kind: "email",
templatedContent: {
kind: "SUGGEST_EDIT_FORM_ESTABLISHMENT",
recipients: ["joe@mail.com"],
params: { editFrontUrl: "http://edit-front.com" },
},
createdAt: subDays(before, 1).toISOString(),
});

const eventWithRecentNotificationSaved =
new EstablishmentAggregateBuilder()
.withEstablishmentSiret("44440000444400")
.withEstablishmentUpdatedAt(toUpdateDate)
.withContactId("44444444-4444-4000-4444-444444444444")
.build();

await pgNotificationRepository.save({
id: "44444444-4444-4000-4444-000000000000",
followedIds: {
establishmentSiret:
eventWithRecentNotificationSaved.establishment.siret,
},
kind: "email",
templatedContent: {
kind: "SUGGEST_EDIT_FORM_ESTABLISHMENT",
recipients: ["jack@mail.com"],
params: { editFrontUrl: "http://edit-jack-front.com" },
},
createdAt: addDays(before, 1).toISOString(),
});

const recentlyUpdatedEstablishment = new EstablishmentAggregateBuilder()
.withEstablishmentSiret("99990000999900")
.withEstablishmentUpdatedAt(addDays(before, 1))
.withContactId("99999999-9999-4000-9999-999999999999")
.build();

await pgEstablishmentAggregateRepository.insertEstablishmentAggregates([
establishmentToUpdate,
eventWithNotificationSavedButLongAgo,
eventWithRecentNotificationSaved,
establishmentWithLinkSentEvent,
recentlyUpdatedEstablishment,
]);

// Act
const sirets =
await pgEstablishmentAggregateRepository.getSiretOfEstablishmentsToSuggestUpdate(
before,
);

// Assert
expectToEqual(sirets, [
establishmentToUpdate.establishment.siret,
eventWithNotificationSavedButLongAgo.establishment.siret,
]);
});
});
});
Loading

0 comments on commit c3ba7fb

Please sign in to comment.