Rebuild order flow: settle after pricing, notifications, and invoice expiry.
Hide prices until admin sends the invoice, enforce status transitions, add customer/admin SMS+in-app notifications, schedule cash invoice reminders, and ship updated dist/prisma-client/deploy-bundle for git-pull deploys. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Vendored
+132
-72
@@ -17,6 +17,10 @@ const order_number_1 = require("../common/utils/order-number");
|
||||
const check_rass_1 = require("../common/utils/check-rass");
|
||||
const cash_expiry_1 = require("../common/utils/cash-expiry");
|
||||
const order_status_1 = require("../common/utils/order-status");
|
||||
const order_transitions_1 = require("../common/utils/order-transitions");
|
||||
const meterage_1 = require("../common/utils/meterage");
|
||||
const notifications_service_1 = require("../notifications/notifications.service");
|
||||
const notification_events_1 = require("../notifications/notification-events");
|
||||
const name_1 = require("../common/utils/name");
|
||||
const phone_1 = require("../common/utils/phone");
|
||||
const orderInclude = {
|
||||
@@ -32,19 +36,21 @@ function roundMoney(value) {
|
||||
return Math.round(value * 100) / 100;
|
||||
}
|
||||
const CHECK_EDITABLE_STATUSES = [
|
||||
client_1.OrderStatus.awaiting_review,
|
||||
client_1.OrderStatus.pricing,
|
||||
client_1.OrderStatus.needs_correction,
|
||||
client_1.OrderStatus.awaiting_settlement_fix,
|
||||
client_1.OrderStatus.check_in_progress,
|
||||
client_1.OrderStatus.awaiting_check_review,
|
||||
client_1.OrderStatus.check_needs_fix,
|
||||
client_1.OrderStatus.check_rejected,
|
||||
];
|
||||
const PRICE_HIDDEN_STATUSES = [
|
||||
client_1.OrderStatus.awaiting_review,
|
||||
client_1.OrderStatus.pricing,
|
||||
];
|
||||
let OrdersService = class OrdersService {
|
||||
prisma;
|
||||
constructor(prisma) {
|
||||
notifications;
|
||||
constructor(prisma, notifications) {
|
||||
this.prisma = prisma;
|
||||
this.notifications = notifications;
|
||||
}
|
||||
async findMyOrders(userId) {
|
||||
const orders = await this.prisma.order.findMany({
|
||||
@@ -95,12 +101,17 @@ let OrdersService = class OrdersService {
|
||||
return this.mapOrder(order, true);
|
||||
}
|
||||
async submitOrder(userId, dto) {
|
||||
const company = await this.prisma.company.findUnique({
|
||||
where: { id: dto.companyId },
|
||||
});
|
||||
const [company, settings] = await Promise.all([
|
||||
this.prisma.company.findUnique({ where: { id: dto.companyId } }),
|
||||
this.prisma.systemSettings.findUnique({
|
||||
where: { id: 'default' },
|
||||
select: { maxMeteragePerInvoice: true },
|
||||
}),
|
||||
]);
|
||||
if (!company || !company.isActive) {
|
||||
throw new common_1.NotFoundException('شرکت یافت نشد یا غیرفعال است');
|
||||
}
|
||||
const maxMeterage = (0, meterage_1.resolveMaxMeterage)(company.maxMeteragePerInvoice, settings?.maxMeteragePerInvoice);
|
||||
const productIds = dto.items.map((i) => i.productId);
|
||||
const products = await this.prisma.product.findMany({
|
||||
where: { id: { in: productIds }, companyId: company.id, isActive: true },
|
||||
@@ -128,31 +139,12 @@ let OrdersService = class OrdersService {
|
||||
productCode: product.code,
|
||||
color: product.color,
|
||||
requestedMeterage: item.meterage,
|
||||
unitPrice: product.basePrice,
|
||||
notes: item.notes?.trim() || null,
|
||||
};
|
||||
});
|
||||
if (totalMeterage > company.maxMeteragePerInvoice) {
|
||||
throw new common_1.BadRequestException(`حداکثر متراژ مجاز هر فاکتور ${company.maxMeteragePerInvoice} متر است`);
|
||||
if (totalMeterage > maxMeterage) {
|
||||
throw new common_1.BadRequestException(`حداکثر متراژ مجاز هر فاکتور ${maxMeterage} متر است`);
|
||||
}
|
||||
const settlementType = dto.settlementType;
|
||||
if (settlementType === client_1.SettlementType.check) {
|
||||
if (!dto.checks?.length) {
|
||||
throw new common_1.BadRequestException('برای تسویه چکی حداقل یک چک لازم است');
|
||||
}
|
||||
}
|
||||
const checkMode = settlementType === client_1.SettlementType.check
|
||||
? (dto.checkMode ?? client_1.CheckRegistrationMode.full)
|
||||
: null;
|
||||
const rass = settlementType === client_1.SettlementType.check && dto.checks?.length
|
||||
? (0, check_rass_1.calculateCheckRass)(dto.checks.map((c) => ({
|
||||
amount: c.amount,
|
||||
dueDate: c.dueDate,
|
||||
})))
|
||||
: null;
|
||||
const cashInvoiceExpiresAt = settlementType === client_1.SettlementType.cash
|
||||
? (0, cash_expiry_1.computeCashInvoiceExpiry)(company)
|
||||
: null;
|
||||
const order = await this.createWithUniqueOrderNumber((orderNumber) => this.prisma.$transaction(async (tx) => {
|
||||
const created = await tx.order.create({
|
||||
data: {
|
||||
@@ -160,33 +152,19 @@ let OrdersService = class OrdersService {
|
||||
userId,
|
||||
companyId: company.id,
|
||||
companyName: company.name,
|
||||
settlementType,
|
||||
checkMode,
|
||||
settlementType: null,
|
||||
checkMode: null,
|
||||
status: client_1.OrderStatus.awaiting_review,
|
||||
totalMeterage,
|
||||
notes: dto.notes?.trim() || null,
|
||||
checkRassDays: rass?.rassDays ?? null,
|
||||
checkRassDate: rass?.rassDate ? new Date(rass.rassDate) : null,
|
||||
cashInvoiceExpiresAt,
|
||||
items: { create: itemsData },
|
||||
checks: settlementType === client_1.SettlementType.check && dto.checks
|
||||
? {
|
||||
create: dto.checks.map((c, index) => this.mapCheckCreate(c, index + 1, checkMode)),
|
||||
}
|
||||
: undefined,
|
||||
statusHistory: {
|
||||
create: [
|
||||
{
|
||||
fromStatus: null,
|
||||
toStatus: client_1.OrderStatus.submitted,
|
||||
changedBy: 'customer',
|
||||
note: 'سفارش ثبت شد',
|
||||
},
|
||||
{
|
||||
fromStatus: client_1.OrderStatus.submitted,
|
||||
toStatus: client_1.OrderStatus.awaiting_review,
|
||||
changedBy: 'customer',
|
||||
note: 'ارسال برای بررسی مدیریت',
|
||||
note: 'سفارش ثبت شد و برای بررسی مدیریت ارسال شد',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -201,13 +179,57 @@ let OrdersService = class OrdersService {
|
||||
}
|
||||
return created;
|
||||
}));
|
||||
this.notifications.dispatchOrderEvent(order.id, 'order_submitted');
|
||||
this.notifications.dispatchAdmin({
|
||||
title: 'سفارش جدید ثبت شد',
|
||||
body: `سفارش ${order.orderNumber} با ${order.items.length} قلم و ${order.totalMeterage} متر در انتظار قیمتگذاری است.`,
|
||||
link: `/orders/${order.id}`,
|
||||
});
|
||||
return this.mapOrder(order);
|
||||
}
|
||||
async adminCreate(adminId, dto) {
|
||||
const settlementType = dto.settlementType ?? dto.settlementMethod;
|
||||
if (!settlementType) {
|
||||
throw new common_1.BadRequestException('نوع تسویه الزامی است');
|
||||
async chooseSettlement(userId, id, dto) {
|
||||
const order = await this.getOrder(id);
|
||||
if (order.userId !== userId) {
|
||||
throw new common_1.ForbiddenException('دسترسی به این سفارش مجاز نیست');
|
||||
}
|
||||
if (order.status !== client_1.OrderStatus.awaiting_settlement) {
|
||||
throw new common_1.BadRequestException('انتخاب روش تسویه فقط پس از صدور فاکتور توسط مدیریت امکانپذیر است');
|
||||
}
|
||||
const isCash = dto.settlementType === client_1.SettlementType.cash;
|
||||
const nextStatus = isCash
|
||||
? client_1.OrderStatus.awaiting_cash_payment
|
||||
: client_1.OrderStatus.check_in_progress;
|
||||
(0, order_transitions_1.assertTransition)(order.status, nextStatus);
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
await tx.order.update({
|
||||
where: { id },
|
||||
data: {
|
||||
settlementType: dto.settlementType,
|
||||
checkMode: isCash
|
||||
? null
|
||||
: (dto.checkMode ?? client_1.CheckRegistrationMode.full),
|
||||
cashInvoiceExpiresAt: isCash
|
||||
? (0, cash_expiry_1.computeCashInvoiceExpiry)(order.company)
|
||||
: null,
|
||||
cashReminderSentAt: null,
|
||||
status: nextStatus,
|
||||
},
|
||||
});
|
||||
await tx.orderStatusHistory.create({
|
||||
data: {
|
||||
orderId: id,
|
||||
fromStatus: order.status,
|
||||
toStatus: nextStatus,
|
||||
changedBy: 'customer',
|
||||
note: isCash
|
||||
? 'مشتری تسویه نقدی را انتخاب کرد'
|
||||
: 'مشتری تسویه چکی را انتخاب کرد',
|
||||
},
|
||||
});
|
||||
});
|
||||
return this.findMyOrder(userId, id);
|
||||
}
|
||||
async adminCreate(adminId, dto) {
|
||||
const company = await this.prisma.company.findUnique({
|
||||
where: { id: dto.companyId },
|
||||
});
|
||||
@@ -245,7 +267,7 @@ let OrdersService = class OrdersService {
|
||||
userId: ownerId,
|
||||
companyId: company.id,
|
||||
companyName: company.name,
|
||||
settlementType,
|
||||
settlementType: null,
|
||||
status: client_1.OrderStatus.awaiting_review,
|
||||
totalMeterage,
|
||||
notes: dto.notes?.trim() || null,
|
||||
@@ -338,6 +360,18 @@ let OrdersService = class OrdersService {
|
||||
if (order.status === dto.status) {
|
||||
return this.mapOrder(order, true);
|
||||
}
|
||||
if (dto.force) {
|
||||
if (!dto.reason?.trim()) {
|
||||
throw new common_1.BadRequestException('برای تغییر دستی وضعیت، ذکر دلیل الزامی است');
|
||||
}
|
||||
}
|
||||
else {
|
||||
(0, order_transitions_1.assertTransition)(order.status, dto.status);
|
||||
}
|
||||
if (dto.status === client_1.OrderStatus.awaiting_settlement &&
|
||||
(order.finalAmount ?? 0) <= 0) {
|
||||
throw new common_1.BadRequestException('ابتدا سفارش را قیمتگذاری کنید تا فاکتور قابل ارسال باشد');
|
||||
}
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
await tx.order.update({
|
||||
where: { id },
|
||||
@@ -349,7 +383,9 @@ let OrdersService = class OrdersService {
|
||||
fromStatus: order.status,
|
||||
toStatus: dto.status,
|
||||
changedBy: `admin:${adminId}`,
|
||||
note: dto.note || null,
|
||||
note: dto.force
|
||||
? `تغییر دستی وضعیت${dto.note ? ` — ${dto.note}` : ''}`
|
||||
: dto.note || null,
|
||||
reason: dto.reason || null,
|
||||
},
|
||||
});
|
||||
@@ -365,6 +401,12 @@ let OrdersService = class OrdersService {
|
||||
}
|
||||
}
|
||||
});
|
||||
const event = (0, notification_events_1.eventForStatus)(dto.status);
|
||||
if (event) {
|
||||
this.notifications.dispatchOrderEvent(id, event, {
|
||||
note: dto.reason ?? dto.note ?? null,
|
||||
});
|
||||
}
|
||||
return this.findAdminOrder(id);
|
||||
}
|
||||
async updateChecks(userId, id, dto) {
|
||||
@@ -378,8 +420,18 @@ let OrdersService = class OrdersService {
|
||||
if (!CHECK_EDITABLE_STATUSES.includes(order.status)) {
|
||||
throw new common_1.BadRequestException('در وضعیت فعلی سفارش امکان ویرایش چکها وجود ندارد');
|
||||
}
|
||||
if (!dto.checks.length) {
|
||||
throw new common_1.BadRequestException('حداقل یک چک لازم است');
|
||||
}
|
||||
const checkMode = dto.checkMode ?? order.checkMode ?? client_1.CheckRegistrationMode.full;
|
||||
const rass = (0, check_rass_1.calculateCheckRass)(dto.checks.map((c) => ({ amount: c.amount, dueDate: c.dueDate })), order.createdAt);
|
||||
const goesToReview = order.status === client_1.OrderStatus.check_in_progress ||
|
||||
order.status === client_1.OrderStatus.check_needs_fix ||
|
||||
order.status === client_1.OrderStatus.check_rejected;
|
||||
const nextStatus = goesToReview
|
||||
? client_1.OrderStatus.awaiting_check_review
|
||||
: order.status;
|
||||
const isFix = order.status !== client_1.OrderStatus.check_in_progress;
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
await tx.check.deleteMany({ where: { orderId: id } });
|
||||
await tx.check.createMany({
|
||||
@@ -394,23 +446,30 @@ let OrdersService = class OrdersService {
|
||||
checkMode,
|
||||
checkRassDays: rass.rassDays,
|
||||
checkRassDate: rass.rassDate ? new Date(rass.rassDate) : null,
|
||||
status: order.status === client_1.OrderStatus.check_needs_fix
|
||||
? client_1.OrderStatus.awaiting_check_review
|
||||
: order.status,
|
||||
status: nextStatus,
|
||||
},
|
||||
});
|
||||
if (order.status === client_1.OrderStatus.check_needs_fix) {
|
||||
if (nextStatus !== order.status) {
|
||||
await tx.orderStatusHistory.create({
|
||||
data: {
|
||||
orderId: id,
|
||||
fromStatus: order.status,
|
||||
toStatus: client_1.OrderStatus.awaiting_check_review,
|
||||
toStatus: nextStatus,
|
||||
changedBy: 'customer',
|
||||
note: 'چکها اصلاح و مجدداً ارسال شد',
|
||||
note: isFix
|
||||
? 'چکها اصلاح و مجدداً ارسال شد'
|
||||
: 'چکها برای بررسی ارسال شد',
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
if (nextStatus !== order.status) {
|
||||
this.notifications.dispatchAdmin({
|
||||
title: isFix ? 'چکهای اصلاحشده رسید' : 'چکهای سفارش ثبت شد',
|
||||
body: `مشتری ${dto.checks.length} چک برای سفارش ${order.orderNumber} ثبت کرد و منتظر بررسی است.`,
|
||||
link: `/orders/${id}`,
|
||||
});
|
||||
}
|
||||
return this.findMyOrder(userId, id);
|
||||
}
|
||||
mapCheckCreate(c, row, defaultMode) {
|
||||
@@ -467,6 +526,7 @@ let OrdersService = class OrdersService {
|
||||
}
|
||||
mapOrder(order, adminView = false) {
|
||||
const customerName = (0, name_1.joinFullName)(order.user.firstName, order.user.lastName);
|
||||
const hidePrices = !adminView && PRICE_HIDDEN_STATUSES.includes(order.status);
|
||||
const base = {
|
||||
id: order.id,
|
||||
orderNumber: order.orderNumber,
|
||||
@@ -478,12 +538,14 @@ let OrdersService = class OrdersService {
|
||||
checkMode: order.checkMode ?? undefined,
|
||||
status: order.status,
|
||||
adminStatus: (0, order_status_1.toAdminStatus)(order.status),
|
||||
allowedTransitions: adminView ? (0, order_transitions_1.allowedTransitions)(order.status) : [],
|
||||
totalMeterage: order.totalMeterage,
|
||||
meterage: order.totalMeterage,
|
||||
totalAmount: order.totalAmount,
|
||||
shippingCost: order.shippingCost,
|
||||
totalDiscount: order.totalDiscount,
|
||||
finalAmount: order.finalAmount,
|
||||
totalAmount: hidePrices ? null : order.totalAmount,
|
||||
shippingCost: hidePrices ? null : order.shippingCost,
|
||||
totalDiscount: hidePrices ? null : order.totalDiscount,
|
||||
finalAmount: hidePrices ? null : order.finalAmount,
|
||||
pricesVisible: !hidePrices,
|
||||
notes: order.notes ?? undefined,
|
||||
checkRassDays: order.checkRassDays ?? undefined,
|
||||
checkRassDate: order.checkRassDate?.toISOString(),
|
||||
@@ -503,12 +565,12 @@ let OrdersService = class OrdersService {
|
||||
color: item.color,
|
||||
requestedMeterage: item.requestedMeterage,
|
||||
confirmedMeterage: item.confirmedMeterage,
|
||||
unitPrice: item.unitPrice,
|
||||
supplyPrice: item.unitPrice,
|
||||
unitDiscount: item.unitDiscount ?? 0,
|
||||
discount: item.unitDiscount ?? 0,
|
||||
lineTotal: item.lineTotal,
|
||||
totalAmount: item.lineTotal,
|
||||
unitPrice: hidePrices ? null : item.unitPrice,
|
||||
supplyPrice: hidePrices ? null : item.unitPrice,
|
||||
unitDiscount: hidePrices ? null : (item.unitDiscount ?? 0),
|
||||
discount: hidePrices ? null : (item.unitDiscount ?? 0),
|
||||
lineTotal: hidePrices ? null : item.lineTotal,
|
||||
totalAmount: hidePrices ? null : item.lineTotal,
|
||||
notes: item.notes ?? undefined,
|
||||
})),
|
||||
checks: order.checks.map((c) => ({
|
||||
@@ -573,15 +635,13 @@ let OrdersService = class OrdersService {
|
||||
changedAt: h.changedAt.toISOString(),
|
||||
})),
|
||||
};
|
||||
if (adminView) {
|
||||
return base;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
};
|
||||
exports.OrdersService = OrdersService;
|
||||
exports.OrdersService = OrdersService = __decorate([
|
||||
(0, common_1.Injectable)(),
|
||||
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
|
||||
__metadata("design:paramtypes", [prisma_service_1.PrismaService,
|
||||
notifications_service_1.NotificationsService])
|
||||
], OrdersService);
|
||||
//# sourceMappingURL=orders.service.js.map
|
||||
Reference in New Issue
Block a user