mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
102 lines
4.1 KiB
TypeScript
102 lines
4.1 KiB
TypeScript
import { Injectable, Logger } from '@nestjs/common';
|
|
import { Cron, CronExpression } from '@nestjs/schedule';
|
|
import { NotificationAudience, NotificationType } from '@edr/types';
|
|
|
|
import { NotificationInboxService } from '../notification-inbox/notification-inbox.service';
|
|
import { ContractsRepository } from './contracts.repository';
|
|
import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry';
|
|
|
|
/**
|
|
* How many days before a contract lapses the customer is reminded. Mirrored by
|
|
* the portal contract list (EXPIRY_NOTICE_DAYS in contract-ui.tsx), which shows
|
|
* the same countdown on the row.
|
|
*/
|
|
const EXPIRY_NOTICE_DAYS = 10;
|
|
|
|
/** Nightly sweep that flips contracts past contractValidUntil to EXPIRED. */
|
|
@Injectable()
|
|
export class ContractExpiryService {
|
|
private readonly logger = new Logger(ContractExpiryService.name);
|
|
|
|
constructor(
|
|
private readonly contractsRepository: ContractsRepository,
|
|
private readonly inbox: NotificationInboxService,
|
|
) {}
|
|
|
|
/**
|
|
* Warn every customer whose contract lapses in ~10 days, once. The repository
|
|
* window is a rolling 24h slice, so a contract is picked up by exactly one
|
|
* daily run — no reminded-flag column needed.
|
|
*
|
|
* ponytail: a missed run (API down over the slice) skips that contract's
|
|
* reminder; the portal list still shows its countdown for the whole window.
|
|
*/
|
|
@Cron(CronExpression.EVERY_DAY_AT_2AM, { name: 'contract-expiry-reminder' })
|
|
async remindExpiringContracts(): Promise<void> {
|
|
try {
|
|
const expiring =
|
|
await this.contractsRepository.findExpiringInDays(EXPIRY_NOTICE_DAYS);
|
|
let notified = 0;
|
|
for (const contract of expiring) {
|
|
if (!contract.companyId || !contract.contractValidUntil) continue;
|
|
const endsOn = contract.contractValidUntil.toLocaleDateString('en-GB');
|
|
await this.inbox.notify({
|
|
recipients: { companyId: contract.companyId },
|
|
audience: NotificationAudience.PORTAL,
|
|
type: NotificationType.CONTRACT_STATUS,
|
|
title: `Contract ${contract.reference} expires in ${EXPIRY_NOTICE_DAYS} days`,
|
|
body:
|
|
`Your contract ${contract.reference} is valid until ${endsOn}. ` +
|
|
'After that date it stops accepting new bookings — contact EDR if ' +
|
|
'you need it renewed.',
|
|
link: '/contracts',
|
|
data: { contractId: contract.id, action: 'CONTRACT_EXPIRING' },
|
|
});
|
|
notified += 1;
|
|
}
|
|
this.logger.log(
|
|
`Contract expiry reminder: ${notified} customer(s) warned of a contract ` +
|
|
`lapsing in ${EXPIRY_NOTICE_DAYS} days`,
|
|
);
|
|
} catch (err) {
|
|
// Never throws into the scheduler — a failed reminder must not stop the
|
|
// expiry sweep from running.
|
|
this.logger.error(
|
|
`Contract expiry reminder failed: ${(err as Error).message}`,
|
|
(err as Error).stack,
|
|
);
|
|
}
|
|
}
|
|
|
|
@Cron(CronExpression.EVERY_DAY_AT_1AM, { name: 'contract-expiry-sweep' })
|
|
async expireLapsedContracts(): Promise<void> {
|
|
try {
|
|
const affected = await this.contractsRepository.expireLapsedContracts();
|
|
this.logger.log(`Contract expiry sweep: ${affected} contract(s) marked EXPIRED`);
|
|
} catch (err) {
|
|
this.logger.error(
|
|
`Contract expiry sweep failed: ${(err as Error).message}`,
|
|
(err as Error).stack,
|
|
);
|
|
try {
|
|
await this.inbox.notify({
|
|
// The people who would notice expired contracts still listed as
|
|
// active are the ones working the contract desk.
|
|
recipients: {
|
|
permissionKeys: [FREIGHT_PERMS.contracts.getNotification],
|
|
},
|
|
audience: NotificationAudience.BACKOFFICE,
|
|
type: NotificationType.GENERIC,
|
|
title: 'Contract expiry sweep failed',
|
|
body: `The nightly job that expires lapsed contracts failed: ${(err as Error).message}. Contracts past their validity date may still show as active until this is fixed.`,
|
|
data: { action: 'CONTRACT_EXPIRY_SWEEP_FAILED' },
|
|
});
|
|
} catch (notifyErr) {
|
|
this.logger.error(
|
|
`Contract expiry sweep failure alert also failed: ${(notifyErr as Error).message}`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|