feat(clientes): alta de cliente in one screen, opening balance as a payment
Build and Push Images / Build jorgecuadros-web (push) Successful in 1m57s
Build and Push Images / Build jorgecuadros-api (push) Successful in 2m51s

The 2026-10-08 ask: open an account without four screens — cliente,
propiedad, servicios and the saldo inicial, taken as the first payment.

POST /customers/onboard writes it all in one Prisma transaction; a bad
service or a taken folder number leaves nothing behind (rolled back on dev).
A cash opening balance goes through the EFE receipt book inside that same
transaction, so it gets its folio and its C<folio> credit; a check posts a
CHECK DEPOSIT against the number given. /clientes/alta pre-fills the next
free folder number and links the receipt for printing. The old
customer-only form stays as "Solo cliente".

Built without the screen sketch from the meeting (it never arrived); the
fields are the legacy ones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rmancinasandClaude Opus 5.5 committed 2026-10-12 19:27:00 -07:00
1 parent ca76f4d57a
commit 6468811955
11 files changed
+568 -8

No files matched your search

@@ -0,0 +1,56 @@
import {
ArrayMaxSize,
IsArray,
IsEnum,
IsIn,
IsNumber,
IsOptional,
IsString,
MinLength,
ValidateNested,
} from "class-validator";
import { Type } from "class-transformer";
import { Currency } from "@jorgecuadros/database";
import { CreateCustomerDto } from "./create-customer.dto";
import { ServiceDto } from "../properties/property.dto";
/** The property, minus the customer it is being created for. */
export class OnboardPropertyDto {
@IsOptional() @IsString() addressLine1?: string;
@IsOptional() @IsString() addressLine2?: string;
@IsOptional() @IsString() phone1?: string;
}
/**
* The opening balance the office takes when an account is opened, captured as
* the customer's first payment. Cash issues an EFE receipt; a check posts a
* CHECK DEPOSIT against that check number.
*/
export class OpeningPaymentDto {
@IsIn(["CASH", "CHECK"]) method!: "CASH" | "CHECK";
@IsNumber() amount!: number;
@IsEnum(Currency) currency!: Currency;
@IsString() @MinLength(1) receivedAt!: string;
@IsOptional() @IsString() checkNumber?: string;
/** Cash only: dollars credited to a pesos ledger. */
@IsOptional() @IsNumber() exchangeRate?: number;
@IsOptional() @IsString() concept?: string;
}
/** One screen: cliente + propiedad + servicios + saldo inicial. */
export class OnboardCustomerDto {
@ValidateNested() @Type(() => CreateCustomerDto) customer!: CreateCustomerDto;
@IsOptional() @ValidateNested() @Type(() => OnboardPropertyDto)
property?: OnboardPropertyDto;
@IsOptional()
@IsArray()
@ArrayMaxSize(12)
@ValidateNested({ each: true })
@Type(() => ServiceDto)
services?: ServiceDto[];
@IsOptional() @ValidateNested() @Type(() => OpeningPaymentDto)
openingPayment?: OpeningPaymentDto;
}