feat(renovaciones): renewal notification emails over SES
INSURANCE_FEATURES_SPEC §1. The office printed and mailed renewal letters from the legacy CONTROL <ramo> RENEW[2/3] paper log; 91% of policyholders have an email on file, so send the notice instead and keep the paper log as the fallback. A daily cron (06:00 America/Tijuana) sweeps three generations off policyTo — 30 and 15 days before expiry, 7 days after — sends each through SES, and upserts RenewalNotice by [policyId, generation] so a policy is never notified twice for the same milestone. RenewalNotice now records providerMessageId, so a later bounce or complaint webhook can be traced back to the row that sent it. - customers.emailOptOut excludes a customer from every sweep; editable from the customer form - scheduled_job_states holds the sweep's lock and last successful run; the window is widened to cover days the job did not run, so a weekend outage does not silently drop a generation - SES unconfigured is not an error outside production — messages are logged and skipped, so dev and CI never send - /renovaciones (renewal:send, MANAGER+) lists what is pending per generation, runs the sweep by hand, and marks a notice sent by mail for the customers with no email - POST /policies/:id/renewal-notices records that manual mark - the aviso-renovacion report and the emails now share one projection (reports/renewal-letter.ts) instead of two copies of the mapping
This commit is contained in:
@@ -33,3 +33,9 @@ COMPANY_EMAIL=
|
|||||||
COMPANY_TAX_ID=
|
COMPANY_TAX_ID=
|
||||||
COMPANY_WEBSITE=
|
COMPANY_WEBSITE=
|
||||||
COMPANY_LOGO_PATH=
|
COMPANY_LOGO_PATH=
|
||||||
|
|
||||||
|
SES_REGION=
|
||||||
|
SES_FROM=
|
||||||
|
SES_ACCESS_KEY=
|
||||||
|
SES_SECRET_KEY=
|
||||||
|
SES_CONFIGURATION_SET=
|
||||||
|
|||||||
@@ -12,20 +12,22 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.665.0",
|
"@aws-sdk/client-s3": "^3.665.0",
|
||||||
|
"@aws-sdk/client-sesv2": "^3.1101.0",
|
||||||
"@jorgecuadros/database": "workspace:*",
|
"@jorgecuadros/database": "workspace:*",
|
||||||
"@nestjs/common": "^10.4.4",
|
"@nestjs/common": "^10.4.4",
|
||||||
"@nestjs/config": "^3.3.0",
|
"@nestjs/config": "^3.3.0",
|
||||||
"@nestjs/core": "^10.4.4",
|
"@nestjs/core": "^10.4.4",
|
||||||
"@nestjs/passport": "^10.0.3",
|
"@nestjs/passport": "^10.0.3",
|
||||||
"@nestjs/platform-express": "^10.4.4",
|
"@nestjs/platform-express": "^10.4.4",
|
||||||
|
"@nestjs/schedule": "^4.1.2",
|
||||||
"argon2": "^0.41.1",
|
"argon2": "^0.41.1",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.1",
|
"class-validator": "^0.14.1",
|
||||||
"exceljs": "^4.4.0",
|
"exceljs": "^4.4.0",
|
||||||
"express-session": "^1.18.0",
|
"express-session": "^1.18.0",
|
||||||
"pdfkit": "^0.15.1",
|
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
|
"pdfkit": "^0.15.1",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1"
|
"rxjs": "^7.8.1"
|
||||||
},
|
},
|
||||||
@@ -34,11 +36,11 @@
|
|||||||
"@nestjs/testing": "^10.4.4",
|
"@nestjs/testing": "^10.4.4",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/express-session": "^1.18.0",
|
"@types/express-session": "^1.18.0",
|
||||||
"@types/pdfkit": "^0.13.5",
|
|
||||||
"@types/jest": "^29.5.13",
|
"@types/jest": "^29.5.13",
|
||||||
"@types/node": "^20.16.11",
|
"@types/node": "^20.16.11",
|
||||||
"@types/passport": "^1.0.17",
|
"@types/passport": "^1.0.17",
|
||||||
"@types/passport-local": "^1.0.38",
|
"@types/passport-local": "^1.0.38",
|
||||||
|
"@types/pdfkit": "^0.13.5",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"ts-jest": "^29.2.5",
|
"ts-jest": "^29.2.5",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
import { ConfigModule } from "@nestjs/config";
|
import { ConfigModule } from "@nestjs/config";
|
||||||
|
import { ScheduleModule } from "@nestjs/schedule";
|
||||||
import { PrismaModule } from "./prisma/prisma.module";
|
import { PrismaModule } from "./prisma/prisma.module";
|
||||||
import { StorageModule } from "./storage/storage.module";
|
import { StorageModule } from "./storage/storage.module";
|
||||||
import { CommonModule } from "./common/common.module";
|
import { CommonModule } from "./common/common.module";
|
||||||
@@ -14,11 +15,13 @@ import { PolicyOcrModule } from "./policy-ocr/policy-ocr.module";
|
|||||||
import { BankModule } from "./bank/bank.module";
|
import { BankModule } from "./bank/bank.module";
|
||||||
import { OpsModule } from "./ops/ops.module";
|
import { OpsModule } from "./ops/ops.module";
|
||||||
import { ReportsModule } from "./reports/reports.module";
|
import { ReportsModule } from "./reports/reports.module";
|
||||||
|
import { RenewalsModule } from "./renewals/renewals.module";
|
||||||
import { AppController } from "./app.controller";
|
import { AppController } from "./app.controller";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot({ isGlobal: true }),
|
ConfigModule.forRoot({ isGlobal: true }),
|
||||||
|
ScheduleModule.forRoot(),
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
StorageModule,
|
StorageModule,
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@@ -33,6 +36,7 @@ import { AppController } from "./app.controller";
|
|||||||
BankModule,
|
BankModule,
|
||||||
OpsModule,
|
OpsModule,
|
||||||
ReportsModule,
|
ReportsModule,
|
||||||
|
RenewalsModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export type Ability =
|
|||||||
| "policy:delete"
|
| "policy:delete"
|
||||||
| "policy:ingest"
|
| "policy:ingest"
|
||||||
| "policy:ocr-review"
|
| "policy:ocr-review"
|
||||||
|
| "renewal:send"
|
||||||
| "property:create"
|
| "property:create"
|
||||||
| "property:update"
|
| "property:update"
|
||||||
| "property:delete"
|
| "property:delete"
|
||||||
@@ -52,6 +53,7 @@ export const ABILITY_MIN: Record<Ability, Role> = {
|
|||||||
// upload + confirm, nothing reaches the books unconfirmed.
|
// upload + confirm, nothing reaches the books unconfirmed.
|
||||||
"policy:ingest": "STAFF",
|
"policy:ingest": "STAFF",
|
||||||
"policy:ocr-review": "STAFF",
|
"policy:ocr-review": "STAFF",
|
||||||
|
"renewal:send": "MANAGER",
|
||||||
"property:create": "STAFF",
|
"property:create": "STAFF",
|
||||||
"property:update": "STAFF",
|
"property:update": "STAFF",
|
||||||
"property:delete": "MANAGER",
|
"property:delete": "MANAGER",
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export class CreateCustomerDto {
|
|||||||
@IsOptional() @IsString() mobile?: string;
|
@IsOptional() @IsString() mobile?: string;
|
||||||
@IsOptional() @IsString() fax?: string;
|
@IsOptional() @IsString() fax?: string;
|
||||||
@IsOptional() @IsEmail() email?: string;
|
@IsOptional() @IsEmail() email?: string;
|
||||||
|
@IsOptional() @IsBoolean() emailOptOut?: boolean;
|
||||||
@IsOptional() @IsString() notes?: string;
|
@IsOptional() @IsString() notes?: string;
|
||||||
@IsOptional() @IsString() identificationType?: string;
|
@IsOptional() @IsString() identificationType?: string;
|
||||||
@IsOptional() @IsString() identificationNumber?: string;
|
@IsOptional() @IsString() identificationNumber?: string;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export class UpdateCustomerDto {
|
|||||||
@IsOptional() @IsString() mobile?: string;
|
@IsOptional() @IsString() mobile?: string;
|
||||||
@IsOptional() @IsString() fax?: string;
|
@IsOptional() @IsString() fax?: string;
|
||||||
@IsOptional() @IsEmail() email?: string;
|
@IsOptional() @IsEmail() email?: string;
|
||||||
|
@IsOptional() @IsBoolean() emailOptOut?: boolean;
|
||||||
@IsOptional() @IsString() notes?: string;
|
@IsOptional() @IsString() notes?: string;
|
||||||
@IsOptional() @IsString() identificationType?: string;
|
@IsOptional() @IsString() identificationType?: string;
|
||||||
@IsOptional() @IsString() identificationNumber?: string;
|
@IsOptional() @IsString() identificationNumber?: string;
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
type PolicyStatus,
|
type PolicyStatus,
|
||||||
} from "./policies.service";
|
} from "./policies.service";
|
||||||
import { CreatePolicyDto, UpdatePolicyDto } from "./policy.dto";
|
import { CreatePolicyDto, UpdatePolicyDto } from "./policy.dto";
|
||||||
|
import { MarkRenewalNoticeDto } from "./renewal-notice.dto";
|
||||||
import {
|
import {
|
||||||
BeneficiaryDto,
|
BeneficiaryDto,
|
||||||
ClaimDto,
|
ClaimDto,
|
||||||
@@ -145,6 +146,26 @@ export class PoliciesController {
|
|||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post(":id/renewal-notices")
|
||||||
|
@RequireAbility("renewal:send")
|
||||||
|
async markRenewalNotice(
|
||||||
|
@Param("id") id: string,
|
||||||
|
@Body() dto: MarkRenewalNoticeDto,
|
||||||
|
@Req() req: Request,
|
||||||
|
) {
|
||||||
|
const notice = await this.policies.markRenewalNotice(
|
||||||
|
id,
|
||||||
|
dto,
|
||||||
|
this.actingId(req),
|
||||||
|
);
|
||||||
|
void this.audit.log(this.actingId(req), "renewalNotice.markSent", {
|
||||||
|
policyId: id,
|
||||||
|
generation: dto.generation,
|
||||||
|
channel: dto.channel,
|
||||||
|
});
|
||||||
|
return notice;
|
||||||
|
}
|
||||||
|
|
||||||
// --- children (all editing a policy => policy:update) ---------------------
|
// --- children (all editing a policy => policy:update) ---------------------
|
||||||
|
|
||||||
@Post(":id/installments")
|
@Post(":id/installments")
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { StorageService } from "../storage/storage.service";
|
|||||||
import { extForUpload, type UploadedFileLike } from "../storage/upload-file";
|
import { extForUpload, type UploadedFileLike } from "../storage/upload-file";
|
||||||
import { toDate } from "../common/coerce";
|
import { toDate } from "../common/coerce";
|
||||||
import { CreatePolicyDto, UpdatePolicyDto } from "./policy.dto";
|
import { CreatePolicyDto, UpdatePolicyDto } from "./policy.dto";
|
||||||
|
import { MarkRenewalNoticeDto } from "./renewal-notice.dto";
|
||||||
import {
|
import {
|
||||||
BeneficiaryDto,
|
BeneficiaryDto,
|
||||||
ClaimDto,
|
ClaimDto,
|
||||||
@@ -358,6 +359,34 @@ export class PoliciesService {
|
|||||||
return this.prisma.policy.update({ where: { id }, data: { archivedAt: null } });
|
return this.prisma.policy.update({ where: { id }, data: { archivedAt: null } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async markRenewalNotice(
|
||||||
|
policyId: string,
|
||||||
|
dto: MarkRenewalNoticeDto,
|
||||||
|
sentById: string,
|
||||||
|
) {
|
||||||
|
await this.ensurePolicy(policyId);
|
||||||
|
const sentAt = toDate(dto.sentAt) ?? new Date();
|
||||||
|
return this.prisma.renewalNotice.upsert({
|
||||||
|
where: {
|
||||||
|
policyId_generation: { policyId, generation: dto.generation },
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
policyId,
|
||||||
|
generation: dto.generation,
|
||||||
|
channel: dto.channel,
|
||||||
|
sentAt,
|
||||||
|
sentById,
|
||||||
|
notes: dto.notes,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
channel: dto.channel,
|
||||||
|
sentAt,
|
||||||
|
sentById,
|
||||||
|
notes: dto.notes,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private async ensurePolicy(id: string) {
|
private async ensurePolicy(id: string) {
|
||||||
const found = await this.prisma.policy.findUnique({
|
const found = await this.prisma.policy.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { RenewalNoticeChannel } from "@jorgecuadros/database";
|
||||||
|
import {
|
||||||
|
IsDateString,
|
||||||
|
IsEnum,
|
||||||
|
IsInt,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
Max,
|
||||||
|
Min,
|
||||||
|
} from "class-validator";
|
||||||
|
|
||||||
|
export class MarkRenewalNoticeDto {
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
@Max(3)
|
||||||
|
generation!: number;
|
||||||
|
|
||||||
|
@IsEnum(RenewalNoticeChannel)
|
||||||
|
channel!: RenewalNoticeChannel;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
sentAt?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
notes?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { ServiceUnavailableException } from "@nestjs/common";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
import { MailService } from "./mail.service";
|
||||||
|
|
||||||
|
function config(values: Record<string, string>): ConfigService {
|
||||||
|
return { get: (key: string) => values[key] } as ConfigService;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("MailService", () => {
|
||||||
|
it("uses a no-op provider when SES is absent in development", async () => {
|
||||||
|
const service = new MailService(config({ NODE_ENV: "development" }));
|
||||||
|
|
||||||
|
expect(service.available).toBe(true);
|
||||||
|
await expect(
|
||||||
|
service.send({ to: "test@example.com", subject: "Test", html: "<p>Test</p>" }),
|
||||||
|
).resolves.toEqual({ providerId: expect.stringMatching(/^dev-/) });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps production bootable but refuses sends when SES is absent", async () => {
|
||||||
|
const service = new MailService(config({ NODE_ENV: "production" }));
|
||||||
|
|
||||||
|
expect(service.available).toBe(false);
|
||||||
|
await expect(
|
||||||
|
service.send({ to: "test@example.com", subject: "Test", html: "<p>Test</p>" }),
|
||||||
|
).rejects.toBeInstanceOf(ServiceUnavailableException);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
Logger,
|
||||||
|
ServiceUnavailableException,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
import {
|
||||||
|
SendEmailCommand,
|
||||||
|
SESv2Client,
|
||||||
|
} from "@aws-sdk/client-sesv2";
|
||||||
|
import { randomUUID } from "node:crypto";
|
||||||
|
|
||||||
|
export interface MailMessage {
|
||||||
|
to: string;
|
||||||
|
subject: string;
|
||||||
|
html: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MailProvider {
|
||||||
|
send(message: MailMessage): Promise<{ providerId: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MailService implements MailProvider {
|
||||||
|
private readonly logger = new Logger(MailService.name);
|
||||||
|
private readonly client: SESv2Client | null;
|
||||||
|
private readonly from: string | null;
|
||||||
|
private readonly configurationSet: string | undefined;
|
||||||
|
private readonly developmentNoop: boolean;
|
||||||
|
|
||||||
|
constructor(config: ConfigService) {
|
||||||
|
const region = config.get<string>("SES_REGION");
|
||||||
|
const from = config.get<string>("SES_FROM");
|
||||||
|
const accessKeyId = config.get<string>("SES_ACCESS_KEY");
|
||||||
|
const secretAccessKey = config.get<string>("SES_SECRET_KEY");
|
||||||
|
this.configurationSet = config.get<string>("SES_CONFIGURATION_SET") || undefined;
|
||||||
|
this.developmentNoop =
|
||||||
|
config.get<string>("NODE_ENV") !== "production" &&
|
||||||
|
!region &&
|
||||||
|
!from &&
|
||||||
|
!accessKeyId &&
|
||||||
|
!secretAccessKey;
|
||||||
|
|
||||||
|
if (this.developmentNoop) {
|
||||||
|
this.logger.warn("SES no configurado; los correos se registrarán sin enviarse.");
|
||||||
|
this.client = null;
|
||||||
|
this.from = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!region || !from || !accessKeyId || !secretAccessKey) {
|
||||||
|
this.logger.warn("SES no configurado; las notificaciones por correo están deshabilitadas.");
|
||||||
|
this.client = null;
|
||||||
|
this.from = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.client = new SESv2Client({
|
||||||
|
region,
|
||||||
|
credentials: { accessKeyId, secretAccessKey },
|
||||||
|
});
|
||||||
|
this.from = from;
|
||||||
|
}
|
||||||
|
|
||||||
|
get available(): boolean {
|
||||||
|
return this.developmentNoop || (this.client !== null && this.from !== null);
|
||||||
|
}
|
||||||
|
|
||||||
|
async send(message: MailMessage): Promise<{ providerId: string }> {
|
||||||
|
if (this.developmentNoop) {
|
||||||
|
const providerId = `dev-${randomUUID()}`;
|
||||||
|
this.logger.log(`Correo de renovación simulado (${providerId}).`);
|
||||||
|
return { providerId };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.client || !this.from) {
|
||||||
|
throw new ServiceUnavailableException(
|
||||||
|
"El servicio de correo no está configurado.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.client.send(
|
||||||
|
new SendEmailCommand({
|
||||||
|
FromEmailAddress: this.from,
|
||||||
|
Destination: { ToAddresses: [message.to] },
|
||||||
|
Content: {
|
||||||
|
Simple: {
|
||||||
|
Subject: { Data: message.subject, Charset: "UTF-8" },
|
||||||
|
Body: { Html: { Data: message.html, Charset: "UTF-8" } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ConfigurationSetName: this.configurationSet,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result.MessageId) {
|
||||||
|
throw new Error("SES no devolvió identificador de mensaje.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return { providerId: result.MessageId };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import type { RenewalLetterRow } from "../reports/renewal-letter";
|
||||||
|
import { renderRenewalEmail } from "./renewal-email";
|
||||||
|
|
||||||
|
function letter(overrides: Partial<RenewalLetterRow> = {}): RenewalLetterRow {
|
||||||
|
return {
|
||||||
|
__kind: "letter",
|
||||||
|
policyId: "policy-1",
|
||||||
|
policyNumber: "POL-123",
|
||||||
|
policyType: "AUTO",
|
||||||
|
customerName: "Ana Pérez",
|
||||||
|
customerEmail: "ana@example.com",
|
||||||
|
customerPhone: "664-111-2222",
|
||||||
|
customerMobile: null,
|
||||||
|
customerAddress: ["Calle Uno 123", "Tijuana, BC, 22000"],
|
||||||
|
provider: "Aseguradora Uno",
|
||||||
|
policyTo: "2026-09-01",
|
||||||
|
netPremium: "1200.00",
|
||||||
|
policyFee: null,
|
||||||
|
total: "1392.00",
|
||||||
|
currency: "MXN",
|
||||||
|
coverageDays: null,
|
||||||
|
cslLimit: null,
|
||||||
|
medicalCoverage: null,
|
||||||
|
propertyDamage: null,
|
||||||
|
perPersonLiability: null,
|
||||||
|
additionalService: null,
|
||||||
|
vehicle: null,
|
||||||
|
generation: 1,
|
||||||
|
sentAt: null,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("renderRenewalEmail", () => {
|
||||||
|
it("includes policy, premium, expiration, type, and customer information", () => {
|
||||||
|
const result = renderRenewalEmail(letter());
|
||||||
|
|
||||||
|
expect(result.subject).toContain("POL-123");
|
||||||
|
expect(result.html).toContain("primer aviso");
|
||||||
|
expect(result.html).toContain("AUTO");
|
||||||
|
expect(result.html).toContain("01/09/2026");
|
||||||
|
expect(result.html).toContain("1,392.00");
|
||||||
|
expect(result.html).toContain("Ana Pérez");
|
||||||
|
expect(result.html).toContain("ana@example.com");
|
||||||
|
expect(result.html).toContain("664-111-2222");
|
||||||
|
expect(result.html).toContain("Calle Uno 123");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses overdue wording for generation three", () => {
|
||||||
|
const result = renderRenewalEmail(letter({ generation: 3 }));
|
||||||
|
|
||||||
|
expect(result.subject).toContain("Póliza vencida");
|
||||||
|
expect(result.html).toContain("está vencida");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("escapes customer-provided HTML", () => {
|
||||||
|
const result = renderRenewalEmail(
|
||||||
|
letter({ customerName: '<img src=x onerror="alert(1)">' }),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.html).not.toContain("<img");
|
||||||
|
expect(result.html).toContain("<img");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import type { RenewalLetterRow } from "../reports/renewal-letter";
|
||||||
|
|
||||||
|
const GENERATION_TEXT: Record<number, string> = {
|
||||||
|
1: "Le enviamos el primer aviso para renovar su póliza.",
|
||||||
|
2: "Le enviamos el segundo aviso para renovar su póliza.",
|
||||||
|
3: "Le informamos que su póliza está vencida.",
|
||||||
|
};
|
||||||
|
|
||||||
|
function escapeHtml(value: unknown): string {
|
||||||
|
return String(value ?? "")
|
||||||
|
.replaceAll("&", "&")
|
||||||
|
.replaceAll("<", "<")
|
||||||
|
.replaceAll(">", ">")
|
||||||
|
.replaceAll('"', """)
|
||||||
|
.replaceAll("'", "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayDate(value: string): string {
|
||||||
|
if (value === "—") return value;
|
||||||
|
const [year, month, day] = value.split("-");
|
||||||
|
return `${day}/${month}/${year}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function money(value: string | null, currency: string): string {
|
||||||
|
if (!value) return "No disponible";
|
||||||
|
return new Intl.NumberFormat("es-MX", {
|
||||||
|
style: "currency",
|
||||||
|
currency,
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
}).format(Number(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function row(label: string, value: string): string {
|
||||||
|
return `<tr><th style="padding:8px 12px;text-align:left;background:#f4f4f4;border:1px solid #ddd">${escapeHtml(label)}</th><td style="padding:8px 12px;border:1px solid #ddd">${escapeHtml(value)}</td></tr>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderRenewalEmail(letter: RenewalLetterRow): {
|
||||||
|
subject: string;
|
||||||
|
html: string;
|
||||||
|
} {
|
||||||
|
const expired = letter.generation === 3;
|
||||||
|
const subject = expired
|
||||||
|
? `Póliza vencida: ${letter.policyNumber}`
|
||||||
|
: `Aviso de renovación: póliza ${letter.policyNumber}`;
|
||||||
|
const phone = letter.customerMobile ?? letter.customerPhone ?? "No disponible";
|
||||||
|
const address = letter.customerAddress.join(", ") || "No disponible";
|
||||||
|
const premium = letter.total ?? letter.netPremium;
|
||||||
|
|
||||||
|
const details = [
|
||||||
|
row("Número de póliza", letter.policyNumber),
|
||||||
|
row("Tipo de póliza", letter.policyType),
|
||||||
|
row("Aseguradora", letter.provider),
|
||||||
|
row("Fecha de vencimiento", displayDate(letter.policyTo)),
|
||||||
|
row("Prima", money(premium, letter.currency)),
|
||||||
|
row("Cliente", letter.customerName),
|
||||||
|
row("Correo", letter.customerEmail ?? "No disponible"),
|
||||||
|
row("Teléfono", phone),
|
||||||
|
row("Dirección", address),
|
||||||
|
].join("");
|
||||||
|
|
||||||
|
return {
|
||||||
|
subject,
|
||||||
|
html: `<div style="font-family:Arial,sans-serif;color:#222;line-height:1.5"><p>Estimado(a) ${escapeHtml(letter.customerName)}:</p><p>${escapeHtml(GENERATION_TEXT[letter.generation] ?? "Le enviamos un aviso sobre la renovación de su póliza.")}</p><table style="border-collapse:collapse;width:100%;max-width:680px">${details}</table><p>Por favor, comuníquese con Jorge Cuadros & Asociados para revisar su renovación.</p><p>Atentamente,<br>Jorge Cuadros & Asociados</p></div>`,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Query,
|
||||||
|
Req,
|
||||||
|
UseGuards,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { Request } from "express";
|
||||||
|
import { AbilityGuard } from "../auth/ability.guard";
|
||||||
|
import { AuthenticatedGuard } from "../auth/authenticated.guard";
|
||||||
|
import { RequireAbility } from "../auth/require-ability.decorator";
|
||||||
|
import { RenewalsService } from "./renewals.service";
|
||||||
|
|
||||||
|
@UseGuards(AuthenticatedGuard, AbilityGuard)
|
||||||
|
@Controller("renewals")
|
||||||
|
export class RenewalsController {
|
||||||
|
constructor(private readonly renewals: RenewalsService) {}
|
||||||
|
|
||||||
|
@Get("pending")
|
||||||
|
pending(@Query("days") days?: string) {
|
||||||
|
return this.renewals.pending(
|
||||||
|
Math.min(365, Math.max(1, Number(days) || 30)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("sweep")
|
||||||
|
@RequireAbility("renewal:send")
|
||||||
|
sweep(@Req() req: Request) {
|
||||||
|
return this.renewals.sweep((req.user as { id: string }).id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
import { RenewalsController } from "./renewals.controller";
|
||||||
|
import { MailService } from "./mail.service";
|
||||||
|
import { RenewalsService } from "./renewals.service";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [RenewalsController],
|
||||||
|
providers: [MailService, RenewalsService],
|
||||||
|
})
|
||||||
|
export class RenewalsModule {}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import {
|
||||||
|
addUtcDays,
|
||||||
|
dateInTimeZone,
|
||||||
|
renewalWindow,
|
||||||
|
RENEWAL_CADENCE,
|
||||||
|
} from "./renewals.service";
|
||||||
|
|
||||||
|
describe("renewal scheduling dates", () => {
|
||||||
|
it("uses the America/Tijuana calendar date", () => {
|
||||||
|
expect(dateInTimeZone(new Date("2026-08-01T05:00:00.000Z"))).toEqual(
|
||||||
|
new Date("2026-07-31T00:00:00.000Z"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps generations to 30 days, 15 days, and 7 days overdue", () => {
|
||||||
|
const today = new Date("2026-08-01T00:00:00.000Z");
|
||||||
|
|
||||||
|
expect(
|
||||||
|
RENEWAL_CADENCE.map(({ generation, offsetDays }) => ({
|
||||||
|
generation,
|
||||||
|
target: addUtcDays(today, offsetDays).toISOString().slice(0, 10),
|
||||||
|
})),
|
||||||
|
).toEqual([
|
||||||
|
{ generation: 1, target: "2026-08-31" },
|
||||||
|
{ generation: 2, target: "2026-08-16" },
|
||||||
|
{ generation: 3, target: "2026-07-25" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses an inclusive catch-up window after a missed run", () => {
|
||||||
|
const window = renewalWindow(
|
||||||
|
new Date("2026-08-10T00:00:00.000Z"),
|
||||||
|
30,
|
||||||
|
new Date("2026-08-07T18:00:00.000Z"),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(window).toEqual({
|
||||||
|
from: new Date("2026-09-07T00:00:00.000Z"),
|
||||||
|
to: new Date("2026-09-09T00:00:00.000Z"),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
import {
|
||||||
|
ConflictException,
|
||||||
|
Injectable,
|
||||||
|
Logger,
|
||||||
|
ServiceUnavailableException,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { Cron } from "@nestjs/schedule";
|
||||||
|
import { AuditService } from "../common/audit.service";
|
||||||
|
import { PrismaService } from "../prisma/prisma.service";
|
||||||
|
import {
|
||||||
|
renewalLetterSelect,
|
||||||
|
toRenewalLetterRow,
|
||||||
|
} from "../reports/renewal-letter";
|
||||||
|
import { MailService } from "./mail.service";
|
||||||
|
import { renderRenewalEmail } from "./renewal-email";
|
||||||
|
|
||||||
|
export const RENEWAL_CADENCE = [
|
||||||
|
{ generation: 1, offsetDays: 30 },
|
||||||
|
{ generation: 2, offsetDays: 15 },
|
||||||
|
{ generation: 3, offsetDays: -7 },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const JOB_NAME = "renewal-email-sweep";
|
||||||
|
const TIME_ZONE = "America/Tijuana";
|
||||||
|
const DAY_MS = 86400000;
|
||||||
|
|
||||||
|
export function dateInTimeZone(now: Date, timeZone = TIME_ZONE): Date {
|
||||||
|
const parts = new Intl.DateTimeFormat("en-US", {
|
||||||
|
timeZone,
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
}).formatToParts(now);
|
||||||
|
const value = (type: Intl.DateTimeFormatPartTypes) =>
|
||||||
|
Number(parts.find((part) => part.type === type)?.value);
|
||||||
|
return new Date(Date.UTC(value("year"), value("month") - 1, value("day")));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addUtcDays(date: Date, days: number): Date {
|
||||||
|
return new Date(date.getTime() + days * DAY_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renewalWindow(
|
||||||
|
today: Date,
|
||||||
|
offsetDays: number,
|
||||||
|
lastSuccessfulAt?: Date | null,
|
||||||
|
): { from: Date; to: Date } {
|
||||||
|
const to = addUtcDays(today, offsetDays);
|
||||||
|
if (!lastSuccessfulAt) return { from: to, to };
|
||||||
|
const previousDay = dateInTimeZone(lastSuccessfulAt);
|
||||||
|
if (previousDay >= today) return { from: to, to };
|
||||||
|
return { from: addUtcDays(previousDay, offsetDays + 1), to };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RenewalsService {
|
||||||
|
private readonly logger = new Logger(RenewalsService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
|
private readonly mail: MailService,
|
||||||
|
private readonly audit: AuditService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Cron("0 6 * * *", { timeZone: TIME_ZONE })
|
||||||
|
async scheduledSweep(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.sweep();
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(
|
||||||
|
`Falló el barrido de renovaciones: ${(error as Error).message}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async pending(days = 30) {
|
||||||
|
const today = dateInTimeZone(new Date());
|
||||||
|
const state = await this.prisma.scheduledJobState.findUnique({
|
||||||
|
where: { name: JOB_NAME },
|
||||||
|
select: { lastSuccessfulAt: true },
|
||||||
|
});
|
||||||
|
const cadence = RENEWAL_CADENCE.filter(
|
||||||
|
(item) => item.offsetDays < 0 || item.offsetDays <= days,
|
||||||
|
);
|
||||||
|
const groups = await Promise.all(
|
||||||
|
cadence.map(async (item) => ({
|
||||||
|
generation: item.generation,
|
||||||
|
rows: await this.findCandidates(
|
||||||
|
item,
|
||||||
|
today,
|
||||||
|
state?.lastSuccessfulAt ?? null,
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
return groups.flatMap(({ generation, rows }) =>
|
||||||
|
rows
|
||||||
|
.filter((policy) => Boolean(policy.customer.email?.trim()))
|
||||||
|
.map((policy) => toRenewalLetterRow(policy, generation)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sweep(userId?: string) {
|
||||||
|
const now = new Date();
|
||||||
|
const state = await this.acquireLock(now);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!this.mail.available) {
|
||||||
|
throw new ServiceUnavailableException(
|
||||||
|
"El servicio de correo no está configurado.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const today = dateInTimeZone(now);
|
||||||
|
let eligible = 0;
|
||||||
|
let sent = 0;
|
||||||
|
let skipped = 0;
|
||||||
|
const failures: Array<{ policyId: string; generation: number; error: string }> = [];
|
||||||
|
|
||||||
|
for (const cadence of RENEWAL_CADENCE) {
|
||||||
|
const policies = await this.findCandidates(
|
||||||
|
cadence,
|
||||||
|
today,
|
||||||
|
state.lastSuccessfulAt,
|
||||||
|
);
|
||||||
|
eligible += policies.length;
|
||||||
|
|
||||||
|
for (const policy of policies) {
|
||||||
|
const to = policy.customer.email?.trim();
|
||||||
|
if (!to) {
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const letter = toRenewalLetterRow(policy, cadence.generation);
|
||||||
|
const message = renderRenewalEmail(letter);
|
||||||
|
const result = await this.mail.send({ to, ...message });
|
||||||
|
const sentAt = new Date();
|
||||||
|
|
||||||
|
await this.prisma.renewalNotice.upsert({
|
||||||
|
where: {
|
||||||
|
policyId_generation: {
|
||||||
|
policyId: policy.id,
|
||||||
|
generation: cadence.generation,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
policyId: policy.id,
|
||||||
|
generation: cadence.generation,
|
||||||
|
channel: "EMAIL",
|
||||||
|
sentAt,
|
||||||
|
sentById: userId,
|
||||||
|
providerMessageId: result.providerId,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
channel: "EMAIL",
|
||||||
|
sentAt,
|
||||||
|
sentById: userId,
|
||||||
|
providerMessageId: result.providerId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
sent++;
|
||||||
|
void this.audit.log(userId, "renewalNotice.send", {
|
||||||
|
policyId: policy.id,
|
||||||
|
generation: cadence.generation,
|
||||||
|
providerMessageId: result.providerId,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
failures.push({
|
||||||
|
policyId: policy.id,
|
||||||
|
generation: cadence.generation,
|
||||||
|
error: (error as Error).message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = { eligible, sent, skipped, failed: failures.length, failures };
|
||||||
|
await this.releaseLock(failures.length === 0 ? now : null);
|
||||||
|
void this.audit.log(userId, "renewalNotice.sweep", result);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
await this.releaseLock(null);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private findCandidates(
|
||||||
|
cadence: (typeof RENEWAL_CADENCE)[number],
|
||||||
|
today: Date,
|
||||||
|
lastSuccessfulAt: Date | null,
|
||||||
|
) {
|
||||||
|
const window = renewalWindow(today, cadence.offsetDays, lastSuccessfulAt);
|
||||||
|
return this.prisma.policy.findMany({
|
||||||
|
where: {
|
||||||
|
archivedAt: null,
|
||||||
|
policyTo: { gte: window.from, lte: window.to },
|
||||||
|
customer: {
|
||||||
|
archivedAt: null,
|
||||||
|
emailOptOut: false,
|
||||||
|
email: { not: "" },
|
||||||
|
},
|
||||||
|
renewalNotices: {
|
||||||
|
none: { generation: cadence.generation, sentAt: { not: null } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: [{ policyTo: "asc" }, { policyNumber: "asc" }],
|
||||||
|
select: renewalLetterSelect(cadence.generation),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async acquireLock(now: Date) {
|
||||||
|
await this.prisma.scheduledJobState.upsert({
|
||||||
|
where: { name: JOB_NAME },
|
||||||
|
create: { name: JOB_NAME },
|
||||||
|
update: { updatedAt: now },
|
||||||
|
});
|
||||||
|
|
||||||
|
const acquired = await this.prisma.scheduledJobState.updateMany({
|
||||||
|
where: {
|
||||||
|
name: JOB_NAME,
|
||||||
|
OR: [{ lockedUntil: null }, { lockedUntil: { lte: now } }],
|
||||||
|
},
|
||||||
|
data: { lockedUntil: new Date(now.getTime() + 2 * 60 * 60 * 1000) },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (acquired.count !== 1) {
|
||||||
|
throw new ConflictException(
|
||||||
|
"Ya hay un barrido de renovaciones en curso.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.prisma.scheduledJobState.findUniqueOrThrow({
|
||||||
|
where: { name: JOB_NAME },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async releaseLock(lastSuccessfulAt: Date | null): Promise<void> {
|
||||||
|
await this.prisma.scheduledJobState.update({
|
||||||
|
where: { name: JOB_NAME },
|
||||||
|
data: {
|
||||||
|
lockedUntil: null,
|
||||||
|
...(lastSuccessfulAt && { lastSuccessfulAt }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import { Prisma } from "@jorgecuadros/database";
|
||||||
|
|
||||||
|
export function renewalLetterSelect(generation: number) {
|
||||||
|
return Prisma.validator<Prisma.PolicySelect>()({
|
||||||
|
id: true,
|
||||||
|
policyNumber: true,
|
||||||
|
policyTo: true,
|
||||||
|
netPremium: true,
|
||||||
|
policyFee: true,
|
||||||
|
total: true,
|
||||||
|
currency: true,
|
||||||
|
coveragesJson: true,
|
||||||
|
customer: {
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
nameMissing: true,
|
||||||
|
email: true,
|
||||||
|
phone: true,
|
||||||
|
mobile: true,
|
||||||
|
addressLine1: true,
|
||||||
|
addressLine2: true,
|
||||||
|
city: true,
|
||||||
|
state: true,
|
||||||
|
zipCode: true,
|
||||||
|
country: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
policyType: { select: { name: true } },
|
||||||
|
insuranceProvider: { select: { name: true } },
|
||||||
|
vehicles: {
|
||||||
|
take: 1,
|
||||||
|
select: {
|
||||||
|
make: true,
|
||||||
|
model: true,
|
||||||
|
modelYear: true,
|
||||||
|
bodyType: true,
|
||||||
|
engineNumber: true,
|
||||||
|
licensePlate: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
renewalNotices: {
|
||||||
|
where: { generation },
|
||||||
|
select: { sentAt: true, channel: true },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RenewalLetterPolicy = Prisma.PolicyGetPayload<{
|
||||||
|
select: ReturnType<typeof renewalLetterSelect>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export interface RenewalLetterRow extends Record<string, unknown> {
|
||||||
|
__kind: "letter";
|
||||||
|
policyId: string;
|
||||||
|
policyNumber: string;
|
||||||
|
policyType: string;
|
||||||
|
customerName: string;
|
||||||
|
customerEmail: string | null;
|
||||||
|
customerPhone: string | null;
|
||||||
|
customerMobile: string | null;
|
||||||
|
customerAddress: string[];
|
||||||
|
provider: string;
|
||||||
|
policyTo: string;
|
||||||
|
netPremium: string | null;
|
||||||
|
policyFee: string | null;
|
||||||
|
total: string | null;
|
||||||
|
currency: string;
|
||||||
|
coverageDays: unknown;
|
||||||
|
cslLimit: unknown;
|
||||||
|
medicalCoverage: unknown;
|
||||||
|
propertyDamage: unknown;
|
||||||
|
perPersonLiability: unknown;
|
||||||
|
additionalService: unknown;
|
||||||
|
vehicle: {
|
||||||
|
make: string | null;
|
||||||
|
model: string | null;
|
||||||
|
modelYear: string | null;
|
||||||
|
bodyType: string | null;
|
||||||
|
engineNumber: string | null;
|
||||||
|
licensePlate: string | null;
|
||||||
|
} | null;
|
||||||
|
generation: number;
|
||||||
|
sentAt: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toRenewalLetterRow(
|
||||||
|
policy: RenewalLetterPolicy,
|
||||||
|
generation: number,
|
||||||
|
): RenewalLetterRow {
|
||||||
|
const notice = policy.renewalNotices[0];
|
||||||
|
const coverage = (policy.coveragesJson ?? {}) as Record<string, unknown>;
|
||||||
|
const address = [
|
||||||
|
policy.customer.addressLine1,
|
||||||
|
policy.customer.addressLine2,
|
||||||
|
[policy.customer.city, policy.customer.state, policy.customer.zipCode]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(", "),
|
||||||
|
policy.customer.country,
|
||||||
|
].filter((part): part is string => Boolean(part));
|
||||||
|
|
||||||
|
return {
|
||||||
|
__kind: "letter",
|
||||||
|
policyId: policy.id,
|
||||||
|
policyNumber: policy.policyNumber,
|
||||||
|
policyType: policy.policyType?.name ?? "—",
|
||||||
|
customerName: policy.customer.nameMissing ? "(sin nombre)" : policy.customer.name,
|
||||||
|
customerEmail: policy.customer.email,
|
||||||
|
customerPhone: policy.customer.phone,
|
||||||
|
customerMobile: policy.customer.mobile,
|
||||||
|
customerAddress: address,
|
||||||
|
provider: policy.insuranceProvider?.name ?? "—",
|
||||||
|
policyTo: policy.policyTo ? policy.policyTo.toISOString().slice(0, 10) : "—",
|
||||||
|
netPremium: policy.netPremium ? policy.netPremium.toFixed(2) : null,
|
||||||
|
policyFee: policy.policyFee ? policy.policyFee.toFixed(2) : null,
|
||||||
|
total: policy.total ? policy.total.toFixed(2) : null,
|
||||||
|
currency: policy.currency,
|
||||||
|
coverageDays: coverage.cobertura ?? null,
|
||||||
|
cslLimit: coverage.csl_limite ?? null,
|
||||||
|
medicalCoverage: coverage.gastos_medico ?? null,
|
||||||
|
propertyDamage: coverage.propiedades ?? null,
|
||||||
|
perPersonLiability: coverage.personas ?? null,
|
||||||
|
additionalService:
|
||||||
|
coverage.servicio_adicional ?? coverage.servicio_adiconal ?? null,
|
||||||
|
vehicle: policy.vehicles[0]
|
||||||
|
? {
|
||||||
|
make: policy.vehicles[0].make,
|
||||||
|
model: policy.vehicles[0].model,
|
||||||
|
modelYear: policy.vehicles[0].modelYear,
|
||||||
|
bodyType: policy.vehicles[0].bodyType,
|
||||||
|
engineNumber: policy.vehicles[0].engineNumber,
|
||||||
|
licensePlate: policy.vehicles[0].licensePlate,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
generation,
|
||||||
|
sentAt: notice?.sentAt
|
||||||
|
? notice.sentAt.toISOString().slice(0, 10)
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -21,6 +21,10 @@ import {
|
|||||||
parseDate,
|
parseDate,
|
||||||
type ReportDef,
|
type ReportDef,
|
||||||
} from "./reports.types";
|
} from "./reports.types";
|
||||||
|
import {
|
||||||
|
renewalLetterSelect,
|
||||||
|
toRenewalLetterRow,
|
||||||
|
} from "./renewal-letter";
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ helpers */
|
/* ------------------------------------------------------------------ helpers */
|
||||||
|
|
||||||
@@ -615,10 +619,7 @@ const vigente: ReportDef = {
|
|||||||
* covers every carrier and tier instead of a clone per combination.
|
* covers every carrier and tier instead of a clone per combination.
|
||||||
*
|
*
|
||||||
* `sentStatus` is read from `RenewalNotice` (schema.prisma) — the
|
* `sentStatus` is read from `RenewalNotice` (schema.prisma) — the
|
||||||
* replacement for the legacy `CONTROL <ramo> RENEW[2/3] X MES` paper log
|
* replacement for the legacy `CONTROL <ramo> RENEW[2/3] X MES` paper log.
|
||||||
* — but this report is read-only; marking a notice as sent is a separate
|
|
||||||
* mutation (not yet built) that would upsert `RenewalNotice` by
|
|
||||||
* `[policyId, generation]`.
|
|
||||||
*/
|
*/
|
||||||
const avisoRenovacion: ReportDef = {
|
const avisoRenovacion: ReportDef = {
|
||||||
slug: "aviso-renovacion",
|
slug: "aviso-renovacion",
|
||||||
@@ -711,78 +712,16 @@ const avisoRenovacion: ReportDef = {
|
|||||||
: {}),
|
: {}),
|
||||||
},
|
},
|
||||||
orderBy: { policyTo: "asc" },
|
orderBy: { policyTo: "asc" },
|
||||||
select: {
|
select: renewalLetterSelect(generation),
|
||||||
id: true,
|
|
||||||
policyNumber: true,
|
|
||||||
policyTo: true,
|
|
||||||
netPremium: true,
|
|
||||||
policyFee: true,
|
|
||||||
total: true,
|
|
||||||
currency: true,
|
|
||||||
coveragesJson: true,
|
|
||||||
customer: { select: { name: true, nameMissing: true } },
|
|
||||||
insuranceProvider: { select: { name: true } },
|
|
||||||
vehicles: {
|
|
||||||
take: 1,
|
|
||||||
select: {
|
|
||||||
make: true,
|
|
||||||
model: true,
|
|
||||||
modelYear: true,
|
|
||||||
bodyType: true,
|
|
||||||
engineNumber: true,
|
|
||||||
licensePlate: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
renewalNotices: {
|
|
||||||
where: { generation },
|
|
||||||
select: { sentAt: true, channel: true },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let totalPremium = new Prisma.Decimal(0);
|
let totalPremium = new Prisma.Decimal(0);
|
||||||
let sentCount = 0;
|
let sentCount = 0;
|
||||||
const out = rows.map((r) => {
|
const out = rows.map((r) => {
|
||||||
if (r.netPremium) totalPremium = totalPremium.plus(r.netPremium);
|
if (r.netPremium) totalPremium = totalPremium.plus(r.netPremium);
|
||||||
const notice = r.renewalNotices[0];
|
const letter = toRenewalLetterRow(r, generation);
|
||||||
if (notice?.sentAt) sentCount++;
|
if (letter.sentAt) sentCount++;
|
||||||
// Legacy coverage columns not modeled as first-class Policy fields —
|
return letter;
|
||||||
// see docs/RENEWAL_NOTICES.md's column-mapping table. Keys are best-
|
|
||||||
// effort (derived from the source schema, not yet verified against a
|
|
||||||
// live migrated DB) — confirm before relying on them in production.
|
|
||||||
const cov = (r.coveragesJson ?? {}) as Record<string, unknown>;
|
|
||||||
return {
|
|
||||||
__kind: "letter",
|
|
||||||
policyId: r.id,
|
|
||||||
policyNumber: r.policyNumber,
|
|
||||||
customerName: nameOf(r.customer),
|
|
||||||
provider: r.insuranceProvider?.name ?? "—",
|
|
||||||
policyTo: r.policyTo ? r.policyTo.toISOString().slice(0, 10) : "—",
|
|
||||||
netPremium: r.netPremium ? r.netPremium.toFixed(2) : null,
|
|
||||||
policyFee: r.policyFee ? r.policyFee.toFixed(2) : null,
|
|
||||||
total: r.total ? r.total.toFixed(2) : null,
|
|
||||||
currency: r.currency,
|
|
||||||
coverageDays: cov.cobertura ?? null,
|
|
||||||
cslLimit: cov.csl_limite ?? null,
|
|
||||||
medicalCoverage: cov.gastos_medico ?? null,
|
|
||||||
propertyDamage: cov.propiedades ?? null,
|
|
||||||
perPersonLiability: cov.personas ?? null,
|
|
||||||
additionalService: cov.servicio_adicional ?? cov.servicio_adiconal ?? null,
|
|
||||||
vehicle: r.vehicles[0]
|
|
||||||
? {
|
|
||||||
make: r.vehicles[0].make,
|
|
||||||
model: r.vehicles[0].model,
|
|
||||||
modelYear: r.vehicles[0].modelYear,
|
|
||||||
bodyType: r.vehicles[0].bodyType,
|
|
||||||
engineNumber: r.vehicles[0].engineNumber,
|
|
||||||
licensePlate: r.vehicles[0].licensePlate,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
generation,
|
|
||||||
sentAt: notice?.sentAt
|
|
||||||
? notice.sentAt.toISOString().slice(0, 10)
|
|
||||||
: null,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -0,0 +1,266 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { AppShell } from "@/components/AppShell";
|
||||||
|
import { useCan } from "@/lib/abilities";
|
||||||
|
import { formatDate, formatMoney } from "@/lib/labels";
|
||||||
|
import { apiFetch } from "@/lib/api";
|
||||||
|
|
||||||
|
export interface RenewalLetter {
|
||||||
|
policyId: string;
|
||||||
|
policyNumber: string;
|
||||||
|
policyType: string;
|
||||||
|
customerName: string;
|
||||||
|
customerEmail: string | null;
|
||||||
|
provider: string;
|
||||||
|
policyTo: string;
|
||||||
|
netPremium: string | null;
|
||||||
|
total: string | null;
|
||||||
|
currency: string;
|
||||||
|
generation: number;
|
||||||
|
sentAt: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RenewalSweepResult {
|
||||||
|
eligible: number;
|
||||||
|
sent: number;
|
||||||
|
skipped: number;
|
||||||
|
failed: number;
|
||||||
|
failures: { policyId: string; generation: number; error: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RenewalMarkInput {
|
||||||
|
generation: number;
|
||||||
|
channel: "MAIL" | "EMAIL";
|
||||||
|
sentAt?: string;
|
||||||
|
notes?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RenovacionesPage() {
|
||||||
|
return (
|
||||||
|
<AppShell>
|
||||||
|
<Renovaciones />
|
||||||
|
</AppShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const GENERATION_LABEL: Record<number, string> = {
|
||||||
|
1: "Primer aviso (30 días antes)",
|
||||||
|
2: "Segundo aviso (15 días antes)",
|
||||||
|
3: "Tercer aviso (7 días después)",
|
||||||
|
};
|
||||||
|
|
||||||
|
const CHANNEL_LABEL: Record<"MAIL" | "EMAIL", string> = {
|
||||||
|
MAIL: "Impreso",
|
||||||
|
EMAIL: "Correo electrónico",
|
||||||
|
};
|
||||||
|
|
||||||
|
function Renovaciones() {
|
||||||
|
const allowed = useCan("renewal:send");
|
||||||
|
const [days, setDays] = useState(30);
|
||||||
|
const [pending, setPending] = useState<RenewalLetter[] | null>(null);
|
||||||
|
const [pendingError, setPendingError] = useState<string | null>(null);
|
||||||
|
const [actionError, setActionError] = useState<string | null>(null);
|
||||||
|
const [notice, setNotice] = useState<string | null>(null);
|
||||||
|
const [sweeping, setSweeping] = useState(false);
|
||||||
|
|
||||||
|
const refresh = useCallback(async () => {
|
||||||
|
setPendingError(null);
|
||||||
|
try {
|
||||||
|
const data = await apiFetch<RenewalLetter[]>(
|
||||||
|
`/renewals/pending?days=${days}`,
|
||||||
|
);
|
||||||
|
setPending(data);
|
||||||
|
} catch (e) {
|
||||||
|
setPendingError(
|
||||||
|
(e as Error)?.message ?? "No se pudo cargar la lista de avisos.",
|
||||||
|
);
|
||||||
|
setPending([]);
|
||||||
|
}
|
||||||
|
}, [days]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (allowed) refresh();
|
||||||
|
}, [allowed, refresh]);
|
||||||
|
|
||||||
|
async function handleSweep() {
|
||||||
|
setActionError(null);
|
||||||
|
setNotice(null);
|
||||||
|
setSweeping(true);
|
||||||
|
try {
|
||||||
|
const result = await apiFetch<RenewalSweepResult>("/renewals/sweep", {
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
setNotice(
|
||||||
|
`Enviados ${result.sent} avisos (${result.failed} con error).`,
|
||||||
|
);
|
||||||
|
await refresh();
|
||||||
|
} catch (e) {
|
||||||
|
setActionError((e as Error)?.message ?? "No se pudo ejecutar el barrido.");
|
||||||
|
} finally {
|
||||||
|
setSweeping(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleMark(letter: RenewalLetter, channel: "MAIL" | "EMAIL") {
|
||||||
|
setActionError(null);
|
||||||
|
setNotice(null);
|
||||||
|
try {
|
||||||
|
await apiFetch(`/policies/${letter.policyId}/renewal-notices`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
generation: letter.generation,
|
||||||
|
channel,
|
||||||
|
} satisfies RenewalMarkInput),
|
||||||
|
});
|
||||||
|
setNotice(`Aviso marcado como enviado (${CHANNEL_LABEL[channel]}).`);
|
||||||
|
await refresh();
|
||||||
|
} catch (e) {
|
||||||
|
setActionError(
|
||||||
|
(e as Error)?.message ?? "No se pudo registrar el aviso.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allowed) {
|
||||||
|
return (
|
||||||
|
<div className="page-head">
|
||||||
|
<h1 className="page-title">Renovaciones</h1>
|
||||||
|
<div className="state-box state-error">
|
||||||
|
No tiene permisos para enviar avisos de renovación.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const counts = (pending ?? []).reduce<Record<number, number>>(
|
||||||
|
(acc, item) => ({
|
||||||
|
...acc,
|
||||||
|
[item.generation]: (acc[item.generation] ?? 0) + 1,
|
||||||
|
}),
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
const grouped = [1, 2, 3].filter((gen) => (counts[gen] ?? 0) > 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="page-head">
|
||||||
|
<p className="eyebrow">Renovaciones</p>
|
||||||
|
<h1 className="page-title">Avisos de renovación</h1>
|
||||||
|
<p className="muted" style={{ marginTop: 6, maxWidth: 720 }}>
|
||||||
|
El sistema ejecuta un barrido diario a las 06:00 hora local que
|
||||||
|
notifica a los clientes a 30, 15 y 7 días antes o después del
|
||||||
|
vencimiento de su póliza. Esta pantalla muestra qué avisos están
|
||||||
|
pendientes y permite ejecutarlo manualmente.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{actionError && <div className="state-box state-error">{actionError}</div>}
|
||||||
|
{notice && <div className="state-box">{notice}</div>}
|
||||||
|
|
||||||
|
<div className="card" style={{ padding: 20, marginBottom: 20 }}>
|
||||||
|
<div className="row-actions" style={{ justifyContent: "space-between" }}>
|
||||||
|
<div>
|
||||||
|
<h2 className="section-title">Barrido manual</h2>
|
||||||
|
<p className="muted small" style={{ marginTop: 4 }}>
|
||||||
|
Usa la fecha actual del servidor como referencia para seleccionar
|
||||||
|
avisos vencidos a 30 y 15 días, y vencidos hace 7 días.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-primary"
|
||||||
|
disabled={sweeping}
|
||||||
|
onClick={handleSweep}
|
||||||
|
>
|
||||||
|
{sweeping ? "Enviando…" : "Ejecutar barrido"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="field" style={{ maxWidth: 180, marginTop: 12 }}>
|
||||||
|
<span className="field-label">Ventana (días)</span>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={365}
|
||||||
|
value={days}
|
||||||
|
onChange={(e) =>
|
||||||
|
setDays(Math.min(365, Math.max(1, Number(e.target.value) || 30)))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{pendingError && (
|
||||||
|
<div className="state-box state-error">{pendingError}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!pendingError && grouped.length === 0 && (
|
||||||
|
<div className="empty-inline">
|
||||||
|
No hay avisos pendientes en esta ventana.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{grouped.map((generation) => (
|
||||||
|
<section className="card" key={generation} style={{ padding: 20 }}>
|
||||||
|
<h2 className="section-title">{GENERATION_LABEL[generation]}</h2>
|
||||||
|
<div className="tx-scroll">
|
||||||
|
<table className="tx-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Cliente</th>
|
||||||
|
<th>Póliza</th>
|
||||||
|
<th>Tipo</th>
|
||||||
|
<th>Aseguradora</th>
|
||||||
|
<th>Vence</th>
|
||||||
|
<th className="num">Prima</th>
|
||||||
|
<th>Acciones</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{(pending ?? [])
|
||||||
|
.filter((item) => item.generation === generation)
|
||||||
|
.map((item) => (
|
||||||
|
<tr key={`${item.policyId}-${item.generation}`}>
|
||||||
|
<td>
|
||||||
|
<div>{item.customerName}</div>
|
||||||
|
<div className="muted small">
|
||||||
|
{item.customerEmail ?? "Sin correo"}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="mono">{item.policyNumber}</td>
|
||||||
|
<td>{item.policyType}</td>
|
||||||
|
<td>{item.provider}</td>
|
||||||
|
<td>{formatDate(item.policyTo)}</td>
|
||||||
|
<td className="num">
|
||||||
|
{formatMoney(item.total ?? item.netPremium, item.currency)}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="row-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-outline btn-sm"
|
||||||
|
onClick={() => handleMark(item, "EMAIL")}
|
||||||
|
disabled={!item.customerEmail}
|
||||||
|
>
|
||||||
|
Marcar EMAIL
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-outline btn-sm"
|
||||||
|
onClick={() => handleMark(item, "MAIL")}
|
||||||
|
>
|
||||||
|
Marcar impreso
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -79,6 +79,7 @@ const NAV: NavEntry[] = [
|
|||||||
ability: "bank:manage-accounts",
|
ability: "bank:manage-accounts",
|
||||||
},
|
},
|
||||||
{ href: "/usuarios", label: "Usuarios", ability: "user:manage" },
|
{ href: "/usuarios", label: "Usuarios", ability: "user:manage" },
|
||||||
|
{ href: "/renovaciones", label: "Renovaciones", ability: "renewal:send" },
|
||||||
{ href: "/operaciones", label: "Operaciones", ability: "db:manage" },
|
{ href: "/operaciones", label: "Operaciones", ability: "db:manage" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ type Values = {
|
|||||||
mobile: string;
|
mobile: string;
|
||||||
fax: string;
|
fax: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
emailOptOut: boolean;
|
||||||
identificationType: string;
|
identificationType: string;
|
||||||
identificationNumber: string;
|
identificationNumber: string;
|
||||||
identificationExpiration: string;
|
identificationExpiration: string;
|
||||||
@@ -54,6 +55,7 @@ function initial(c?: CustomerDetail): Values {
|
|||||||
mobile: c?.mobile ?? "",
|
mobile: c?.mobile ?? "",
|
||||||
fax: c?.fax ?? "",
|
fax: c?.fax ?? "",
|
||||||
email: c?.email ?? "",
|
email: c?.email ?? "",
|
||||||
|
emailOptOut: c?.emailOptOut ?? false,
|
||||||
identificationType: c?.identificationType ?? "",
|
identificationType: c?.identificationType ?? "",
|
||||||
identificationNumber: c?.identificationNumber ?? "",
|
identificationNumber: c?.identificationNumber ?? "",
|
||||||
identificationExpiration: toDateInput(c?.identificationExpiration),
|
identificationExpiration: toDateInput(c?.identificationExpiration),
|
||||||
@@ -103,6 +105,7 @@ export function CustomerForm({ customer }: { customer?: CustomerDetail }) {
|
|||||||
mobile: s(v.mobile),
|
mobile: s(v.mobile),
|
||||||
fax: s(v.fax),
|
fax: s(v.fax),
|
||||||
email: s(v.email),
|
email: s(v.email),
|
||||||
|
emailOptOut: v.emailOptOut,
|
||||||
identificationType: s(v.identificationType),
|
identificationType: s(v.identificationType),
|
||||||
identificationNumber: s(v.identificationNumber),
|
identificationNumber: s(v.identificationNumber),
|
||||||
identificationExpiration: s(v.identificationExpiration),
|
identificationExpiration: s(v.identificationExpiration),
|
||||||
@@ -139,6 +142,13 @@ export function CustomerForm({ customer }: { customer?: CustomerDetail }) {
|
|||||||
<input className="input" type="email" value={v.email}
|
<input className="input" type="email" value={v.email}
|
||||||
onChange={(e) => set("email", e.target.value)} />
|
onChange={(e) => set("email", e.target.value)} />
|
||||||
</Field>
|
</Field>
|
||||||
|
<Field label="Notificaciones de renovación">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" checked={v.emailOptOut}
|
||||||
|
onChange={(e) => set("emailOptOut", e.target.checked)} />
|
||||||
|
{" "}No enviar correos
|
||||||
|
</label>
|
||||||
|
</Field>
|
||||||
<Field label="Teléfono">
|
<Field label="Teléfono">
|
||||||
<input className="input" value={v.phone}
|
<input className="input" value={v.phone}
|
||||||
onChange={(e) => set("phone", e.target.value)} />
|
onChange={(e) => set("phone", e.target.value)} />
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ export class ApiError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function apiFetch<T>(
|
export async function apiFetch<T>(
|
||||||
path: string,
|
path: string,
|
||||||
init?: RequestInit,
|
init?: RequestInit,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export type Ability =
|
|||||||
| "policy:delete"
|
| "policy:delete"
|
||||||
| "policy:ingest"
|
| "policy:ingest"
|
||||||
| "policy:ocr-review"
|
| "policy:ocr-review"
|
||||||
|
| "renewal:send"
|
||||||
| "property:create"
|
| "property:create"
|
||||||
| "property:update"
|
| "property:update"
|
||||||
| "property:delete"
|
| "property:delete"
|
||||||
@@ -963,6 +964,7 @@ export interface CustomerDetail {
|
|||||||
mobile: string | null;
|
mobile: string | null;
|
||||||
fax: string | null;
|
fax: string | null;
|
||||||
email: string | null;
|
email: string | null;
|
||||||
|
emailOptOut: boolean;
|
||||||
notes: string | null;
|
notes: string | null;
|
||||||
identificationType: string | null;
|
identificationType: string | null;
|
||||||
identificationNumber: string | null;
|
identificationNumber: string | null;
|
||||||
@@ -993,6 +995,7 @@ export interface CustomerInput {
|
|||||||
mobile?: string;
|
mobile?: string;
|
||||||
fax?: string;
|
fax?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
|
emailOptOut?: boolean;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
identificationType?: string;
|
identificationType?: string;
|
||||||
identificationNumber?: string;
|
identificationNumber?: string;
|
||||||
|
|||||||
@@ -32,3 +32,9 @@ S3_ENDPOINT=http://192.168.4.212:9000
|
|||||||
S3_BUCKET=jorgecuadros-documents
|
S3_BUCKET=jorgecuadros-documents
|
||||||
MINIO_ROOT_USER=jc_minio
|
MINIO_ROOT_USER=jc_minio
|
||||||
MINIO_ROOT_PASSWORD=CHANGE_ME
|
MINIO_ROOT_PASSWORD=CHANGE_ME
|
||||||
|
|
||||||
|
SES_REGION=
|
||||||
|
SES_FROM=
|
||||||
|
SES_ACCESS_KEY=
|
||||||
|
SES_SECRET_KEY=
|
||||||
|
SES_CONFIGURATION_SET=
|
||||||
|
|||||||
@@ -55,6 +55,11 @@ services:
|
|||||||
S3_BUCKET: ${S3_BUCKET:-jorgecuadros-documents}
|
S3_BUCKET: ${S3_BUCKET:-jorgecuadros-documents}
|
||||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER:?MINIO_ROOT_USER must be set}
|
MINIO_ROOT_USER: ${MINIO_ROOT_USER:?MINIO_ROOT_USER must be set}
|
||||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD must be set}
|
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD must be set}
|
||||||
|
SES_REGION: ${SES_REGION:-}
|
||||||
|
SES_FROM: ${SES_FROM:-}
|
||||||
|
SES_ACCESS_KEY: ${SES_ACCESS_KEY:-}
|
||||||
|
SES_SECRET_KEY: ${SES_SECRET_KEY:-}
|
||||||
|
SES_CONFIGURATION_SET: ${SES_CONFIGURATION_SET:-}
|
||||||
ports:
|
ports:
|
||||||
- target: 3001
|
- target: 3001
|
||||||
published: ${API_PORT:-3001}
|
published: ${API_PORT:-3001}
|
||||||
|
|||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
ALTER TABLE `customers`
|
||||||
|
ADD COLUMN `emailOptOut` BOOLEAN NOT NULL DEFAULT false;
|
||||||
|
|
||||||
|
ALTER TABLE `renewal_notices`
|
||||||
|
ADD COLUMN `providerMessageId` VARCHAR(191) NULL;
|
||||||
|
|
||||||
|
CREATE TABLE `scheduled_job_states` (
|
||||||
|
`name` VARCHAR(191) NOT NULL,
|
||||||
|
`lockedUntil` DATETIME(3) NULL,
|
||||||
|
`lastSuccessfulAt` DATETIME(3) NULL,
|
||||||
|
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
`updatedAt` DATETIME(3) NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (`name`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
@@ -99,6 +99,7 @@ model Customer {
|
|||||||
mobile String?
|
mobile String?
|
||||||
fax String?
|
fax String?
|
||||||
email String?
|
email String?
|
||||||
|
emailOptOut Boolean @default(false)
|
||||||
notes String? @db.Text
|
notes String? @db.Text
|
||||||
identificationType String?
|
identificationType String?
|
||||||
identificationNumber String?
|
identificationNumber String?
|
||||||
@@ -238,6 +239,7 @@ model RenewalNotice {
|
|||||||
channel RenewalNoticeChannel @default(MAIL)
|
channel RenewalNoticeChannel @default(MAIL)
|
||||||
sentAt DateTime?
|
sentAt DateTime?
|
||||||
sentById String?
|
sentById String?
|
||||||
|
providerMessageId String?
|
||||||
notes String? @db.Text
|
notes String? @db.Text
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
@@ -953,3 +955,13 @@ model OpsJob {
|
|||||||
@@index([startedAt])
|
@@index([startedAt])
|
||||||
@@map("ops_jobs")
|
@@map("ops_jobs")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model ScheduledJobState {
|
||||||
|
name String @id
|
||||||
|
lockedUntil DateTime?
|
||||||
|
lastSuccessfulAt DateTime?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@map("scheduled_job_states")
|
||||||
|
}
|
||||||
|
|||||||
Generated
+288
@@ -13,6 +13,9 @@ importers:
|
|||||||
'@aws-sdk/client-s3':
|
'@aws-sdk/client-s3':
|
||||||
specifier: ^3.665.0
|
specifier: ^3.665.0
|
||||||
version: 3.1093.0
|
version: 3.1093.0
|
||||||
|
'@aws-sdk/client-sesv2':
|
||||||
|
specifier: ^3.1101.0
|
||||||
|
version: 3.1101.0
|
||||||
'@jorgecuadros/database':
|
'@jorgecuadros/database':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/database
|
version: link:../../packages/database
|
||||||
@@ -31,6 +34,9 @@ importers:
|
|||||||
'@nestjs/platform-express':
|
'@nestjs/platform-express':
|
||||||
specifier: ^10.4.4
|
specifier: ^10.4.4
|
||||||
version: 10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)
|
version: 10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)
|
||||||
|
'@nestjs/schedule':
|
||||||
|
specifier: ^4.1.2
|
||||||
|
version: 4.1.2(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)
|
||||||
argon2:
|
argon2:
|
||||||
specifier: ^0.41.1
|
specifier: ^0.41.1
|
||||||
version: 0.41.1
|
version: 0.41.1
|
||||||
@@ -165,18 +171,38 @@ packages:
|
|||||||
resolution: {integrity: sha512-7452vEdp/nihIBWijnmcTBujXEFfbs4F02wyBDGqmNr6pwyo5GmQorx0zQIVg8QGFLXiBvsWKXBhCdiBcxNnGA==}
|
resolution: {integrity: sha512-7452vEdp/nihIBWijnmcTBujXEFfbs4F02wyBDGqmNr6pwyo5GmQorx0zQIVg8QGFLXiBvsWKXBhCdiBcxNnGA==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
|
'@aws-sdk/client-sesv2@3.1101.0':
|
||||||
|
resolution: {integrity: sha512-5n4COAW5u6T1gOz4t6RueAncImDjK0wdoQEXu1ABnRfLAoNa2dlouubKq3lHLliQdhhEo4so7Cn88ylprab0Ng==}
|
||||||
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/core@3.976.0':
|
'@aws-sdk/core@3.976.0':
|
||||||
resolution: {integrity: sha512-0cjRaEdlVoOrsNb9pP5q1Syyc8pXw5xSj2Np2ryReRTr9FppIIRVSdZK4lbnfmc2Hvgux/xBOUU6baB7z8//uA==}
|
resolution: {integrity: sha512-0cjRaEdlVoOrsNb9pP5q1Syyc8pXw5xSj2Np2ryReRTr9FppIIRVSdZK4lbnfmc2Hvgux/xBOUU6baB7z8//uA==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
|
'@aws-sdk/core@3.977.4':
|
||||||
|
resolution: {integrity: sha512-CEkcQlMOQJCvul60U7wdAOACjtdgFWDsfJI+6wUOGdhGNV2lGbuJpi/R50QLpFG3Tp+sQxa/RmzC3X7KHbhuTA==}
|
||||||
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-env@3.972.60':
|
'@aws-sdk/credential-provider-env@3.972.60':
|
||||||
resolution: {integrity: sha512-BAkxdoe7tpDDqCghGpuOeHQRbm/2znVvOQm0AvpQbA2tbfMN46doN4zx65fv85ImP3KADwc2zQPmbrlI9MPfMg==}
|
resolution: {integrity: sha512-BAkxdoe7tpDDqCghGpuOeHQRbm/2znVvOQm0AvpQbA2tbfMN46doN4zx65fv85ImP3KADwc2zQPmbrlI9MPfMg==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
|
'@aws-sdk/credential-provider-env@3.972.65':
|
||||||
|
resolution: {integrity: sha512-lJT2aRw9wCV8jPHyFJjdZLD4HTydL6/22AnCSOB8e/LqOc55nEJGLHkJQeSxhn8QiqyjFwPKQFtMw0ovjRUY/g==}
|
||||||
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-http@3.972.62':
|
'@aws-sdk/credential-provider-http@3.972.62':
|
||||||
resolution: {integrity: sha512-g/0fGqKTb9xpKdd9AtpmV5Eo3DFKbnkpA2+w0peISSlu7NfAoWOuYBFxsu+yWBtxU89ka55ezoZBCbFaS8pjYQ==}
|
resolution: {integrity: sha512-g/0fGqKTb9xpKdd9AtpmV5Eo3DFKbnkpA2+w0peISSlu7NfAoWOuYBFxsu+yWBtxU89ka55ezoZBCbFaS8pjYQ==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
|
'@aws-sdk/credential-provider-http@3.972.67':
|
||||||
|
resolution: {integrity: sha512-N7fw/15hSwI/CPxe5ohOyb7O4ge9f5me1gVIn8OIkBRB0squ8OJqQyDyH/HoL+Sb1W5xdC88jVC+bHkw73iu+Q==}
|
||||||
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
|
'@aws-sdk/credential-provider-ini@3.973.10':
|
||||||
|
resolution: {integrity: sha512-Zh9XRaPnDN9buO7GfWBubS22R6Nq5D6hbyYEMN05LiOnXugm/8WDjUx6y756bSPbdn3aJB2qG4zFW3bN82QhoQ==}
|
||||||
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-ini@3.973.5':
|
'@aws-sdk/credential-provider-ini@3.973.5':
|
||||||
resolution: {integrity: sha512-ylubazcRfq2TVus/qXucSXeC42Qdjp5HQxTu68K/BsdMiZlcSLD1zkpoCgApXZX1Y6YJhtGGs7ZHhO/GuIgBlw==}
|
resolution: {integrity: sha512-ylubazcRfq2TVus/qXucSXeC42Qdjp5HQxTu68K/BsdMiZlcSLD1zkpoCgApXZX1Y6YJhtGGs7ZHhO/GuIgBlw==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
@@ -185,22 +211,42 @@ packages:
|
|||||||
resolution: {integrity: sha512-CCygIKJ9YbI3n84OClSaSppkgKKHVj2TGT33c6FRORZrYNZQ1POmD+ip0FLYokiJAK7sSdc3YVkOsBm90oxWMQ==}
|
resolution: {integrity: sha512-CCygIKJ9YbI3n84OClSaSppkgKKHVj2TGT33c6FRORZrYNZQ1POmD+ip0FLYokiJAK7sSdc3YVkOsBm90oxWMQ==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
|
'@aws-sdk/credential-provider-login@3.972.72':
|
||||||
|
resolution: {integrity: sha512-zZapIKwaHp7TdTf9hbH1I3CVUdEupmt7FXO/BoTQGC+4h6NkXKWpqF2p5WyfpjurDLHCpSyh+BzMlAg8arqWLA==}
|
||||||
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-node@3.972.71':
|
'@aws-sdk/credential-provider-node@3.972.71':
|
||||||
resolution: {integrity: sha512-HIg7Q2osBzajQwL+1Vkyh2E7Gim3eTNb9RHIsOxDGjW0eZg4oEKtRs5sioCnc73ilhaOm4gX2lHVF8J7+nt2rg==}
|
resolution: {integrity: sha512-HIg7Q2osBzajQwL+1Vkyh2E7Gim3eTNb9RHIsOxDGjW0eZg4oEKtRs5sioCnc73ilhaOm4gX2lHVF8J7+nt2rg==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
|
'@aws-sdk/credential-provider-node@3.972.76':
|
||||||
|
resolution: {integrity: sha512-1yzLmRiYSgGC25v7ZZEwJn/auhHHTIHgFOmzL2f36hf1+7jSLcX+1QrAz4760WEzPiiQl8xmlpFhHfl2OoyVzA==}
|
||||||
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-process@3.972.60':
|
'@aws-sdk/credential-provider-process@3.972.60':
|
||||||
resolution: {integrity: sha512-YIo3f99hM43QdYG8hDzwGemnR/pU95b0kramqSJUTleCqaB7+HwKf7YZFHqvOgTqZTPx/mRmNIqoDRr3U0Z3Tw==}
|
resolution: {integrity: sha512-YIo3f99hM43QdYG8hDzwGemnR/pU95b0kramqSJUTleCqaB7+HwKf7YZFHqvOgTqZTPx/mRmNIqoDRr3U0Z3Tw==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
|
'@aws-sdk/credential-provider-process@3.972.65':
|
||||||
|
resolution: {integrity: sha512-e5DbbNteOSalN58U83G6kFa4ECLEuGbGqNBHIXE7zYXA/m4GHblIGjFbSH7wYv6gBV8iNSDcRZBKfQZF5vF9nw==}
|
||||||
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-sso@3.973.4':
|
'@aws-sdk/credential-provider-sso@3.973.4':
|
||||||
resolution: {integrity: sha512-BPdmL8sSBOCv4ngZ+3LHxyc3CNqDCEK37CHioCk7zGrTMY5sUtkH8q+o6qA80nn6w3/fyBPGNE7OIRlmoOxRQA==}
|
resolution: {integrity: sha512-BPdmL8sSBOCv4ngZ+3LHxyc3CNqDCEK37CHioCk7zGrTMY5sUtkH8q+o6qA80nn6w3/fyBPGNE7OIRlmoOxRQA==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
|
'@aws-sdk/credential-provider-sso@3.973.9':
|
||||||
|
resolution: {integrity: sha512-0V0u4t+KBku9fbh5CPCaC5hUWwSzDafp8nCuDy817zWbp2gz80jO44rMQkiwnZ+k54B+tjAtzRy00DJRGTKGBg==}
|
||||||
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-web-identity@3.972.66':
|
'@aws-sdk/credential-provider-web-identity@3.972.66':
|
||||||
resolution: {integrity: sha512-kSAziJboOmZmsR9/MTbiNjowl2BPes1bQuJpne4qAZ62ubi8fjfr/aupJSQje6udBoYxXTQbsL0e0kby2la3ng==}
|
resolution: {integrity: sha512-kSAziJboOmZmsR9/MTbiNjowl2BPes1bQuJpne4qAZ62ubi8fjfr/aupJSQje6udBoYxXTQbsL0e0kby2la3ng==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
|
'@aws-sdk/credential-provider-web-identity@3.972.71':
|
||||||
|
resolution: {integrity: sha512-e4dwiRltGAaQ+2yxw57Hj0l/BF3BHiG14+QpYE7bGYBlpAq/fkIri2BDhjWon8c0mhhtd2txQBAkQb9BcTStFg==}
|
||||||
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/middleware-sdk-s3@3.972.65':
|
'@aws-sdk/middleware-sdk-s3@3.972.65':
|
||||||
resolution: {integrity: sha512-udwNhRfDTfCB98mAHjjgsnKQlxygB4e0X+Obne/XjJpvVsF0YCQC8ZErd/8Z6IPoLQjtiKHzwqEDbZiLrJEnOg==}
|
resolution: {integrity: sha512-udwNhRfDTfCB98mAHjjgsnKQlxygB4e0X+Obne/XjJpvVsF0YCQC8ZErd/8Z6IPoLQjtiKHzwqEDbZiLrJEnOg==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
@@ -209,14 +255,26 @@ packages:
|
|||||||
resolution: {integrity: sha512-Y9REVrSwmLM+Qy6sZJ7ofMC2S3Hr3tPP/4CzL5U1olPP7OGoF+6+Px0E49cVQBtSxJtyeLJMf0UaBErfeSahAA==}
|
resolution: {integrity: sha512-Y9REVrSwmLM+Qy6sZJ7ofMC2S3Hr3tPP/4CzL5U1olPP7OGoF+6+Px0E49cVQBtSxJtyeLJMf0UaBErfeSahAA==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
|
'@aws-sdk/nested-clients@3.997.39':
|
||||||
|
resolution: {integrity: sha512-wU5NPnj62Sb7A8xn/Zb+xThe05P3otNtDl37iOIi5DDMeCesNeCckaG+eXWGUs12Z9R34I8CD05TaTe6SIa61g==}
|
||||||
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/signature-v4-multi-region@3.996.41':
|
'@aws-sdk/signature-v4-multi-region@3.996.41':
|
||||||
resolution: {integrity: sha512-QMUytg+FQMGouc8gHS00KoYih3+N6cqmVI/pQGOIo7Nr7OpQaiXjSYOuL+vsPZ1tymY4LAQ8MYcHJmws5LRxng==}
|
resolution: {integrity: sha512-QMUytg+FQMGouc8gHS00KoYih3+N6cqmVI/pQGOIo7Nr7OpQaiXjSYOuL+vsPZ1tymY4LAQ8MYcHJmws5LRxng==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
|
'@aws-sdk/signature-v4-multi-region@3.996.43':
|
||||||
|
resolution: {integrity: sha512-lKekx8bLBXSv4O+cslk9Zfnw2XKSkWBs3uWL5QGhH2ZAQfNS7FE0vcSSN2vD/AhxX54ZTywWxR4STThoeOXlBA==}
|
||||||
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/token-providers@3.1092.0':
|
'@aws-sdk/token-providers@3.1092.0':
|
||||||
resolution: {integrity: sha512-hBYUAr6iBLNFcsiWTgtBb0stdSw39VOUq4Sp4A5caCNf66BAZplWN4FleKrVpJx5li2YgdnK2DqoFSMWC642FQ==}
|
resolution: {integrity: sha512-hBYUAr6iBLNFcsiWTgtBb0stdSw39VOUq4Sp4A5caCNf66BAZplWN4FleKrVpJx5li2YgdnK2DqoFSMWC642FQ==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
|
'@aws-sdk/token-providers@3.1100.0':
|
||||||
|
resolution: {integrity: sha512-THf3MkgY3fNJZ3zdgSenLqR7gSE68KccCj1RCKretlG73Ppszvues02VpCUO9NlB/tZDC483FvGCld+AiPCkvg==}
|
||||||
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/types@3.974.2':
|
'@aws-sdk/types@3.974.2':
|
||||||
resolution: {integrity: sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA==}
|
resolution: {integrity: sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
@@ -225,6 +283,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA==}
|
resolution: {integrity: sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
|
'@aws-sdk/xml-builder@3.972.37':
|
||||||
|
resolution: {integrity: sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg==}
|
||||||
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws/lambda-invoke-store@0.3.0':
|
'@aws/lambda-invoke-store@0.3.0':
|
||||||
resolution: {integrity: sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==}
|
resolution: {integrity: sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
@@ -580,6 +642,12 @@ packages:
|
|||||||
'@nestjs/common': ^10.0.0
|
'@nestjs/common': ^10.0.0
|
||||||
'@nestjs/core': ^10.0.0
|
'@nestjs/core': ^10.0.0
|
||||||
|
|
||||||
|
'@nestjs/schedule@4.1.2':
|
||||||
|
resolution: {integrity: sha512-hCTQ1lNjIA5EHxeu8VvQu2Ed2DBLS1GSC6uKPYlBiQe6LL9a7zfE9iVSK+zuK8E2odsApteEBmfAQchc8Hx0Gg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0
|
||||||
|
'@nestjs/core': ^8.0.0 || ^9.0.0 || ^10.0.0
|
||||||
|
|
||||||
'@nestjs/schematics@10.2.3':
|
'@nestjs/schematics@10.2.3':
|
||||||
resolution: {integrity: sha512-4e8gxaCk7DhBxVUly2PjYL4xC2ifDFexCqq1/u4TtivLGXotVk0wHdYuPYe1tHTHuR1lsOkRbfOCpkdTnigLVg==}
|
resolution: {integrity: sha512-4e8gxaCk7DhBxVUly2PjYL4xC2ifDFexCqq1/u4TtivLGXotVk0wHdYuPYe1tHTHuR1lsOkRbfOCpkdTnigLVg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -709,18 +777,38 @@ packages:
|
|||||||
resolution: {integrity: sha512-BiEE2bnnGoPKdlGe3L+gOYORDHFGPuYVRLP7iUow/Sflm0B4hC4XY3FC1MRuc7ltzpW2xNnXopKi34TTkULlKQ==}
|
resolution: {integrity: sha512-BiEE2bnnGoPKdlGe3L+gOYORDHFGPuYVRLP7iUow/Sflm0B4hC4XY3FC1MRuc7ltzpW2xNnXopKi34TTkULlKQ==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
|
'@smithy/core@3.31.1':
|
||||||
|
resolution: {integrity: sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
'@smithy/credential-provider-imds@4.4.12':
|
'@smithy/credential-provider-imds@4.4.12':
|
||||||
resolution: {integrity: sha512-ZZPDbl/aRp77aycuoMlo3BTayT4CE2a3uoqETYZU5ySnVbhpl5IJiY7dCZedn+ZusyDLqVv44IvKBiXd2/nK0Q==}
|
resolution: {integrity: sha512-ZZPDbl/aRp77aycuoMlo3BTayT4CE2a3uoqETYZU5ySnVbhpl5IJiY7dCZedn+ZusyDLqVv44IvKBiXd2/nK0Q==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
|
'@smithy/credential-provider-imds@4.4.16':
|
||||||
|
resolution: {integrity: sha512-QfuLWAkLzptffFW980AFeHZFdqds2B64rpEd3uJ6lgs3xVn9QegGMUgUcj+4d7dRrAsya3r58ZKpku97WcFb4w==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
|
'@smithy/fetch-http-handler@5.6.13':
|
||||||
|
resolution: {integrity: sha512-4fW86pEUOMbrD5nkbyl/tTvPHHWJFbuB2odl6ps9lWfHoXf9HWh3Q/Smh59qH1g7+c/BSZghX6bbUk4gsiMs8A==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
'@smithy/fetch-http-handler@5.6.9':
|
'@smithy/fetch-http-handler@5.6.9':
|
||||||
resolution: {integrity: sha512-EJktha5m5MXCwzdXrlWyqb9UCNHNFKlg+PmTpRsdX3dncJPTiqYleM9OKj2mLgdVJHR01d2tU4alG+z2NdH5rQ==}
|
resolution: {integrity: sha512-EJktha5m5MXCwzdXrlWyqb9UCNHNFKlg+PmTpRsdX3dncJPTiqYleM9OKj2mLgdVJHR01d2tU4alG+z2NdH5rQ==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
|
'@smithy/node-http-handler@4.9.13':
|
||||||
|
resolution: {integrity: sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
'@smithy/node-http-handler@4.9.9':
|
'@smithy/node-http-handler@4.9.9':
|
||||||
resolution: {integrity: sha512-xVBZ3hptB99iNO9XyWqEhC7KD9bP9UPXhuy3h5Y2ItCfBv160D9IIC/Fmmp3EbnWwit4C+KVqlSE+E29Nk/pPg==}
|
resolution: {integrity: sha512-xVBZ3hptB99iNO9XyWqEhC7KD9bP9UPXhuy3h5Y2ItCfBv160D9IIC/Fmmp3EbnWwit4C+KVqlSE+E29Nk/pPg==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
|
'@smithy/signature-v4@5.6.12':
|
||||||
|
resolution: {integrity: sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
'@smithy/signature-v4@5.6.8':
|
'@smithy/signature-v4@5.6.8':
|
||||||
resolution: {integrity: sha512-iGBm6hIwD2MGvVRSgrjVWa4FXtXDq3akxu0DCpnkmBo0xtEHZ/siMRt7ycfZAefYr2UdywUgmGtoRLaq5u56pg==}
|
resolution: {integrity: sha512-iGBm6hIwD2MGvVRSgrjVWa4FXtXDq3akxu0DCpnkmBo0xtEHZ/siMRt7ycfZAefYr2UdywUgmGtoRLaq5u56pg==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
@@ -814,6 +902,9 @@ packages:
|
|||||||
'@types/json-schema@7.0.15':
|
'@types/json-schema@7.0.15':
|
||||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||||
|
|
||||||
|
'@types/luxon@3.4.2':
|
||||||
|
resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==}
|
||||||
|
|
||||||
'@types/mime@1.3.5':
|
'@types/mime@1.3.5':
|
||||||
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
|
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
|
||||||
|
|
||||||
@@ -1349,6 +1440,9 @@ packages:
|
|||||||
create-require@1.1.1:
|
create-require@1.1.1:
|
||||||
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
|
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
|
||||||
|
|
||||||
|
cron@3.2.1:
|
||||||
|
resolution: {integrity: sha512-w2n5l49GMmmkBFEsH9FIDhjZ1n1QgTMOCMGuQtOXs5veNiosZmso6bQGuqOJSYAXXrG84WQFVneNk+Yt0Ua9iw==}
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@@ -2220,6 +2314,10 @@ packages:
|
|||||||
lru-cache@5.1.1:
|
lru-cache@5.1.1:
|
||||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||||
|
|
||||||
|
luxon@3.5.0:
|
||||||
|
resolution: {integrity: sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
magic-string@0.30.8:
|
magic-string@0.30.8:
|
||||||
resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==}
|
resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -3125,6 +3223,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
|
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
|
||||||
engines: {node: '>= 0.4.0'}
|
engines: {node: '>= 0.4.0'}
|
||||||
|
|
||||||
|
uuid@11.0.3:
|
||||||
|
resolution: {integrity: sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
uuid@8.3.2:
|
uuid@8.3.2:
|
||||||
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
||||||
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
|
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
|
||||||
@@ -3308,6 +3410,18 @@ snapshots:
|
|||||||
'@smithy/types': 4.16.1
|
'@smithy/types': 4.16.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@aws-sdk/client-sesv2@3.1101.0':
|
||||||
|
dependencies:
|
||||||
|
'@aws-sdk/core': 3.977.4
|
||||||
|
'@aws-sdk/credential-provider-node': 3.972.76
|
||||||
|
'@aws-sdk/signature-v4-multi-region': 3.996.43
|
||||||
|
'@aws-sdk/types': 3.974.2
|
||||||
|
'@smithy/core': 3.31.1
|
||||||
|
'@smithy/fetch-http-handler': 5.6.13
|
||||||
|
'@smithy/node-http-handler': 4.9.13
|
||||||
|
'@smithy/types': 4.16.1
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/core@3.976.0':
|
'@aws-sdk/core@3.976.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/types': 3.974.2
|
'@aws-sdk/types': 3.974.2
|
||||||
@@ -3319,6 +3433,17 @@ snapshots:
|
|||||||
bowser: 2.14.1
|
bowser: 2.14.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@aws-sdk/core@3.977.4':
|
||||||
|
dependencies:
|
||||||
|
'@aws-sdk/types': 3.974.2
|
||||||
|
'@aws-sdk/xml-builder': 3.972.37
|
||||||
|
'@aws/lambda-invoke-store': 0.3.0
|
||||||
|
'@smithy/core': 3.31.1
|
||||||
|
'@smithy/signature-v4': 5.6.12
|
||||||
|
'@smithy/types': 4.16.1
|
||||||
|
bowser: 2.14.1
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-env@3.972.60':
|
'@aws-sdk/credential-provider-env@3.972.60':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.976.0
|
'@aws-sdk/core': 3.976.0
|
||||||
@@ -3327,6 +3452,14 @@ snapshots:
|
|||||||
'@smithy/types': 4.16.1
|
'@smithy/types': 4.16.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@aws-sdk/credential-provider-env@3.972.65':
|
||||||
|
dependencies:
|
||||||
|
'@aws-sdk/core': 3.977.4
|
||||||
|
'@aws-sdk/types': 3.974.2
|
||||||
|
'@smithy/core': 3.31.1
|
||||||
|
'@smithy/types': 4.16.1
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-http@3.972.62':
|
'@aws-sdk/credential-provider-http@3.972.62':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.976.0
|
'@aws-sdk/core': 3.976.0
|
||||||
@@ -3337,6 +3470,32 @@ snapshots:
|
|||||||
'@smithy/types': 4.16.1
|
'@smithy/types': 4.16.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@aws-sdk/credential-provider-http@3.972.67':
|
||||||
|
dependencies:
|
||||||
|
'@aws-sdk/core': 3.977.4
|
||||||
|
'@aws-sdk/types': 3.974.2
|
||||||
|
'@smithy/core': 3.31.1
|
||||||
|
'@smithy/fetch-http-handler': 5.6.13
|
||||||
|
'@smithy/node-http-handler': 4.9.13
|
||||||
|
'@smithy/types': 4.16.1
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@aws-sdk/credential-provider-ini@3.973.10':
|
||||||
|
dependencies:
|
||||||
|
'@aws-sdk/core': 3.977.4
|
||||||
|
'@aws-sdk/credential-provider-env': 3.972.65
|
||||||
|
'@aws-sdk/credential-provider-http': 3.972.67
|
||||||
|
'@aws-sdk/credential-provider-login': 3.972.72
|
||||||
|
'@aws-sdk/credential-provider-process': 3.972.65
|
||||||
|
'@aws-sdk/credential-provider-sso': 3.973.9
|
||||||
|
'@aws-sdk/credential-provider-web-identity': 3.972.71
|
||||||
|
'@aws-sdk/nested-clients': 3.997.39
|
||||||
|
'@aws-sdk/types': 3.974.2
|
||||||
|
'@smithy/core': 3.31.1
|
||||||
|
'@smithy/credential-provider-imds': 4.4.16
|
||||||
|
'@smithy/types': 4.16.1
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-ini@3.973.5':
|
'@aws-sdk/credential-provider-ini@3.973.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.976.0
|
'@aws-sdk/core': 3.976.0
|
||||||
@@ -3362,6 +3521,15 @@ snapshots:
|
|||||||
'@smithy/types': 4.16.1
|
'@smithy/types': 4.16.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@aws-sdk/credential-provider-login@3.972.72':
|
||||||
|
dependencies:
|
||||||
|
'@aws-sdk/core': 3.977.4
|
||||||
|
'@aws-sdk/nested-clients': 3.997.39
|
||||||
|
'@aws-sdk/types': 3.974.2
|
||||||
|
'@smithy/core': 3.31.1
|
||||||
|
'@smithy/types': 4.16.1
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-node@3.972.71':
|
'@aws-sdk/credential-provider-node@3.972.71':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/credential-provider-env': 3.972.60
|
'@aws-sdk/credential-provider-env': 3.972.60
|
||||||
@@ -3376,6 +3544,20 @@ snapshots:
|
|||||||
'@smithy/types': 4.16.1
|
'@smithy/types': 4.16.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@aws-sdk/credential-provider-node@3.972.76':
|
||||||
|
dependencies:
|
||||||
|
'@aws-sdk/credential-provider-env': 3.972.65
|
||||||
|
'@aws-sdk/credential-provider-http': 3.972.67
|
||||||
|
'@aws-sdk/credential-provider-ini': 3.973.10
|
||||||
|
'@aws-sdk/credential-provider-process': 3.972.65
|
||||||
|
'@aws-sdk/credential-provider-sso': 3.973.9
|
||||||
|
'@aws-sdk/credential-provider-web-identity': 3.972.71
|
||||||
|
'@aws-sdk/types': 3.974.2
|
||||||
|
'@smithy/core': 3.31.1
|
||||||
|
'@smithy/credential-provider-imds': 4.4.16
|
||||||
|
'@smithy/types': 4.16.1
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-process@3.972.60':
|
'@aws-sdk/credential-provider-process@3.972.60':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.976.0
|
'@aws-sdk/core': 3.976.0
|
||||||
@@ -3384,6 +3566,14 @@ snapshots:
|
|||||||
'@smithy/types': 4.16.1
|
'@smithy/types': 4.16.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@aws-sdk/credential-provider-process@3.972.65':
|
||||||
|
dependencies:
|
||||||
|
'@aws-sdk/core': 3.977.4
|
||||||
|
'@aws-sdk/types': 3.974.2
|
||||||
|
'@smithy/core': 3.31.1
|
||||||
|
'@smithy/types': 4.16.1
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-sso@3.973.4':
|
'@aws-sdk/credential-provider-sso@3.973.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.976.0
|
'@aws-sdk/core': 3.976.0
|
||||||
@@ -3394,6 +3584,16 @@ snapshots:
|
|||||||
'@smithy/types': 4.16.1
|
'@smithy/types': 4.16.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@aws-sdk/credential-provider-sso@3.973.9':
|
||||||
|
dependencies:
|
||||||
|
'@aws-sdk/core': 3.977.4
|
||||||
|
'@aws-sdk/nested-clients': 3.997.39
|
||||||
|
'@aws-sdk/token-providers': 3.1100.0
|
||||||
|
'@aws-sdk/types': 3.974.2
|
||||||
|
'@smithy/core': 3.31.1
|
||||||
|
'@smithy/types': 4.16.1
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-web-identity@3.972.66':
|
'@aws-sdk/credential-provider-web-identity@3.972.66':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.976.0
|
'@aws-sdk/core': 3.976.0
|
||||||
@@ -3403,6 +3603,15 @@ snapshots:
|
|||||||
'@smithy/types': 4.16.1
|
'@smithy/types': 4.16.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@aws-sdk/credential-provider-web-identity@3.972.71':
|
||||||
|
dependencies:
|
||||||
|
'@aws-sdk/core': 3.977.4
|
||||||
|
'@aws-sdk/nested-clients': 3.997.39
|
||||||
|
'@aws-sdk/types': 3.974.2
|
||||||
|
'@smithy/core': 3.31.1
|
||||||
|
'@smithy/types': 4.16.1
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/middleware-sdk-s3@3.972.65':
|
'@aws-sdk/middleware-sdk-s3@3.972.65':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.976.0
|
'@aws-sdk/core': 3.976.0
|
||||||
@@ -3423,6 +3632,17 @@ snapshots:
|
|||||||
'@smithy/types': 4.16.1
|
'@smithy/types': 4.16.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@aws-sdk/nested-clients@3.997.39':
|
||||||
|
dependencies:
|
||||||
|
'@aws-sdk/core': 3.977.4
|
||||||
|
'@aws-sdk/signature-v4-multi-region': 3.996.43
|
||||||
|
'@aws-sdk/types': 3.974.2
|
||||||
|
'@smithy/core': 3.31.1
|
||||||
|
'@smithy/fetch-http-handler': 5.6.13
|
||||||
|
'@smithy/node-http-handler': 4.9.13
|
||||||
|
'@smithy/types': 4.16.1
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/signature-v4-multi-region@3.996.41':
|
'@aws-sdk/signature-v4-multi-region@3.996.41':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/types': 3.974.2
|
'@aws-sdk/types': 3.974.2
|
||||||
@@ -3430,6 +3650,13 @@ snapshots:
|
|||||||
'@smithy/types': 4.16.1
|
'@smithy/types': 4.16.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@aws-sdk/signature-v4-multi-region@3.996.43':
|
||||||
|
dependencies:
|
||||||
|
'@aws-sdk/types': 3.974.2
|
||||||
|
'@smithy/signature-v4': 5.6.12
|
||||||
|
'@smithy/types': 4.16.1
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/token-providers@3.1092.0':
|
'@aws-sdk/token-providers@3.1092.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.976.0
|
'@aws-sdk/core': 3.976.0
|
||||||
@@ -3439,6 +3666,15 @@ snapshots:
|
|||||||
'@smithy/types': 4.16.1
|
'@smithy/types': 4.16.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@aws-sdk/token-providers@3.1100.0':
|
||||||
|
dependencies:
|
||||||
|
'@aws-sdk/core': 3.977.4
|
||||||
|
'@aws-sdk/nested-clients': 3.997.39
|
||||||
|
'@aws-sdk/types': 3.974.2
|
||||||
|
'@smithy/core': 3.31.1
|
||||||
|
'@smithy/types': 4.16.1
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/types@3.974.2':
|
'@aws-sdk/types@3.974.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@smithy/types': 4.16.1
|
'@smithy/types': 4.16.1
|
||||||
@@ -3449,6 +3685,11 @@ snapshots:
|
|||||||
'@smithy/types': 4.16.1
|
'@smithy/types': 4.16.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@aws-sdk/xml-builder@3.972.37':
|
||||||
|
dependencies:
|
||||||
|
'@smithy/types': 4.16.1
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws/lambda-invoke-store@0.3.0': {}
|
'@aws/lambda-invoke-store@0.3.0': {}
|
||||||
|
|
||||||
'@babel/code-frame@7.29.7':
|
'@babel/code-frame@7.29.7':
|
||||||
@@ -3974,6 +4215,13 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
'@nestjs/schedule@4.1.2(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)':
|
||||||
|
dependencies:
|
||||||
|
'@nestjs/common': 10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
|
'@nestjs/core': 10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
|
cron: 3.2.1
|
||||||
|
uuid: 11.0.3
|
||||||
|
|
||||||
'@nestjs/schematics@10.2.3(chokidar@3.6.0)(typescript@5.7.2)':
|
'@nestjs/schematics@10.2.3(chokidar@3.6.0)(typescript@5.7.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@angular-devkit/core': 17.3.11(chokidar@3.6.0)
|
'@angular-devkit/core': 17.3.11(chokidar@3.6.0)
|
||||||
@@ -4075,24 +4323,53 @@ snapshots:
|
|||||||
'@smithy/types': 4.16.1
|
'@smithy/types': 4.16.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@smithy/core@3.31.1':
|
||||||
|
dependencies:
|
||||||
|
'@smithy/types': 4.16.1
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@smithy/credential-provider-imds@4.4.12':
|
'@smithy/credential-provider-imds@4.4.12':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@smithy/core': 3.29.7
|
'@smithy/core': 3.29.7
|
||||||
'@smithy/types': 4.16.1
|
'@smithy/types': 4.16.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@smithy/credential-provider-imds@4.4.16':
|
||||||
|
dependencies:
|
||||||
|
'@smithy/core': 3.31.1
|
||||||
|
'@smithy/types': 4.16.1
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@smithy/fetch-http-handler@5.6.13':
|
||||||
|
dependencies:
|
||||||
|
'@smithy/core': 3.31.1
|
||||||
|
'@smithy/types': 4.16.1
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@smithy/fetch-http-handler@5.6.9':
|
'@smithy/fetch-http-handler@5.6.9':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@smithy/core': 3.29.7
|
'@smithy/core': 3.29.7
|
||||||
'@smithy/types': 4.16.1
|
'@smithy/types': 4.16.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@smithy/node-http-handler@4.9.13':
|
||||||
|
dependencies:
|
||||||
|
'@smithy/core': 3.31.1
|
||||||
|
'@smithy/types': 4.16.1
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@smithy/node-http-handler@4.9.9':
|
'@smithy/node-http-handler@4.9.9':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@smithy/core': 3.29.7
|
'@smithy/core': 3.29.7
|
||||||
'@smithy/types': 4.16.1
|
'@smithy/types': 4.16.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@smithy/signature-v4@5.6.12':
|
||||||
|
dependencies:
|
||||||
|
'@smithy/core': 3.31.1
|
||||||
|
'@smithy/types': 4.16.1
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@smithy/signature-v4@5.6.8':
|
'@smithy/signature-v4@5.6.8':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@smithy/core': 3.29.7
|
'@smithy/core': 3.29.7
|
||||||
@@ -4215,6 +4492,8 @@ snapshots:
|
|||||||
|
|
||||||
'@types/json-schema@7.0.15': {}
|
'@types/json-schema@7.0.15': {}
|
||||||
|
|
||||||
|
'@types/luxon@3.4.2': {}
|
||||||
|
|
||||||
'@types/mime@1.3.5': {}
|
'@types/mime@1.3.5': {}
|
||||||
|
|
||||||
'@types/node@14.18.63': {}
|
'@types/node@14.18.63': {}
|
||||||
@@ -4842,6 +5121,11 @@ snapshots:
|
|||||||
|
|
||||||
create-require@1.1.1: {}
|
create-require@1.1.1: {}
|
||||||
|
|
||||||
|
cron@3.2.1:
|
||||||
|
dependencies:
|
||||||
|
'@types/luxon': 3.4.2
|
||||||
|
luxon: 3.5.0
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-key: 3.1.1
|
path-key: 3.1.1
|
||||||
@@ -5960,6 +6244,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
yallist: 3.1.1
|
yallist: 3.1.1
|
||||||
|
|
||||||
|
luxon@3.5.0: {}
|
||||||
|
|
||||||
magic-string@0.30.8:
|
magic-string@0.30.8:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
@@ -6788,6 +7074,8 @@ snapshots:
|
|||||||
|
|
||||||
utils-merge@1.0.1: {}
|
utils-merge@1.0.1: {}
|
||||||
|
|
||||||
|
uuid@11.0.3: {}
|
||||||
|
|
||||||
uuid@8.3.2: {}
|
uuid@8.3.2: {}
|
||||||
|
|
||||||
v8-compile-cache-lib@3.0.1: {}
|
v8-compile-cache-lib@3.0.1: {}
|
||||||
|
|||||||
Reference in New Issue
Block a user