import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import { PrismaService } from '../../prisma/prisma.service'; import { TableService } from '../admin-table/table.service'; import type { TableQueryDto } from '../admin-table/table-query.dto'; /** * Branch stock allocations — legacy views/invoice/* ("فاکتور انتقال"), served by * BranchController as `branch/product`: * POST branch/product/table -> the grid * POST branch/product -> create { branch_id, products: [id...] } * GET branch/product/{id} -> one allocation * DELETE branch/product/{id} -> delete * * Despite the screen being labelled an invoice, it is not a sales document: it * records which products head office allocated to a branch, and who did it. */ @Injectable() export class AdminInvoicesService { constructor( private readonly prisma: PrismaService, private readonly table: TableService, ) {} private static readonly ALLOWED = [ 'id', 'note', 'createdAt', 'branchId', 'branch.name', 'allocatorAdminUser.fullName', // legacy filter keys 'name', 'caj', ]; private static readonly FIELD_ALIASES: Record = { name: 'branch.name', caj: 'createdAt', 'allocator_user.name': 'allocatorAdminUser.fullName', }; /** Legacy `POST branch/product/table`. */ async list(query: TableQueryDto) { this.table.assertNotExport(query); const mapField = (f?: string) => (f ? (AdminInvoicesService.FIELD_ALIASES[f] ?? f) : f); const aliased: TableQueryDto = { ...query, filters: (query.filters ?? []).map((f) => ({ ...f, field: mapField(f.field) })), order: query.order ? `${query.order.startsWith('-') ? '-' : '+'}${mapField(query.order.replace(/^[+-]/, ''))}` : undefined, }; const { where, orderBy, skip, take, page, limit } = this.table.build(aliased, { searchable: ['note', 'branch.name'], allowed: AdminInvoicesService.ALLOWED, defaultOrderBy: { createdAt: 'desc' }, }); const [rows, total] = await Promise.all([ this.prisma.branchAllocation.findMany({ where: where as Prisma.BranchAllocationWhereInput, orderBy: orderBy as Prisma.BranchAllocationOrderByWithRelationInput, skip, take, include: { branch: true, allocatorAdminUser: true, items: { include: { product: true } }, }, }), this.prisma.branchAllocation.count({ where: where as Prisma.BranchAllocationWhereInput }), ]); return this.table.respond(rows.map((r) => this.toRow(r)), total, query, page, limit); } private toRow( a: Prisma.BranchAllocationGetPayload<{ include: { branch: true; allocatorAdminUser: true; items: { include: { product: true } }; }; }>, ) { const sumWeight = a.items.reduce( (sum, it) => sum.add(it.weightGrams ?? new Prisma.Decimal(0)), new Prisma.Decimal(0), ); return { id: a.id, // legacy column key is `name` — the receiving branch name: a.branch.name, branch_id: a.branchId, branch: { id: a.branch.id, name: a.branch.name }, sum_weight: sumWeight, count_item: a.items.length, note: a.note, allocator_user: a.allocatorAdminUser ? { id: a.allocatorAdminUser.id, name: a.allocatorAdminUser.fullName } : null, caj: a.createdAt, products: a.items.map((it) => ({ id: it.productId, pid: it.productId, title: it.product.title, product_code: it.product.productCode, grams: it.weightGrams, })), }; } /** Legacy `GET branch/product/{id}`. */ async findOne(id: string) { const a = await this.prisma.branchAllocation.findUnique({ where: { id }, include: { branch: true, allocatorAdminUser: true, items: { include: { product: true } }, }, }); if (!a) throw new NotFoundException('فاکتور یافت نشد'); return this.toRow(a); } /** * Legacy `POST branch/product` with `{ branch_id, products: [id...] }`. * * Each product's current lightest variant weight is snapshotted, so a later * catalog edit does not silently rewrite what this document says was moved. */ async create(data: { branch_id?: string; products?: string[]; note?: string }, actorId: string) { const branchId = String(data.branch_id ?? ''); if (!branchId) throw new BadRequestException('شعبه انتخاب نشده است'); const branch = await this.prisma.branch.findUnique({ where: { id: branchId } }); if (!branch) throw new NotFoundException('شعبه یافت نشد'); const productIds = [...new Set((data.products ?? []).map((p) => String(p)).filter(Boolean))]; if (productIds.length === 0) throw new BadRequestException('هیچ محصولی انتخاب نشده است'); const products = await this.prisma.product.findMany({ where: { id: { in: productIds } }, include: { variants: { where: { isActive: true } } }, }); if (products.length !== productIds.length) { throw new BadRequestException('برخی از محصولات انتخاب‌شده یافت نشدند'); } const allocation = await this.prisma.branchAllocation.create({ data: { branchId, allocatorAdminUserId: actorId, note: data.note ?? null, items: { create: products.map((p) => ({ productId: p.id, weightGrams: p.variants.length ? p.variants.reduce( (min, v) => (v.rashWeightGrams.lt(min) ? v.rashWeightGrams : min), p.variants[0].rashWeightGrams, ) : null, })), }, }, }); return this.findOne(allocation.id); } /** Legacy `PUT branch/product/{id}` — the edit screen replaces the product set. */ async update(id: string, data: { branch_id?: string; products?: string[]; note?: string }) { const existing = await this.prisma.branchAllocation.findUnique({ where: { id } }); if (!existing) throw new NotFoundException('فاکتور یافت نشد'); if (data.branch_id) { const branch = await this.prisma.branch.findUnique({ where: { id: String(data.branch_id) }, }); if (!branch) throw new NotFoundException('شعبه یافت نشد'); } if (Array.isArray(data.products)) { const productIds = [...new Set(data.products.map((p) => String(p)).filter(Boolean))]; if (productIds.length === 0) { throw new BadRequestException('هیچ محصولی انتخاب نشده است'); } const products = await this.prisma.product.findMany({ where: { id: { in: productIds } }, include: { variants: { where: { isActive: true } } }, }); await this.prisma.$transaction([ this.prisma.branchAllocationItem.deleteMany({ where: { allocationId: id } }), this.prisma.branchAllocationItem.createMany({ data: products.map((p) => ({ allocationId: id, productId: p.id, weightGrams: p.variants.length ? p.variants.reduce( (min, v) => (v.rashWeightGrams.lt(min) ? v.rashWeightGrams : min), p.variants[0].rashWeightGrams, ) : null, })), skipDuplicates: true, }), ]); } await this.prisma.branchAllocation.update({ where: { id }, data: { ...(data.branch_id ? { branchId: String(data.branch_id) } : {}), ...(data.note !== undefined ? { note: data.note } : {}), }, }); return this.findOne(id); } /** Legacy `DELETE branch/product/{id}`. Items cascade. */ async remove(id: string) { const a = await this.prisma.branchAllocation.findUnique({ where: { id } }); if (!a) throw new NotFoundException('فاکتور یافت نشد'); await this.prisma.branchAllocation.delete({ where: { id } }); return { success: true }; } /** Legacy `GET branch/list` — the branch picker for the invoice form. */ async branchList() { const rows = await this.prisma.branch.findMany({ where: { isActive: true }, orderBy: { name: 'asc' }, select: { id: true, name: true }, }); return rows; } }