Skip to content
This repository was archived by the owner on Apr 21, 2025. It is now read-only.
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
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "brainbox-backend",
"version": "1.6.0",
"version": "1.7.0",
"description": "Backend for BrainBox",
"author": "lzaycoe (Lazy Code)",
"private": true,
Expand Down Expand Up @@ -39,6 +39,7 @@
"cookie-parser": "1.4.7",
"express": "4.21.2",
"morgan": "1.10.0",
"nodemailer": "6.10.0",
"passport": "0.7.0",
"passport-custom": "1.1.1",
"passport-jwt": "4.0.1",
Expand All @@ -58,6 +59,7 @@
"@types/express": "5.0.0",
"@types/morgan": "1.9.9",
"@types/node": "20.17.0",
"@types/nodemailer": "^6.4.17",
"@types/passport-jwt": "4.0.1",
"@types/passport-local": "1.0.38",
"@typescript-eslint/eslint-plugin": "8.24.0",
Expand Down
19 changes: 19 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
Warnings:

- You are about to drop the column `courseId` on the `Revenue` table. All the data in the column will be lost.

*/
-- DropForeignKey
ALTER TABLE "Revenue" DROP CONSTRAINT "Revenue_courseId_fkey";

-- AlterTable
ALTER TABLE "Revenue" DROP COLUMN "courseId";
3 changes: 0 additions & 3 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ model Course {
sections Section[]
payments Payment[]
progress Progress[]
revenues Revenue[]
}

model Section {
Expand Down Expand Up @@ -174,8 +173,6 @@ model Revenue {
id Int @id @default(autoincrement())
teacherId Int
teacher User @relation(fields: [teacherId], references: [id])
courseId Int
course Course @relation(fields: [courseId], references: [id])
totalRevenue Decimal @default(0.0)
totalWithdrawn Decimal @default(0.0)
serviceFee Decimal @default(0.0)
Expand Down
2 changes: 2 additions & 0 deletions src/domains/domains.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { CoursesModule } from '@/courses/courses.module';
import { PaymentsModule } from '@/payments/payments.module';
import { RevenuesModule } from '@/revenues/revenues.module';
import { UsersModule } from '@/users/users.module';
import { WithdrawalsModule } from '@/withdrawals/withdrawals.module';

@Module({
imports: [
Expand All @@ -15,6 +16,7 @@ import { UsersModule } from '@/users/users.module';
PaymentsModule,
RevenuesModule,
UsersModule,
WithdrawalsModule,
],
})
export class DomainsModule {}
80 changes: 80 additions & 0 deletions src/domains/notifies/email.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Injectable } from '@nestjs/common';
import * as nodemailer from 'nodemailer';

import { emailSignature } from '@/notifies/template/email-signature';
import { withdrawalRejected } from '@/notifies/template/withdrawal-rejected';
import { withdrawalSuccess } from '@/notifies/template/withdrawal-success';

@Injectable()
export class EmailService {
private readonly transporter;

constructor() {
this.transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.EMAIL_USERNAME,
pass: process.env.EMAIL_PASSWORD,
},
debug: true,
logger: true,
secure: true,
tls: {
rejectUnauthorized: false,
},
});
}

async sendEmail(
to: string,
subject: string,
teacherName: string,
amount: string,
transactionDate: string,
template: 'withdrawal-success' | 'withdrawal-rejected',
) {
let htmlContent = '';

const templateMap = {
'withdrawal-success': withdrawalSuccess,
'withdrawal-rejected': withdrawalRejected,
};

if (templateMap[template]) {
htmlContent = templateMap[template]
.replaceAll('{{TEACHER_NAME}}', teacherName)
.replaceAll('{{AMOUNT}}', amount)
.replaceAll('{{TRANSACTION_DATE}}', transactionDate)
.replaceAll(
'{{EMAIL_USERNAME}}',
process.env.EMAIL_USERNAME || 'brainbox.platform@gmail.com',
);
}

let signature = emailSignature;
if (typeof signature === 'string') {
signature = signature
.replaceAll(
'{{FRONTEND_URL}}',
process.env.FRONTEND_URL || 'https://brainbox-platform.vercel.app',
)
.replaceAll(
'{{EMAIL_USERNAME}}',
process.env.EMAIL_USERNAME || 'brainbox.platform@gmail.com',
);
}

const mailOptions = {
from: process.env.EMAIL_USERNAME,
to,
subject,
html: htmlContent + signature,
};

try {
await this.transporter.sendMail(mailOptions);
} catch (error) {
console.error('Error sending email:', error);
}
}
}
19 changes: 19 additions & 0 deletions src/domains/notifies/template/email-signature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export const emailSignature = `
<hr style="border: 0; border-top: 1px solid #ddd;">
<table style="font-family: Arial, sans-serif; font-size: 14px; color: #333; border-collapse: collapse; width: 100%; max-width: 500px;">
<tr>
<td style="padding: 10px; vertical-align: middle;">
<img src="https://res.cloudinary.com/dmggkeyon/image/upload/v1741569119/rsyf1u7bwviuxbfjv91h.png" alt="BrainBox Logo" style="border-radius: 8px; width: 80px; height: 80px;">
</td>
<td style="padding: 10px; vertical-align: middle;">
<strong style="font-size: 16px; color: #ff6636;">BrainBox Platform</strong><br>
<span style="color: #555;">E-learning Platform</span><br>
<a href="mailto:{{EMAIL_USERNAME}}" style="color: #ff6636; text-decoration: none;">{{EMAIL_USERNAME}}</a><br>
<a href="{{FRONTEND_URL}}" style="color: #ff6636; text-decoration: none;">{{FRONTEND_URL}}</a>
</td>
</tr>
</table>
<div class="footer">
<p>&copy; 2025 BrainBox Platform. All rights reserved.</p>
</div>
`;
16 changes: 16 additions & 0 deletions src/domains/notifies/template/withdrawal-rejected.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const withdrawalRejected = `
<div class="container">
<div class="header">
<h2 style="color: #ff6636">Withdrawal Failed 😞</h2>
</div>
<div class="content">
<p>Hello <strong>{{TEACHER_NAME}}</strong>,</p>
<p>We regret to inform you that your withdrawal request could not be processed. 🚫</p>
<p><strong>Amount: </strong> {{AMOUNT}}</p>
<p><strong>Request Time: </strong> {{TRANSACTION_DATE}}</p>
<p>Please check your bank account details and try again. 🏦</p>
<p>If you have any questions, please contact <a href="mailto:{{EMAIL_USERNAME}}" style="color: #ff6636; text-decoration: none;">{{EMAIL_USERNAME}}</a>. 📧</p>
<p>Thank you for being with <strong>BrainBox Platform</strong>! 🙏</p>
</div>
</div>
`;
16 changes: 16 additions & 0 deletions src/domains/notifies/template/withdrawal-success.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const withdrawalSuccess = `
<div class="container">
<div class="header">
<h2 style="color: #ff6636">Withdrawal Successful! 🎉</h2>
</div>
<div class="content">
<p>Hello <strong>{{TEACHER_NAME}}</strong>,</p>
<p>Congratulations! Your withdrawal request has been successfully processed. ✅</p>
<p><strong>Amount: </strong> {{AMOUNT}} 💰</p>
<p><strong>Processing Time: </strong> {{TRANSACTION_DATE}} ⏰</p>
<p>This amount has been transferred to your bank account. 🏦</p>
<p>If you have any questions, please contact <a href="mailto:{{EMAIL_USERNAME}}" style="color: #ff6636; text-decoration: none;">{{EMAIL_USERNAME}}</a>. 📧</p>
<p>Thank you for being with <strong>BrainBox Platform</strong>! 🙏</p>
</div>
</div>
`;
2 changes: 2 additions & 0 deletions src/domains/payments/payments.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { PaymentsService } from '@/payments/payments.service';
import { PayOSService } from '@/payments/payos.service';
import { ClerkClientProvider } from '@/providers/clerk.service';
import { PrismaService } from '@/providers/prisma.service';
import { RevenuesService } from '@/revenues/revenues.service';

@Module({
imports: [ConfigModule.forFeature(payOSConfig)],
Expand All @@ -17,6 +18,7 @@ import { PrismaService } from '@/providers/prisma.service';
PrismaService,
CoursesService,
PayOSService,
RevenuesService,
ClerkClientProvider,
],
exports: [PaymentsService],
Expand Down
33 changes: 30 additions & 3 deletions src/domains/payments/payments.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { CoursesService } from '@/courses/courses.service';
import { CreatePaymentDto } from '@/payments/dto/create-payment.dto';
import { PayOSService } from '@/payments/payos.service';
import { PrismaService } from '@/providers/prisma.service';
import { RevenuesService } from '@/revenues/revenues.service';

@Injectable()
export class PaymentsService {
Expand All @@ -20,6 +21,7 @@ export class PaymentsService {
private readonly prismaService: PrismaService,
private readonly coursesService: CoursesService,
private readonly payOSService: PayOSService,
private readonly revenuesService: RevenuesService,
@Inject('ClerkClient')
private readonly clerkClient: ClerkClient,
) {}
Expand Down Expand Up @@ -72,10 +74,11 @@ export class PaymentsService {

const orderCode = payload?.data?.orderCode;
const description = payload?.data?.description;
const amount = payload?.data?.amount;

this.logger.debug(`Processing orderCode: ${orderCode}`);

if (!orderCode || (orderCode == '123' && description == 'VQRIO123')) {
if (!orderCode || (orderCode === '123' && description === 'VQRIO123')) {
this.logger.warn(
'Received test webhook payload from PayOS, skipping processing.',
);
Expand All @@ -84,8 +87,8 @@ export class PaymentsService {

const payment = await this.prismaService.payment.findUnique({
where: { id: orderCode },
select: { id: true, userId: true },
});

if (!payment) {
this.logger.error(`Payment with ID ${orderCode} not found`);
throw new Error(`Payment with ID ${orderCode} not found`);
Expand Down Expand Up @@ -114,13 +117,37 @@ export class PaymentsService {
})
: Promise.resolve();

if (payment.courseId === null) {
this.logger.error(
`Payment with ID ${orderCode} has no associated course`,
);
throw new Error(`Payment with ID ${orderCode} has no associated course`);
}
const course = await this.coursesService.findOne(payment.courseId);
let updateRevenuePromise = Promise.resolve();

if (course) {
this.logger.debug(`Updating revenue for teacher ${course.teacherId}`);
updateRevenuePromise = this.revenuesService
.calculateRevenue(course.teacherId, amount)
.then(() => {});
} else {
this.logger.warn(
`Order ${payment.courseId} is not associated with a course, skipping revenue update.`,
);
}

const status = payload.success ? 'paid' : 'canceled';
const updatePaymentPromise = this.prismaService.payment.update({
where: { id: orderCode },
data: { status },
});

await Promise.all([updateRolePromise, updatePaymentPromise]);
await Promise.all([
updateRolePromise,
updatePaymentPromise,
updateRevenuePromise,
]);

this.logger.debug(`Payment ${orderCode} marked as ${status}`);
return 'Webhook processed successfully';
Expand Down
4 changes: 0 additions & 4 deletions src/domains/revenues/dto/create-revenue.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@ export class CreateRevenueDto {
@IsNotEmpty()
teacherId: number;

@ApiProperty()
@IsNotEmpty()
courseId: number;

@ApiProperty()
@IsNotEmpty()
totalRevenue: number;
Expand Down
Loading