feat: complete admin panel and backend admin api migration
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
import { Body, Controller, Get, HttpCode, Param, Post } from '@nestjs/common';
|
||||
import { AdminOrdersService } from './admin-orders.service';
|
||||
import { TableQueryDto } from '../admin-table/table-query.dto';
|
||||
import { UpdateOrderStatusDto } from './update-status.dto';
|
||||
import { CurrentAdmin, RequirePermission } from '../auth/guards';
|
||||
import type { AuthenticatedAdmin } from '../auth/jwt.strategy';
|
||||
|
||||
/**
|
||||
* Legacy endpoints (henza_server routes/admin.php):
|
||||
* POST single-order -> the single-sale order grid
|
||||
* GET order/{id} -> order detail + status timeline
|
||||
*
|
||||
* Each route is gated by the same rid the legacy router used in `meta.role`,
|
||||
* so the API enforces what the menu already hides.
|
||||
*/
|
||||
@Controller('admin/orders')
|
||||
export class AdminOrdersController {
|
||||
constructor(private readonly orders: AdminOrdersService) {}
|
||||
|
||||
/** Legacy POST single-order. POST, not GET, because XTable posts its filters. */
|
||||
@Post('list')
|
||||
@HttpCode(200)
|
||||
@RequirePermission('order.list')
|
||||
list(@Body() query: TableQueryDto, @CurrentAdmin() admin: AuthenticatedAdmin) {
|
||||
/**
|
||||
* Branch scoping, ported from legacy AdminController::getSingleOrders:
|
||||
*
|
||||
* if (!is_null(auth()->user()->branch_id)) {
|
||||
* $records->where('branch_id', auth()->user()->branch_id);
|
||||
* }
|
||||
*
|
||||
* i.e. ANY user carrying a branch_id sees only that branch. Faithful to legacy.
|
||||
*
|
||||
* Consequence worth knowing: the seed gives head-office admins a home branch,
|
||||
* so under this rule they see only that branch's orders. That is legacy's real
|
||||
* behaviour, not a defect introduced here — head office was expected to have a
|
||||
* null branch_id. Recorded in docs/ADMIN-PORT-PLAN.md.
|
||||
*/
|
||||
return this.orders.list(query, admin.branchId ?? undefined);
|
||||
}
|
||||
|
||||
/** Legacy POST major/order/table — the wholesale grid. */
|
||||
@Post('wholesale/list')
|
||||
@HttpCode(200)
|
||||
@RequirePermission('order.omde')
|
||||
listWholesale(@Body() query: TableQueryDto, @CurrentAdmin() admin: AuthenticatedAdmin) {
|
||||
return this.orders.listWholesale(query, admin.branchId ?? undefined);
|
||||
}
|
||||
|
||||
/** Legacy POST order-tamin — orders that reached the sourcing stage. */
|
||||
@Post('tamin/list')
|
||||
@HttpCode(200)
|
||||
@RequirePermission('order.tamin')
|
||||
listTamin(@Body() query: TableQueryDto, @CurrentAdmin() admin: AuthenticatedAdmin) {
|
||||
return this.orders.listTamin(query, admin.branchId ?? undefined);
|
||||
}
|
||||
|
||||
/** Legacy POST delete-order. */
|
||||
@Post('delete')
|
||||
@HttpCode(200)
|
||||
@RequirePermission('order.omde')
|
||||
remove(@Body() body: { id: string }) {
|
||||
return this.orders.remove(body.id);
|
||||
}
|
||||
|
||||
/** Legacy POST /order from the admin order builder (views/order/create.vue). */
|
||||
@Post('create')
|
||||
@HttpCode(200)
|
||||
@RequirePermission('order.create')
|
||||
createFromAdmin(
|
||||
@Body()
|
||||
body: {
|
||||
customer_id: string;
|
||||
order_items: { vid: string; qty: number; percent?: number; discount?: number }[];
|
||||
sts?: number | string;
|
||||
note?: string;
|
||||
},
|
||||
@CurrentAdmin() admin: AuthenticatedAdmin,
|
||||
) {
|
||||
return this.orders.createFromAdmin(body, admin.id);
|
||||
}
|
||||
|
||||
/** Legacy GET order/type. */
|
||||
@Get('type-options')
|
||||
@RequirePermission('order.create')
|
||||
typeOptions() {
|
||||
return this.orders.typeOptions();
|
||||
}
|
||||
|
||||
@Get('status-options')
|
||||
@RequirePermission('order.list')
|
||||
statusOptions() {
|
||||
return this.orders.statusOptions();
|
||||
}
|
||||
|
||||
/** Legacy POST single/order-sts/edit. */
|
||||
@Post('status')
|
||||
@HttpCode(200)
|
||||
@RequirePermission('order.list')
|
||||
setStatus(@Body() dto: UpdateOrderStatusDto, @CurrentAdmin() admin: AuthenticatedAdmin) {
|
||||
return this.orders.setStatus(dto, admin.id);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@RequirePermission('order.list')
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.orders.findOne(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PrismaModule } from '../../prisma/prisma.module';
|
||||
import { OrdersModule } from '../orders/orders.module';
|
||||
import { AdminOrdersController } from './admin-orders.controller';
|
||||
import { AdminOrdersService } from './admin-orders.service';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, OrdersModule],
|
||||
controllers: [AdminOrdersController],
|
||||
providers: [AdminOrdersService],
|
||||
})
|
||||
export class AdminOrdersModule {}
|
||||
@@ -0,0 +1,406 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { OrderChannel, OrderStatus, 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';
|
||||
import { OrdersService } from '../orders/orders.service';
|
||||
import type { UpdateOrderStatusDto } from './update-status.dto';
|
||||
|
||||
/**
|
||||
* Farsi status labels, matching legacy's `sts_str` column.
|
||||
*
|
||||
* Legacy stored status as an integer `sts` and the server derived the string via
|
||||
* Order::$SELECT_STS_STR. The new schema uses an enum, so the mapping is explicit.
|
||||
* Debt is recognised at sts>4 (DELIVERED_TO_AGENT) — see the schema notes.
|
||||
*/
|
||||
const STATUS_LABELS: Record<OrderStatus, string> = {
|
||||
SUSPENDED: 'معلق (سقف اعتبار)',
|
||||
PLACED: 'ثبت شده',
|
||||
ADMIN_APPROVED: 'تایید مدیر',
|
||||
SOURCING: 'در حال تامین',
|
||||
PREPARING: 'در حال آمادهسازی',
|
||||
DELIVERED_TO_AGENT: 'تحویل به نماینده',
|
||||
DELIVERED_TO_CUSTOMER: 'تحویل به مشتری',
|
||||
CANCELLED: 'لغو شده',
|
||||
};
|
||||
|
||||
/** Order of the status timeline shown in the legacy "وضعیت سفارش" modal. */
|
||||
const STATUS_FLOW: OrderStatus[] = [
|
||||
OrderStatus.PLACED,
|
||||
OrderStatus.ADMIN_APPROVED,
|
||||
OrderStatus.SOURCING,
|
||||
OrderStatus.PREPARING,
|
||||
OrderStatus.DELIVERED_TO_AGENT,
|
||||
OrderStatus.DELIVERED_TO_CUSTOMER,
|
||||
];
|
||||
|
||||
@Injectable()
|
||||
export class AdminOrdersService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly table: TableService,
|
||||
private readonly orders: OrdersService,
|
||||
) {}
|
||||
|
||||
/** Fields the XTable filter bar and sorter may reference. */
|
||||
private static readonly ALLOWED = [
|
||||
'id',
|
||||
'orderNumber',
|
||||
'status',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'totalGoldWeightGrams',
|
||||
'flatRialCharge',
|
||||
'isSuspendedOverCreditLimit',
|
||||
'customer.fullName',
|
||||
'customer.mobile',
|
||||
'branch.name',
|
||||
];
|
||||
|
||||
/**
|
||||
* Legacy `POST single-order` — the order list grid.
|
||||
*
|
||||
* `branchId` narrows the result to one branch, which is how the branch-mode
|
||||
* build of the panel scopes its list.
|
||||
*/
|
||||
async list(query: TableQueryDto, branchId?: string, channel: OrderChannel = OrderChannel.RETAIL) {
|
||||
this.table.assertNotExport(query);
|
||||
|
||||
const { where, orderBy, skip, take, page, limit } = this.table.build(query, {
|
||||
searchable: ['orderNumber', 'customer.fullName', 'customer.mobile'],
|
||||
allowed: AdminOrdersService.ALLOWED,
|
||||
defaultOrderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
const scoped: Prisma.OrderWhereInput = {
|
||||
AND: [
|
||||
where as Prisma.OrderWhereInput,
|
||||
{ channel },
|
||||
...(branchId ? [{ branchId }] : []),
|
||||
],
|
||||
};
|
||||
|
||||
const [rows, total] = await Promise.all([
|
||||
this.prisma.order.findMany({
|
||||
where: scoped,
|
||||
orderBy: orderBy as Prisma.OrderOrderByWithRelationInput,
|
||||
skip,
|
||||
take,
|
||||
include: {
|
||||
customer: true,
|
||||
branch: true,
|
||||
items: { include: { productVariant: { include: { product: true } } } },
|
||||
},
|
||||
}),
|
||||
this.prisma.order.count({ where: scoped }),
|
||||
]);
|
||||
|
||||
return this.table.respond(rows.map((r) => this.toRow(r)), total, query, page, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shapes a row the way the ported list view reads it.
|
||||
*
|
||||
* Legacy field names are preserved (`sts_str`, `caj`, `total_price`, `items`)
|
||||
* so the ported template bindings keep working unchanged.
|
||||
*/
|
||||
private toRow(o: Prisma.OrderGetPayload<{
|
||||
include: {
|
||||
customer: true;
|
||||
branch: true;
|
||||
items: { include: { productVariant: { include: { product: true } } } };
|
||||
};
|
||||
}>) {
|
||||
return {
|
||||
id: o.id,
|
||||
orderNumber: o.orderNumber,
|
||||
status: o.status,
|
||||
sts_str: STATUS_LABELS[o.status],
|
||||
caj: o.createdAt,
|
||||
uaj: o.updatedAt,
|
||||
total_price: o.flatRialCharge,
|
||||
totalGoldWeightGrams: o.totalGoldWeightGrams,
|
||||
wageWeightGrams: o.wageWeightGrams,
|
||||
isSuspendedOverCreditLimit: o.isSuspendedOverCreditLimit,
|
||||
deliveredAt: o.deliveredAt,
|
||||
branch_id: o.branchId,
|
||||
branch: o.branch ? { id: o.branch.id, name: o.branch.name } : null,
|
||||
customer: {
|
||||
id: o.customer.id,
|
||||
// Legacy split names into fname/lname; the new schema has one fullName.
|
||||
// Both are exposed so ported templates binding either keep working.
|
||||
fname: o.customer.fullName,
|
||||
lname: '',
|
||||
fullName: o.customer.fullName,
|
||||
phone: o.customer.mobile,
|
||||
},
|
||||
items: o.items.map((it) => ({
|
||||
id: it.id,
|
||||
title: it.productVariant.product.title,
|
||||
variant_title: it.productVariant.title,
|
||||
qty: it.quantity,
|
||||
grams: it.computedWeightGrams,
|
||||
rash: it.rashWeightGramsSnapshot,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy `POST major/order/table` — the wholesale (عمده) order grid.
|
||||
*
|
||||
* Legacy kept the two sales channels in SEPARATE tables: `orders_orders`
|
||||
* (SingleCustomerOrders, served by AdminController::getSingleOrders) and
|
||||
* `orders` (Order, served by MajorOrderController::index). Neither controller
|
||||
* applied a type filter because the table itself was the discriminator, so the
|
||||
* two lists never overlapped.
|
||||
*
|
||||
* The redesigned schema has one Order model, so `Order.channel` carries that
|
||||
* distinction instead and each list filters on it.
|
||||
*/
|
||||
async listWholesale(query: TableQueryDto, branchId?: string) {
|
||||
return this.list(query, branchId, OrderChannel.WHOLESALE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy `POST order-tamin` — MajorOrderController::getTaminOrders.
|
||||
*
|
||||
* Legacy narrowed to orders whose activity contained Order::$STS[3], i.e. those
|
||||
* that have reached the sourcing stage. The equivalent here is a status event
|
||||
* recording a transition into SOURCING.
|
||||
*/
|
||||
async listTamin(query: TableQueryDto, branchId?: string) {
|
||||
this.table.assertNotExport(query);
|
||||
|
||||
const { where, orderBy, skip, take, page, limit } = this.table.build(query, {
|
||||
searchable: ['orderNumber', 'customer.fullName', 'customer.mobile'],
|
||||
allowed: AdminOrdersService.ALLOWED,
|
||||
defaultOrderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
const conditions: Prisma.OrderWhereInput[] = [
|
||||
where as Prisma.OrderWhereInput,
|
||||
{ statusEvents: { some: { toStatus: OrderStatus.SOURCING } } },
|
||||
];
|
||||
if (branchId) conditions.push({ branchId });
|
||||
|
||||
const scoped: Prisma.OrderWhereInput = { AND: conditions };
|
||||
|
||||
const [rows, total] = await Promise.all([
|
||||
this.prisma.order.findMany({
|
||||
where: scoped,
|
||||
orderBy: orderBy as Prisma.OrderOrderByWithRelationInput,
|
||||
skip,
|
||||
take,
|
||||
include: {
|
||||
customer: true,
|
||||
branch: true,
|
||||
items: { include: { productVariant: { include: { product: true } } } },
|
||||
},
|
||||
}),
|
||||
this.prisma.order.count({ where: scoped }),
|
||||
]);
|
||||
|
||||
return this.table.respond(rows.map((r) => this.toRow(r)), total, query, page, limit);
|
||||
}
|
||||
|
||||
/** Legacy `POST delete-order` — remove a wholesale order. */
|
||||
async remove(id: string) {
|
||||
const order = await this.prisma.order.findUnique({
|
||||
where: { id },
|
||||
include: { ledgerEntries: true },
|
||||
});
|
||||
if (!order) throw new NotFoundException('سفارش یافت نشد');
|
||||
|
||||
// Refuses once debt has been posted: deleting the order would orphan the
|
||||
// ledger entries and leave the customer permanently in debt. Legacy had no
|
||||
// such guard. Recorded in docs/ADMIN-PORT-PLAN.md.
|
||||
if (order.ledgerEntries.length > 0) {
|
||||
throw new BadRequestException(
|
||||
'حذف این سفارش ممکن نیست، چون برای آن سند حسابداری ثبت شده است.',
|
||||
);
|
||||
}
|
||||
|
||||
await this.prisma.$transaction([
|
||||
this.prisma.orderStatusEvent.deleteMany({ where: { orderId: id } }),
|
||||
this.prisma.orderItem.deleteMany({ where: { orderId: id } }),
|
||||
this.prisma.order.delete({ where: { id } }),
|
||||
]);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/** Legacy `GET order/:id` detail, including the status timeline. */
|
||||
async findOne(id: string) {
|
||||
const order = await this.prisma.order.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
customer: true,
|
||||
branch: true,
|
||||
items: { include: { productVariant: { include: { product: true } } } },
|
||||
statusEvents: { include: { actorAdminUser: true }, orderBy: { createdAt: 'asc' } },
|
||||
},
|
||||
});
|
||||
if (!order) throw new NotFoundException('سفارش یافت نشد');
|
||||
|
||||
const reached = new Set(order.statusEvents.map((e) => e.toStatus));
|
||||
|
||||
return {
|
||||
...this.toRow(order),
|
||||
// legacy `activity`: one entry per step, `proceed` marking those reached
|
||||
activity: STATUS_FLOW.map((s) => {
|
||||
const event = order.statusEvents.find((e) => e.toStatus === s);
|
||||
return {
|
||||
status: s,
|
||||
title: STATUS_LABELS[s],
|
||||
proceed: reached.has(s),
|
||||
uaj: event?.createdAt ?? null,
|
||||
note: event?.note ?? '',
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/** Farsi labels for the status dropdowns. */
|
||||
statusOptions() {
|
||||
return Object.entries(STATUS_LABELS).map(([value, title]) => ({ value, title }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy `GET order/type` — the "نوع فروش" dropdown.
|
||||
* Legacy Order::$TYPES = [1 => 'ثبت سفارش توسط مسئول فروش', 2 => 'ثبت مشتری'].
|
||||
*/
|
||||
typeOptions() {
|
||||
return { 1: 'ثبت سفارش توسط مسئول فروش', 2: 'ثبت مشتری' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy `POST /order` from views/order/create.vue — the admin order builder.
|
||||
*
|
||||
* Delegates to OrdersService.createOrder, which owns the weight formula
|
||||
* w = rash * (1 + (wagePercent - discountPercent)/100)
|
||||
* and the credit-limit gate that suspends rather than rejects an over-limit
|
||||
* order. Reimplementing either here would let the two paths drift.
|
||||
*
|
||||
* Legacy also posted total_price / total_weight / gold_price_original computed
|
||||
* in the BROWSER. Those are ignored: the server recomputes them from the
|
||||
* variant snapshots, so a tampered or stale client total cannot alter a debt.
|
||||
*/
|
||||
async createFromAdmin(
|
||||
payload: {
|
||||
customer_id: string;
|
||||
order_items: {
|
||||
vid: string;
|
||||
qty: number;
|
||||
percent?: number;
|
||||
discount?: number;
|
||||
rial_input?: number;
|
||||
}[];
|
||||
sts?: number | string;
|
||||
note?: string;
|
||||
},
|
||||
actorAdminUserId: string,
|
||||
) {
|
||||
if (!payload.customer_id) throw new BadRequestException('مشتری انتخاب نشده است');
|
||||
if (!Array.isArray(payload.order_items) || payload.order_items.length === 0) {
|
||||
throw new BadRequestException('سبد سفارش خالی است');
|
||||
}
|
||||
|
||||
const items = await Promise.all(
|
||||
payload.order_items.map(async (it) => {
|
||||
const variant = await this.prisma.productVariant.findUnique({ where: { id: it.vid } });
|
||||
if (!variant) throw new NotFoundException(`تنوع محصول یافت نشد: ${it.vid}`);
|
||||
return {
|
||||
productVariantId: it.vid,
|
||||
quantity: Number(it.qty) || 1,
|
||||
// Fall back to the variant's own default wage when the builder did not
|
||||
// negotiate a different one.
|
||||
wagePercent: Number(it.percent ?? variant.defaultWagePercent),
|
||||
discountPercent: Number(it.discount ?? 0),
|
||||
flatRialCharge: Number(it.rial_input ?? 0),
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const order = await this.orders.createOrder({
|
||||
customerId: payload.customer_id,
|
||||
items,
|
||||
// The admin builder serves the omde (major/*) flow, so orders it creates
|
||||
// belong to the wholesale channel — legacy wrote them to the `orders`
|
||||
// table, which MajorOrderController::index listed.
|
||||
channel: OrderChannel.WHOLESALE,
|
||||
});
|
||||
|
||||
// legacy `sts`: advance the new order to the chosen delivery status.
|
||||
const target = this.statusFromLegacyCode(payload.sts);
|
||||
if (target && target !== OrderStatus.PLACED) {
|
||||
await this.orders.updateStatus(order.id, target, actorAdminUserId, payload.note);
|
||||
}
|
||||
|
||||
return this.findOne(order.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy delivery-status codes from create.vue's `sts` map:
|
||||
* 1 ثبت سفارش | 2 تایید مدیر سیستم | 3 در حال تامین
|
||||
* 4 در حال آماده سازی | 5 ارسال | 6 تحویل مشتری
|
||||
*/
|
||||
private statusFromLegacyCode(code?: number | string): OrderStatus | undefined {
|
||||
const map: Record<number, OrderStatus> = {
|
||||
1: OrderStatus.PLACED,
|
||||
2: OrderStatus.ADMIN_APPROVED,
|
||||
3: OrderStatus.SOURCING,
|
||||
4: OrderStatus.PREPARING,
|
||||
5: OrderStatus.DELIVERED_TO_AGENT,
|
||||
6: OrderStatus.DELIVERED_TO_CUSTOMER,
|
||||
};
|
||||
return code === undefined ? undefined : map[Number(code)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
async setStatus(dto: UpdateOrderStatusDto, actorAdminUserId: string) {
|
||||
if (dto.remove) {
|
||||
return this.removeStatusEvent(dto.orderId, dto.status);
|
||||
}
|
||||
return this.orders.updateStatus(dto.orderId, dto.status, actorAdminUserId, dto.note);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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' },
|
||||
});
|
||||
return this.prisma.order.update({
|
||||
where: { id: orderId },
|
||||
data: { status: latest?.toStatus ?? OrderStatus.PLACED },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { IsBoolean, IsEnum, IsOptional, IsString } from 'class-validator';
|
||||
import { OrderStatus } from '@prisma/client';
|
||||
|
||||
/**
|
||||
* Body of legacy `POST single/order-sts/edit`.
|
||||
*
|
||||
* Legacy posted the whole timeline row back, so it carried `status`, an optional
|
||||
* `note`, and `remove: true` when the operator was deleting a status instead of
|
||||
* setting one. The order id travelled on the row as `order_id`/`orderId`.
|
||||
*/
|
||||
export class UpdateOrderStatusDto {
|
||||
@IsString()
|
||||
orderId!: string;
|
||||
|
||||
@IsEnum(OrderStatus)
|
||||
status!: OrderStatus;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
note?: string;
|
||||
|
||||
/** legacy: row.remove = true -> delete this status event rather than add one */
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
remove?: boolean;
|
||||
}
|
||||
Reference in New Issue
Block a user