Repository: vivid-planet/comet-brevo-module
Pull request #287: Add logging for created contacts (with form in admin)
Author: login: juliawegmayr
Created: 2025-03-17T16:16:07Z | Merged: 2025-04-02T08:22:45Z

## Description

Add logging for contacts, who were created without accepting a double opt-in email via the form in admin. Data is stored in the database only. 



## Changeset

- [x] I have verified if my change requires a changeset

## Related tasks and documents

https://vivid-planet.atlassian.net/browse/XXXL-970





Diff:
diff --git a/.changeset/smart-chairs-check.md b/.changeset/smart-chairs-check.md
new file mode 100644
index 00000000..479cbb58
--- /dev/null
+++ b/.changeset/smart-chairs-check.md
@@ -0,0 +1,20 @@
+@comet/brevo-admin": major
+"@comet/brevo-api": major
+
+---
+
+Added logging for contacts created manually via the admin contact form without double opt-in confirmation
+
+When a user adds a contact manually and skips sending the double opt-in email, the action is logged.
+
+Use `createBrevoEmailImportLogEntity` for creating `brevo-email-import-log` entity. Pass `Scope` and add it to the `AppModule`:
+
+    ```diff
+          BrevoModule.register({
+            brevo: {
+                  //...
+      +       BrevoEmailImportLog
+               }
+            //...
+          });
+    ```
diff --git a/demo/api/schema.gql b/demo/api/schema.gql
index aa2b6bf8..543440fc 100644
--- a/demo/api/schema.gql
+++ b/demo/api/schema.gql
@@ -982,6 +982,7 @@ input BrevoContactInput {
   blocked: Boolean!
   redirectionUrl: String!
   sendDoubleOptIn: Boolean! = true
+  responsibleUserId: String!
   attributes: BrevoContactAttributesInput
 }
 
diff --git a/demo/api/src/app.module.ts b/demo/api/src/app.module.ts
index 102e3165..b8b5162b 100644
--- a/demo/api/src/app.module.ts
+++ b/demo/api/src/app.module.ts
@@ -33,6 +33,7 @@ import { AuthLocalModule } from "./auth/auth-local.module";
 import { BlacklistedContacts } from "./blacklisted-contacts/entity/blacklisted-contacts.entity";
 import { BrevoContactSubscribeModule } from "./brevo-contact/brevo-contact-subscribe.module";
 import { BrevoContactAttributes, BrevoContactFilterAttributes } from "./brevo-contact/dto/brevo-contact-attributes";
+import { BrevoEmailImportLog } from "./brevo-email-import-log/entity/brevo-email-import-log.entity";
 import { BrevoTransactionalMailsModule } from "./brevo-transactional-mails/brevo-transactional-mails.module";
 import { Config } from "./config/config";
 import { ConfigModule } from "./config/config.module";
@@ -163,6 +164,7 @@ export class AppModule {
                         BrevoContactFilterAttributes,
                         EmailCampaign,
                         TargetGroup,
+                        BrevoEmailImportLog,
                     },
                     encryptionKey: config.encryptionKey,
                     ecgRtrList: {
diff --git a/demo/api/src/brevo-email-import-log/entity/brevo-email-import-log.entity.ts b/demo/api/src/brevo-email-import-log/entity/brevo-email-import-log.entity.ts
new file mode 100644
index 00000000..bfd8426a
--- /dev/null
+++ b/demo/api/src/brevo-email-import-log/entity/brevo-email-import-log.entity.ts
@@ -0,0 +1,4 @@
+import { createBrevoEmailImportLogEntity } from "@comet/brevo-api/lib/brevo-email-import-log/entity/brevo-email-import-log.entity.factory";
+import { EmailCampaignContentScope } from "@src/email-campaign/email-campaign-content-scope";
+
+export const BrevoEmailImportLog = createBrevoEmailImportLogEntity({ Scope: EmailCampaignContentScope });
diff --git a/demo/api/src/db/migrations/Migration20250318110932.ts b/demo/api/src/db/migrations/Migration20250318110932.ts
new file mode 100644
index 00000000..33b47020
--- /dev/null
+++ b/demo/api/src/db/migrations/Migration20250318110932.ts
@@ -0,0 +1,7 @@
+import { Migration } from "@mikro-orm/migrations";
+
+export class Migration20250318110932 extends Migration {
+    async up(): Promise<void> {
+        this.addSql('alter table "BrevoEmailImportLog"  add column "scope_domain" text not null, add column "scope_language" text not null;');
+    }
+}
diff --git a/packages/admin/src/brevoContacts/form/BrevoContactForm.tsx b/packages/admin/src/brevoContacts/form/BrevoContactForm.tsx
index 35bb88ab..83784150 100644
--- a/packages/admin/src/brevoContacts/form/BrevoContactForm.tsx
+++ b/packages/admin/src/brevoContacts/form/BrevoContactForm.tsx
@@ -18,7 +18,7 @@ import {
     useStackApi,
 } from "@comet/admin";
 import { ArrowLeft } from "@comet/admin-icons";
-import { ContentScopeIndicator, ContentScopeInterface, resolveHasSaveConflict, useFormSaveConflict } from "@comet/cms-admin";
+import { ContentScopeIndicator, ContentScopeInterface, resolveHasSaveConflict, useCurrentUser, useFormSaveConflict } from "@comet/cms-admin";
 import { Card, IconButton } from "@mui/material";
 import { Box } from "@mui/system";
 import { FormApi } from "final-form";
@@ -64,6 +64,7 @@ export function BrevoContactForm({ id, scope, input2State, additionalFormFields,
     const client = useApolloClient();
     const mode = id ? "edit" : "add";
     const formApiRef = useFormApiRef<EditBrevoContactFormValuesWithAttributes>();
+    const currentUser = useCurrentUser();
 
     const brevoContactFormFragment = gql`
         fragment BrevoContactForm on BrevoContact {
@@ -128,6 +129,7 @@ export function BrevoContactForm({ id, scope, input2State, additionalFormFields,
         const output = {
             ...state,
             blocked: false,
+            responsibleUserId: currentUser.id,
         };
 
         if (mode === "edit") {
diff --git a/packages/api/schema.gql b/packages/api/schema.gql
index ea5fce20..a0d5544c 100644
--- a/packages/api/schema.gql
+++ b/packages/api/schema.gql
@@ -348,6 +348,7 @@ input BrevoContactInput {
   blocked: Boolean!
   redirectionUrl: String!
   sendDoubleOptIn: Boolean! = true
+  responsibleUserId: String!
 }
 
 input BrevoTestContactInput {
diff --git a/packages/api/src/brevo-contact/brevo-contact-import.service.ts b/packages/api/src/brevo-contact/brevo-contact-import.service.ts
index b9951ff3..e9c292ab 100644
--- a/packages/api/src/brevo-contact/brevo-contact-import.service.ts
+++ b/packages/api/src/brevo-contact/brevo-contact-import.service.ts
@@ -143,6 +143,7 @@ export class BrevoContactImportService {
         scope: EmailCampaignScopeInterface,
         targetGroupBrevoIds: number[],
         sendDoubleOptIn: boolean,
+        responsibleUserId?: string,
     ): Promise<"created" | "updated" | "error"> {
         try {
             const brevoContact = await this.brevoApiContactsService.findContact(contact.email, scope);
@@ -164,6 +165,7 @@ export class BrevoContactImportService {
                     templateId: brevoConfig.doubleOptInTemplateId,
                     listIds: [mainTargetGroupForScope.brevoId, ...targetGroupBrevoIds],
                     sendDoubleOptIn,
+                    responsibleUserId,
                 });
                 if (success) return "created";
             }
diff --git a/packages/api/src/brevo-contact/brevo-contact.module.ts b/packages/api/src/brevo-contact/brevo-contact.module.ts
index 7853220d..07a2b740 100644
--- a/packages/api/src/brevo-contact/brevo-contact.module.ts
+++ b/packages/api/src/brevo-contact/brevo-contact.module.ts
@@ -5,6 +5,7 @@ import { DynamicModule, Module, Type } from "@nestjs/common";
 import { BrevoApiModule } from "../brevo-api/brevo-api.module";
 import { createBrevoContactImportConsole } from "../brevo-contact/brevo-contact-import.console";
 import { BrevoContactImportService } from "../brevo-contact/brevo-contact-import.service";
+import { BrevoEmailImportLogModule } from "../brevo-email-import-log/brevo-email-import-log.module";
 import { ConfigModule } from "../config/config.module";
 import { TargetGroupInterface } from "../target-group/entity/target-group-entity.factory";
 import { BrevoContactAttributesInterface, EmailCampaignScopeInterface } from "../types";
@@ -47,7 +48,13 @@ export class BrevoContactModule {
 
         return {
             module: BrevoContactModule,
-            imports: [BrevoApiModule, ConfigModule, MikroOrmModule.forFeature([TargetGroup, FileUpload, "BrevoConfig", "BlacklistedContacts"])],
+            imports: [
+                BrevoApiModule,
+                BrevoContactModule,
+                BrevoEmailImportLogModule,
+                ConfigModule,
+                MikroOrmModule.forFeature([TargetGroup, FileUpload, "BrevoConfig", "BlacklistedContacts"]),
+            ],
             providers: [
                 BrevoContactImportService,
                 BrevoContactsService,
diff --git a/packages/api/src/brevo-contact/brevo-contact.resolver.ts b/packages/api/src/brevo-contact/brevo-contact.resolver.ts
index 2f09052f..152432cc 100644
--- a/packages/api/src/brevo-contact/brevo-contact.resolver.ts
+++ b/packages/api/src/brevo-contact/brevo-contact.resolver.ts
@@ -6,6 +6,7 @@ import { Args, ArgsType, Int, Mutation, ObjectType, Query, Resolver } from "@nes
 import { BrevoConfigInterface } from "src/brevo-config/entities/brevo-config-entity.factory";
 
 import { BrevoApiContactsService } from "../brevo-api/brevo-api-contact.service";
+import { ContactSource } from "../brevo-email-import-log/entity/brevo-email-import-log.entity.factory";
 import { BrevoModuleConfig } from "../config/brevo-module.config";
 import { BREVO_MODULE_CONFIG } from "../config/brevo-module.constants";
 import { TargetGroupInterface } from "../target-group/entity/target-group-entity.factory";
@@ -222,6 +223,8 @@ export function createBrevoContactResolver({
                 scope,
                 templateId: brevoConfig.doubleOptInTemplateId,
                 sendDoubleOptIn: input.sendDoubleOptIn,
+                responsibleUserId: input.responsibleUserId,
+                contactSource: ContactSource.manualCreation,
             });
 
             if (created) {
diff --git a/packages/api/src/brevo-contact/brevo-contacts.service.ts b/packages/api/src/brevo-contact/brevo-contacts.service.ts
index debacfc2..62bcd789 100644
--- a/packages/api/src/brevo-contact/brevo-contacts.service.ts
+++ b/packages/api/src/brevo-contact/brevo-contacts.service.ts
@@ -5,6 +5,8 @@ import { BrevoConfigInterface } from "src/brevo-config/entities/brevo-config-ent
 
 import { BlacklistedContactsInterface } from "../blacklisted-contacts/entity/blacklisted-contacts.entity.factory";
 import { BrevoApiContactsService } from "../brevo-api/brevo-api-contact.service";
+import { BrevoEmailImportLogService } from "../brevo-email-import-log/brevo-email-import-log.service";
+import { ContactSource } from "../brevo-email-import-log/entity/brevo-email-import-log.entity.factory";
 import { BrevoModuleConfig } from "../config/brevo-module.config";
 import { BREVO_MODULE_CONFIG } from "../config/brevo-module.constants";
 import { TargetGroupsService } from "../target-group/target-groups.service";
@@ -25,6 +27,7 @@ export class BrevoContactsService {
         private readonly brevoContactsApiService: BrevoApiContactsService,
         private readonly ecgRtrListService: EcgRtrListService,
         private readonly targetGroupService: TargetGroupsService,
+        private readonly brevoEmailImportLogService: BrevoEmailImportLogService,
     ) {
         this.secretKey = this.config.encryptionKey;
     }
@@ -37,6 +40,8 @@ export class BrevoContactsService {
         templateId,
         listIds,
         sendDoubleOptIn,
+        responsibleUserId,
+        contactSource,
     }: {
         email: string;
         attributes?: BrevoContactAttributesInterface;
@@ -45,6 +50,8 @@ export class BrevoContactsService {
         templateId: number;
         listIds?: number[];
         sendDoubleOptIn: boolean;
+        responsibleUserId?: string;
+        contactSource?: ContactSource;
     }): Promise<boolean> {
         const mainTargetGroupForScope = await this.targetGroupService.createIfNotExistMainTargetGroupForScope(scope);
         const targetGroupIds = await this.getTargetGroupIdsForNewContact({ scope, contactAttributes: attributes });
@@ -56,12 +63,13 @@ export class BrevoContactsService {
             brevoIds.push(...listIds);
         }
 
-        if (!sendDoubleOptIn) {
+        if (!sendDoubleOptIn && responsibleUserId) {
             const encryptedEmail = encrypt(email, this.secretKey);
             const blacklistedContactAvailable = await this.blacklistedContactsRepository.findOne({ hashedEmail: encryptedEmail });
 
-            if (!blacklistedContactAvailable) {
+            if (!blacklistedContactAvailable && contactSource) {
                 created = await this.brevoContactsApiService.createBrevoContactWithoutDoubleOptIn({ email, attributes }, brevoIds, templateId, scope);
+                await this.brevoEmailImportLogService.addContactToLogs(email, responsibleUserId, scope, contactSource);
             }
         } else {
             created = await this.brevoContactsApiService.createDoubleOptInBrevoContact(
diff --git a/packages/api/src/brevo-contact/dto/brevo-contact-input.factory.ts b/packages/api/src/brevo-contact/dto/brevo-contact-input.factory.ts
index ea1bb0e8..f6e7452d 100644
--- a/packages/api/src/brevo-contact/dto/brevo-contact-input.factory.ts
+++ b/packages/api/src/brevo-contact/dto/brevo-contact-input.factory.ts
@@ -13,6 +13,7 @@ export interface BrevoContactInputInterface {
     attributes?: BrevoContactAttributesInterface;
     redirectionUrl?: string;
     sendDoubleOptIn: boolean;
+    responsibleUserId?: string;
 }
 
 export interface BrevoContactUpdateInputInterface {
@@ -52,6 +53,11 @@ export class BrevoContactInputFactory {
             @IsBoolean()
             @Field({ defaultValue: true })
             sendDoubleOptIn: boolean;
+
+            @IsNotEmpty()
+            @IsString()
+            @Field()
+            responsibleUserId: string;
         }
 
         @InputType({
diff --git a/packages/api/src/brevo-email-import-log/brevo-email-import-log.module.ts b/packages/api/src/brevo-email-import-log/brevo-email-import-log.module.ts
new file mode 100644
index 00000000..8fa969af
--- /dev/null
+++ b/packages/api/src/brevo-email-import-log/brevo-email-import-log.module.ts
@@ -0,0 +1,25 @@
+import { MikroOrmModule } from "@mikro-orm/nestjs";
+import { DynamicModule, Module, Type } from "@nestjs/common";
+import { EmailCampaignScopeInterface } from "src/types";
+
+import { BrevoApiModule } from "../brevo-api/brevo-api.module";
+import { ConfigModule } from "../config/config.module";
+import { BrevoEmailImportLogService } from "./brevo-email-import-log.service";
+import { BrevoEmailImportLogInterface } from "./entity/brevo-email-import-log.entity.factory";
+
+interface BrevoEmailImportLogModuleConfig {
+    BrevoEmailImportLog: Type<BrevoEmailImportLogInterface>;
+    Scope: Type<EmailCampaignScopeInterface>;
+}
+
+@Module({})
+export class BrevoEmailImportLogModule {
+    static register({ Scope, BrevoEmailImportLog }: BrevoEmailImportLogModuleConfig): DynamicModule {
+        return {
+            module: BrevoEmailImportLogModule,
+            imports: [ConfigModule, BrevoApiModule, MikroOrmModule.forFeature([BrevoEmailImportLog])],
+            providers: [BrevoEmailImportLogService],
+            exports: [BrevoEmailImportLogService],
+        };
+    }
+}
diff --git a/packages/api/src/brevo-email-import-log/brevo-email-import-log.service.ts b/packages/api/src/brevo-email-import-log/brevo-email-import-log.service.ts
new file mode 100644
index 00000000..5ffaea53
--- /dev/null
+++ b/packages/api/src/brevo-email-import-log/brevo-email-import-log.service.ts
@@ -0,0 +1,33 @@
+import { EntityManager, EntityRepository } from "@mikro-orm/core";
+import { InjectRepository } from "@mikro-orm/nestjs";
+import { Injectable } from "@nestjs/common";
+import { EmailCampaignScopeInterface } from "src/types";
+
+import { BrevoEmailImportLogInterface, ContactSource } from "./entity/brevo-email-import-log.entity.factory";
+
+@Injectable()
+export class BrevoEmailImportLogService {
+    constructor(
+        @InjectRepository("BrevoEmailImportLog") private readonly repository: EntityRepository<BrevoEmailImportLogInterface>,
+        private readonly entityManager: EntityManager,
+    ) {}
+    public async addContactToLogs(
+        email: string,
+        responsibleUserId: string,
+        scope: EmailCampaignScopeInterface,
+        contactSource: ContactSource,
+        importId?: string,
+    ): Promise<BrevoEmailImportLogInterface> {
+        const log = this.repository.create({
+            importedEmail: email,
+            responsibleUserId,
+            scope,
+            createdAt: new Date(),
+            updatedAt: new Date(),
+            contactSource,
+            importId,
+        });
+        await this.entityManager.flush();
+        return log;
+    }
+}
diff --git a/packages/api/src/brevo-email-import-log/entity/brevo-email-import-log.entity.factory.ts b/packages/api/src/brevo-email-import-log/entity/brevo-email-import-log.entity.factory.ts
new file mode 100644
index 00000000..d251110d
--- /dev/null
+++ b/packages/api/src/brevo-email-import-log/entity/brevo-email-import-log.entity.factory.ts
@@ -0,0 +1,73 @@
+import { DocumentInterface, IsUndefinable } from "@comet/cms-api";
+import { Embedded, Entity, Enum, OptionalProps, PrimaryKey, Property } from "@mikro-orm/core";
+import { Type } from "@nestjs/common";
+import { Field, ID, ObjectType } from "@nestjs/graphql";
+import { v4 } from "uuid";
+
+import { EmailCampaignScopeInterface } from "../../types";
+
+export interface BrevoEmailImportLogInterface {
+    importedEmail: string;
+    responsibleUserId: string;
+    scope: EmailCampaignScopeInterface;
+    createdAt: Date;
+    updatedAt: Date;
+    contactSource: ContactSource;
+    importId?: string;
+}
+
+export enum ContactSource {
+    manualCreation = "manualCreation",
+    csvImport = "csvImport",
+}
+
+export function createBrevoEmailImportLogEntity({ Scope }: { Scope: EmailCampaignScopeInterface }): Type<BrevoEmailImportLogInterface> {
+    @Entity()
+    @ObjectType({
+        implements: () => [DocumentInterface],
+    })
+    class BrevoEmailImportLog implements BrevoEmailImportLogInterface, DocumentInterface {
+        [OptionalProps]?: "createdAt" | "updatedAt";
+
+        @PrimaryKey({ columnType: "uuid" })
+        @Field(() => ID)
+        id: string = v4();
+
+        @Property({ columnType: "text" })
+        @Field()
+        importedEmail: string;
+
+        @Property()
+        @Field()
+        responsibleUserId: string;
+
+        @Property({
+            columnType: "timestamp with time zone",
+        })
+        @Field()
+        createdAt: Date = new Date();
+
+        @Property({
+            columnType: "timestamp with time zone",
+            onUpdate: () => new Date(),
+        })
+        @Field()
+        updatedAt: Date = new Date();
+
+        @Embedded(() => Scope)
+        @Field(() => Scope)
+        scope: typeof Scope;
+
+        @Property({ columnType: "uuid" })
+        @IsUndefinable()
+        @Field(() => ID)
+        importId?: string = v4();
+
+        @Property({ columnType: "text" })
+        @Field(() => ContactSource)
+        @Enum({ items: () => ContactSource })
+        contactSource: ContactSource;
+    }
+
+    return BrevoEmailImportLog;
+}
diff --git a/packages/api/src/brevo-module.ts b/packages/api/src/brevo-module.ts
index 3447ee4a..b9d96e9f 100644
--- a/packages/api/src/brevo-module.ts
+++ b/packages/api/src/brevo-module.ts
@@ -9,6 +9,7 @@ import { BrevoApiModule } from "./brevo-api/brevo-api.module";
 import { BrevoConfigModule } from "./brevo-config/brevo-config.module";
 import { BrevoConfigEntityFactory } from "./brevo-config/entities/brevo-config-entity.factory";
 import { BrevoContactModule } from "./brevo-contact/brevo-contact.module";
+import { BrevoEmailImportLogModule } from "./brevo-email-import-log/brevo-email-import-log.module";
 import { BrevoModuleConfig } from "./config/brevo-module.config";
 import { ConfigModule } from "./config/config.module";
 import { EmailCampaignModule } from "./email-campaign/email-campaign.module";
@@ -61,8 +62,12 @@ export class BrevoModule {
                     BlacklistedContacts: config.brevo.BlacklistedContacts,
                 }),
                 ConfigModule.forRoot(config),
+                BrevoEmailImportLogModule.register({
+                    Scope: config.emailCampaigns.Scope,
+                    BrevoEmailImportLog: config.brevo.BrevoEmailImportLog,
+                }),
             ],
-            exports: [TargetGroupModule, BrevoContactModule, BrevoApiModule, BlacklistedContactsModule],
+            exports: [TargetGroupModule, BrevoContactModule, BrevoApiModule, BrevoEmailImportLogModule, BlacklistedContactsModule],
         };
     }
 }
diff --git a/packages/api/src/config/brevo-module.config.ts b/packages/api/src/config/brevo-module.config.ts
index ad10a2c5..98853c6f 100644
--- a/packages/api/src/config/brevo-module.config.ts
+++ b/packages/api/src/config/brevo-module.config.ts
@@ -5,6 +5,7 @@ import { EmailCampaignInterface } from "src/email-campaign/entities/email-campai
 import { TargetGroupInterface } from "src/target-group/entity/target-group-entity.factory";
 import { BrevoContactAttributesInterface, EmailCampaignScopeInterface } from "src/types";
 
+import { BrevoEmailImportLogInterface } from "../brevo-email-import-log/entity/brevo-email-import-log.entity.factory";
 import { BrevoContactFilterAttributesInterface } from "../types";
 
 export interface BrevoModuleConfig {
@@ -18,6 +19,7 @@ export interface BrevoModuleConfig {
         BrevoContactFilterAttributes?: Type<BrevoContactFilterAttributesInterface>;
         EmailCampaign: Type<EmailCampaignInterface>;
         TargetGroup: Type<TargetGroupInterface>;
+        BrevoEmailImportLog: Type<BrevoEmailImportLogInterface>;
     };
     ecgRtrList: {
         apiKey: string;
diff --git a/packages/api/src/mikro-orm/migrations/Migration20250317131301.ts b/packages/api/src/mikro-orm/migrations/Migration20250317131301.ts
new file mode 100644
index 00000000..9599fd91
--- /dev/null
+++ b/packages/api/src/mikro-orm/migrations/Migration20250317131301.ts
@@ -0,0 +1,9 @@
+import { Migration } from "@mikro-orm/migrations";
+
+export class Migration20250317131301 extends Migration {
+    async up(): Promise<void> {
+        this.addSql(
+            'create table "BrevoEmailImportLog" ("id" uuid not null, "createdAt" timestamp with time zone not null, "updatedAt" timestamp with time zone not null, "importedEmail" text not null, "responsibleUserId" text not null, "importId" uuid null, "contactSource" text check ("contactSource" in (\'manualCreation\', \'csvImport\')) not null)',
+        );
+    }
+}
diff --git a/packages/api/src/mikro-orm/migrations/migrations.ts b/packages/api/src/mikro-orm/migrations/migrations.ts
index 894614fb..eb2883f7 100644
--- a/packages/api/src/mikro-orm/migrations/migrations.ts
+++ b/packages/api/src/mikro-orm/migrations/migrations.ts
@@ -15,6 +15,7 @@ import { Migration20241022144400 } from "./Migration20241022144400";
 import { Migration20241024071748 } from "./Migration20241024071748";
 import { Migration20241119101706 } from "./Migration20241119101706";
 import { Migration20250221073825 } from "./Migration20250221073825";
+import { Migration20250317131301 } from "./Migration20250317131301";
 import { Migration20250321132034 } from "./Migration20250321132034";
 
 export const migrationsList: MigrationObject[] = [
@@ -34,4 +35,5 @@ export const migrationsList: MigrationObject[] = [
     { name: "Migration20241024071748", class: Migration20241024071748 },
     { name: "Migration20250221073825", class: Migration20250221073825 },
     { name: "Migration20250321132034", class: Migration20250321132034 },
+    { name: "Migration20250317131301", class: Migration20250317131301 },
 ];


Reviews:
user: login: raphaelblum
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-01T11:50:49Z
user: login: juliawegmayr
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T08:01:53Z
user: login: juliawegmayr
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T08:03:35Z
user: login: juliawegmayr
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T08:03:44Z
user: login: juliawegmayr
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T08:04:32Z
user: login: raphaelblum
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-02T08:17:51Z

Review comments:
diff_hunk: @@ -0,0 +1,56 @@
+import { DocumentInterface } from "@comet/cms-api";
+import { Embedded, Entity, OptionalProps, PrimaryKey, Property } from "@mikro-orm/core";
+import { Type } from "@nestjs/common";
+import { Field, ID, ObjectType } from "@nestjs/graphql";
+import { v4 } from "uuid";
+
+import { EmailCampaignScopeInterface } from "../../types";
+
+export interface BrevoContactLogInterface {
+    email: string;
+    userId: string;
+    scope: EmailCampaignScopeInterface;
+    createdAt: Date;
+    updatedAt: Date;
+}
+
+export function createBrevoContactLogEntity({ Scope }: { Scope: EmailCampaignScopeInterface }): Type<BrevoContactLogInterface> {
+    @Entity()
+    @ObjectType({
+        implements: () => [DocumentInterface],
+    })
+    class BrevoContactLog implements BrevoContactLogInterface, DocumentInterface {
+        [OptionalProps]?: "createdAt" | "updatedAt";
+
+        @PrimaryKey({ columnType: "uuid" })
+        @Field(() => ID)
+        id: string = v4();
+
+        @Property({ columnType: "text" })
+        @Field()
+        email: string;
+
+        @Property()
+        @Field()
+        userId: string;
path: packages/api/src/brevo-contact-log/entity/brevo-contact-log.entity.factory.ts
user: login: raphaelblum
body: Am i correct here:
 - email is from the user that was created in brevo
 - userId is the user that created the new brevo contact

We should clarify that more. Suggestions:
 - importedEmail/...
 - responsibleUserId/adminUserId/...


created_at: 2025-04-01T11:42:17Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,56 @@
+import { DocumentInterface } from "@comet/cms-api";
+import { Embedded, Entity, OptionalProps, PrimaryKey, Property } from "@mikro-orm/core";
+import { Type } from "@nestjs/common";
+import { Field, ID, ObjectType } from "@nestjs/graphql";
+import { v4 } from "uuid";
+
+import { EmailCampaignScopeInterface } from "../../types";
+
+export interface BrevoContactLogInterface {
+    email: string;
+    userId: string;
+    scope: EmailCampaignScopeInterface;
+    createdAt: Date;
+    updatedAt: Date;
+}
+
+export function createBrevoContactLogEntity({ Scope }: { Scope: EmailCampaignScopeInterface }): Type<BrevoContactLogInterface> {
+    @Entity()
+    @ObjectType({
+        implements: () => [DocumentInterface],
+    })
+    class BrevoContactLog implements BrevoContactLogInterface, DocumentInterface {
path: packages/api/src/brevo-contact-log/entity/brevo-contact-log.entity.factory.ts
user: login: raphaelblum
body: I think the name should be more precise. Suggestion: BrevoEmailImportLog
created_at: 2025-04-01T11:43:08Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,56 @@
+import { DocumentInterface } from "@comet/cms-api";
+import { Embedded, Entity, OptionalProps, PrimaryKey, Property } from "@mikro-orm/core";
+import { Type } from "@nestjs/common";
+import { Field, ID, ObjectType } from "@nestjs/graphql";
+import { v4 } from "uuid";
+
+import { EmailCampaignScopeInterface } from "../../types";
+
+export interface BrevoContactLogInterface {
+    email: string;
+    userId: string;
+    scope: EmailCampaignScopeInterface;
+    createdAt: Date;
+    updatedAt: Date;
+}
+
+export function createBrevoContactLogEntity({ Scope }: { Scope: EmailCampaignScopeInterface }): Type<BrevoContactLogInterface> {
+    @Entity()
+    @ObjectType({
+        implements: () => [DocumentInterface],
+    })
+    class BrevoContactLog implements BrevoContactLogInterface, DocumentInterface {
+        [OptionalProps]?: "createdAt" | "updatedAt";
+
+        @PrimaryKey({ columnType: "uuid" })
+        @Field(() => ID)
+        id: string = v4();
+
+        @Property({ columnType: "text" })
+        @Field()
+        email: string;
path: packages/api/src/brevo-contact-log/entity/brevo-contact-log.entity.factory.ts
user: login: raphaelblum
body: Is it okay that we lose the information if the user was imported in bulk (with csv import) or manually created?

We could:
 - Save it as a string array (probably bad because potentially a lot of users can be imported in csv file)
 - Save it in an extra table and make a relation here
 - add a type field ("csvImport"/"manualCreation") and another importId where every user that was in the same csv has the same importId

created_at: 2025-04-01T11:47:50Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,28 @@
+import { EntityManager, EntityRepository } from "@mikro-orm/core";
+import { InjectRepository } from "@mikro-orm/nestjs";
+import { Injectable } from "@nestjs/common";
+import { EmailCampaignScopeInterface } from "src/types";
+
+import { BrevoContactLogInterface } from "./entity/brevo-contact-log.entity.factory";
+
+@Injectable()
+export class BrevoContactLogService {
+    constructor(
+        @InjectRepository("BrevoContactLog") private readonly repository: EntityRepository<BrevoContactLogInterface>,
+        private readonly entityManager: EntityManager,
+    ) {}
+    public async addContactsToLogs(emails: string[], userId: string, scope: EmailCampaignScopeInterface): Promise<BrevoContactLogInterface[]> {
path: packages/api/src/brevo-contact-log/brevo-contact-log.service.ts
user: login: raphaelblum
body: Do we need to pass an array here? I can't find the usage 
created_at: 2025-04-01T11:50:11Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,28 @@
+import { EntityManager, EntityRepository } from "@mikro-orm/core";
+import { InjectRepository } from "@mikro-orm/nestjs";
+import { Injectable } from "@nestjs/common";
+import { EmailCampaignScopeInterface } from "src/types";
+
+import { BrevoContactLogInterface } from "./entity/brevo-contact-log.entity.factory";
+
+@Injectable()
+export class BrevoContactLogService {
+    constructor(
+        @InjectRepository("BrevoContactLog") private readonly repository: EntityRepository<BrevoContactLogInterface>,
+        private readonly entityManager: EntityManager,
+    ) {}
+    public async addContactsToLogs(emails: string[], userId: string, scope: EmailCampaignScopeInterface): Promise<BrevoContactLogInterface[]> {
path: packages/api/src/brevo-contact-log/brevo-contact-log.service.ts
user: login: juliawegmayr
body: bf775ec9cc889a12e732a0f20b47ff709ac18c00
created_at: 2025-04-02T08:01:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,56 @@
+import { DocumentInterface } from "@comet/cms-api";
+import { Embedded, Entity, OptionalProps, PrimaryKey, Property } from "@mikro-orm/core";
+import { Type } from "@nestjs/common";
+import { Field, ID, ObjectType } from "@nestjs/graphql";
+import { v4 } from "uuid";
+
+import { EmailCampaignScopeInterface } from "../../types";
+
+export interface BrevoContactLogInterface {
+    email: string;
+    userId: string;
+    scope: EmailCampaignScopeInterface;
+    createdAt: Date;
+    updatedAt: Date;
+}
+
+export function createBrevoContactLogEntity({ Scope }: { Scope: EmailCampaignScopeInterface }): Type<BrevoContactLogInterface> {
+    @Entity()
+    @ObjectType({
+        implements: () => [DocumentInterface],
+    })
+    class BrevoContactLog implements BrevoContactLogInterface, DocumentInterface {
+        [OptionalProps]?: "createdAt" | "updatedAt";
+
+        @PrimaryKey({ columnType: "uuid" })
+        @Field(() => ID)
+        id: string = v4();
+
+        @Property({ columnType: "text" })
+        @Field()
+        email: string;
path: packages/api/src/brevo-contact-log/entity/brevo-contact-log.entity.factory.ts
user: login: juliawegmayr
body: I added a field called contactSource (taking "csvImport"/"manualCreation") and one for the importId as well. The importId is not relevant in this PR, as I only add the logs for manual created contacts here. I will update the upcoming PR for the imports as well. 

bb05f806bca0f82f4e42dee66b6dce9894a691fb
created_at: 2025-04-02T08:03:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,56 @@
+import { DocumentInterface } from "@comet/cms-api";
+import { Embedded, Entity, OptionalProps, PrimaryKey, Property } from "@mikro-orm/core";
+import { Type } from "@nestjs/common";
+import { Field, ID, ObjectType } from "@nestjs/graphql";
+import { v4 } from "uuid";
+
+import { EmailCampaignScopeInterface } from "../../types";
+
+export interface BrevoContactLogInterface {
+    email: string;
+    userId: string;
+    scope: EmailCampaignScopeInterface;
+    createdAt: Date;
+    updatedAt: Date;
+}
+
+export function createBrevoContactLogEntity({ Scope }: { Scope: EmailCampaignScopeInterface }): Type<BrevoContactLogInterface> {
+    @Entity()
+    @ObjectType({
+        implements: () => [DocumentInterface],
+    })
+    class BrevoContactLog implements BrevoContactLogInterface, DocumentInterface {
path: packages/api/src/brevo-contact-log/entity/brevo-contact-log.entity.factory.ts
user: login: juliawegmayr
body: 4a0a9cc4b4d5af4fabaec9446aa81f0ae33f7375 & aab4e8357cecabfbb79a0009c1ac27f2957a76ef
created_at: 2025-04-02T08:03:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,56 @@
+import { DocumentInterface } from "@comet/cms-api";
+import { Embedded, Entity, OptionalProps, PrimaryKey, Property } from "@mikro-orm/core";
+import { Type } from "@nestjs/common";
+import { Field, ID, ObjectType } from "@nestjs/graphql";
+import { v4 } from "uuid";
+
+import { EmailCampaignScopeInterface } from "../../types";
+
+export interface BrevoContactLogInterface {
+    email: string;
+    userId: string;
+    scope: EmailCampaignScopeInterface;
+    createdAt: Date;
+    updatedAt: Date;
+}
+
+export function createBrevoContactLogEntity({ Scope }: { Scope: EmailCampaignScopeInterface }): Type<BrevoContactLogInterface> {
+    @Entity()
+    @ObjectType({
+        implements: () => [DocumentInterface],
+    })
+    class BrevoContactLog implements BrevoContactLogInterface, DocumentInterface {
+        [OptionalProps]?: "createdAt" | "updatedAt";
+
+        @PrimaryKey({ columnType: "uuid" })
+        @Field(() => ID)
+        id: string = v4();
+
+        @Property({ columnType: "text" })
+        @Field()
+        email: string;
+
+        @Property()
+        @Field()
+        userId: string;
path: packages/api/src/brevo-contact-log/entity/brevo-contact-log.entity.factory.ts
user: login: juliawegmayr
body: ec0d4ba216f543f0e23eac8ce2dfc0b3c668893a
created_at: 2025-04-02T08:04:32Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-04-02T08:17:21Z
body: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=vivid-planet_comet-brevo-module&pullRequest=287) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=vivid-planet_comet-brevo-module&pullRequest=287&metric=new_coverage&view=list) (required ≥ 80%)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=vivid-planet_comet-brevo-module&pullRequest=287)


author_association: NONE