mirror of
https://github.com/HamzaSha1/zod-backend.git
synced 2026-03-10 17:21:45 +00:00
- Add DELETE /guardians/me/allowances/:scheduleId endpoint - Add ALLOWANCE_TEST_MODE env variable for testing intervals: - true: DAILY=5min, WEEKLY=10min, MONTHLY=15min - false: DAILY=1day, WEEKLY=1week, MONTHLY=1month - Add deleteById to repository and deleteSchedule to service
214 lines
7.7 KiB
TypeScript
214 lines
7.7 KiB
TypeScript
import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common';
|
|
import { ConfigService } from '@nestjs/config';
|
|
import moment from 'moment';
|
|
import { Junior } from '~/junior/entities';
|
|
import { JuniorService } from '~/junior/services';
|
|
import { CreateAllowanceScheduleRequestDto, UpdateAllowanceScheduleRequestDto } from '../dtos/request';
|
|
import { AllowanceSchedule } from '../entities/allowance-schedule.entity';
|
|
import { AllowanceFrequency, AllowanceScheduleStatus } from '../enums';
|
|
import { AllowanceSchedulesGrouped, AllowanceSummary } from '../interfaces';
|
|
import { AllowanceScheduleRepository } from '../repositories';
|
|
|
|
@Injectable()
|
|
export class AllowanceService {
|
|
private readonly logger = new Logger(AllowanceService.name);
|
|
private readonly isTestMode: boolean;
|
|
|
|
constructor(
|
|
private readonly allowanceScheduleRepository: AllowanceScheduleRepository,
|
|
private readonly juniorService: JuniorService,
|
|
private readonly configService: ConfigService,
|
|
) {
|
|
this.isTestMode = this.configService.get<string>('ALLOWANCE_TEST_MODE') === 'true';
|
|
if (this.isTestMode) {
|
|
this.logger.warn('ALLOWANCE_TEST_MODE is enabled - using short intervals (5/10/15 min)');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets all allowance schedules for a guardian, grouped by juniors with and without schedules.
|
|
*/
|
|
async getSchedulesByGuardian(guardianId: string): Promise<AllowanceSchedulesGrouped> {
|
|
// Fetch all juniors for this guardian (with pagination workaround - large size)
|
|
const [juniors] = await this.juniorService.findJuniorsByGuardianId(guardianId, {
|
|
page: 1,
|
|
size: 1000, // Assuming no guardian has more than 1000 children
|
|
});
|
|
|
|
// Fetch all schedules for this guardian
|
|
const schedules = await this.allowanceScheduleRepository.findByGuardianId(guardianId);
|
|
|
|
// Create a map of juniorId -> schedule for quick lookup
|
|
const scheduleMap = new Map<string, AllowanceSchedule>();
|
|
for (const schedule of schedules) {
|
|
scheduleMap.set(schedule.juniorId, schedule);
|
|
}
|
|
|
|
// Separate juniors into two groups
|
|
const withSchedule: { junior: Junior; schedule: AllowanceSchedule }[] = [];
|
|
const withoutSchedule: Junior[] = [];
|
|
|
|
for (const junior of juniors) {
|
|
const schedule = scheduleMap.get(junior.id);
|
|
if (schedule) {
|
|
withSchedule.push({ junior, schedule });
|
|
} else {
|
|
withoutSchedule.push(junior);
|
|
}
|
|
}
|
|
|
|
const monthlyTotal = this.calculateMonthlyTotal(schedules);
|
|
|
|
return { withSchedule, withoutSchedule, monthlyTotal };
|
|
}
|
|
|
|
/**
|
|
* Calculates the monthly equivalent total for all active schedules.
|
|
* - DAILY: amount * 30
|
|
* - WEEKLY: amount * 4
|
|
* - MONTHLY: amount * 1
|
|
*/
|
|
private calculateMonthlyTotal(schedules: AllowanceSchedule[]): number {
|
|
return schedules
|
|
.filter((s) => s.status === AllowanceScheduleStatus.ON)
|
|
.reduce((total, schedule) => {
|
|
const amount = Number(schedule.amount);
|
|
switch (schedule.frequency) {
|
|
case AllowanceFrequency.DAILY:
|
|
return total + amount * 30;
|
|
case AllowanceFrequency.WEEKLY:
|
|
return total + amount * 4;
|
|
case AllowanceFrequency.MONTHLY:
|
|
return total + amount;
|
|
default:
|
|
return total;
|
|
}
|
|
}, 0);
|
|
}
|
|
|
|
async createSchedule(
|
|
guardianId: string,
|
|
juniorId: string,
|
|
body: CreateAllowanceScheduleRequestDto,
|
|
): Promise<AllowanceSchedule> {
|
|
const doesBelong = await this.juniorService.doesJuniorBelongToGuardian(guardianId, juniorId);
|
|
if (!doesBelong) {
|
|
this.logger.error(`Junior ${juniorId} does not belong to guardian ${guardianId}`);
|
|
throw new BadRequestException('JUNIOR.NOT_BELONG_TO_GUARDIAN');
|
|
}
|
|
|
|
const existingSchedule = await this.allowanceScheduleRepository.findByGuardianAndJunior(guardianId, juniorId);
|
|
if (existingSchedule) {
|
|
throw new BadRequestException('ALLOWANCE.ALREADY_EXISTS');
|
|
}
|
|
|
|
const nextRunAt = this.computeNextRunAt(body.frequency, body.status);
|
|
return this.allowanceScheduleRepository.createSchedule(guardianId, juniorId, body, nextRunAt);
|
|
}
|
|
|
|
private computeNextRunAt(frequency: AllowanceFrequency, status: AllowanceScheduleStatus): Date {
|
|
const base = moment();
|
|
if (status === AllowanceScheduleStatus.OFF) {
|
|
return base.toDate();
|
|
}
|
|
|
|
if (this.isTestMode) {
|
|
// Test mode: DAILY=5min, WEEKLY=10min, MONTHLY=15min
|
|
switch (frequency) {
|
|
case AllowanceFrequency.DAILY:
|
|
return base.add(5, 'minutes').toDate();
|
|
case AllowanceFrequency.WEEKLY:
|
|
return base.add(10, 'minutes').toDate();
|
|
case AllowanceFrequency.MONTHLY:
|
|
return base.add(15, 'minutes').toDate();
|
|
default:
|
|
return base.toDate();
|
|
}
|
|
}
|
|
|
|
// Production mode: real intervals
|
|
switch (frequency) {
|
|
case AllowanceFrequency.DAILY:
|
|
return base.add(1, 'day').toDate();
|
|
case AllowanceFrequency.WEEKLY:
|
|
return base.add(1, 'week').toDate();
|
|
case AllowanceFrequency.MONTHLY:
|
|
return base.add(1, 'month').toDate();
|
|
default:
|
|
return base.toDate();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates an existing allowance schedule.
|
|
* Recalculates nextRunAt if frequency or status changes.
|
|
*/
|
|
async updateSchedule(
|
|
guardianId: string,
|
|
scheduleId: string,
|
|
body: UpdateAllowanceScheduleRequestDto,
|
|
): Promise<AllowanceSchedule> {
|
|
const schedule = await this.allowanceScheduleRepository.findByIdAndGuardian(scheduleId, guardianId);
|
|
|
|
if (!schedule) {
|
|
this.logger.error(`Schedule ${scheduleId} not found for guardian ${guardianId}`);
|
|
throw new NotFoundException('ALLOWANCE.NOT_FOUND');
|
|
}
|
|
|
|
// Check if frequency or status is changing (need to recalculate nextRunAt)
|
|
const frequencyChanged = body.frequency && body.frequency !== schedule.frequency;
|
|
const statusChanged = body.status && body.status !== schedule.status;
|
|
|
|
// Update fields if provided
|
|
if (body.amount !== undefined) {
|
|
schedule.amount = body.amount;
|
|
}
|
|
if (body.frequency !== undefined) {
|
|
schedule.frequency = body.frequency;
|
|
}
|
|
if (body.status !== undefined) {
|
|
schedule.status = body.status;
|
|
}
|
|
|
|
// Recalculate nextRunAt if frequency or status changed
|
|
if (frequencyChanged || statusChanged) {
|
|
schedule.nextRunAt = this.computeNextRunAt(schedule.frequency, schedule.status);
|
|
}
|
|
|
|
this.logger.log(`Updating schedule ${scheduleId} for guardian ${guardianId}`);
|
|
return this.allowanceScheduleRepository.updateSchedule(schedule);
|
|
}
|
|
|
|
/**
|
|
* Gets a lightweight summary of allowances for the home page.
|
|
* Only fetches active schedules for efficiency.
|
|
*/
|
|
async getSummary(guardianId: string): Promise<AllowanceSummary> {
|
|
// Only fetch active schedules, ordered by nextRunAt (nearest first)
|
|
const activeSchedules = await this.allowanceScheduleRepository.findActiveByGuardianId(guardianId);
|
|
|
|
// The first one is the nearest (already sorted by nextRunAt ASC)
|
|
const nextPaymentAt = activeSchedules.length > 0 ? activeSchedules[0].nextRunAt : null;
|
|
|
|
// Calculate monthly total from active schedules
|
|
const monthlyTotal = this.calculateMonthlyTotal(activeSchedules);
|
|
|
|
return { nextPaymentAt, monthlyTotal };
|
|
}
|
|
|
|
/**
|
|
* Deletes an allowance schedule.
|
|
*/
|
|
async deleteSchedule(guardianId: string, scheduleId: string): Promise<void> {
|
|
const schedule = await this.allowanceScheduleRepository.findByIdAndGuardian(scheduleId, guardianId);
|
|
|
|
if (!schedule) {
|
|
this.logger.error(`Schedule ${scheduleId} not found for guardian ${guardianId}`);
|
|
throw new NotFoundException('ALLOWANCE.NOT_FOUND');
|
|
}
|
|
|
|
this.logger.log(`Deleting schedule ${scheduleId} for guardian ${guardianId}`);
|
|
await this.allowanceScheduleRepository.deleteById(scheduleId);
|
|
}
|
|
}
|