mirror of
https://git.meshkee.com/Meshkee/backend.git
synced 2026-08-12 06:40:58 +04:30
NestJS backend with Prisma, Docker Compose for Postgres/Redis, and deploy docs for the production VM.
296 lines
8.0 KiB
TypeScript
296 lines
8.0 KiB
TypeScript
import {
|
|
BadRequestException,
|
|
ForbiddenException,
|
|
Injectable,
|
|
NotFoundException,
|
|
} from '@nestjs/common';
|
|
import { Prisma } from '@prisma/client';
|
|
import { AuthUser } from '../auth/auth.types';
|
|
import { PermissionsService } from '../auth/permissions.service';
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
import { TenantService } from '../tenant/tenant.service';
|
|
import {
|
|
CreateWebsiteBrandGroupDto,
|
|
ListWebsiteBrandGroupsDto,
|
|
UpdateWebsiteBrandGroupDto,
|
|
} from './dto/website-brand-groups.dto';
|
|
|
|
const groupInclude = {
|
|
website_brand_group_items: {
|
|
orderBy: [{ sort_order: 'asc' as const }, { id: 'asc' as const }],
|
|
include: {
|
|
brands: {
|
|
include: { imageMedia: true },
|
|
},
|
|
},
|
|
},
|
|
} satisfies Prisma.website_brand_groupsInclude;
|
|
|
|
type GroupWithItems = Prisma.website_brand_groupsGetPayload<{
|
|
include: typeof groupInclude;
|
|
}>;
|
|
|
|
@Injectable()
|
|
export class WebsiteBrandGroupsService {
|
|
constructor(
|
|
private readonly prisma: PrismaService,
|
|
private readonly permissions: PermissionsService,
|
|
private readonly tenant: TenantService,
|
|
) {}
|
|
|
|
async listPublic(host: string) {
|
|
const business = await this.tenant.resolveBusinessByDomain(host);
|
|
const groups = await this.prisma.website_brand_groups.findMany({
|
|
where: { business_id: business.id, is_active: true },
|
|
orderBy: [{ sort_order: 'asc' }, { created_at: 'asc' }],
|
|
include: groupInclude,
|
|
});
|
|
|
|
return {
|
|
items: groups.map((group) => this.serializeGroup(group)),
|
|
};
|
|
}
|
|
|
|
async list(
|
|
businessIdRaw: string,
|
|
query: ListWebsiteBrandGroupsDto,
|
|
actor: AuthUser,
|
|
) {
|
|
const businessId = BigInt(businessIdRaw);
|
|
await this.assertPermission(businessId, actor.id, 'website.read');
|
|
|
|
const page = query.page ?? 1;
|
|
const pageSize = query.pageSize ?? 20;
|
|
const skip = (page - 1) * pageSize;
|
|
|
|
const where: Prisma.website_brand_groupsWhereInput = {
|
|
business_id: businessId,
|
|
...(query.isActive !== undefined ? { is_active: query.isActive } : {}),
|
|
};
|
|
|
|
const [items, total] = await Promise.all([
|
|
this.prisma.website_brand_groups.findMany({
|
|
where,
|
|
orderBy: [{ sort_order: 'asc' }, { created_at: 'asc' }],
|
|
skip,
|
|
take: pageSize,
|
|
include: groupInclude,
|
|
}),
|
|
this.prisma.website_brand_groups.count({ where }),
|
|
]);
|
|
|
|
return {
|
|
items: items.map((group) => this.serializeGroup(group)),
|
|
total,
|
|
page,
|
|
pageSize,
|
|
};
|
|
}
|
|
|
|
async getOne(
|
|
businessIdRaw: string,
|
|
groupIdRaw: string,
|
|
actor: AuthUser,
|
|
) {
|
|
const businessId = BigInt(businessIdRaw);
|
|
const groupId = BigInt(groupIdRaw);
|
|
await this.assertPermission(businessId, actor.id, 'website.read');
|
|
|
|
const group = await this.findGroupOrThrow(businessId, groupId);
|
|
return { group: this.serializeGroup(group) };
|
|
}
|
|
|
|
async create(
|
|
businessIdRaw: string,
|
|
dto: CreateWebsiteBrandGroupDto,
|
|
actor: AuthUser,
|
|
) {
|
|
const businessId = BigInt(businessIdRaw);
|
|
await this.assertPermission(businessId, actor.id, 'website.update');
|
|
|
|
const brandIds = this.parseUniqueIds(dto.brandIds ?? []);
|
|
if (brandIds.length > 0) {
|
|
await this.assertBrandsBelongToBusiness(businessId, brandIds);
|
|
}
|
|
|
|
const created = await this.prisma.$transaction(async (tx) => {
|
|
const group = await tx.website_brand_groups.create({
|
|
data: {
|
|
business_id: businessId,
|
|
title: dto.title.trim(),
|
|
sort_order: dto.sortOrder ?? 0,
|
|
is_active: dto.isActive ?? true,
|
|
},
|
|
});
|
|
|
|
await this.replaceItems(tx, group.id, brandIds);
|
|
return tx.website_brand_groups.findUniqueOrThrow({
|
|
where: { id: group.id },
|
|
include: groupInclude,
|
|
});
|
|
});
|
|
|
|
return {
|
|
message: 'Website brand group created successfully',
|
|
group: this.serializeGroup(created),
|
|
};
|
|
}
|
|
|
|
async update(
|
|
businessIdRaw: string,
|
|
groupIdRaw: string,
|
|
dto: UpdateWebsiteBrandGroupDto,
|
|
actor: AuthUser,
|
|
) {
|
|
const businessId = BigInt(businessIdRaw);
|
|
const groupId = BigInt(groupIdRaw);
|
|
await this.assertPermission(businessId, actor.id, 'website.update');
|
|
|
|
await this.findGroupOrThrow(businessId, groupId);
|
|
|
|
let brandIds: bigint[] | undefined;
|
|
if (dto.brandIds !== undefined) {
|
|
brandIds = this.parseUniqueIds(dto.brandIds);
|
|
await this.assertBrandsBelongToBusiness(businessId, brandIds);
|
|
}
|
|
|
|
const updated = await this.prisma.$transaction(async (tx) => {
|
|
await tx.website_brand_groups.update({
|
|
where: { id: groupId },
|
|
data: {
|
|
...(dto.title !== undefined ? { title: dto.title.trim() } : {}),
|
|
...(dto.sortOrder !== undefined ? { sort_order: dto.sortOrder } : {}),
|
|
...(dto.isActive !== undefined ? { is_active: dto.isActive } : {}),
|
|
},
|
|
});
|
|
|
|
if (brandIds !== undefined) {
|
|
await this.replaceItems(tx, groupId, brandIds);
|
|
}
|
|
|
|
return tx.website_brand_groups.findUniqueOrThrow({
|
|
where: { id: groupId },
|
|
include: groupInclude,
|
|
});
|
|
});
|
|
|
|
return {
|
|
message: 'Website brand group updated successfully',
|
|
group: this.serializeGroup(updated),
|
|
};
|
|
}
|
|
|
|
async remove(
|
|
businessIdRaw: string,
|
|
groupIdRaw: string,
|
|
actor: AuthUser,
|
|
) {
|
|
const businessId = BigInt(businessIdRaw);
|
|
const groupId = BigInt(groupIdRaw);
|
|
await this.assertPermission(businessId, actor.id, 'website.update');
|
|
|
|
await this.findGroupOrThrow(businessId, groupId);
|
|
await this.prisma.website_brand_groups.delete({ where: { id: groupId } });
|
|
|
|
return { message: 'Website brand group deleted successfully' };
|
|
}
|
|
|
|
private async findGroupOrThrow(businessId: bigint, groupId: bigint) {
|
|
const group = await this.prisma.website_brand_groups.findFirst({
|
|
where: { id: groupId, business_id: businessId },
|
|
include: groupInclude,
|
|
});
|
|
|
|
if (!group) {
|
|
throw new NotFoundException('Website brand group not found');
|
|
}
|
|
|
|
return group;
|
|
}
|
|
|
|
private parseUniqueIds(ids: string[]) {
|
|
const unique = [...new Set(ids.map((id) => id.trim()).filter(Boolean))];
|
|
return unique.map((id) => BigInt(id));
|
|
}
|
|
|
|
private async assertBrandsBelongToBusiness(
|
|
businessId: bigint,
|
|
brandIds: bigint[],
|
|
) {
|
|
const found = await this.prisma.brand.findMany({
|
|
where: { businessId, id: { in: brandIds } },
|
|
select: { id: true },
|
|
});
|
|
|
|
if (found.length !== brandIds.length) {
|
|
throw new BadRequestException(
|
|
'One or more brands were not found for this business',
|
|
);
|
|
}
|
|
}
|
|
|
|
private async replaceItems(
|
|
tx: Prisma.TransactionClient,
|
|
groupId: bigint,
|
|
brandIds: bigint[],
|
|
) {
|
|
await tx.website_brand_group_items.deleteMany({ where: { group_id: groupId } });
|
|
|
|
if (brandIds.length === 0) {
|
|
return;
|
|
}
|
|
|
|
await tx.website_brand_group_items.createMany({
|
|
data: brandIds.map((brandId, index) => ({
|
|
group_id: groupId,
|
|
brand_id: brandId,
|
|
sort_order: index,
|
|
})),
|
|
});
|
|
}
|
|
|
|
private serializeGroup(group: GroupWithItems) {
|
|
const items = group.website_brand_group_items.map((entry) => {
|
|
const brand = entry.brands;
|
|
return {
|
|
id: brand.id.toString(),
|
|
nameEn: brand.nameEn,
|
|
nameFa: brand.nameFa,
|
|
slug: brand.slug,
|
|
about: brand.about,
|
|
imageMediaId: brand.imageMediaId?.toString() ?? null,
|
|
imageUrl: brand.imageMedia?.publicUrl ?? null,
|
|
sortOrder: entry.sort_order,
|
|
};
|
|
});
|
|
|
|
return {
|
|
id: group.id.toString(),
|
|
title: group.title,
|
|
sortOrder: group.sort_order,
|
|
isActive: group.is_active,
|
|
createdAt: group.created_at,
|
|
updatedAt: group.updated_at,
|
|
items,
|
|
};
|
|
}
|
|
|
|
private async assertPermission(
|
|
businessId: bigint,
|
|
userId: bigint,
|
|
permission: string,
|
|
) {
|
|
const allowed = await this.permissions.hasBusinessPermission(
|
|
userId,
|
|
businessId,
|
|
permission,
|
|
);
|
|
|
|
if (!allowed) {
|
|
throw new ForbiddenException(
|
|
`Missing permission: ${permission} for this business`,
|
|
);
|
|
}
|
|
}
|
|
}
|