153 lines
5.5 KiB
TypeScript
153 lines
5.5 KiB
TypeScript
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<CustomerSummary[]> {
|
|
return this.customers.list();
|
|
}
|
|
|
|
get(customerId: string): Promise<CustomerSummary> {
|
|
return this.customers.get(customerId);
|
|
}
|
|
|
|
create(body: CreateCustomerDto, actorId?: string): Promise<CustomerSummary> {
|
|
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<CustomerSummary> {
|
|
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<CustomerSummary> {
|
|
return this.customers.setStatus(customerId, 'ENABLED', actorId);
|
|
}
|
|
|
|
disable(customerId: string, actorId?: string): Promise<CustomerSummary> {
|
|
return this.customers.setStatus(customerId, 'DISABLED', actorId);
|
|
}
|
|
|
|
remove(customerId: string, actorId?: string): Promise<CustomerSummary> {
|
|
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')}`;
|
|
}
|
|
}
|