Files
hedena_gold/apps/api/src/modules/admin-invoices/admin-invoices.service.ts
T

246 lines
8.3 KiB
TypeScript

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<string, string> = {
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;
}
}