Skip to content

Commit

Permalink
Merge branch 'dev' into nacho-fix
Browse files Browse the repository at this point in the history
  • Loading branch information
na-cho-dev authored Mar 1, 2025
2 parents 6c02059 + 06283ce commit 0d24033
Show file tree
Hide file tree
Showing 18 changed files with 335 additions and 55 deletions.
8 changes: 8 additions & 0 deletions config/s3.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { registerAs } from '@nestjs/config';

export default registerAs('s3', () => ({
accessKey: process.env.AWS_ACCESS_KEY || '',
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '',
region: process.env.AWS_REGION || 'us-east-2',
bucketName: process.env.AWS_S3_BUCKET_NAME || '',
}));
6 changes: 6 additions & 0 deletions local repo
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
gerge branch 'dev' into feat/s3-resume-upload
# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,14 @@
"@nestjs/jwt": "^11.0.0",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.0.10",
"@nestjs/schedule": "^5.0.1",
"@nestjs/serve-static": "^5.0.3",
"@nestjs/swagger": "^11.0.5",
"@nestjs/typeorm": "^11.0.0",
"@types/nodemailer": "^6.4.17",
"@types/speakeasy": "^2.0.10",
"@vitalets/google-translate-api": "^9.2.1",
"aws-sdk": "^2.1692.0",
"bcrypt": "^5.1.1",
"bcryptjs": "^3.0.2",
"bull": "^4.16.5",
Expand All @@ -57,7 +59,7 @@
"file-type-mime": "^0.4.6",
"google-auth-library": "^9.15.1",
"handlebars": "^4.7.8",
"html-validator": "^6.0.1",
"html-validator": "^5.1.18",
"ioredis": "^5.5.0",
"joi": "^17.13.3",
"module-alias": "^2.2.3",
Expand All @@ -82,7 +84,7 @@
"devDependencies": {
"@commitlint/cli": "^19.7.1",
"@commitlint/config-conventional": "^19.7.1",
"@nestjs-modules/mailer": "^2.0.2",
"@nestjs-modules/mailer": "^1.6.1",
"@nestjs/cli": "^11.0.5",
"@nestjs/common": "^11.0.10",
"@nestjs/schematics": "^11.0.1",
Expand Down
4 changes: 2 additions & 2 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ import { ServeStaticModule } from '@nestjs/serve-static';
import { join } from 'path';
import { LanguageGuard } from '@guards/language.guard';
import { ApiStatusModule } from '@modules/api-status/api-status.module';

import s3Config from '@config/s3.config';
@Module({
providers: [
{
Expand Down Expand Up @@ -82,7 +82,7 @@ import { ApiStatusModule } from '@modules/api-status/api-status.module';
*/
envFilePath: ['.env.development.local', `.env.${process.env.PROFILE}`],
isGlobal: true,
load: [serverConfig, authConfig],
load: [serverConfig, authConfig, s3Config],
validationSchema: Joi.object({
NODE_ENV: Joi.string().valid('development', 'production', 'test', 'provision').required(),
PROFILE: Joi.string().valid('local', 'development', 'production', 'ci', 'testing', 'staging').required(),
Expand Down
1 change: 1 addition & 0 deletions src/modules/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { RequestSigninTokenDto } from './dto/request-signin-token.dto';
import { OtpDto } from '@modules/otp/dto/otp.dto';
import { DataSource, EntityManager } from 'typeorm';
import { CreateOrganisationRecordOptions } from '@modules/organisations/dto/create-organisation-options';

@Injectable()
export default class AuthenticationService {
constructor(
Expand Down
21 changes: 17 additions & 4 deletions src/modules/billing-plans/billing-plan.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,18 @@ import {
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { SuperAdminGuard } from '../../guards/super-admin.guard';
import { BillingPlanService } from './billing-plan.service';
import { skipAuth } from '@shared/helpers/skipAuth';
import { BillingPlanDto } from './dto/billing-plan.dto';
import {
createBillingPlanDocs,
deleteBillingPlanDocs,
getAllBillingPlansDocs,
getSingleBillingPlanDocs,
updateBillingPlanDocs,
} from './docs/billing-plan-docs';
import { SuperAdminGuard } from '@guards/super-admin.guard';
import { BillingPlanDto } from './dto/billing-plan.dto';
import { skipAuth } from '@shared/helpers/skipAuth';
import { UpdateBillingPlanDto } from './dto/update-billing-plan.dto';

@ApiTags('Billing Plans')
Expand Down Expand Up @@ -68,4 +68,17 @@ export class BillingPlanController {
async deleteBillingPlan(@Param('id', ParseUUIDPipe) id: string) {
return this.billingPlanService.deleteBillingPlan(id);
}

// For sending renewal reminders

@Post('send-renewal-reminder/:id')
@ApiOperation({ summary: 'Send renewal reminder for a billing plan' })
@ApiResponse({
status: 200,
description: 'Renewal reminder sent successfully.',
})
@ApiResponse({ status: 404, description: 'Billing plan not found' })
async sendRenewalReminder(@Param('id') id: string) {
return this.billingPlanService.sendRenewalReminder(id);
}
}
43 changes: 35 additions & 8 deletions src/modules/billing-plans/billing-plan.module.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,43 @@
import { Module } from '@nestjs/common';
import { BillingPlanService } from './billing-plan.service';
import { BillingPlanController } from './billing-plan.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BillingPlan } from './entities/billing-plan.entity';
import { User } from '@modules/user/entities/user.entity';
import { Organisation } from '@modules/organisations/entities/organisations.entity';
import { OrganisationUserRole } from '@modules/role/entities/organisation-user-role.entity';
import { Role } from '@modules/role/entities/role.entity';
import { BillingPlanController } from './billing-plan.controller';
import { BillingPlanService } from './billing-plan.service';
import { User } from '../user/entities/user.entity';
import { Organisation } from '../organisations/entities/organisations.entity';
import { OrganisationUserRole } from '../role/entities/organisation-user-role.entity';
import { Role } from '../role/entities/role.entity';
import { MailerModule } from '@nestjs-modules/mailer';
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter';
import { ScheduleModule } from '@nestjs/schedule';
import { SubscriptionScheduler } from './subscription-scheduler';

@Module({
imports: [TypeOrmModule.forFeature([BillingPlan, User, Organisation, OrganisationUserRole, Role])],
imports: [
TypeOrmModule.forFeature([BillingPlan, User, Organisation, OrganisationUserRole, Role]),
MailerModule.forRoot({
transport: {
host: 'smtp.example.com',
port: 587,
auth: {
user: '[email protected]',
pass: 'password',
},
},
defaults: {
from: '"No Reply" <[email protected]>',
},
template: {
dir: __dirname + '/../../email/hng-templates',
adapter: new HandlebarsAdapter(),
options: {
strict: true,
},
},
}),
ScheduleModule.forRoot(),
],
controllers: [BillingPlanController],
providers: [BillingPlanService],
providers: [BillingPlanService, SubscriptionScheduler],
})
export class BillingPlanModule {}
50 changes: 46 additions & 4 deletions src/modules/billing-plans/billing-plan.service.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
import { Injectable, HttpStatus, BadRequestException, NotFoundException } from '@nestjs/common';
import {
Injectable,
HttpStatus,
HttpException,
BadRequestException,
NotFoundException,
InternalServerErrorException,
} from '@nestjs/common';
import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { BillingPlan } from './entities/billing-plan.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { BillingPlanDto } from './dto/billing-plan.dto';
import { CustomHttpException } from '@shared/helpers/custom-http-filter';
import * as SYS_MSG from '@shared/constants/SystemMessages';
import { CustomHttpException } from '@shared/helpers/custom-http-filter';
import { BillingPlanMapper } from './mapper/billing-plan.mapper';
import { UpdateBillingPlanDto } from './dto/update-billing-plan.dto';
import { MailerService } from '@nestjs-modules/mailer';

@Injectable()
export class BillingPlanService {
constructor(
@InjectRepository(BillingPlan)
private readonly billingPlanRepository: Repository<BillingPlan>
private readonly billingPlanRepository: Repository<BillingPlan>,
private readonly mailerService: MailerService
) {}

async createBillingPlan(createBillingPlanDto: BillingPlanDto) {
Expand Down Expand Up @@ -81,4 +90,37 @@ export class BillingPlanService {
}
await this.billingPlanRepository.delete(id);
}

// New methods for subscription renewal reminders
async getAllSubscriptions(): Promise<BillingPlan[]> {
return this.billingPlanRepository.find();
}

async sendRenewalReminder(subscriptionId: string): Promise<void> {
const subscription = await this.billingPlanRepository.findOne({ where: { id: subscriptionId } });
if (!subscription) {
throw new NotFoundException('Subscription not found');
}

const expirationDate = new Date(subscription.expirationDate);
const currentDate = new Date();
const daysUntilExpiration = (expirationDate.getTime() - currentDate.getTime()) / (1000 * 3600 * 24);

if (daysUntilExpiration <= 7) {
try {
await this.mailerService.sendMail({
to: subscription.email,
subject: 'Subscription Renewal Reminder',
template: './renewal-reminder',
context: {
name: subscription.name,
expirationDate: subscription.expirationDate,
},
});
console.log(`Renewal reminder sent to ${subscription.email}`);
} catch (error) {
console.error(`Failed to send renewal reminder to ${subscription.email}: ${error.message}`);
}
}
}
}
6 changes: 6 additions & 0 deletions src/modules/billing-plans/entities/billing-plan.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,10 @@ export class BillingPlan extends AbstractBaseEntity {

@Column({ type: 'int', nullable: true })
amount: number;

@Column({ type: 'date', nullable: true })
expirationDate: Date;

@Column({ type: 'text', nullable: true })
email: string;
}
17 changes: 17 additions & 0 deletions src/modules/billing-plans/subscription-scheduler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Injectable } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { BillingPlanService } from './billing-plan.service';

@Injectable()
export class SubscriptionScheduler {
constructor(private readonly billingPlanService: BillingPlanService) {}

@Cron('0 0 * * *')
async handleCron() {
console.log('Running daily subscription renewal reminder job');
const subscriptions = await this.billingPlanService.getAllSubscriptions();
for (const subscription of subscriptions) {
await this.billingPlanService.sendRenewalReminder(subscription.id);
}
}
}
57 changes: 54 additions & 3 deletions src/modules/billing-plans/tests/billing-plan.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import 'module-alias/register';
import 'reflect-metadata';
import { Test, TestingModule } from '@nestjs/testing';
import { BillingPlanService } from '../billing-plan.service';
import { Repository } from 'typeorm';
Expand All @@ -9,6 +7,8 @@ import { NotFoundException, BadRequestException, HttpStatus } from '@nestjs/comm
import { CustomHttpException } from '@shared/helpers/custom-http-filter';
import * as SYS_MSG from '@shared/constants/SystemMessages';
import { BillingPlanMapper } from '../mapper/billing-plan.mapper';
import { SubscriptionScheduler } from '../subscription-scheduler';
import { MailerService } from '@nestjs-modules/mailer';

describe('BillingPlanService', () => {
let service: BillingPlanService;
Expand All @@ -22,6 +22,12 @@ describe('BillingPlanService', () => {
provide: getRepositoryToken(BillingPlan),
useClass: Repository,
},
{
provide: MailerService,
useValue: {
sendMail: jest.fn().mockResolvedValue(undefined),
},
},
],
}).compile();

Expand All @@ -48,6 +54,8 @@ describe('BillingPlanService', () => {
is_active: true,
created_at: new Date(),
updated_at: new Date(),
expirationDate: new Date(),
email: '[email protected]',
};

jest.spyOn(repository, 'findOne').mockResolvedValue(billingPlan as BillingPlan);
Expand All @@ -70,6 +78,8 @@ describe('BillingPlanService', () => {
is_active: true,
created_at: new Date(),
updated_at: new Date(),
expirationDate: new Date(),
email: '[email protected]',
},
{
id: '2',
Expand All @@ -80,16 +90,20 @@ describe('BillingPlanService', () => {
is_active: true,
created_at: new Date(),
updated_at: new Date(),
expirationDate: new Date(),
email: '[email protected]',
},
{
id: '1',
id: '3',
name: 'Premium',
description: 'premium plan',
amount: 120,
frequency: 'monthly',
is_active: true,
created_at: new Date(),
updated_at: new Date(),
expirationDate: new Date(),
email: '[email protected]',
},
];

Expand Down Expand Up @@ -121,6 +135,8 @@ describe('BillingPlanService', () => {
is_active: true,
created_at: new Date(),
updated_at: new Date(),
expirationDate: new Date(),
email: '[email protected]',
};

jest.spyOn(repository, 'findOneBy').mockResolvedValue(billingPlan as BillingPlan);
Expand All @@ -144,3 +160,38 @@ describe('BillingPlanService', () => {
});
});
});

describe('SubscriptionScheduler', () => {
let scheduler: SubscriptionScheduler;
let billingPlanService: BillingPlanService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
SubscriptionScheduler,
{
provide: BillingPlanService,
useValue: {
getAllSubscriptions: jest.fn().mockResolvedValue([
{ id: 1, expirationDate: new Date(), email: '[email protected]' },
{ id: 2, expirationDate: new Date(), email: '[email protected]' },
]),
sendRenewalReminder: jest.fn().mockResolvedValue(undefined),
},
},
],
}).compile();

scheduler = module.get<SubscriptionScheduler>(SubscriptionScheduler);
billingPlanService = module.get<BillingPlanService>(BillingPlanService);
});

it('should send renewal reminders to all subscriptions', async () => {
await scheduler.handleCron();

expect(billingPlanService.getAllSubscriptions).toHaveBeenCalled();
expect(billingPlanService.sendRenewalReminder).toHaveBeenCalledTimes(2);
expect(billingPlanService.sendRenewalReminder).toHaveBeenCalledWith(1);
expect(billingPlanService.sendRenewalReminder).toHaveBeenCalledWith(2);
});
});
11 changes: 11 additions & 0 deletions src/modules/email/hng-templates/renewal-reminder.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<html>
<head>
<title>Subscription Renewal Reminder</title>
</head>
<body>
<p>Dear {{name}},</p>
<p>This is a friendly reminder that your subscription is set to expire on {{expirationDate}}.</p>
<p>Please renew your subscription to continue enjoying our services.</p>
<p>Thank you!</p>
</body>
</html>
Loading

0 comments on commit 0d24033

Please sign in to comment.