Files
jorgecuadros-platform/apps/api/src/customers/update-customer.dto.ts
T
rmancinas 87d8743251
Build and Push Images / Build jorgecuadros-web (push) Successful in 1m48s
Build and Push Images / Build jorgecuadros-api (push) Successful in 2m4s
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
2026-08-02 02:00:02 -07:00

36 lines
1.3 KiB
TypeScript

import {
IsBoolean,
IsEmail,
IsEnum,
IsNumber,
IsOptional,
IsString,
MinLength,
} from "class-validator";
import { Currency } from "@jorgecuadros/database";
/** Same editable fields as create, all optional. */
export class UpdateCustomerDto {
@IsOptional() @IsString() @MinLength(1) name?: string;
@IsOptional() @IsString() addressLine1?: string;
@IsOptional() @IsString() addressLine2?: string;
@IsOptional() @IsString() city?: string;
@IsOptional() @IsString() state?: string;
@IsOptional() @IsString() zipCode?: string;
@IsOptional() @IsString() country?: string;
@IsOptional() @IsString() phone?: string;
@IsOptional() @IsString() mobile?: string;
@IsOptional() @IsString() fax?: string;
@IsOptional() @IsEmail() email?: string;
@IsOptional() @IsBoolean() emailOptOut?: boolean;
@IsOptional() @IsString() notes?: string;
@IsOptional() @IsString() identificationType?: string;
@IsOptional() @IsString() identificationNumber?: string;
@IsOptional() @IsString() identificationExpiration?: string;
@IsOptional() @IsString() customerSince?: string;
@IsOptional() @IsBoolean() status?: boolean;
@IsOptional() @IsNumber() minimumBalance?: number;
@IsOptional() @IsNumber() feeAmount?: number;
@IsOptional() @IsEnum(Currency) preferredCurrency?: Currency;
}