Files
lislgosms/api/src/tenants/tenants.service.ts
T

205 lines
6.6 KiB
TypeScript

import { Injectable, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
export interface CreateTenantDto {
name: string;
code?: string;
status?: string;
creditCode?: string;
province?: string;
city?: string;
address?: string;
contactName?: string;
contactIdCard?: string;
contactPhone?: string;
contactEmail?: string;
photoFileObjectId?: string;
}
export interface UpdateTenantDto {
name?: string;
code?: string;
status?: string;
creditCode?: string;
province?: string;
city?: string;
address?: string;
contactName?: string;
contactIdCard?: string;
contactPhone?: string;
contactEmail?: string;
photoFileObjectId?: string;
}
@Injectable()
export class TenantsService {
constructor(private readonly prisma: PrismaService) {}
list() {
return this.prisma.tenant.findMany({
include: { enterpriseCertifications: { orderBy: { submittedAt: 'desc' }, take: 1 } },
orderBy: { createdAt: 'desc' },
take: 100,
}).then((items) => items.map(withEnterpriseProfile));
}
async listManagementRows() {
const sinceToday = startOfToday();
const [tenants, accounts, todaySpendGroups] = await Promise.all([
this.prisma.tenant.findMany({
where: { status: { not: 'deleted' } },
include: { enterpriseCertifications: { orderBy: { submittedAt: 'desc' }, take: 1 } },
orderBy: { createdAt: 'desc' },
take: 100,
}),
this.prisma.tenantAccount.findMany({ take: 200 }),
this.prisma.smsMessageRecord.groupBy({
by: ['tenantId'],
where: { queuedAt: { gte: sinceToday } },
_sum: { amountCents: true },
}),
]);
const accountsByTenant = new Map(accounts.map((account) => [account.tenantId, account]));
const todaySpendByTenant = new Map(todaySpendGroups.map((group) => [group.tenantId, group._sum.amountCents ?? 0]));
return tenants.map((tenant) => ({
...withEnterpriseProfile(tenant),
account: accountsByTenant.get(tenant.id) ?? null,
todaySpendCents: todaySpendByTenant.get(tenant.id) ?? 0,
}));
}
get(id: string) {
return this.prisma.tenant.findUnique({
where: { id },
include: { enterpriseCertifications: { orderBy: { submittedAt: 'desc' }, take: 1 } },
}).then((tenant) => {
if (!tenant) {
throw new NotFoundException('Tenant not found');
}
return withEnterpriseProfile(tenant);
});
}
async create(data: CreateTenantDto) {
const code = data.code?.trim() || generateTenantCode(data);
const tenant = await this.prisma.tenant.create({
data: { name: data.name, code, status: data.status ?? 'active' },
});
await this.upsertProfile(tenant.id, data);
return this.get(tenant.id);
}
async update(id: string, data: UpdateTenantDto) {
await this.ensureTenant(id);
await this.prisma.tenant.update({
where: { id },
data: {
name: data.name,
code: data.code,
status: data.status,
},
});
await this.upsertProfile(id, data);
return this.get(id);
}
async changeStatus(id: string, status: string) {
await this.ensureTenant(id);
return this.prisma.tenant.update({
where: { id },
data: { status },
});
}
delete(id: string) {
return this.changeStatus(id, 'deleted');
}
private async ensureTenant(id: string) {
const tenant = await this.prisma.tenant.findUnique({ where: { id } });
if (!tenant) {
throw new NotFoundException('Tenant not found');
}
return tenant;
}
private async upsertProfile(tenantId: string, data: CreateTenantDto | UpdateTenantDto) {
const hasProfileData = ['creditCode', 'province', 'city', 'address', 'contactName', 'contactIdCard', 'contactPhone', 'contactEmail', 'photoFileObjectId']
.some((key) => data[key as keyof (CreateTenantDto | UpdateTenantDto)] !== undefined);
if (!hasProfileData) {
return;
}
const latest = await this.prisma.enterpriseCertification.findFirst({
where: { tenantId },
orderBy: { submittedAt: 'desc' },
});
const materials = cleanObject({
...(latest?.materials && typeof latest.materials === 'object' && !Array.isArray(latest.materials) ? latest.materials as Record<string, unknown> : {}),
province: data.province,
city: data.city,
address: data.address,
contactIdCard: data.contactIdCard,
contactEmail: data.contactEmail,
photoFileObjectId: data.photoFileObjectId,
});
const profileData = {
companyName: data.name ?? latest?.companyName ?? tenantId,
licenseNo: data.creditCode,
contactName: data.contactName,
contactPhone: data.contactPhone,
materials: materials as Prisma.InputJsonValue,
status: 'approved',
};
if (latest) {
await this.prisma.enterpriseCertification.update({
where: { id: latest.id },
data: profileData,
});
return;
}
await this.prisma.enterpriseCertification.create({
data: { tenantId, ...profileData },
});
}
}
function cleanObject(value: Record<string, unknown>) {
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
}
function withEnterpriseProfile<T extends { enterpriseCertifications?: Array<{ licenseNo?: string | null; contactName?: string | null; contactPhone?: string | null; materials?: Prisma.JsonValue | null }> }>(tenant: T) {
const [profile] = tenant.enterpriseCertifications ?? [];
const materials = profile?.materials && typeof profile.materials === 'object' && !Array.isArray(profile.materials)
? profile.materials as Record<string, unknown>
: {};
const { enterpriseCertifications, ...rest } = tenant;
return {
...rest,
enterpriseProfile: profile ? {
creditCode: profile.licenseNo ?? '',
province: String(materials.province ?? ''),
city: String(materials.city ?? ''),
address: String(materials.address ?? ''),
contactName: profile.contactName ?? '',
contactIdCard: String(materials.contactIdCard ?? ''),
contactPhone: profile.contactPhone ?? '',
contactEmail: String(materials.contactEmail ?? ''),
photoFileObjectId: String(materials.photoFileObjectId ?? ''),
} : null,
};
}
function startOfToday() {
const date = new Date();
date.setHours(0, 0, 0, 0);
return date;
}
function generateTenantCode(data: CreateTenantDto) {
const source = data.creditCode?.trim() || data.name.trim();
const normalized = source.replace(/[^\da-zA-Z]/g, '').toLowerCase();
const suffix = Date.now().toString(36).slice(-6);
return `ent-${(normalized || 'tenant').slice(0, 18)}-${suffix}`;
}