fix(ledger): an order's debt follows its status, as legacy recomputed it (ACC-03)
Legacy never posted order debt; updateRemain recomputed it as SUM(orders.input) WHERE sts > 4. The port posted it once on entering DELIVERED_TO_AGENT, guarded on deliveredAt, which: - posted it twice when an order reached that status again - posted nothing for an order sent straight to DELIVERED_TO_CUSTOMER - kept it after a rollback or cancellation syncOrderDebt compares what the order should owe with the confirmed ORDER_STOCK entries on it and posts only the difference (append-only, idempotent). Status removal runs it too, so removing «تحویل به نماینده» is allowed again, as in legacy. Existing data: 0 orders out of line. Owner approval: 2026-10-08. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5d386bf752
commit
fb25b2251b
@@ -1,11 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PrismaModule } from '../../prisma/prisma.module';
|
||||
import { OrdersModule } from '../orders/orders.module';
|
||||
import { AccountingModule } from '../accounting/accounting.module';
|
||||
import { AdminOrdersController } from './admin-orders.controller';
|
||||
import { AdminOrdersService } from './admin-orders.service';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, OrdersModule],
|
||||
imports: [PrismaModule, OrdersModule, AccountingModule],
|
||||
controllers: [AdminOrdersController],
|
||||
providers: [AdminOrdersService],
|
||||
})
|
||||
|
||||
@@ -5,6 +5,7 @@ import { TableService } from '../admin-table/table.service';
|
||||
import type { TableQueryDto } from '../admin-table/table-query.dto';
|
||||
import { AccountingHooksService } from '../accounting-sync/accounting-hooks.service';
|
||||
import { OrdersService } from '../orders/orders.service';
|
||||
import { LedgerService } from '../accounting/ledger.service';
|
||||
import type { UpdateOrderStatusDto } from './update-status.dto';
|
||||
|
||||
/**
|
||||
@@ -42,6 +43,7 @@ export class AdminOrdersService {
|
||||
private readonly table: TableService,
|
||||
private readonly orders: OrdersService,
|
||||
private readonly accounting: AccountingHooksService,
|
||||
private readonly ledger: LedgerService,
|
||||
) {}
|
||||
|
||||
/** Fields the XTable filter bar and sorter may reference. */
|
||||
@@ -371,8 +373,8 @@ export class AdminOrdersService {
|
||||
* Legacy `POST single/order-sts/edit` — advance or remove a status.
|
||||
*
|
||||
* Setting a status delegates to OrdersService.updateStatus rather than writing
|
||||
* the row here, because that method also posts the gold debt to the ledger at
|
||||
* DELIVERED_TO_AGENT. Duplicating the update would silently skip that.
|
||||
* the row here, because that method also keeps the order's debt in line with its status
|
||||
* (syncOrderDebt). Duplicating the update would silently skip that.
|
||||
*/
|
||||
async setStatus(dto: UpdateOrderStatusDto, actorAdminUserId: string) {
|
||||
if (dto.remove) {
|
||||
@@ -384,35 +386,33 @@ export class AdminOrdersService {
|
||||
/**
|
||||
* Legacy's `remove: true` branch: drop a recorded status event.
|
||||
*
|
||||
* Deliberately refuses to remove DELIVERED_TO_AGENT. In legacy, deleting that
|
||||
* row left the posted debt ledger entries behind, so the customer stayed in
|
||||
* debt for an order no longer marked delivered. Rather than reproduce a
|
||||
* data-corrupting path, this rejects and explains — recorded in
|
||||
* docs/ADMIN-PORT-PLAN.md.
|
||||
* Any status may be removed, as in legacy (MajorOrderController.php:180-203),
|
||||
* which stepped `sts` back and then RECOMPUTED the debt (updateRemain). The
|
||||
* rollback below does the same through syncOrderDebt, so taking an order
|
||||
* back below «تحویل به نماینده» takes its debt off, and nothing is left
|
||||
* behind. (This used to be refused, on the belief that legacy left the debt
|
||||
* in place; legacy recomputed it.)
|
||||
*/
|
||||
private async removeStatusEvent(orderId: string, status: OrderStatus) {
|
||||
if (status === OrderStatus.DELIVERED_TO_AGENT) {
|
||||
throw new BadRequestException(
|
||||
'حذف وضعیت «تحویل به نماینده» ممکن نیست، چون بدهی طلا در همین مرحله ثبت شده است.',
|
||||
);
|
||||
}
|
||||
|
||||
const event = await this.prisma.orderStatusEvent.findFirst({
|
||||
where: { orderId, toStatus: status },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
if (!event) throw new NotFoundException('وضعیت مورد نظر یافت نشد');
|
||||
|
||||
await this.prisma.orderStatusEvent.delete({ where: { id: event.id } });
|
||||
|
||||
// Roll the order back to whatever status is now the latest recorded one.
|
||||
const latest = await this.prisma.orderStatusEvent.findFirst({
|
||||
where: { orderId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
const rolledBackTo = latest?.toStatus ?? OrderStatus.PLACED;
|
||||
// One transaction: the event, the status and the debt move together or not at all.
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
await tx.orderStatusEvent.delete({ where: { id: event.id } });
|
||||
|
||||
// Roll the order back to whatever status is now the latest recorded one.
|
||||
const latest = await tx.orderStatusEvent.findFirst({
|
||||
where: { orderId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
const rolledBackTo = latest?.toStatus ?? OrderStatus.PLACED;
|
||||
const updated = await tx.order.update({ where: { id: orderId }, data: { status: rolledBackTo } });
|
||||
// Legacy updateRemain after the step back: the debt follows the status.
|
||||
await this.ledger.syncOrderDebt(orderId, tx);
|
||||
// D27 — rolled back below the document rule: the پیشفاکتور is withdrawn.
|
||||
await this.accounting.orderStatusReached(tx, orderId, rolledBackTo);
|
||||
return updated;
|
||||
|
||||
Reference in New Issue
Block a user