-
Notifications
You must be signed in to change notification settings - Fork 47
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Add endpoint to send newsletter consent email #645
Changes from all commits
b88021a
1bbf282
ef9cc24
bca70ed
750a7cf
7376710
5848f97
3ab25f8
c438aad
1d19db7
803b53f
4336ecb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
/** | ||
* Create a chunked array of new Map() | ||
* @param map map to be chunked | ||
* @param chunkSize The size of the chunk | ||
* @returns Array chunk of new Map() | ||
*/ | ||
|
||
export function mapChunk<T extends Map<any, any>>(map: T, chunkSize: number) { | ||
return Array.from(map.entries()).reduce<T[]>((chunk, curr, index) => { | ||
const ch = Math.floor(index / chunkSize) | ||
if (!chunk[ch]) { | ||
chunk[ch] = new Map() as T | ||
} | ||
chunk[ch].set(curr[0], curr[1]) | ||
return chunk | ||
}, []) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import { ApiProperty } from '@nestjs/swagger' | ||
import { Expose } from 'class-transformer' | ||
import { IsDateString, IsNumber, IsOptional, IsString } from 'class-validator' | ||
|
||
export class MassMailDto { | ||
@ApiProperty() | ||
@Expose() | ||
@IsString() | ||
listId: string | ||
|
||
@ApiProperty() | ||
@Expose() | ||
@IsString() | ||
templateId: string | ||
|
||
@ApiProperty() | ||
@Expose() | ||
@IsString() | ||
@IsOptional() | ||
subject: string | ||
|
||
//Sendgrid limits sending emails to 1000 at once. | ||
@ApiProperty() | ||
@Expose() | ||
@IsNumber() | ||
@IsOptional() | ||
chunkSize = 1000 | ||
|
||
//Remove users registered after the dateThreshold from mail list | ||
@ApiProperty() | ||
@Expose() | ||
@IsDateString() | ||
@IsOptional() | ||
dateThreshold: Date = new Date() | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,11 +27,28 @@ import { | |
UnregisteredNotificationConsent, | ||
} from '@prisma/client' | ||
import { NotificationsProviderInterface } from './providers/notifications.interface.providers' | ||
import { SendGridParams } from './providers/notifications.sendgrid.types' | ||
import { ContactsResponse, SendGridParams } from './providers/notifications.sendgrid.types' | ||
import { DateTime } from 'luxon' | ||
import * as crypto from 'crypto' | ||
import { CampaignService } from '../campaign/campaign.service' | ||
import { KeycloakTokenParsed } from '../auth/keycloak' | ||
import { MassMailDto } from './dto/massmail.dto' | ||
import { randomUUID } from 'crypto' | ||
import { mapChunk } from '../common/mapChunk' | ||
|
||
type UnregisteredInsert = { | ||
id: string | ||
email: string | ||
consent: boolean | ||
} | ||
|
||
type MailList = { | ||
id: string | ||
hash: string | ||
registered: boolean | ||
} | ||
|
||
export type ContactsMap = Map<string, MailList> | ||
|
||
@Injectable() | ||
export class MarketingNotificationsService { | ||
|
@@ -492,4 +509,88 @@ export class MarketingNotificationsService { | |
|
||
return minutesPassed <= period | ||
} | ||
|
||
private generateMapFromMailList(emailList: string[], contacts: ContactsMap): void { | ||
for (const email of emailList) { | ||
const id = randomUUID() | ||
|
||
contacts.set(email, { | ||
id: id, | ||
hash: this.generateHash(id), | ||
registered: false, | ||
}) | ||
} | ||
} | ||
|
||
private updateMailListMap( | ||
regUser: Person[], | ||
contacts: ContactsMap, | ||
skipAfterDate: Date, | ||
unregisteredConsent: UnregisteredNotificationConsent[], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. unregisteredConsent -> unregisteredConsents |
||
) { | ||
for (const registeredUser of regUser) { | ||
const createdAt = new Date(registeredUser.createdAt) | ||
|
||
// Remove email if it belongs to user created after the change has been deployed, as they had already decided | ||
// whether to give consent or not. | ||
if (contacts.get(registeredUser.email as string) && createdAt > skipAfterDate) { | ||
Logger.debug(`Removing email ${registeredUser.email} from list`) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we add a bit more in the log: removing email XXX from the list because of date and same later below: removing email XXX because the user don't want them. |
||
contacts.delete(registeredUser.email as string) | ||
continue | ||
} | ||
//Update the value of this mail | ||
contacts.set(registeredUser.email as string, { | ||
id: registeredUser.id, | ||
hash: this.generateHash(registeredUser.id), | ||
registered: true, | ||
}) | ||
} | ||
|
||
Logger.debug('Removing emails in unregistered consent emails') | ||
for (const consent of unregisteredConsent) { | ||
if (contacts.has(consent.email)) { | ||
Logger.debug(`Removing email ${consent.email}`) | ||
contacts.delete(consent.email) | ||
continue | ||
} | ||
} | ||
} | ||
|
||
private async insertUnregisteredConsentFromContacts(contacts: ContactsMap) { | ||
const emailsToAdd: UnregisteredInsert[] = [] | ||
for (const [key, value] of contacts) { | ||
if (value.registered) continue | ||
emailsToAdd.push({ id: value.id, email: key, consent: false }) | ||
} | ||
|
||
await this.prisma.unregisteredNotificationConsent.createMany({ | ||
data: emailsToAdd, | ||
}) | ||
} | ||
async sendConsentMail(data: MassMailDto) { | ||
const contacts = await this.marketingNotificationsProvider.getContactsFromList(data) | ||
|
||
const sendList: ContactsMap = new Map() | ||
const emailList = contacts.map((contact: ContactsResponse) => contact.email) | ||
this.generateMapFromMailList(emailList, sendList) | ||
const registeredMails = await this.prisma.person.findMany({ | ||
where: { email: { in: emailList } }, | ||
}) | ||
|
||
const unregisteredUsers = await this.prisma.unregisteredNotificationConsent.findMany() | ||
|
||
const skipUsersAfterDate = new Date(data.dateThreshold) | ||
this.updateMailListMap(registeredMails, sendList, skipUsersAfterDate, unregisteredUsers) | ||
|
||
await this.insertUnregisteredConsentFromContacts(sendList) | ||
|
||
const contactsChunked = mapChunk<ContactsMap>(sendList, data.chunkSize) | ||
Logger.debug(`Splitted email list into ${contactsChunked.length} chunk`) | ||
await this.marketingNotificationsProvider.sendBulkEmail( | ||
data, | ||
contactsChunked, | ||
'Podkrepi.BG Newsletter Subscription Consent', | ||
) | ||
return { contactCount: sendList.size } | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
regUser -> regUsers