add email builder class for replacing sendEmailVariants later

This commit is contained in:
einhorn_b 2023-11-11 17:30:49 +01:00
parent 99a98f6e84
commit bc064bdecf
2 changed files with 234 additions and 0 deletions

View File

@ -0,0 +1,220 @@
import { Contribution } from '@entity/Contribution'
import { Transaction } from '@entity/Transaction'
import { User } from '@entity/User'
import { CONFIG } from '@/config'
import { LogError } from '@/server/LogError'
import { decimalSeparatorByLanguage, resetInterface } from '@/util/utilities'
import { sendEmailTranslated } from './sendEmailTranslated'
export interface EmailLocals {
firstName: string
lastName: string
locale: string
supportEmail: string
communityURL: string
senderFirstName?: string
senderLastName?: string
senderEmail?: string
contributionMemo?: string
contributionAmount?: string
overviewURL?: string
activationLink?: string
timeDurationObject?: Date
resendLink?: string
resetLink?: string
transactionMemo?: string
transactionAmount?: string
[key: string]: string | Date | undefined
}
export enum EmailType {
NONE = 'none',
ACCOUNT_ACTIVATION = 'accountActivation',
ACCOUNT_MULTI_REGISTRATION = 'accountMultiRegistration',
ADDED_CONTRIBUTION_MESSAGE = 'addedContributionMessage',
CONTRIBUTION_CONFIRMED = 'contributionConfirmed',
CONTRIBUTION_DELETED = 'contributionDeleted',
CONTRIBUTION_DENIED = 'contributionDenied',
CONTRIBUTION_CHANGED_BY_MODERATOR = 'contributionChangedByModerator',
RESET_PASSWORD = 'resetPassword',
TRANSACTION_LINK_REDEEMED = 'transactionLinkRedeemed',
TRANSACTION_RECEIVED = 'transactionReceived',
}
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
export class EmailBuilder {
private receiver: { to: string }
private type: EmailType
private locals: EmailLocals
// https://refactoring.guru/design-patterns/builder/typescript/example
/**
* A fresh builder instance should contain a blank product object, which is
* used in further assembly.
*/
constructor() {
this.reset()
}
public reset(): void {
this.receiver.to = ''
this.type = EmailType.NONE
this.locals = resetInterface(this.locals)
}
protected setLocalsFromConfig(): void {
this.locals.overviewURL = CONFIG.EMAIL_LINK_OVERVIEW
this.locals.supportEmail = CONFIG.COMMUNITY_SUPPORT_MAIL
this.locals.communityURL = CONFIG.COMMUNITY_URL
switch (this.type) {
case EmailType.ACCOUNT_ACTIVATION:
case EmailType.ACCOUNT_MULTI_REGISTRATION:
case EmailType.RESET_PASSWORD:
this.locals.resendLink = CONFIG.EMAIL_LINK_FORGOTPASSWORD
}
}
protected checkIfFieldsSet(names: string[]): void {
for (const name of names) {
// eslint-disable-next-line security/detect-object-injection
if (!this.locals[name]) {
throw new LogError(`missing field with ${name}`)
}
}
}
/**
* check if non default fields a set for type
*/
protected checkRequiredFields(): void {
switch (this.type) {
case EmailType.NONE:
throw new LogError('please call setType before to set email type')
case EmailType.ACCOUNT_ACTIVATION:
this.checkIfFieldsSet(['activationLink', 'timeDurationObject', 'resendLink'])
break
case EmailType.ACCOUNT_MULTI_REGISTRATION:
this.checkIfFieldsSet(['resendLink'])
break
// CONTRIBUTION_CONFIRMED has same required fields as ADDED_CONTRIBUTION_MESSAGE plus contributionAmount
case EmailType.CONTRIBUTION_CONFIRMED:
this.checkIfFieldsSet(['contributionAmount'])
// eslint-disable-next-line no-fallthrough
case EmailType.ADDED_CONTRIBUTION_MESSAGE:
case EmailType.CONTRIBUTION_DELETED:
case EmailType.CONTRIBUTION_DENIED:
this.checkIfFieldsSet(['senderFirstName', 'senderLastName', 'contributionMemo'])
break
case EmailType.CONTRIBUTION_CHANGED_BY_MODERATOR:
// this.checkIfFieldsSet([''])
break
case EmailType.RESET_PASSWORD:
this.checkIfFieldsSet(['resetLink', 'timeDurationObject', 'resendLink'])
break
// TRANSACTION_LINK_REDEEMED has same required fields as TRANSACTION_RECEIVED plus transactionMemo
case EmailType.TRANSACTION_LINK_REDEEMED:
this.checkIfFieldsSet(['transactionMemo'])
// eslint-disable-next-line no-fallthrough
case EmailType.TRANSACTION_RECEIVED:
this.checkIfFieldsSet([
'senderFirstName',
'senderLastName',
'senderEmail',
'transactionAmount',
])
break
}
}
/**
* Concrete Builders are supposed to provide their own methods for
* retrieving results. That's because various types of builders may create
* entirely different products that don't follow the same interface.
* Therefore, such methods cannot be declared in the base Builder interface
* (at least in a statically typed programming language).
*
* Usually, after returning the end result to the client, a builder instance
* is expected to be ready to start producing another product. That's why
* it's a usual practice to call the reset method at the end of the
* `getProduct` method body. However, this behavior is not mandatory, and
* you can make your builders wait for an explicit reset call from the
* client code before disposing of the previous result.
*/
public sendEmail(): Promise<Record<string, unknown> | boolean | null> {
this.setLocalsFromConfig()
// will throw if a field is missing
this.checkRequiredFields()
const result = sendEmailTranslated({
receiver: this.receiver,
template: this.type.toString(),
locals: this.locals,
})
this.reset()
return result
}
public setRecipient(recipient: User): this {
this.receiver.to = `${recipient.firstName} ${recipient.lastName} <${recipient.emailContact.email}>`
this.locals.firstName = recipient.firstName
this.locals.lastName = recipient.lastName
return this
}
public setSender(sender: User): this {
this.locals.senderEmail = sender.emailContact.email
this.locals.senderFirstName = sender.firstName
this.locals.senderLastName = sender.lastName
return this
}
public setType(type: EmailType): this {
this.type = type
return this
}
public setLanguage(locale: string): this {
this.locals.locale = locale
return this
}
public setResetLink(resetLink: string): this {
this.locals.resentLink = resetLink
return this
}
public setContribution(contribution: Contribution): this {
this.locals.contributionMemo = contribution.memo
if (!this.locals.locale || this.locals.locale === '') {
throw new LogError('missing locale please call setLanguage before')
}
this.locals.contributionAmount = decimalSeparatorByLanguage(
contribution.amount,
this.locals.locale,
)
return this
}
public setTransaction(transaction: Transaction): this {
this.locals.transactionMemo = transaction.memo
if (!this.locals.locale || this.locals.locale === '') {
throw new LogError('missing locale please call setLanguage before')
}
this.locals.transactionAmount = decimalSeparatorByLanguage(
transaction.amount,
this.locals.locale,
)
return this
}
public setActivationLink(activationLink: string): this {
this.locals.activationLink = activationLink
return this
}
public setTimeDurationObject(timeDurationObject: Date): this {
this.locals.timeDurationObject = timeDurationObject
return this
}
}

View File

@ -15,3 +15,17 @@ export const decimalSeparatorByLanguage = (a: Decimal, language: string): string
export const fullName = (firstName: string, lastName: string): string =>
[firstName, lastName].filter(Boolean).join(' ')
// Function to reset an interface by chatGPT
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function resetInterface<T extends Record<string, any>>(obj: T): T {
// Iterate over all properties of the object
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
// Set all optional properties to undefined
// eslint-disable-next-line security/detect-object-injection
obj[key] = undefined as T[Extract<keyof T, string>]
}
}
return obj
}