import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { CUSTOMERS_REPOSITORY, type CreateCustomerInput, type CustomerBillingMode, type CustomerStatus, type CustomerSummary, type CustomersRepository, type UpdateCustomerInput } from './customers.repository.js'; interface CreateCustomerDto { name?: unknown; contactName?: unknown; phone?: unknown; email?: unknown; domain?: unknown; status?: unknown; billingMode?: unknown; creditLimit?: unknown; minBalance?: unknown; notes?: unknown; } interface UpdateCustomerDto { name?: unknown; contactName?: unknown; phone?: unknown; email?: unknown; domain?: unknown; status?: unknown; billingMode?: unknown; creditLimit?: unknown; minBalance?: unknown; notes?: unknown; } @Injectable() export class CustomersService { constructor(@Inject(CUSTOMERS_REPOSITORY) private readonly customers: CustomersRepository) {} list(): Promise { return this.customers.list(); } get(customerId: string): Promise { return this.customers.get(customerId); } create(body: CreateCustomerDto, actorId?: string): Promise { const input: CreateCustomerInput = { name: this.limitedString(body.name, 'name', 120), contactName: this.optionalString(body.contactName, 'contactName', 80), phone: this.optionalString(body.phone, 'phone', 32), email: this.optionalString(body.email, 'email', 160), domain: this.optionalString(body.domain, 'domain', 160), status: body.status === undefined ? 'ENABLED' : this.status(body.status), billingMode: body.billingMode === undefined ? 'PREPAID' : this.billingMode(body.billingMode), creditLimit: body.creditLimit === undefined ? '0.000000' : this.money(body.creditLimit, 'creditLimit'), minBalance: body.minBalance === undefined ? '0.000000' : this.money(body.minBalance, 'minBalance'), notes: this.optionalString(body.notes, 'notes', 500), actorId }; return this.customers.create(input); } update(customerId: string, body: UpdateCustomerDto, actorId?: string): Promise { const input: UpdateCustomerInput = { name: body.name === undefined ? undefined : this.limitedString(body.name, 'name', 120), contactName: body.contactName === undefined ? undefined : this.nullableString(body.contactName, 'contactName', 80), phone: body.phone === undefined ? undefined : this.nullableString(body.phone, 'phone', 32), email: body.email === undefined ? undefined : this.nullableString(body.email, 'email', 160), domain: body.domain === undefined ? undefined : this.nullableString(body.domain, 'domain', 160), status: body.status === undefined ? undefined : this.status(body.status), billingMode: body.billingMode === undefined ? undefined : this.billingMode(body.billingMode), creditLimit: body.creditLimit === undefined ? undefined : this.money(body.creditLimit, 'creditLimit'), minBalance: body.minBalance === undefined ? undefined : this.money(body.minBalance, 'minBalance'), notes: body.notes === undefined ? undefined : this.nullableString(body.notes, 'notes', 500), actorId }; return this.customers.update(customerId, input); } enable(customerId: string, actorId?: string): Promise { return this.customers.setStatus(customerId, 'ENABLED', actorId); } disable(customerId: string, actorId?: string): Promise { return this.customers.setStatus(customerId, 'DISABLED', actorId); } remove(customerId: string, actorId?: string): Promise { return this.customers.softDelete(customerId, actorId); } private limitedString(value: unknown, field: string, maxLength: number): string { if (typeof value !== 'string' || value.trim().length === 0) { throw new BadRequestException({ code: 'VALIDATION_ERROR', message: `${field} is required.` }); } const trimmed = value.trim(); if (trimmed.length > maxLength) { throw new BadRequestException({ code: 'VALIDATION_ERROR', message: `${field} is too long.` }); } return trimmed; } private optionalString(value: unknown, field: string, maxLength: number): string | undefined { if (value === undefined) { return undefined; } return this.limitedString(value, field, maxLength); } private nullableString(value: unknown, field: string, maxLength: number): string | null { if (value === null) { return null; } return this.limitedString(value, field, maxLength); } private status(value: unknown): CustomerStatus { if (value !== 'ENABLED' && value !== 'DISABLED') { throw new BadRequestException({ code: 'STATUS_INVALID', message: 'Status is invalid.' }); } return value; } private billingMode(value: unknown): CustomerBillingMode { if (value !== 'PREPAID' && value !== 'POSTPAID') { throw new BadRequestException({ code: 'BILLING_MODE_INVALID', message: 'Billing mode is invalid.' }); } return value; } private money(value: unknown, field: string): string { const raw = typeof value === 'number' ? value.toString() : typeof value === 'string' ? value.trim() : ''; if (!/^\d{1,14}(?:\.\d{1,6})?$/.test(raw)) { throw new BadRequestException({ code: 'MONEY_INVALID', message: `${field} must be a non-negative decimal with up to 6 places.` }); } const [integerPart, fractionPart = ''] = raw.split('.'); return `${integerPart}.${fractionPart.padEnd(6, '0')}`; } }