210 lines
7.0 KiB
TypeScript
210 lines
7.0 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' },
|
|
}).then((items) => items.map(withEnterpriseProfile));
|
|
}
|
|
|
|
async listManagementRows() {
|
|
const sinceToday = startOfToday();
|
|
const [tenants, accounts, todaySpendGroups, todayRefundGroups] = await Promise.all([
|
|
this.prisma.tenant.findMany({
|
|
where: { status: { not: 'deleted' } },
|
|
include: { enterpriseCertifications: { orderBy: { submittedAt: 'desc' }, take: 1 } },
|
|
orderBy: { createdAt: 'desc' },
|
|
}),
|
|
this.prisma.tenantAccount.findMany(),
|
|
this.prisma.smsMessageRecord.groupBy({
|
|
by: ['tenantId'],
|
|
where: { queuedAt: { gte: sinceToday } },
|
|
_sum: { amountCents: true },
|
|
}),
|
|
this.prisma.accountTransaction.groupBy({
|
|
by: ['tenantId'],
|
|
where: { transactionType: 'refunded', createdAt: { 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]));
|
|
const todayRefundByTenant = new Map(todayRefundGroups.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,
|
|
todayRefundCents: todayRefundByTenant.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}`;
|
|
}
|