Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 12 additions & 17 deletions apps/api/src/notifications/email/email.module.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,25 @@
import { Module } from '@nestjs/common';
import { EmailService } from './email.service';
import { MockEmailAdapter } from './adapters/mock.adapter';
import { SendGridEmailAdapter } from './adapters/sendgrid.adapter';
import { EmailTemplateService } from './templates/template.service';
import { EmailAdapter } from "./adapters/email.adapter",
import { MockEmailProvider } from './providers/mock.provider';
import { SendGridEmailProvider } from './providers/sendgrid.provider';

@Module({
providers: [
EmailService,
EmailTemplateService,
{
provide: 'EMAIL_ADAPTER',
provide: EmailAdapter,
useFactory: () => {
const provider = process.env.EMAIL_PROVIDER;

if (provider === 'sendgrid') {
return new SendGridEmailAdapter(
process.env.SENDGRID_API_KEY!,
process.env.EMAIL_FROM!,
);
switch (process.env.EMAIL_PROVIDER) {
case 'sendgrid':
return new SendGridEmailProvider();
default:
return new MockEmailProvider();
}

return new MockEmailAdapter();
},
},
{
provide: EmailService,
useFactory: (adapter) => new EmailService(adapter),
inject: ['EMAIL_ADAPTER'],
},
],
exports: [EmailService],
})
Expand Down
20 changes: 20 additions & 0 deletions apps/api/src/notifications/email/email.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
it('renders template and sends email', async () => {
const adapter = { send: jest.fn() };
const templates = {
render: jest.fn().mockReturnValue('<html />'),
};

const service = new EmailService(
adapter as any,
templates as any,
);

await service.sendEmail({
to: 'test@mail.com',
subject: 'Hello',
template: 'booking-confirmed',
data: { name: 'MD' },
});

expect(adapter.send).toHaveBeenCalled();
});
77 changes: 22 additions & 55 deletions apps/api/src/notifications/email/email.service.ts
Original file line number Diff line number Diff line change
@@ -1,70 +1,37 @@
import { Injectable, Logger } from '@nestjs/common';
import { SendEmailInput } from './email.types';
import { EmailTemplateService } from './templates/template.service';
import { EmailAdapter } from './adapters/email.adapter';
import { SendEmailPayload } from './email.types';
import { EmailLogger } from './email.logger';

import * as Handlebars from 'handlebars';
import * as fs from 'fs';
import * as path from 'path';

@Injectable()
export class EmailService {
private readonly logger = new Logger(EmailService.name);

constructor(private readonly adapter: EmailAdapter) {}
constructor(
private readonly adapter: EmailAdapter,
private readonly templates: EmailTemplateService,
) {}

async sendEmail(payload: SendEmailPayload) {
const html = this.renderTemplate(payload.template, payload.data);
async sendEmail(input: SendEmailInput): Promise<void> {
try {
const html = this.templates.render(
input.template,
input.data,
);

const result = await this.adapter.send({
...payload,
data: { html },
});
await this.adapter.send({
to: input.to,
subject: input.subject,
html,
});

if (!result.success) {
this.logger.log(`Email sent to ${input.to}`);
} catch (error) {
this.logger.error(
`Email failed → ${payload.to}`,
result.error,
);
} else {
this.logger.log(
`Email sent → ${payload.to} (${result.provider})`,
`Failed to send email to ${input.to}`,
error.stack,
);
throw error;
}

return result;
}

private renderTemplate(templateName: string, data: Record<string, any>) {
const templatePath = path.join(
__dirname,
'templates',
`${templateName}.hbs`,
);

const source = fs.readFileSync(templatePath, 'utf-8');
const template = Handlebars.compile(source);

return template(data);
}

async sendEmail(payload: SendEmailPayload) {
EmailLogger.logSendAttempt(payload);
EmailLogger.logTemplateRender(payload.template);

const html = this.renderTemplate(payload.template, payload.data);

const result = await this.adapter.send({
...payload,
data: { html },
});

if (result.success) {
EmailLogger.logSuccess(payload, result);
} else {
EmailLogger.logFailure(payload, result);
}

return result;
}
}
13 changes: 7 additions & 6 deletions apps/api/src/notifications/email/email.types.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
export interface SendEmailPayload {
export interface SendEmailInput {
to: string;
subject: string;
template: string;
data: Record<string, any>;
}

export interface EmailSendResult {
success: boolean;
provider: string;
messageId?: string;
error?: any;
export interface EmailProvider {
send(options: {
to: string;
subject: string;
html: string;
}): Promise<void>;
}
14 changes: 14 additions & 0 deletions apps/api/src/notifications/email/providers/mock.provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { EmailAdapter } from '../adapters/email.adapter';
import { Logger } from '@nestjs/common';

export class MockEmailProvider implements EmailAdapter {
private readonly logger = new Logger(MockEmailProvider.name);

async send({ to, subject, html }: any): Promise<void> {
this.logger.log(`[MOCK EMAIL]
To: ${to}
Subject: ${subject}
Body: ${html}
`);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { EmailAdapter } from "../adapters/email.adapter";

export class SendGridEmailProvider implements EmailAdapter {
async send({ to, subject, html }: any): Promise<void> {
// sendgrid.send({ to, subject, html })
}
}
Empty file.
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<h2>Booking Confirmed 🎉</h2>
<p>Hello {{userName}},</p>
<p>Hello {{name}},</p>

<p>Your booking for <strong>{{serviceName}}</strong> has been confirmed.</p>
<p>Your session with {{mentorName}} has been confirmed.</p>

<p>
Date: {{date}} <br/>
Reference: {{reference}}
Date: {{date}} <br />
Time: {{time}}
</p>
Empty file.
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
<h2>Payment Successful ✅</h2>

<p>Hi {{userName}},</p>
<p>Hi {{name}},</p>

<p>
Your payment of <strong>{{amount}}</strong> was successful.
</p>
<p>Your payment of <strong>{{amount}}</strong> was successful.</p>

<p>Transaction ID: {{transactionId}}</p>
20 changes: 20 additions & 0 deletions apps/api/src/notifications/email/templates/template.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as fs from 'fs';
import * as path from 'path';
import * as Handlebars from 'handlebars';
import { Injectable } from '@nestjs/common';

@Injectable()
export class EmailTemplateService {
render(templateName: string, data: Record<string, any>): string {
const filePath = path.join(
__dirname,
'templates',
`${templateName}.hbs`,
);

const source = fs.readFileSync(filePath, 'utf8');
const template = Handlebars.compile(source);

return template(data);
}
}
15 changes: 15 additions & 0 deletions apps/api/src/reviews/dto/create-review.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { IsInt, IsOptional, IsString, Max, Min, IsUUID } from 'class-validator';

export class CreateReviewDto {
@IsUUID()
sessionId: string;

@IsInt()
@Min(1)
@Max(5)
rating: number;

@IsOptional()
@IsString()
comment?: string;
}
41 changes: 41 additions & 0 deletions apps/api/src/reviews/entities/review.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
ManyToOne,
Index,
Unique,
} from 'typeorm';
import { User } from '@/users/entities/user.entity';
import { Session } from '@/sessions/entities/session.entity';



@Entity('reviews')
@Unique(['session', 'reviewer'])
@Index(['session', 'reviewer'])
export class Review {
@PrimaryGeneratedColumn('uuid')
id: string;

/** Mentee */
@ManyToOne(() => User, { eager: false })
reviewer: User;

/** Mentor */
@ManyToOne(() => User, { eager: false })
reviewee: User;

@ManyToOne(() => Session, { eager: false })
session: Session;

@Column({ type: 'int' })
rating: number; // 1–5

@Column({ type: 'text', nullable: true })
comment?: string;

@CreateDateColumn()
createdAt: Date;
}
29 changes: 29 additions & 0 deletions apps/api/src/reviews/reviews.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {
Body,
Controller,
Post,
UseGuards,
} from '@nestjs/common';

import { ReviewsService } from './reviews.service';
import { CreateReviewDto } from './dto/create-review.dto';

// import { AuthGuard } from '../auth/guards/auth.guard';
import { CurrentUser } from '../decorators/auth/current-user.decorator';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';

@UseGuards(JwtAuthGuard)
@Controller('reviews')
export class ReviewsController {
constructor(
private readonly reviewsService: ReviewsService,
) {}

@Post()
async create(
@CurrentUser('id') userId: string,
@Body() dto: CreateReviewDto,
) {
return this.reviewsService.createReview(userId, dto);
}
}
Empty file.
Empty file.
Loading
Loading