2026-09-13 21:02:25 +03:30
|
|
|
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
|
|
|
|
import { LedgerCurrency, LedgerDirection, LedgerSourceType, Prisma } from '@prisma/client';
|
|
|
|
|
import { PrismaService } from '../../prisma/prisma.service';
|
|
|
|
|
import { TableService } from '../admin-table/table.service';
|
|
|
|
|
import { LedgerService } from '../accounting/ledger.service';
|
2026-10-08 16:34:45 +03:30
|
|
|
import { SmsService } from '../sms/sms.service';
|
2026-09-13 21:02:25 +03:30
|
|
|
import type { TableQueryDto } from '../admin-table/table-query.dto';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Financial documents ("اسناد مالی") — legacy views/finical/*, served by
|
|
|
|
|
* CashController (`POST cash/table`, `POST cash`, `PUT cash/{id}`,
|
|
|
|
|
* `GET cash/{id}`, `DELETE cash/{id}`).
|
|
|
|
|
*
|
|
|
|
|
* A legacy "cash" row IS a ledger entry: a customer repayment recorded as melted
|
|
|
|
|
* gold, a transfer, a cheque or cash. The four legacy `type` codes map onto the
|
|
|
|
|
* existing LedgerSourceType enum:
|
|
|
|
|
*
|
|
|
|
|
* 1 آب شده -> REPAYMENT_MELTED_GOLD (karat-normalized: value * ayar / 750)
|
|
|
|
|
* 2 حواله -> REPAYMENT_TRANSFER
|
|
|
|
|
* 3 چک -> REPAYMENT_CHEQUE
|
|
|
|
|
* 4 پولی -> REPAYMENT_CASH (normalized via the gold price per gram)
|
|
|
|
|
*
|
|
|
|
|
* Posting delegates to LedgerService, which owns the karat and price-per-gram
|
|
|
|
|
* conversions and the confirm/balance rules. Re-implementing them here would let
|
|
|
|
|
* the admin path drift from the customer-facing one.
|
|
|
|
|
*/
|
|
|
|
|
const TYPE_BY_CODE: Record<number, LedgerSourceType> = {
|
|
|
|
|
1: LedgerSourceType.REPAYMENT_MELTED_GOLD,
|
|
|
|
|
2: LedgerSourceType.REPAYMENT_TRANSFER,
|
|
|
|
|
3: LedgerSourceType.REPAYMENT_CHEQUE,
|
|
|
|
|
4: LedgerSourceType.REPAYMENT_CASH,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** The instrument name LedgerService.postGoldRepayment expects. */
|
|
|
|
|
const INSTRUMENT_BY_TYPE: Record<string, 'MELTED_GOLD' | 'TRANSFER' | 'CHEQUE' | 'CASH'> = {
|
|
|
|
|
REPAYMENT_MELTED_GOLD: 'MELTED_GOLD',
|
|
|
|
|
REPAYMENT_TRANSFER: 'TRANSFER',
|
|
|
|
|
REPAYMENT_CHEQUE: 'CHEQUE',
|
|
|
|
|
REPAYMENT_CASH: 'CASH',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const CODE_BY_TYPE: Partial<Record<LedgerSourceType, number>> = {
|
|
|
|
|
REPAYMENT_MELTED_GOLD: 1,
|
|
|
|
|
REPAYMENT_TRANSFER: 2,
|
|
|
|
|
REPAYMENT_CHEQUE: 3,
|
|
|
|
|
REPAYMENT_CASH: 4,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const TYPE_LABELS: Record<LedgerSourceType, string> = {
|
|
|
|
|
ORDER_STOCK: 'سفارش',
|
|
|
|
|
ORDER_PRE_ORDER: 'سفارش ساختنی',
|
|
|
|
|
REPAYMENT_MELTED_GOLD: 'آب شده',
|
|
|
|
|
REPAYMENT_TRANSFER: 'حواله',
|
|
|
|
|
REPAYMENT_CHEQUE: 'چک',
|
|
|
|
|
REPAYMENT_CASH: 'پولی',
|
|
|
|
|
WALLET_BONUS: 'هدیه کیف پول',
|
|
|
|
|
ADJUSTMENT: 'اصلاح',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
|
export class AdminDocumentsService {
|
|
|
|
|
constructor(
|
|
|
|
|
private readonly prisma: PrismaService,
|
|
|
|
|
private readonly table: TableService,
|
|
|
|
|
private readonly ledger: LedgerService,
|
2026-10-08 16:34:45 +03:30
|
|
|
private readonly sms: SmsService,
|
2026-09-13 21:02:25 +03:30
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
private static readonly ALLOWED = [
|
|
|
|
|
'id',
|
|
|
|
|
'sourceType',
|
|
|
|
|
'currency',
|
|
|
|
|
'direction',
|
|
|
|
|
'amount',
|
|
|
|
|
'confirmed',
|
|
|
|
|
'createdAt',
|
|
|
|
|
'karatPerMille',
|
|
|
|
|
'labName',
|
|
|
|
|
'customer.fullName',
|
|
|
|
|
// legacy filter keys
|
|
|
|
|
'type_str',
|
|
|
|
|
'ayar',
|
|
|
|
|
'lab',
|
|
|
|
|
'caj',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
private static readonly FIELD_ALIASES: Record<string, string> = {
|
|
|
|
|
type_str: 'sourceType',
|
|
|
|
|
ayar: 'karatPerMille',
|
|
|
|
|
lab: 'labName',
|
|
|
|
|
caj: 'createdAt',
|
|
|
|
|
uaj: 'createdAt',
|
|
|
|
|
'customer.name': 'customer.fullName',
|
|
|
|
|
value: 'amount',
|
|
|
|
|
price: 'priceRial',
|
|
|
|
|
original_value: 'originalValue',
|
|
|
|
|
account_name: 'accountName',
|
|
|
|
|
bank_name: 'bankName',
|
|
|
|
|
to: 'transferTo',
|
|
|
|
|
recive_date: 'receivedAt',
|
|
|
|
|
price_per_gram: 'pricePerGramRialAtEntry',
|
|
|
|
|
ang: 'hallmark',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** Legacy `POST cash/table` — the documents grid. */
|
|
|
|
|
async list(query: TableQueryDto) {
|
|
|
|
|
this.table.assertNotExport(query);
|
|
|
|
|
|
|
|
|
|
const mapField = (f?: string) => (f ? (AdminDocumentsService.FIELD_ALIASES[f] ?? f) : f);
|
|
|
|
|
const aliased: TableQueryDto = {
|
|
|
|
|
...query,
|
|
|
|
|
filters: (query.filters ?? []).map((f) => {
|
|
|
|
|
const mapped = { ...f, field: mapField(f.field) };
|
|
|
|
|
if (mapped.field === 'sourceType' && mapped.value !== undefined) {
|
|
|
|
|
mapped.value = TYPE_BY_CODE[Number(mapped.value)] ?? mapped.value;
|
|
|
|
|
}
|
|
|
|
|
if (mapped.field === 'confirmed' && mapped.value !== undefined) {
|
|
|
|
|
mapped.value = String(mapped.value) === '1';
|
|
|
|
|
}
|
|
|
|
|
return mapped;
|
|
|
|
|
}),
|
|
|
|
|
order: query.order
|
|
|
|
|
? `${query.order.startsWith('-') ? '-' : '+'}${mapField(query.order.replace(/^[+-]/, ''))}`
|
|
|
|
|
: undefined,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const { where, orderBy, skip, take, page, limit } = this.table.build(aliased, {
|
|
|
|
|
searchable: ['note', 'customer.fullName', 'labName'],
|
|
|
|
|
allowed: AdminDocumentsService.ALLOWED,
|
|
|
|
|
defaultOrderBy: { createdAt: 'desc' },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// The grid is the REPAYMENT document list; order-generated debt entries are
|
|
|
|
|
// not documents an operator files, so they are excluded as legacy's separate
|
|
|
|
|
// `cash` table naturally did.
|
|
|
|
|
const scoped: Prisma.LedgerEntryWhereInput = {
|
|
|
|
|
AND: [
|
|
|
|
|
where as Prisma.LedgerEntryWhereInput,
|
|
|
|
|
{ sourceType: { in: Object.values(TYPE_BY_CODE) } },
|
|
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const [rows, total] = await Promise.all([
|
|
|
|
|
this.prisma.ledgerEntry.findMany({
|
|
|
|
|
where: scoped,
|
|
|
|
|
orderBy: orderBy as Prisma.LedgerEntryOrderByWithRelationInput,
|
|
|
|
|
skip,
|
|
|
|
|
take,
|
|
|
|
|
include: { customer: true, postedByAdminUser: true },
|
|
|
|
|
}),
|
|
|
|
|
this.prisma.ledgerEntry.count({ where: scoped }),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return this.table.respond(rows.map((r) => this.toRow(r)), total, query, page, limit);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Legacy field names preserved so the ported table model binds unchanged. */
|
|
|
|
|
private toRow(
|
|
|
|
|
e: Prisma.LedgerEntryGetPayload<{ include: { customer: true; postedByAdminUser: true } }>,
|
|
|
|
|
) {
|
|
|
|
|
return {
|
|
|
|
|
id: e.id,
|
|
|
|
|
type: CODE_BY_TYPE[e.sourceType] ?? null,
|
|
|
|
|
type_str: TYPE_LABELS[e.sourceType],
|
|
|
|
|
sourceType: e.sourceType,
|
|
|
|
|
currency: e.currency,
|
|
|
|
|
direction: e.direction,
|
|
|
|
|
// legacy `value` is the normalized final weight, `original_value` the raw one
|
|
|
|
|
value: e.amount,
|
|
|
|
|
original_value: e.originalValue,
|
|
|
|
|
price: e.priceRial,
|
|
|
|
|
price_per_gram: e.pricePerGramRialAtEntry,
|
|
|
|
|
ayar: e.karatPerMille,
|
|
|
|
|
lab: e.labName,
|
|
|
|
|
ang: e.hallmark,
|
|
|
|
|
account_name: e.accountName,
|
|
|
|
|
bank_name: e.bankName,
|
|
|
|
|
to: e.transferTo,
|
|
|
|
|
recive_date: e.receivedAt,
|
|
|
|
|
note: e.note,
|
|
|
|
|
// legacy grid renders `confirmed` through a slot using 0/1
|
|
|
|
|
confirmed: e.confirmed ? 1 : 0,
|
|
|
|
|
confirmedAt: e.confirmedAt,
|
|
|
|
|
customer: {
|
|
|
|
|
id: e.customer.id,
|
|
|
|
|
name: e.customer.fullName,
|
2026-09-22 17:50:47 +03:30
|
|
|
// D0 — real name parts
|
|
|
|
|
fname: e.customer.firstName ?? e.customer.fullName,
|
|
|
|
|
lname: e.customer.lastName ?? '',
|
2026-09-13 21:02:25 +03:30
|
|
|
mobile: e.customer.mobile,
|
|
|
|
|
},
|
|
|
|
|
posted_by: e.postedByAdminUser ? e.postedByAdminUser.fullName : null,
|
|
|
|
|
caj: e.createdAt,
|
|
|
|
|
uaj: e.createdAt,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Legacy `GET cash/{id}`. */
|
|
|
|
|
async findOne(id: string) {
|
|
|
|
|
const e = await this.prisma.ledgerEntry.findUnique({
|
|
|
|
|
where: { id },
|
|
|
|
|
include: { customer: true, postedByAdminUser: true },
|
|
|
|
|
});
|
|
|
|
|
if (!e) throw new NotFoundException('سند یافت نشد');
|
|
|
|
|
return this.toRow(e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Legacy `POST cash` — file a repayment document.
|
|
|
|
|
*
|
|
|
|
|
* The gram/karat/price conversions are LedgerService's job; this method only
|
|
|
|
|
* translates the legacy form shape and attaches the instrument metadata.
|
|
|
|
|
*/
|
|
|
|
|
async create(data: Record<string, unknown>, actorAdminUserId: string) {
|
|
|
|
|
const typeCode = Number(data.type ?? 2);
|
|
|
|
|
const sourceType = TYPE_BY_CODE[typeCode];
|
|
|
|
|
if (!sourceType) throw new BadRequestException('نوع سند نامعتبر است');
|
|
|
|
|
|
|
|
|
|
const customerId = String(data.customer_id ?? '');
|
|
|
|
|
if (!customerId) throw new BadRequestException('مشتری انتخاب نشده است');
|
|
|
|
|
|
|
|
|
|
// LedgerService takes the legacy instrument name and the RAW figure, then
|
|
|
|
|
// applies the karat / price-per-gram conversion itself.
|
|
|
|
|
const instrument = INSTRUMENT_BY_TYPE[sourceType];
|
|
|
|
|
|
|
|
|
|
// Which field carries the raw figure depends on the instrument, exactly as
|
|
|
|
|
// the legacy form did: cash posts a Rial price, melted gold posts the
|
|
|
|
|
// pre-normalization weight, transfer/cheque post grams directly.
|
|
|
|
|
const rawValue =
|
|
|
|
|
instrument === 'CASH'
|
|
|
|
|
? String(data.price ?? 0)
|
|
|
|
|
: instrument === 'MELTED_GOLD'
|
|
|
|
|
? String(data.original_value ?? data.value ?? 0)
|
|
|
|
|
: String(data.value ?? 0);
|
|
|
|
|
|
|
|
|
|
const entry = await this.ledger.postGoldRepayment({
|
|
|
|
|
customerId,
|
|
|
|
|
instrument,
|
|
|
|
|
rawValue,
|
|
|
|
|
karatPerMille: data.ayar ? Number(data.ayar) : undefined,
|
|
|
|
|
pricePerGramRial: data.price_per_gram ? String(data.price_per_gram) : undefined,
|
|
|
|
|
note: (data.note as string) ?? undefined,
|
|
|
|
|
postedByAdminUserId: actorAdminUserId,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Attach the instrument metadata the ledger service does not model.
|
|
|
|
|
await this.prisma.ledgerEntry.update({
|
|
|
|
|
where: { id: entry.id },
|
|
|
|
|
data: this.metadataPatch(data),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return this.findOne(entry.id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private metadataPatch(data: Record<string, unknown>): Prisma.LedgerEntryUpdateInput {
|
|
|
|
|
const patch: Prisma.LedgerEntryUpdateInput = {};
|
|
|
|
|
if (data.lab !== undefined) patch.labName = (data.lab as string) || null;
|
|
|
|
|
if (data.ang !== undefined) patch.hallmark = (data.ang as string) || null;
|
|
|
|
|
if (data.account_name !== undefined) patch.accountName = (data.account_name as string) || null;
|
|
|
|
|
if (data.bank_name !== undefined) patch.bankName = (data.bank_name as string) || null;
|
|
|
|
|
if (data.to !== undefined) patch.transferTo = (data.to as string) || null;
|
|
|
|
|
if (data.original_value !== undefined && data.original_value !== null) {
|
|
|
|
|
patch.originalValue = new Prisma.Decimal(String(data.original_value));
|
|
|
|
|
}
|
|
|
|
|
if (data.price !== undefined && data.price !== null) {
|
|
|
|
|
patch.priceRial = new Prisma.Decimal(String(data.price));
|
|
|
|
|
}
|
|
|
|
|
if (data.recive_date) patch.receivedAt = new Date(String(data.recive_date));
|
|
|
|
|
return patch;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Legacy `PUT cash/{id}` — edit a document.
|
|
|
|
|
*
|
|
|
|
|
* Only metadata and the note are editable. Changing the AMOUNT of a confirmed
|
|
|
|
|
* entry would silently rewrite a customer's debt, so that is refused: the
|
|
|
|
|
* correct accounting action is a reversing adjustment, not an edit. Legacy
|
|
|
|
|
* allowed it. Recorded in docs/ADMIN-PORT-PLAN.md.
|
|
|
|
|
*/
|
|
|
|
|
async update(id: string, data: Record<string, unknown>) {
|
|
|
|
|
const existing = await this.prisma.ledgerEntry.findUnique({ where: { id } });
|
|
|
|
|
if (!existing) throw new NotFoundException('سند یافت نشد');
|
|
|
|
|
|
|
|
|
|
const amountChanged =
|
|
|
|
|
data.value !== undefined && !new Prisma.Decimal(String(data.value)).equals(existing.amount);
|
|
|
|
|
|
|
|
|
|
if (existing.confirmed && amountChanged) {
|
|
|
|
|
throw new BadRequestException(
|
|
|
|
|
'مبلغ سند تاییدشده قابل ویرایش نیست، چون بدهی مشتری بر اساس آن محاسبه شده است. برای اصلاح، سند جدید ثبت کنید.',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const patch = this.metadataPatch(data);
|
|
|
|
|
if (data.note !== undefined) patch.note = (data.note as string) || null;
|
|
|
|
|
if (!existing.confirmed && amountChanged) {
|
|
|
|
|
patch.amount = new Prisma.Decimal(String(data.value));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await this.prisma.ledgerEntry.update({ where: { id }, data: patch });
|
|
|
|
|
return this.findOne(id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Legacy `POST cash/confirem/{id}` (legacy's own spelling) — a TOGGLE: the grid
|
|
|
|
|
* shows a tick on an unconfirmed row and a cross on a confirmed one, both
|
|
|
|
|
* calling this same endpoint.
|
|
|
|
|
*
|
|
|
|
|
* Confirming delegates to LedgerService so the customer balance moves through
|
|
|
|
|
* the one code path that owns it. Un-confirming reverses that same amount
|
|
|
|
|
* rather than editing the balance directly, so the two stay consistent.
|
|
|
|
|
*/
|
|
|
|
|
async toggleConfirm(id: string, adminUserId: string) {
|
|
|
|
|
const e = await this.prisma.ledgerEntry.findUnique({ where: { id } });
|
|
|
|
|
if (!e) throw new NotFoundException('سند یافت نشد');
|
|
|
|
|
|
|
|
|
|
if (!e.confirmed) {
|
|
|
|
|
await this.ledger.confirmEntry(id, adminUserId);
|
2026-10-08 16:34:45 +03:30
|
|
|
this.notifyDocumentConfirm(e.customerId, true);
|
2026-09-13 21:02:25 +03:30
|
|
|
return this.findOne(id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Un-confirm: undo this entry's effect on the cached balance, then clear the
|
|
|
|
|
// flag. A repayment is a CREDIT against gold debt, so reversing it adds the
|
|
|
|
|
// amount back.
|
|
|
|
|
await this.prisma.$transaction(async (tx) => {
|
|
|
|
|
const delta =
|
|
|
|
|
e.direction === LedgerDirection.CREDIT ? e.amount : e.amount.negated();
|
|
|
|
|
const field =
|
|
|
|
|
e.currency === LedgerCurrency.GOLD_GRAM ? 'goldDebtGrams' : 'fiatDebtRial';
|
|
|
|
|
|
|
|
|
|
await tx.customer.update({
|
|
|
|
|
where: { id: e.customerId },
|
|
|
|
|
data: { [field]: { increment: delta } },
|
|
|
|
|
});
|
|
|
|
|
await tx.ledgerEntry.update({
|
|
|
|
|
where: { id },
|
|
|
|
|
data: { confirmed: false, confirmedAt: null },
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-10-08 16:34:45 +03:30
|
|
|
this.notifyDocumentConfirm(e.customerId, false);
|
2026-09-13 21:02:25 +03:30
|
|
|
return this.findOne(id);
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-08 16:34:45 +03:30
|
|
|
// Legacy CashController::toggleConfiremCash sent an SMS on both outcomes.
|
|
|
|
|
private notifyDocumentConfirm(customerId: string, confirmed: boolean) {
|
|
|
|
|
const text = confirmed
|
|
|
|
|
? 'هنزا گلد\nسند مالی شما توسط مدیر سیستم تایید شد.'
|
|
|
|
|
: 'هنزا گلد\nسند مالی شما توسط مدیر سیستم رد شد.';
|
|
|
|
|
|
|
|
|
|
this.prisma.customer
|
|
|
|
|
.findUnique({ where: { id: customerId }, select: { mobile: true } })
|
|
|
|
|
.then((c) => {
|
|
|
|
|
if (c) this.sms.send(c.mobile, text);
|
|
|
|
|
})
|
|
|
|
|
.catch(() => {});
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-13 21:02:25 +03:30
|
|
|
/**
|
|
|
|
|
* Legacy `DELETE cash/{id}`.
|
|
|
|
|
*
|
|
|
|
|
* A confirmed entry has already moved the customer's balance; deleting it
|
|
|
|
|
* would leave that balance overstated with no trace. Refused — legacy allowed
|
|
|
|
|
* it. Unconfirmed entries never touched a balance, so they delete cleanly.
|
|
|
|
|
*/
|
|
|
|
|
async remove(id: string) {
|
|
|
|
|
const e = await this.prisma.ledgerEntry.findUnique({ where: { id } });
|
|
|
|
|
if (!e) throw new NotFoundException('سند یافت نشد');
|
|
|
|
|
if (e.confirmed) {
|
|
|
|
|
throw new BadRequestException(
|
|
|
|
|
'حذف سند تاییدشده ممکن نیست، چون در تراز مشتری اعمال شده است.',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
await this.prisma.ledgerEntry.delete({ where: { id } });
|
|
|
|
|
return { success: true };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
typeOptions() {
|
|
|
|
|
return Object.entries(TYPE_BY_CODE).map(([code, sourceType]) => ({
|
|
|
|
|
value: Number(code),
|
|
|
|
|
title: TYPE_LABELS[sourceType],
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Wallet ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/** Legacy `POST wallet` grid — the list of granted wallet bonuses. */
|
|
|
|
|
async listWallet(query: TableQueryDto) {
|
|
|
|
|
this.table.assertNotExport(query);
|
|
|
|
|
|
|
|
|
|
const { where, orderBy, skip, take, page, limit } = this.table.build(query, {
|
|
|
|
|
searchable: ['customer.fullName'],
|
|
|
|
|
allowed: ['id', 'goldGrams', 'grantedAt', 'expiresAt', 'isActive', 'customer.fullName'],
|
|
|
|
|
defaultOrderBy: { grantedAt: 'desc' },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const [rows, total] = await Promise.all([
|
|
|
|
|
this.prisma.walletBonus.findMany({
|
|
|
|
|
where: where as Prisma.WalletBonusWhereInput,
|
|
|
|
|
orderBy: orderBy as Prisma.WalletBonusOrderByWithRelationInput,
|
|
|
|
|
skip,
|
|
|
|
|
take,
|
|
|
|
|
include: { customer: true },
|
|
|
|
|
}),
|
|
|
|
|
this.prisma.walletBonus.count({ where: where as Prisma.WalletBonusWhereInput }),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return this.table.respond(
|
|
|
|
|
rows.map((w) => ({
|
|
|
|
|
id: w.id,
|
|
|
|
|
value: w.goldGrams,
|
|
|
|
|
grams: w.goldGrams,
|
|
|
|
|
is_active: w.isActive,
|
|
|
|
|
consumed: Boolean(w.consumedAt),
|
|
|
|
|
caj: w.grantedAt,
|
|
|
|
|
expires_at: w.expiresAt,
|
|
|
|
|
customer: {
|
|
|
|
|
id: w.customer.id,
|
|
|
|
|
name: w.customer.fullName,
|
|
|
|
|
mobile: w.customer.mobile,
|
|
|
|
|
},
|
|
|
|
|
})),
|
|
|
|
|
total,
|
|
|
|
|
query,
|
|
|
|
|
page,
|
|
|
|
|
limit,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Legacy `GET /wallet/config` and `/wallet/birthday/config`, plus their POST
|
|
|
|
|
* counterparts — the wallet settings screen (views/manage/wallet.vue).
|
|
|
|
|
* Stored in the Setting key/value table that replaces legacy `config_static`.
|
|
|
|
|
*/
|
|
|
|
|
async getSetting(key: string) {
|
|
|
|
|
const row = await this.prisma.setting.findUnique({ where: { key } });
|
|
|
|
|
return row?.value ?? {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async setSetting(key: string, value: Record<string, unknown>) {
|
|
|
|
|
const row = await this.prisma.setting.upsert({
|
|
|
|
|
where: { key },
|
|
|
|
|
update: { value: value as Prisma.InputJsonValue },
|
|
|
|
|
create: { key, value: value as Prisma.InputJsonValue },
|
|
|
|
|
});
|
|
|
|
|
return row.value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** The customer picker used by the document form — legacy `GET query/customer`. */
|
|
|
|
|
async queryCustomers() {
|
|
|
|
|
const rows = await this.prisma.customer.findMany({
|
|
|
|
|
where: { isActive: true },
|
|
|
|
|
orderBy: { fullName: 'asc' },
|
2026-09-22 17:50:47 +03:30
|
|
|
select: { id: true, fullName: true, firstName: true, lastName: true, mobile: true },
|
2026-09-13 21:02:25 +03:30
|
|
|
});
|
2026-09-22 17:50:47 +03:30
|
|
|
// D0 — real name parts.
|
|
|
|
|
return rows.map((c) => ({
|
|
|
|
|
id: c.id,
|
|
|
|
|
name: c.fullName,
|
|
|
|
|
fname: c.firstName ?? c.fullName,
|
|
|
|
|
lname: c.lastName ?? '',
|
|
|
|
|
mobile: c.mobile,
|
|
|
|
|
}));
|
2026-09-13 21:02:25 +03:30
|
|
|
}
|
|
|
|
|
}
|