Merge dev to main #1
@@ -42,10 +42,8 @@ export class PaymentController {
|
||||
|
||||
@Get('unremitted')
|
||||
@Roles('technician', 'collector')
|
||||
async getUnremitted(@CurrentUser() user: CurrentUserPayload, @Query('all') all?: string) {
|
||||
const isManager = user.roles?.some((r: string) => ['manager', 'tenant_admin', 'super_admin'].includes(r));
|
||||
// Managers see all unremitted; collectors see only their own
|
||||
return this.paymentService.getUnremittedPayments(user.tenantId, isManager ? null : user.sub);
|
||||
async getUnremitted(@CurrentUser() user: CurrentUserPayload) {
|
||||
return this.paymentService.getUnremittedPayments(user.tenantId, user.sub);
|
||||
}
|
||||
|
||||
@Get('remittances')
|
||||
@@ -80,4 +78,10 @@ export class PaymentController {
|
||||
) {
|
||||
return this.paymentService.rejectRemittance(user.tenantId, id, user.sub);
|
||||
}
|
||||
|
||||
@Get('unremitted-breakdown')
|
||||
@Roles('manager')
|
||||
async getUnremittedBreakdown(@CurrentUser() user: CurrentUserPayload) {
|
||||
return this.paymentService.getUnremittedBreakdown(user.tenantId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,4 +301,36 @@ export class PaymentService {
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async getUnremittedBreakdown(tenantId: string) {
|
||||
const remittedIds = (await this.prisma.remittancePayment.findMany({
|
||||
where: { remittance: { tenantId } },
|
||||
select: { paymentId: true },
|
||||
})).map((r) => r.paymentId);
|
||||
|
||||
const grouped = await this.prisma.payment.groupBy({
|
||||
by: ['collectedById'],
|
||||
where: {
|
||||
tenantId,
|
||||
id: { notIn: remittedIds.length > 0 ? remittedIds : ['none'] },
|
||||
collectedById: { not: null },
|
||||
},
|
||||
_sum: { amount: true },
|
||||
_count: true,
|
||||
});
|
||||
|
||||
const collectorIds = grouped.map((g) => g.collectedById!).filter(Boolean);
|
||||
const collectors = await this.prisma.user.findMany({
|
||||
where: { id: { in: collectorIds } },
|
||||
select: { id: true, firstName: true, lastName: true },
|
||||
});
|
||||
const nameMap = new Map(collectors.map((c) => [c.id, `${c.firstName} ${c.lastName}`]));
|
||||
|
||||
return grouped.map((g) => ({
|
||||
collectorId: g.collectedById,
|
||||
collectorName: nameMap.get(g.collectedById!) ?? 'Unknown',
|
||||
totalAmount: Number(g._sum.amount ?? 0),
|
||||
count: g._count,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
86
src/scheduler/payment.scheduler.ts
Normal file
86
src/scheduler/payment.scheduler.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Cron } from '@nestjs/schedule';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { NotificationService } from '../notification/notification.service';
|
||||
|
||||
@Injectable()
|
||||
export class PaymentScheduler {
|
||||
private readonly logger = new Logger(PaymentScheduler.name);
|
||||
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly notificationService: NotificationService,
|
||||
) {}
|
||||
|
||||
@Cron('0 17 * * *')
|
||||
async sendUnremittedReminders() {
|
||||
this.logger.log('Sending unremitted payment reminders...');
|
||||
|
||||
const tenants = await this.prisma.tenant.findMany({ where: { isActive: true } });
|
||||
|
||||
for (const tenant of tenants) {
|
||||
try {
|
||||
await this._remindForTenant(tenant.id);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to send reminders for tenant ${tenant.slug}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log('Unremitted payment reminders complete.');
|
||||
}
|
||||
|
||||
private async _remindForTenant(tenantId: string) {
|
||||
// Find remitted payment IDs
|
||||
const remittedIds = (await this.prisma.remittancePayment.findMany({
|
||||
where: { remittance: { tenantId } },
|
||||
select: { paymentId: true },
|
||||
})).map((r) => r.paymentId);
|
||||
|
||||
// Find unremitted payments grouped by collector
|
||||
const unremitted = await this.prisma.payment.findMany({
|
||||
where: {
|
||||
tenantId,
|
||||
id: { notIn: remittedIds.length > 0 ? remittedIds : ['none'] },
|
||||
collectedById: { not: null },
|
||||
},
|
||||
select: { collectedById: true, amount: true },
|
||||
});
|
||||
|
||||
if (unremitted.length === 0) return;
|
||||
|
||||
// Group by collector
|
||||
const byCollector = new Map<string, number>();
|
||||
for (const p of unremitted) {
|
||||
const id = p.collectedById!;
|
||||
byCollector.set(id, (byCollector.get(id) ?? 0) + Number(p.amount));
|
||||
}
|
||||
|
||||
// Check which collectors already got a reminder today
|
||||
const todayStart = new Date();
|
||||
todayStart.setHours(0, 0, 0, 0);
|
||||
|
||||
const alreadyReminded = await this.prisma.notification.findMany({
|
||||
where: {
|
||||
tenantId,
|
||||
channel: 'unremitted_reminder',
|
||||
createdAt: { gte: todayStart },
|
||||
},
|
||||
select: { userId: true },
|
||||
});
|
||||
const remindedSet = new Set(alreadyReminded.map((n) => n.userId).filter(Boolean));
|
||||
|
||||
for (const [collectorId, total] of byCollector) {
|
||||
if (remindedSet.has(collectorId)) continue;
|
||||
|
||||
this.notificationService.create(tenantId, {
|
||||
userId: collectorId,
|
||||
type: 'in_app',
|
||||
channel: 'unremitted_reminder',
|
||||
title: 'End-of-Day Reminder',
|
||||
message: `You have ₱${total.toLocaleString()} in unremitted payments. Please submit your remittance.`,
|
||||
}).catch((err) =>
|
||||
this.logger.error(`Failed to notify ${collectorId}: ${err.message}`),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
import { InvoiceScheduler } from './invoice.scheduler';
|
||||
import { PaymentScheduler } from './payment.scheduler';
|
||||
import { BillingModule } from '../billing/billing.module';
|
||||
import { NotificationModule } from '../notification/notification.module';
|
||||
|
||||
@Module({
|
||||
imports: [ScheduleModule.forRoot(), BillingModule],
|
||||
providers: [InvoiceScheduler],
|
||||
imports: [ScheduleModule.forRoot(), BillingModule, NotificationModule],
|
||||
providers: [InvoiceScheduler, PaymentScheduler],
|
||||
})
|
||||
export class SchedulerModule {}
|
||||
|
||||
Reference in New Issue
Block a user