Skip to content

Commit 8c01052

Browse files
committed
Add notifications module with application service and domain event subscriber
1 parent bf5944e commit 8c01052

11 files changed

Lines changed: 228 additions & 0 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { EmailSender } from '../../domain/EmailSender';
2+
import { EmailAddress } from '../../domain/EmailAddress';
3+
import { WelcomeUserEmail } from '../../domain/WelcomeUserEmail';
4+
import { WelcomeUserEmailError } from '../../domain/WelcomeUserEmailError';
5+
6+
export default class SendWelcomeUserEmail {
7+
constructor(private emailSender: EmailSender) {}
8+
9+
async run(userEmailAddress: EmailAddress): Promise<void> {
10+
const welcomeUserEmail = new WelcomeUserEmail(userEmailAddress);
11+
try {
12+
await this.emailSender.send(welcomeUserEmail);
13+
} catch (error) {
14+
throw new WelcomeUserEmailError(userEmailAddress);
15+
}
16+
}
17+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { DomainEventSubscriber } from '../../../../Shared/domain/DomainEventSubscriber';
2+
import { UserRegisteredDomainEvent } from '../../domain/UserRegisteredDomainEvent';
3+
import { DomainEventClass } from '../../../../Shared/domain/DomainEvent';
4+
import SendWelcomeUserEmail from './SendWelcomeUserEmail';
5+
import { EmailAddress } from '../../domain/EmailAddress';
6+
7+
export default class SendWelcomeUserEmailOnUserRegistered implements DomainEventSubscriber<UserRegisteredDomainEvent> {
8+
constructor(private sendWelcomeUserEmail: SendWelcomeUserEmail) {}
9+
10+
subscribedTo(): DomainEventClass[] {
11+
return [UserRegisteredDomainEvent];
12+
}
13+
14+
async on(domainEvent: UserRegisteredDomainEvent): Promise<void> {
15+
const userEmailAddress = new EmailAddress(domainEvent.userEmailAddress);
16+
await this.sendWelcomeUserEmail.run(userEmailAddress);
17+
}
18+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { EmailAddress } from './EmailAddress';
2+
import { EmailId } from './EmailId';
3+
import { Uuid } from '../../../Shared/domain/value-object/Uuid';
4+
5+
type ConstructorParams = {
6+
id?: EmailId;
7+
from: EmailAddress;
8+
to: EmailAddress;
9+
subject: string;
10+
body: string;
11+
};
12+
13+
export class Email {
14+
readonly id: EmailId;
15+
readonly from: EmailAddress;
16+
readonly to: EmailAddress;
17+
readonly subject: string;
18+
readonly body: string;
19+
20+
constructor(params: ConstructorParams) {
21+
this.id = params.id || new EmailId(Uuid.random().value);
22+
this.from = params.from;
23+
this.to = params.to;
24+
this.subject = params.subject;
25+
this.body = params.body;
26+
}
27+
28+
equals(otherEmail: Email): boolean {
29+
return (
30+
this.id.value === otherEmail.id.value &&
31+
this.from.value === otherEmail.from.value &&
32+
this.to.value === otherEmail.to.value &&
33+
this.subject === otherEmail.subject &&
34+
this.body === otherEmail.body
35+
);
36+
}
37+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { StringValueObject } from '../../../Shared/domain/value-object/StringValueObject';
2+
3+
export class EmailAddress extends StringValueObject {}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { Uuid } from '../../../Shared/domain/value-object/Uuid';
2+
3+
export class EmailId extends Uuid {}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { Email } from './Email';
2+
3+
export interface EmailSender {
4+
send(email: Email): Promise<void>;
5+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { DomainEvent } from '../../../Shared/domain/DomainEvent';
2+
3+
type UserRegisteredDomainEventBody = { userEmailAddress: string };
4+
5+
export class UserRegisteredDomainEvent extends DomainEvent {
6+
static readonly EVENT_NAME = 'user.registered';
7+
readonly userEmailAddress: string;
8+
9+
constructor(data: {
10+
id: string;
11+
userEmailAddress: string;
12+
eventId?: string;
13+
occurredOn?: Date;
14+
}) {
15+
const { id, eventId, occurredOn, userEmailAddress } = data;
16+
super(UserRegisteredDomainEvent.EVENT_NAME, id, eventId, occurredOn);
17+
this.userEmailAddress = userEmailAddress;
18+
}
19+
20+
toPrimitive(): Object {
21+
return { userEmailAddress: this.userEmailAddress };
22+
}
23+
24+
static fromPrimitives(
25+
aggregateId: string,
26+
body: UserRegisteredDomainEventBody,
27+
eventId: string,
28+
occurredOn: Date
29+
): DomainEvent {
30+
return new UserRegisteredDomainEvent({
31+
id: aggregateId,
32+
userEmailAddress: body.userEmailAddress,
33+
eventId,
34+
occurredOn
35+
});
36+
}
37+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Email } from './Email';
2+
import { EmailAddress } from './EmailAddress';
3+
4+
export class WelcomeUserEmail extends Email {
5+
constructor(to: EmailAddress) {
6+
super({
7+
from: new EmailAddress('welcome@foo.com'),
8+
to,
9+
subject: 'Welcome',
10+
body: 'Welcome to our platform'
11+
});
12+
}
13+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { EmailAddress } from './EmailAddress';
2+
3+
export class WelcomeUserEmailError extends Error {
4+
constructor(userEmailAddress: EmailAddress) {
5+
super(`Error sending WelcomeUser email to ${userEmailAddress.value}`);
6+
}
7+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { EmailSender } from '../../../../../src/Contexts/Mooc/Notifications/domain/EmailSender';
2+
import { Email } from '../../../../../src/Contexts/Mooc/Notifications/domain/Email';
3+
4+
export class EmailSenderMock implements EmailSender {
5+
private sendSpy = jest.fn();
6+
7+
async send(email: Email): Promise<void> {
8+
this.sendSpy(email);
9+
}
10+
11+
assertSentTimes(times: number): void {
12+
expect(this.sendSpy.mock.calls.length).toBe(times);
13+
}
14+
15+
lastEmailSent(): Email {
16+
const sendCalls = this.sendSpy.mock.calls;
17+
const lastSendCall = sendCalls[sendCalls.length - 1] || [];
18+
const lastEmailSent = lastSendCall[0] as Email;
19+
20+
return lastEmailSent;
21+
}
22+
}

0 commit comments

Comments
 (0)