Creating a control panel infrastructure
This commit is contained in:
@@ -6,6 +6,7 @@ import { TenantsModule } from './tenants/tenants.module';
|
||||
import { GetUserModule } from './get-user/get-user.module';
|
||||
import { SettingsProxyModule } from './settings-proxy/settings-proxy.module';
|
||||
import { AuditModule } from './audit/audit.module';
|
||||
import { SmsModule } from './sms/sms.module';
|
||||
import { HealthController } from './health.controller';
|
||||
|
||||
@Module({
|
||||
@@ -17,6 +18,7 @@ import { HealthController } from './health.controller';
|
||||
GetUserModule,
|
||||
SettingsProxyModule,
|
||||
AuditModule,
|
||||
SmsModule,
|
||||
],
|
||||
controllers: [HealthController],
|
||||
})
|
||||
|
||||
@@ -11,17 +11,11 @@ export class GetUserService {
|
||||
throw new NotFoundException('domain required');
|
||||
}
|
||||
|
||||
const tenant = await this.prisma.tenant.findFirst({
|
||||
where: {
|
||||
isActive: true,
|
||||
OR: [
|
||||
{ domain: host },
|
||||
{ domain: host.replace(/^www\./, '') },
|
||||
{ domain: `www.${host.replace(/^www\./, '')}` },
|
||||
],
|
||||
},
|
||||
const tenants = await this.prisma.tenant.findMany({
|
||||
where: { isActive: true },
|
||||
});
|
||||
|
||||
const tenant = tenants.find((item) => this.matchesHost(item, host));
|
||||
if (!tenant) {
|
||||
throw new NotFoundException('tenant not found for domain');
|
||||
}
|
||||
@@ -31,7 +25,7 @@ export class GetUserService {
|
||||
|
||||
const branding = {
|
||||
id: tenant.id,
|
||||
name: tenant.slug,
|
||||
name: tenant.nameEn || tenant.slug,
|
||||
name_fa: tenant.nameFa,
|
||||
name_en: tenant.nameEn,
|
||||
avatar: tenant.avatarUrl,
|
||||
@@ -43,7 +37,7 @@ export class GetUserService {
|
||||
app_url: tenant.appUrl,
|
||||
admin_url: tenant.adminUrl,
|
||||
product: tenant.product,
|
||||
theme: ui?.app?.theme?.mode === 'light' ? 'dynamicMode' : 'dynamicMode',
|
||||
theme: 'dynamicMode',
|
||||
mode: 'dynamicMode',
|
||||
app_ui_config: tenant.product === 'khanegi' ? ui : undefined,
|
||||
};
|
||||
@@ -54,6 +48,38 @@ export class GetUserService {
|
||||
};
|
||||
}
|
||||
|
||||
private matchesHost(
|
||||
tenant: {
|
||||
domain: string;
|
||||
domains: unknown;
|
||||
},
|
||||
host: string,
|
||||
) {
|
||||
const candidates = new Set<string>();
|
||||
for (const value of [tenant.domain, ...this.readDomains(tenant.domains)]) {
|
||||
const normalized = this.normalizeHost(value);
|
||||
if (!normalized) continue;
|
||||
candidates.add(normalized);
|
||||
candidates.add(normalized.replace(/^www\./, ''));
|
||||
candidates.add(`www.${normalized.replace(/^www\./, '')}`);
|
||||
}
|
||||
return candidates.has(host) || candidates.has(host.replace(/^www\./, ''));
|
||||
}
|
||||
|
||||
private readDomains(raw: unknown): string[] {
|
||||
if (!raw) return [];
|
||||
if (Array.isArray(raw)) return raw.map(String);
|
||||
if (typeof raw === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed) ? parsed.map(String) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private normalizeHost(value: string) {
|
||||
return value
|
||||
.trim()
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { IsBoolean, IsInt, IsObject, IsOptional, IsString, Min } from 'class-validator';
|
||||
|
||||
export class UpdateSmsAccountDto {
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
pricePerPartRials?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isBlocked?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
alertCriticalRials?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
alertWarningRials?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
gatewayProvider?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
melipayamakUser?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
melipayamakPassword?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
melipayamakFrom?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
panelUsername?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
panelPassword?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
panelFrom?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
extraSettings?: Record<string, string>;
|
||||
}
|
||||
|
||||
export class ChargeSmsWalletDto {
|
||||
@IsInt()
|
||||
amountRials!: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
note?: string;
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
export type SmsKeyDef = {
|
||||
key: string
|
||||
label: string
|
||||
group: 'provider' | 'billing' | 'feature' | 'charge'
|
||||
secret?: boolean
|
||||
hint?: string
|
||||
products: Array<'hamkar' | 'khanegi' | 'both'>
|
||||
}
|
||||
|
||||
/** Catalog only — does not change Hamkar/Khanegi code. */
|
||||
export const SMS_KEY_CATALOG: SmsKeyDef[] = [
|
||||
{
|
||||
key: 'sms_panel',
|
||||
label: 'حالت پنل / billing',
|
||||
group: 'provider',
|
||||
hint: 'همکار: central یا me | خانگی: true/false',
|
||||
products: ['both'],
|
||||
},
|
||||
{
|
||||
key: 'melipayamak_user',
|
||||
label: 'ملیپیامک — کاربر',
|
||||
group: 'provider',
|
||||
products: ['both'],
|
||||
},
|
||||
{
|
||||
key: 'melipayamak_password',
|
||||
label: 'ملیپیامک — رمز',
|
||||
group: 'provider',
|
||||
secret: true,
|
||||
products: ['both'],
|
||||
},
|
||||
{
|
||||
key: 'melipayamak_from',
|
||||
label: 'ملیپیامک — فرستنده',
|
||||
group: 'provider',
|
||||
products: ['both'],
|
||||
},
|
||||
{
|
||||
key: 'sms_panel_username',
|
||||
label: '0098 — کاربر',
|
||||
group: 'provider',
|
||||
hint: 'فقط همکار وقتی sms_panel=me',
|
||||
products: ['hamkar'],
|
||||
},
|
||||
{
|
||||
key: 'sms_panel_password',
|
||||
label: '0098 — رمز',
|
||||
group: 'provider',
|
||||
secret: true,
|
||||
products: ['hamkar'],
|
||||
},
|
||||
{
|
||||
key: 'sms_panel_from',
|
||||
label: '0098 — فرستنده',
|
||||
group: 'provider',
|
||||
products: ['hamkar'],
|
||||
},
|
||||
{
|
||||
key: 'sms_long',
|
||||
label: 'پیامک طولانی',
|
||||
group: 'feature',
|
||||
products: ['hamkar'],
|
||||
},
|
||||
{
|
||||
key: 'send_sms_on_customer_order',
|
||||
label: 'ارسال با سفارش مشتری',
|
||||
group: 'feature',
|
||||
products: ['hamkar'],
|
||||
},
|
||||
{
|
||||
key: 'group_sms',
|
||||
label: 'ارسال گروهی',
|
||||
group: 'feature',
|
||||
products: ['khanegi'],
|
||||
},
|
||||
{
|
||||
key: 'charge_notif',
|
||||
label: 'اعلان شارژ کیف پول',
|
||||
group: 'feature',
|
||||
products: ['khanegi'],
|
||||
},
|
||||
{
|
||||
key: 'sms_charge_card',
|
||||
label: 'کارت شارژ',
|
||||
group: 'charge',
|
||||
products: ['hamkar'],
|
||||
},
|
||||
{
|
||||
key: 'sms_charge_iban',
|
||||
label: 'شبا شارژ',
|
||||
group: 'charge',
|
||||
products: ['hamkar'],
|
||||
},
|
||||
{
|
||||
key: 'sms_charge_account_holder',
|
||||
label: 'صاحب حساب',
|
||||
group: 'charge',
|
||||
products: ['hamkar'],
|
||||
},
|
||||
{
|
||||
key: 'sms_charge_instruction',
|
||||
label: 'راهنمای شارژ',
|
||||
group: 'charge',
|
||||
products: ['hamkar'],
|
||||
},
|
||||
{
|
||||
key: 'sms_charge_card_number',
|
||||
label: 'شماره کارت شارژ',
|
||||
group: 'charge',
|
||||
products: ['khanegi'],
|
||||
},
|
||||
{
|
||||
key: 'sms_charge_sheba',
|
||||
label: 'شبا شارژ',
|
||||
group: 'charge',
|
||||
products: ['khanegi'],
|
||||
},
|
||||
{
|
||||
key: 'sms_charge_account_holder',
|
||||
label: 'صاحب حساب',
|
||||
group: 'charge',
|
||||
products: ['khanegi'],
|
||||
},
|
||||
{
|
||||
key: 'sms_charge_note',
|
||||
label: 'توضیح شارژ',
|
||||
group: 'charge',
|
||||
products: ['khanegi'],
|
||||
},
|
||||
{
|
||||
key: 'name_en',
|
||||
label: 'نام لاتین (host در API مرکزی)',
|
||||
group: 'provider',
|
||||
products: ['both'],
|
||||
},
|
||||
]
|
||||
|
||||
export function catalogForProduct(product: 'hamkar' | 'khanegi') {
|
||||
return SMS_KEY_CATALOG.filter(
|
||||
(k) => k.products.includes('both') || k.products.includes(product),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Body, Controller, Get, Param, Patch, Post, Query, UseGuards } from '@nestjs/common';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { CurrentUser } from '../auth/current-user.decorator';
|
||||
import { ChargeSmsWalletDto, UpdateSmsAccountDto } from './dto/sms-account.dto';
|
||||
import { SmsService } from './sms.service';
|
||||
|
||||
@Controller('sms')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class SmsController {
|
||||
constructor(private readonly smsService: SmsService) {}
|
||||
|
||||
@Get('tenants')
|
||||
listTenants() {
|
||||
return this.smsService.listTenants();
|
||||
}
|
||||
|
||||
@Get('lowest')
|
||||
lowest(@Query('limit') limit?: string) {
|
||||
return this.smsService.getLowestBalances(Number(limit) || 5);
|
||||
}
|
||||
|
||||
@Get('tenants/:id')
|
||||
getAccount(@Param('id') id: string) {
|
||||
return this.smsService.getAccount(id);
|
||||
}
|
||||
|
||||
@Patch('tenants/:id')
|
||||
updateAccount(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateSmsAccountDto,
|
||||
@CurrentUser() user: { id: string },
|
||||
) {
|
||||
return this.smsService.updateAccount(id, dto, user.id);
|
||||
}
|
||||
|
||||
@Post('tenants/:id/charge')
|
||||
charge(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: ChargeSmsWalletDto,
|
||||
@CurrentUser() user: { id: string },
|
||||
) {
|
||||
return this.smsService.chargeWallet(id, dto, user.id);
|
||||
}
|
||||
|
||||
@Post('tenants/:id/block')
|
||||
block(@Param('id') id: string, @CurrentUser() user: { id: string }) {
|
||||
return this.smsService.setBlocked(id, true, user.id);
|
||||
}
|
||||
|
||||
@Post('tenants/:id/unblock')
|
||||
unblock(@Param('id') id: string, @CurrentUser() user: { id: string }) {
|
||||
return this.smsService.setBlocked(id, false, user.id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { SmsController } from './sms.controller';
|
||||
import { SmsService } from './sms.service';
|
||||
|
||||
@Module({
|
||||
controllers: [SmsController],
|
||||
providers: [SmsService],
|
||||
exports: [SmsService],
|
||||
})
|
||||
export class SmsModule {}
|
||||
@@ -0,0 +1,284 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { Prisma, SmsAccount } from '@prisma/client';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { AuditService } from '../audit/audit.service';
|
||||
import { ChargeSmsWalletDto, UpdateSmsAccountDto } from './dto/sms-account.dto';
|
||||
|
||||
type BalanceLevel = 'healthy' | 'warning' | 'critical' | 'unknown';
|
||||
|
||||
@Injectable()
|
||||
export class SmsService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly audit: AuditService,
|
||||
) {}
|
||||
|
||||
async listTenants() {
|
||||
await this.ensureAccountsForAllTenants();
|
||||
|
||||
const rows = await this.prisma.tenant.findMany({
|
||||
where: { isActive: true },
|
||||
orderBy: [{ product: 'asc' }, { nameFa: 'asc' }],
|
||||
include: { smsAccount: true },
|
||||
});
|
||||
|
||||
return rows.map((tenant) => {
|
||||
const account = tenant.smsAccount!;
|
||||
return {
|
||||
id: tenant.id,
|
||||
product: tenant.product,
|
||||
slug: tenant.slug,
|
||||
nameFa: tenant.nameFa,
|
||||
nameEn: tenant.nameEn,
|
||||
domain: tenant.domain,
|
||||
avatarUrl: tenant.avatarUrl,
|
||||
isActive: tenant.isActive,
|
||||
isBlocked: account.isBlocked,
|
||||
sms: this.toSummary(account),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async getLowestBalances(limit = 5) {
|
||||
const rows = await this.listTenants();
|
||||
return [...rows]
|
||||
.sort((a, b) => a.sms.balanceRials - b.sms.balanceRials)
|
||||
.slice(0, limit);
|
||||
}
|
||||
|
||||
async getAccount(tenantId: string) {
|
||||
const tenant = await this.requireTenant(tenantId);
|
||||
const account = await this.ensureAccount(tenant.id);
|
||||
const ledgers = await this.prisma.smsLedger.findMany({
|
||||
where: { smsAccountId: account.id },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 20,
|
||||
});
|
||||
|
||||
return {
|
||||
tenant: {
|
||||
id: tenant.id,
|
||||
product: tenant.product,
|
||||
slug: tenant.slug,
|
||||
nameFa: tenant.nameFa,
|
||||
nameEn: tenant.nameEn,
|
||||
domain: tenant.domain,
|
||||
avatarUrl: tenant.avatarUrl,
|
||||
},
|
||||
account: {
|
||||
id: account.id,
|
||||
balanceRials: account.balanceRials,
|
||||
pricePerPartRials: account.pricePerPartRials,
|
||||
isBlocked: account.isBlocked,
|
||||
alertCriticalRials: account.alertCriticalRials,
|
||||
alertWarningRials: account.alertWarningRials,
|
||||
gatewayProvider: account.gatewayProvider,
|
||||
melipayamakUser: account.melipayamakUser,
|
||||
melipayamakPassword: account.melipayamakPassword,
|
||||
melipayamakFrom: account.melipayamakFrom,
|
||||
panelUsername: account.panelUsername,
|
||||
panelPassword: account.panelPassword,
|
||||
panelFrom: account.panelFrom,
|
||||
extraSettings: (account.extraSettings as Record<string, string>) || {},
|
||||
todayMessages: account.todayMessages,
|
||||
todayParts: account.todayParts,
|
||||
todayCostRials: account.todayCostRials,
|
||||
monthMessages: account.monthMessages,
|
||||
monthParts: account.monthParts,
|
||||
monthCostRials: account.monthCostRials,
|
||||
},
|
||||
summary: this.toSummary(account),
|
||||
ledgers,
|
||||
safeNote:
|
||||
'این پنل متعلق به سوپرادمین است و فعلاً پنل پیامکی داخل همکار/خانگی را تغییر نمیدهد.',
|
||||
};
|
||||
}
|
||||
|
||||
async updateAccount(tenantId: string, dto: UpdateSmsAccountDto, userId?: string) {
|
||||
const account = await this.ensureAccount(tenantId);
|
||||
const before = { ...account };
|
||||
|
||||
if (
|
||||
dto.alertCriticalRials != null &&
|
||||
dto.alertWarningRials != null &&
|
||||
dto.alertCriticalRials > dto.alertWarningRials
|
||||
) {
|
||||
throw new BadRequestException('آستانه قرمز نباید از آستانه زرد بزرگتر باشد');
|
||||
}
|
||||
|
||||
const updated = await this.prisma.smsAccount.update({
|
||||
where: { id: account.id },
|
||||
data: {
|
||||
pricePerPartRials: dto.pricePerPartRials ?? undefined,
|
||||
isBlocked: dto.isBlocked ?? undefined,
|
||||
alertCriticalRials: dto.alertCriticalRials ?? undefined,
|
||||
alertWarningRials: dto.alertWarningRials ?? undefined,
|
||||
gatewayProvider: dto.gatewayProvider ?? undefined,
|
||||
melipayamakUser: dto.melipayamakUser ?? undefined,
|
||||
melipayamakPassword: dto.melipayamakPassword ?? undefined,
|
||||
melipayamakFrom: dto.melipayamakFrom ?? undefined,
|
||||
panelUsername: dto.panelUsername ?? undefined,
|
||||
panelPassword: dto.panelPassword ?? undefined,
|
||||
panelFrom: dto.panelFrom ?? undefined,
|
||||
extraSettings:
|
||||
dto.extraSettings === undefined
|
||||
? undefined
|
||||
: (dto.extraSettings as Prisma.InputJsonValue),
|
||||
},
|
||||
});
|
||||
|
||||
await this.audit.log({
|
||||
action: 'sms.account.update',
|
||||
entityType: 'sms_account',
|
||||
entityId: updated.id,
|
||||
tenantId,
|
||||
userId,
|
||||
before: this.publicAccount(before) as unknown as Prisma.InputJsonValue,
|
||||
after: this.publicAccount(updated) as unknown as Prisma.InputJsonValue,
|
||||
});
|
||||
|
||||
return this.getAccount(tenantId);
|
||||
}
|
||||
|
||||
async chargeWallet(tenantId: string, dto: ChargeSmsWalletDto, userId?: string) {
|
||||
if (!dto.amountRials || dto.amountRials === 0) {
|
||||
throw new BadRequestException('مبلغ شارژ نامعتبر است');
|
||||
}
|
||||
|
||||
const account = await this.ensureAccount(tenantId);
|
||||
const nextBalance = account.balanceRials + dto.amountRials;
|
||||
if (nextBalance < 0) {
|
||||
throw new BadRequestException('موجودی نمیتواند منفی شود');
|
||||
}
|
||||
|
||||
const type = dto.amountRials > 0 ? 'charge' : 'adjust';
|
||||
|
||||
const [updated] = await this.prisma.$transaction([
|
||||
this.prisma.smsAccount.update({
|
||||
where: { id: account.id },
|
||||
data: { balanceRials: nextBalance },
|
||||
}),
|
||||
this.prisma.smsLedger.create({
|
||||
data: {
|
||||
smsAccountId: account.id,
|
||||
type,
|
||||
amountRials: dto.amountRials,
|
||||
note: dto.note || (dto.amountRials > 0 ? 'شارژ کیف پول' : 'اصلاح موجودی'),
|
||||
createdById: userId,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
await this.audit.log({
|
||||
action: 'sms.wallet.charge',
|
||||
entityType: 'sms_account',
|
||||
entityId: account.id,
|
||||
tenantId,
|
||||
userId,
|
||||
after: {
|
||||
amountRials: dto.amountRials,
|
||||
balanceRials: updated.balanceRials,
|
||||
note: dto.note || null,
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
});
|
||||
|
||||
return this.getAccount(tenantId);
|
||||
}
|
||||
|
||||
async setBlocked(tenantId: string, isBlocked: boolean, userId?: string) {
|
||||
return this.updateAccount(tenantId, { isBlocked }, userId);
|
||||
}
|
||||
|
||||
private async ensureAccountsForAllTenants() {
|
||||
const tenants = await this.prisma.tenant.findMany({
|
||||
where: { smsAccount: null },
|
||||
select: { id: true },
|
||||
});
|
||||
for (const tenant of tenants) {
|
||||
await this.ensureAccount(tenant.id);
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureAccount(tenantId: string) {
|
||||
await this.requireTenant(tenantId);
|
||||
const existing = await this.prisma.smsAccount.findUnique({ where: { tenantId } });
|
||||
if (existing) return existing;
|
||||
|
||||
// Seed varied demo balances so color cards are visible until real charges exist
|
||||
const demoBalances = [250000, 85000, 7500, 150000, 12000, 3000, 500000, 45000];
|
||||
const hash = [...tenantId].reduce((sum, ch) => sum + ch.charCodeAt(0), 0);
|
||||
const balanceRials = demoBalances[hash % demoBalances.length];
|
||||
|
||||
return this.prisma.smsAccount.create({
|
||||
data: {
|
||||
tenantId,
|
||||
balanceRials,
|
||||
pricePerPartRials: 2500,
|
||||
alertCriticalRials: 10000,
|
||||
alertWarningRials: 100000,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private toSummary(account: SmsAccount) {
|
||||
const level = this.balanceLevel(
|
||||
account.balanceRials,
|
||||
account.alertCriticalRials,
|
||||
account.alertWarningRials,
|
||||
);
|
||||
return {
|
||||
balanceRials: account.balanceRials,
|
||||
balanceLabel: this.formatMoney(account.balanceRials),
|
||||
balanceLevel: level,
|
||||
perPartRials: account.pricePerPartRials,
|
||||
todayMessages: account.todayMessages,
|
||||
todayParts: account.todayParts,
|
||||
todayCostRials: account.todayCostRials,
|
||||
monthMessages: account.monthMessages,
|
||||
monthParts: account.monthParts,
|
||||
monthCostRials: account.monthCostRials,
|
||||
isBlocked: account.isBlocked,
|
||||
alertCriticalRials: account.alertCriticalRials,
|
||||
alertWarningRials: account.alertWarningRials,
|
||||
};
|
||||
}
|
||||
|
||||
private balanceLevel(
|
||||
balanceRials: number,
|
||||
critical = 10000,
|
||||
warning = 100000,
|
||||
): BalanceLevel {
|
||||
if (balanceRials < critical) return 'critical';
|
||||
if (balanceRials <= warning) return 'warning';
|
||||
return 'healthy';
|
||||
}
|
||||
|
||||
private formatMoney(value: number) {
|
||||
return `${new Intl.NumberFormat('fa-IR').format(value)} ریال`;
|
||||
}
|
||||
|
||||
private publicAccount(account: SmsAccount) {
|
||||
return {
|
||||
balanceRials: account.balanceRials,
|
||||
pricePerPartRials: account.pricePerPartRials,
|
||||
isBlocked: account.isBlocked,
|
||||
alertCriticalRials: account.alertCriticalRials,
|
||||
alertWarningRials: account.alertWarningRials,
|
||||
gatewayProvider: account.gatewayProvider,
|
||||
melipayamakUser: account.melipayamakUser,
|
||||
melipayamakFrom: account.melipayamakFrom,
|
||||
panelUsername: account.panelUsername,
|
||||
panelFrom: account.panelFrom,
|
||||
};
|
||||
}
|
||||
|
||||
private async requireTenant(tenantId: string) {
|
||||
const tenant = await this.prisma.tenant.findUnique({ where: { id: tenantId } });
|
||||
if (!tenant) throw new NotFoundException('نسخه یافت نشد');
|
||||
return tenant;
|
||||
}
|
||||
}
|
||||
@@ -17,9 +17,9 @@ export class TenantsService {
|
||||
) {}
|
||||
|
||||
async findAll(product?: ProductType) {
|
||||
return this.prisma.tenant.findMany({
|
||||
const rows = await this.prisma.tenant.findMany({
|
||||
where: product ? { product } : undefined,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
orderBy: { nameFa: 'asc' },
|
||||
select: {
|
||||
id: true,
|
||||
product: true,
|
||||
@@ -35,12 +35,18 @@ export class TenantsService {
|
||||
nodeWs: true,
|
||||
appUrl: true,
|
||||
adminUrl: true,
|
||||
appUiConfig: true,
|
||||
isActive: true,
|
||||
notes: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
return rows.map(({ appUiConfig, ...rest }) => ({
|
||||
...rest,
|
||||
hasAppUiConfig: Boolean(appUiConfig),
|
||||
}));
|
||||
}
|
||||
|
||||
async findOne(id: string) {
|
||||
|
||||
Reference in New Issue
Block a user