feat(tahesab): net sales by factor, group, and commission

Report weight after returns for the selected period, with parent customer groups and separate silver and investment commission.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-26 11:05:34 +03:30
co-authored by Cursor
parent 15f89aec04
commit 2ff35e55fc
12 changed files with 1408 additions and 18 deletions
@@ -38,6 +38,11 @@ class UpdateCustomerGroupDto {
@IsOptional()
@IsIn([...CUSTOMER_GROUP_COLORS])
color?: CustomerGroupColor;
/** null گروه را اصلی می‌کند */
@IsOptional()
@IsString()
parentId?: string | null;
}
@Controller('customer-groups')
@@ -48,7 +48,10 @@ export class CustomerGroupsService {
const rows = await this.prisma.customerGroup.findMany({
where: { organizationId },
orderBy: [{ position: 'asc' }, { name: 'asc' }],
include: { _count: { select: { people: true, companies: true } } },
include: {
_count: { select: { people: true, companies: true } },
parent: { select: { name: true } },
},
});
return rows.map((r) => ({
id: r.id,
@@ -58,6 +61,8 @@ export class CustomerGroupsService {
fromTahesab: Boolean(r.tahesabGid),
personCount: r._count.people,
companyCount: r._count.companies,
parentId: r.parentId,
parentName: r.parent?.name ?? null,
}));
}
@@ -88,15 +93,21 @@ export class CustomerGroupsService {
async update(
organizationId: string,
id: string,
dto: { name?: string; color?: CustomerGroupColor },
dto: { name?: string; color?: CustomerGroupColor; parentId?: string | null },
): Promise<CustomerGroupDto> {
await this.require(organizationId, id);
const parentId =
dto.parentId === undefined ? undefined : dto.parentId?.trim() || null;
if (parentId !== undefined) {
await this.assertParent(organizationId, id, parentId);
}
try {
await this.prisma.customerGroup.update({
where: { id },
data: {
...(dto.name != null ? { name: this.cleanName(dto.name) } : {}),
...(dto.color != null ? { color: this.cleanColor(dto.color) } : {}),
...(parentId !== undefined ? { parentId } : {}),
},
});
} catch (e) {
@@ -105,6 +116,32 @@ export class CustomerGroupsService {
return (await this.list(organizationId)).find((g) => g.id === id)!;
}
/** فقط یک سطح: گروه اصلی خودش زیرگروه نیست و گروه فرعی زیرگروه ندارد. */
private async assertParent(
organizationId: string,
id: string,
parentId: string | null,
) {
if (parentId == null) return;
if (parentId === id) {
throw new BadRequestException('گروه نمی‌تواند زیرمجموعهٔ خودش باشد.');
}
const parent = await this.prisma.customerGroup.findFirst({
where: { id: parentId, organizationId },
select: { id: true, parentId: true },
});
if (!parent) throw new NotFoundException('گروه اصلی پیدا نشد.');
if (parent.parentId) {
throw new BadRequestException('فقط یک سطح گروه اصلی و فرعی ممکن است.');
}
const childCount = await this.prisma.customerGroup.count({
where: { organizationId, parentId: id },
});
if (childCount > 0) {
throw new BadRequestException('این گروه خودش زیرگروه دارد و نمی‌تواند فرعی شود.');
}
}
/** مشتری‌ها حذف نمی‌شوند؛ فقط این گروه از رویشان برداشته می‌شود */
async remove(organizationId: string, id: string): Promise<void> {
await this.require(organizationId, id);