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:
2026-08-09 12:00:13 +03:30
co-authored by Cursor
parent 06e5f48d07
commit d4aaa8e997
102 changed files with 2989 additions and 509 deletions
Binary file not shown.
+4
View File
@@ -10,6 +10,7 @@ exports.AppModule = void 0;
const common_1 = require("@nestjs/common");
const core_1 = require("@nestjs/core");
const config_1 = require("@nestjs/config");
const schedule_1 = require("@nestjs/schedule");
const prisma_module_1 = require("./prisma/prisma.module");
const auth_module_1 = require("./auth/auth.module");
const jwt_auth_guard_1 = require("./common/guards/jwt-auth.guard");
@@ -25,6 +26,7 @@ const shipments_module_1 = require("./shipments/shipments.module");
const dashboard_module_1 = require("./dashboard/dashboard.module");
const settings_module_1 = require("./settings/settings.module");
const carts_module_1 = require("./carts/carts.module");
const notifications_module_1 = require("./notifications/notifications.module");
let AppModule = class AppModule {
};
exports.AppModule = AppModule;
@@ -32,6 +34,7 @@ exports.AppModule = AppModule = __decorate([
(0, common_1.Module)({
imports: [
config_1.ConfigModule.forRoot({ isGlobal: true }),
schedule_1.ScheduleModule.forRoot(),
prisma_module_1.PrismaModule,
auth_module_1.AuthModule,
companies_module_1.CompaniesModule,
@@ -46,6 +49,7 @@ exports.AppModule = AppModule = __decorate([
dashboard_module_1.DashboardModule,
settings_module_1.SettingsModule,
carts_module_1.CartsModule,
notifications_module_1.NotificationsModule,
],
providers: [
{
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"app.module.js","sourceRoot":"","sources":["../src/app.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,uCAAyC;AACzC,2CAA8C;AAC9C,0DAAsD;AACtD,oDAAgD;AAChD,mEAA8D;AAC9D,mEAA+D;AAC/D,gEAA4D;AAC5D,gEAA4D;AAC5D,mEAA+D;AAC/D,yEAAoE;AACpE,0DAAsD;AACtD,0DAAsD;AACtD,+EAA0E;AAC1E,mEAA+D;AAC/D,mEAA+D;AAC/D,gEAA4D;AAC5D,uDAAmD;AA2B5C,IAAM,SAAS,GAAf,MAAM,SAAS;CAAG,CAAA;AAAZ,8BAAS;oBAAT,SAAS;IAzBrB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE;YACP,qBAAY,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;YACxC,4BAAY;YACZ,wBAAU;YACV,kCAAe;YACf,gCAAc;YACd,gCAAc;YACd,kCAAe;YACf,qCAAgB;YAChB,4BAAY;YACZ,4BAAY;YACZ,yCAAkB;YAClB,kCAAe;YACf,kCAAe;YACf,gCAAc;YACd,0BAAW;SACZ;QACD,SAAS,EAAE;YACT;gBACE,OAAO,EAAE,gBAAS;gBAClB,QAAQ,EAAE,6BAAY;aACvB;SACF;KACF,CAAC;GACW,SAAS,CAAG"}
{"version":3,"file":"app.module.js","sourceRoot":"","sources":["../src/app.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,uCAAyC;AACzC,2CAA8C;AAC9C,+CAAkD;AAClD,0DAAsD;AACtD,oDAAgD;AAChD,mEAA8D;AAC9D,mEAA+D;AAC/D,gEAA4D;AAC5D,gEAA4D;AAC5D,mEAA+D;AAC/D,yEAAoE;AACpE,0DAAsD;AACtD,0DAAsD;AACtD,+EAA0E;AAC1E,mEAA+D;AAC/D,mEAA+D;AAC/D,gEAA4D;AAC5D,uDAAmD;AACnD,+EAA2E;AA6BpE,IAAM,SAAS,GAAf,MAAM,SAAS;CAAG,CAAA;AAAZ,8BAAS;oBAAT,SAAS;IA3BrB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE;YACP,qBAAY,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;YACxC,yBAAc,CAAC,OAAO,EAAE;YACxB,4BAAY;YACZ,wBAAU;YACV,kCAAe;YACf,gCAAc;YACd,gCAAc;YACd,kCAAe;YACf,qCAAgB;YAChB,4BAAY;YACZ,4BAAY;YACZ,yCAAkB;YAClB,kCAAe;YACf,kCAAe;YACf,gCAAc;YACd,0BAAW;YACX,0CAAmB;SACpB;QACD,SAAS,EAAE;YACT;gBACE,OAAO,EAAE,gBAAS;gBAClB,QAAQ,EAAE,6BAAY;aACvB;SACF;KACF,CAAC;GACW,SAAS,CAAG"}
-6
View File
@@ -18,7 +18,6 @@ export declare class CartsController {
productCode: string;
color: string;
meterage: number;
unitPrice: number | undefined;
notes: string | undefined;
companyId: string;
companyName: string;
@@ -42,7 +41,6 @@ export declare class CartsController {
productCode: string;
color: string;
meterage: number;
unitPrice: number | undefined;
notes: string | undefined;
companyId: string;
companyName: string;
@@ -66,7 +64,6 @@ export declare class CartsController {
productCode: string;
color: string;
meterage: number;
unitPrice: number | undefined;
notes: string | undefined;
companyId: string;
companyName: string;
@@ -90,7 +87,6 @@ export declare class CartsController {
productCode: string;
color: string;
meterage: number;
unitPrice: number | undefined;
notes: string | undefined;
companyId: string;
companyName: string;
@@ -114,7 +110,6 @@ export declare class CartsController {
productCode: string;
color: string;
meterage: number;
unitPrice: number | undefined;
notes: string | undefined;
companyId: string;
companyName: string;
@@ -138,7 +133,6 @@ export declare class CartsController {
productCode: string;
color: string;
meterage: number;
unitPrice: number | undefined;
notes: string | undefined;
companyId: string;
companyName: string;
-6
View File
@@ -17,7 +17,6 @@ export declare class CartsService {
productCode: string;
color: string;
meterage: number;
unitPrice: number | undefined;
notes: string | undefined;
companyId: string;
companyName: string;
@@ -41,7 +40,6 @@ export declare class CartsService {
productCode: string;
color: string;
meterage: number;
unitPrice: number | undefined;
notes: string | undefined;
companyId: string;
companyName: string;
@@ -65,7 +63,6 @@ export declare class CartsService {
productCode: string;
color: string;
meterage: number;
unitPrice: number | undefined;
notes: string | undefined;
companyId: string;
companyName: string;
@@ -89,7 +86,6 @@ export declare class CartsService {
productCode: string;
color: string;
meterage: number;
unitPrice: number | undefined;
notes: string | undefined;
companyId: string;
companyName: string;
@@ -113,7 +109,6 @@ export declare class CartsService {
productCode: string;
color: string;
meterage: number;
unitPrice: number | undefined;
notes: string | undefined;
companyId: string;
companyName: string;
@@ -137,7 +132,6 @@ export declare class CartsService {
productCode: string;
color: string;
meterage: number;
unitPrice: number | undefined;
notes: string | undefined;
companyId: string;
companyName: string;
-3
View File
@@ -47,7 +47,6 @@ let CartsService = class CartsService {
data: {
meterage: newMeterage,
notes: dto.notes?.trim() || existing.notes,
unitPrice: product.basePrice,
},
});
}
@@ -61,7 +60,6 @@ let CartsService = class CartsService {
productCode: product.code,
color: product.color,
meterage: dto.meterage,
unitPrice: product.basePrice,
notes: dto.notes?.trim() || null,
companyId: product.companyId,
companyName: product.company.name,
@@ -168,7 +166,6 @@ let CartsService = class CartsService {
productCode: item.productCode,
color: item.color,
meterage: item.meterage,
unitPrice: item.unitPrice ?? undefined,
notes: item.notes ?? undefined,
companyId: item.companyId,
companyName: item.companyName,
+1 -1
View File
File diff suppressed because one or more lines are too long
+2
View File
@@ -10,11 +10,13 @@ exports.CashPaymentsModule = void 0;
const common_1 = require("@nestjs/common");
const cash_payments_controller_1 = require("./cash-payments.controller");
const cash_payments_service_1 = require("./cash-payments.service");
const notifications_module_1 = require("../notifications/notifications.module");
let CashPaymentsModule = class CashPaymentsModule {
};
exports.CashPaymentsModule = CashPaymentsModule;
exports.CashPaymentsModule = CashPaymentsModule = __decorate([
(0, common_1.Module)({
imports: [notifications_module_1.NotificationsModule],
controllers: [cash_payments_controller_1.CashPaymentsController],
providers: [cash_payments_service_1.CashPaymentsService],
exports: [cash_payments_service_1.CashPaymentsService],
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"cash-payments.module.js","sourceRoot":"","sources":["../../src/cash-payments/cash-payments.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,yEAAoE;AACpE,mEAA8D;AAOvD,IAAM,kBAAkB,GAAxB,MAAM,kBAAkB;CAAG,CAAA;AAArB,gDAAkB;6BAAlB,kBAAkB;IAL9B,IAAA,eAAM,EAAC;QACN,WAAW,EAAE,CAAC,iDAAsB,CAAC;QACrC,SAAS,EAAE,CAAC,2CAAmB,CAAC;QAChC,OAAO,EAAE,CAAC,2CAAmB,CAAC;KAC/B,CAAC;GACW,kBAAkB,CAAG"}
{"version":3,"file":"cash-payments.module.js","sourceRoot":"","sources":["../../src/cash-payments/cash-payments.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,yEAAoE;AACpE,mEAA8D;AAC9D,gFAA4E;AAQrE,IAAM,kBAAkB,GAAxB,MAAM,kBAAkB;CAAG,CAAA;AAArB,gDAAkB;6BAAlB,kBAAkB;IAN9B,IAAA,eAAM,EAAC;QACN,OAAO,EAAE,CAAC,0CAAmB,CAAC;QAC9B,WAAW,EAAE,CAAC,iDAAsB,CAAC;QACrC,SAAS,EAAE,CAAC,2CAAmB,CAAC;QAChC,OAAO,EAAE,CAAC,2CAAmB,CAAC;KAC/B,CAAC;GACW,kBAAkB,CAAG"}
+3 -1
View File
@@ -1,9 +1,11 @@
import { CashPaymentStatus } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { SubmitCashReceiptDto, UpdateCashPaymentStatusDto } from './dto/cash-payment.dto';
import { NotificationsService } from '../notifications/notifications.service';
export declare class CashPaymentsService {
private readonly prisma;
constructor(prisma: PrismaService);
private readonly notifications;
constructor(prisma: PrismaService, notifications: NotificationsService);
submitReceipt(userId: string, orderId: string, dto: SubmitCashReceiptDto): Promise<{
id: string;
orderId: string;
+21 -8
View File
@@ -14,12 +14,8 @@ const common_1 = require("@nestjs/common");
const client_1 = require("@prisma/client");
const prisma_service_1 = require("../prisma/prisma.service");
const name_1 = require("../common/utils/name");
const notifications_service_1 = require("../notifications/notifications.service");
const RECEIPT_SUBMITTABLE_STATUSES = [
client_1.OrderStatus.awaiting_review,
client_1.OrderStatus.pricing,
client_1.OrderStatus.needs_correction,
client_1.OrderStatus.awaiting_settlement,
client_1.OrderStatus.awaiting_settlement_fix,
client_1.OrderStatus.cash_in_progress,
client_1.OrderStatus.cash_invoice_issued,
client_1.OrderStatus.awaiting_cash_payment,
@@ -28,8 +24,10 @@ const RECEIPT_SUBMITTABLE_STATUSES = [
];
let CashPaymentsService = class CashPaymentsService {
prisma;
constructor(prisma) {
notifications;
constructor(prisma, notifications) {
this.prisma = prisma;
this.notifications = notifications;
}
async submitReceipt(userId, orderId, dto) {
const order = await this.prisma.order.findUnique({
@@ -98,6 +96,11 @@ let CashPaymentsService = class CashPaymentsService {
});
return saved;
});
this.notifications.dispatchAdmin({
title: 'رسید پرداخت نقدی ثبت شد',
body: `مشتری برای سفارش ${order.orderNumber} رسید به مبلغ ${Math.round(dto.amount).toLocaleString('fa-IR')} تومان ثبت کرد.`,
link: `/orders/${orderId}`,
});
return this.mapReceipt(receipt, order.orderNumber, '');
}
async findAll(status) {
@@ -135,7 +138,7 @@ let CashPaymentsService = class CashPaymentsService {
nextStatus = client_1.OrderStatus.cash_confirmed;
}
else if (dto.status === client_1.CashPaymentStatus.rejected) {
nextStatus = client_1.OrderStatus.needs_correction;
nextStatus = client_1.OrderStatus.awaiting_cash_payment;
}
else if (dto.status === client_1.CashPaymentStatus.expired) {
nextStatus = client_1.OrderStatus.expired;
@@ -157,6 +160,15 @@ let CashPaymentsService = class CashPaymentsService {
}
return result;
});
if (dto.status === client_1.CashPaymentStatus.approved) {
this.notifications.dispatchOrderEvent(receipt.orderId, 'receipt_approved');
}
else if (dto.status === client_1.CashPaymentStatus.rejected) {
this.notifications.dispatchOrderEvent(receipt.orderId, 'receipt_rejected', { note: dto.note ?? null });
}
else if (dto.status === client_1.CashPaymentStatus.expired) {
this.notifications.dispatchOrderEvent(receipt.orderId, 'cash_invoice_expired');
}
return this.mapReceipt(updated, updated.order.orderNumber, (0, name_1.joinFullName)(updated.order.user.firstName, updated.order.user.lastName));
}
mapReceipt(r, orderNumber, customerName) {
@@ -185,6 +197,7 @@ let CashPaymentsService = class CashPaymentsService {
exports.CashPaymentsService = CashPaymentsService;
exports.CashPaymentsService = CashPaymentsService = __decorate([
(0, common_1.Injectable)(),
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
__metadata("design:paramtypes", [prisma_service_1.PrismaService,
notifications_service_1.NotificationsService])
], CashPaymentsService);
//# sourceMappingURL=cash-payments.service.js.map
File diff suppressed because one or more lines are too long
+2
View File
@@ -10,11 +10,13 @@ exports.ChecksModule = void 0;
const common_1 = require("@nestjs/common");
const checks_controller_1 = require("./checks.controller");
const checks_service_1 = require("./checks.service");
const notifications_module_1 = require("../notifications/notifications.module");
let ChecksModule = class ChecksModule {
};
exports.ChecksModule = ChecksModule;
exports.ChecksModule = ChecksModule = __decorate([
(0, common_1.Module)({
imports: [notifications_module_1.NotificationsModule],
controllers: [checks_controller_1.ChecksController],
providers: [checks_service_1.ChecksService],
exports: [checks_service_1.ChecksService],
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"checks.module.js","sourceRoot":"","sources":["../../src/checks/checks.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,2DAAuD;AACvD,qDAAiD;AAO1C,IAAM,YAAY,GAAlB,MAAM,YAAY;CAAG,CAAA;AAAf,oCAAY;uBAAZ,YAAY;IALxB,IAAA,eAAM,EAAC;QACN,WAAW,EAAE,CAAC,oCAAgB,CAAC;QAC/B,SAAS,EAAE,CAAC,8BAAa,CAAC;QAC1B,OAAO,EAAE,CAAC,8BAAa,CAAC;KACzB,CAAC;GACW,YAAY,CAAG"}
{"version":3,"file":"checks.module.js","sourceRoot":"","sources":["../../src/checks/checks.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,2DAAuD;AACvD,qDAAiD;AACjD,gFAA4E;AAQrE,IAAM,YAAY,GAAlB,MAAM,YAAY;CAAG,CAAA;AAAf,oCAAY;uBAAZ,YAAY;IANxB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE,CAAC,0CAAmB,CAAC;QAC9B,WAAW,EAAE,CAAC,oCAAgB,CAAC;QAC/B,SAAS,EAAE,CAAC,8BAAa,CAAC;QAC1B,OAAO,EAAE,CAAC,8BAAa,CAAC;KACzB,CAAC;GACW,YAAY,CAAG"}
+3 -1
View File
@@ -1,8 +1,10 @@
import { PrismaService } from '../prisma/prisma.service';
import { UpdateCheckStatusDto } from './dto/check.dto';
import { NotificationsService } from '../notifications/notifications.service';
export declare class ChecksService {
private readonly prisma;
constructor(prisma: PrismaService);
private readonly notifications;
constructor(prisma: PrismaService, notifications: NotificationsService);
findByOrder(orderId: string): Promise<{
id: string;
orderId: string;
+17 -6
View File
@@ -13,10 +13,13 @@ exports.ChecksService = void 0;
const common_1 = require("@nestjs/common");
const client_1 = require("@prisma/client");
const prisma_service_1 = require("../prisma/prisma.service");
const notifications_service_1 = require("../notifications/notifications.service");
let ChecksService = class ChecksService {
prisma;
constructor(prisma) {
notifications;
constructor(prisma, notifications) {
this.prisma = prisma;
this.notifications = notifications;
}
async findByOrder(orderId) {
const order = await this.prisma.order.findUnique({
@@ -37,6 +40,7 @@ let ChecksService = class ChecksService {
});
if (!check)
throw new common_1.NotFoundException('چک یافت نشد');
let notifyStatus = null;
const updated = await this.prisma.$transaction(async (tx) => {
const result = await tx.check.update({
where: { id },
@@ -54,14 +58,11 @@ let ChecksService = class ChecksService {
const anyRejected = siblings.some((c) => c.status === client_1.CheckStatus.rejected);
let nextStatus = null;
if (anyRejected) {
nextStatus = client_1.OrderStatus.check_rejected;
nextStatus = client_1.OrderStatus.check_needs_fix;
}
else if (allApproved) {
nextStatus = client_1.OrderStatus.checks_approved;
}
else if (dto.status === client_1.CheckStatus.rejected) {
nextStatus = client_1.OrderStatus.check_needs_fix;
}
if (nextStatus && nextStatus !== check.order.status) {
await tx.order.update({
where: { id: check.orderId },
@@ -76,9 +77,18 @@ let ChecksService = class ChecksService {
note: dto.note || `وضعیت چک به ${dto.status} تغییر کرد`,
},
});
notifyStatus = nextStatus;
}
return result;
});
if (notifyStatus === client_1.OrderStatus.checks_approved) {
this.notifications.dispatchOrderEvent(check.orderId, 'checks_approved');
}
else if (notifyStatus === client_1.OrderStatus.check_needs_fix) {
this.notifications.dispatchOrderEvent(check.orderId, 'checks_need_fix', {
note: dto.note ?? null,
});
}
return this.map(updated);
}
map(c) {
@@ -108,6 +118,7 @@ let ChecksService = class ChecksService {
exports.ChecksService = ChecksService;
exports.ChecksService = ChecksService = __decorate([
(0, common_1.Injectable)(),
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
__metadata("design:paramtypes", [prisma_service_1.PrismaService,
notifications_service_1.NotificationsService])
], ChecksService);
//# sourceMappingURL=checks.service.js.map
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"checks.service.js","sourceRoot":"","sources":["../../src/checks/checks.service.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"checks.service.js","sourceRoot":"","sources":["../../src/checks/checks.service.ts"],"names":[],"mappings":"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"}
+2
View File
@@ -0,0 +1,2 @@
export declare const FALLBACK_MAX_METERAGE = 5000;
export declare function resolveMaxMeterage(companyValue: number | null | undefined, settingsValue: number | null | undefined): number;
+13
View File
@@ -0,0 +1,13 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FALLBACK_MAX_METERAGE = void 0;
exports.resolveMaxMeterage = resolveMaxMeterage;
exports.FALLBACK_MAX_METERAGE = 5000;
function resolveMaxMeterage(companyValue, settingsValue) {
if (companyValue != null && companyValue > 0)
return companyValue;
if (settingsValue != null && settingsValue > 0)
return settingsValue;
return exports.FALLBACK_MAX_METERAGE;
}
//# sourceMappingURL=meterage.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"meterage.js","sourceRoot":"","sources":["../../../src/common/utils/meterage.ts"],"names":[],"mappings":";;;AAMA,gDAOC;AAZY,QAAA,qBAAqB,GAAG,IAAI,CAAC;AAK1C,SAAgB,kBAAkB,CAChC,YAAuC,EACvC,aAAwC;IAExC,IAAI,YAAY,IAAI,IAAI,IAAI,YAAY,GAAG,CAAC;QAAE,OAAO,YAAY,CAAC;IAClE,IAAI,aAAa,IAAI,IAAI,IAAI,aAAa,GAAG,CAAC;QAAE,OAAO,aAAa,CAAC;IACrE,OAAO,6BAAqB,CAAC;AAC/B,CAAC"}
+10 -8
View File
@@ -7,22 +7,24 @@ const client_1 = require("@prisma/client");
exports.ADMIN_STATUS_MAP = {
pending_review: [
client_1.OrderStatus.cart,
client_1.OrderStatus.awaiting_settlement,
client_1.OrderStatus.cash_in_progress,
client_1.OrderStatus.check_in_progress,
client_1.OrderStatus.submitted,
client_1.OrderStatus.awaiting_review,
client_1.OrderStatus.pricing,
],
awaiting_customer: [
client_1.OrderStatus.awaiting_settlement,
client_1.OrderStatus.awaiting_settlement_fix,
client_1.OrderStatus.check_in_progress,
client_1.OrderStatus.cash_in_progress,
client_1.OrderStatus.cash_invoice_issued,
client_1.OrderStatus.awaiting_cash_payment,
],
pending_check: [client_1.OrderStatus.awaiting_check_review],
pending_payment: [
client_1.OrderStatus.cash_payment_submitted,
client_1.OrderStatus.awaiting_cash_confirmation,
],
pending_check: [client_1.OrderStatus.awaiting_check_review],
needs_correction: [
client_1.OrderStatus.needs_correction,
client_1.OrderStatus.awaiting_settlement_fix,
],
needs_correction: [client_1.OrderStatus.needs_correction],
check_correction: [client_1.OrderStatus.check_needs_fix, client_1.OrderStatus.check_rejected],
final_approval: [
client_1.OrderStatus.final_approved,
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"order-status.js","sourceRoot":"","sources":["../../../src/common/utils/order-status.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"order-status.js","sourceRoot":"","sources":["../../../src/common/utils/order-status.ts"],"names":[],"mappings":";;;AA2CA,4DASC;AAED,sCAMC;AA5DD,2CAA6C;AAGhC,QAAA,gBAAgB,GAAkC;IAC7D,cAAc,EAAE;QACd,oBAAW,CAAC,IAAI;QAChB,oBAAW,CAAC,SAAS;QACrB,oBAAW,CAAC,eAAe;QAC3B,oBAAW,CAAC,OAAO;KACpB;IACD,iBAAiB,EAAE;QACjB,oBAAW,CAAC,mBAAmB;QAC/B,oBAAW,CAAC,uBAAuB;QACnC,oBAAW,CAAC,iBAAiB;QAC7B,oBAAW,CAAC,gBAAgB;QAC5B,oBAAW,CAAC,mBAAmB;QAC/B,oBAAW,CAAC,qBAAqB;KAClC;IACD,aAAa,EAAE,CAAC,oBAAW,CAAC,qBAAqB,CAAC;IAClD,eAAe,EAAE;QACf,oBAAW,CAAC,sBAAsB;QAClC,oBAAW,CAAC,0BAA0B;KACvC;IACD,gBAAgB,EAAE,CAAC,oBAAW,CAAC,gBAAgB,CAAC;IAChD,gBAAgB,EAAE,CAAC,oBAAW,CAAC,eAAe,EAAE,oBAAW,CAAC,cAAc,CAAC;IAC3E,cAAc,EAAE;QACd,oBAAW,CAAC,cAAc;QAC1B,oBAAW,CAAC,eAAe;QAC3B,oBAAW,CAAC,cAAc;QAC1B,oBAAW,CAAC,SAAS;QACrB,oBAAW,CAAC,aAAa;KAC1B;IACD,OAAO,EAAE,CAAC,oBAAW,CAAC,OAAO,CAAC;IAC9B,SAAS,EAAE,CAAC,oBAAW,CAAC,SAAS,CAAC;CACnC,CAAC;AAGF,MAAM,iBAAiB,GAAkB;IACvC,oBAAW,CAAC,SAAS;IACrB,oBAAW,CAAC,QAAQ;IACpB,oBAAW,CAAC,OAAO;CACpB,CAAC;AAEF,SAAgB,wBAAwB,CACtC,MAAe;IAEf,IAAI,CAAC,MAAM;QAAE,OAAO,SAAS,CAAC;IAC9B,IAAI,wBAAgB,CAAC,MAAM,CAAC;QAAE,OAAO,wBAAgB,CAAC,MAAM,CAAC,CAAC;IAC9D,IAAI,MAAM,CAAC,MAAM,CAAC,oBAAW,CAAC,CAAC,QAAQ,CAAC,MAAqB,CAAC,EAAE,CAAC;QAC/D,OAAO,CAAC,MAAqB,CAAC,CAAC;IACjC,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAgB,aAAa,CAAC,MAAmB;IAC/C,KAAK,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,wBAAgB,CAAC,EAAE,CAAC;QAC7D,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;YAAE,OAAO,KAAK,CAAC;IAC1C,CAAC;IACD,IAAI,iBAAiB,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,MAAM,CAAC;IACtD,OAAO,gBAAgB,CAAC;AAC1B,CAAC;AAEY,QAAA,mBAAmB,GAAgC;IAC9D,IAAI,EAAE,UAAU;IAChB,mBAAmB,EAAE,iBAAiB;IACtC,gBAAgB,EAAE,iBAAiB;IACnC,iBAAiB,EAAE,eAAe;IAClC,SAAS,EAAE,SAAS;IACpB,eAAe,EAAE,iBAAiB;IAClC,OAAO,EAAE,YAAY;IACrB,gBAAgB,EAAE,eAAe;IACjC,uBAAuB,EAAE,aAAa;IACtC,mBAAmB,EAAE,kBAAkB;IACvC,qBAAqB,EAAE,uBAAuB;IAC9C,sBAAsB,EAAE,qBAAqB;IAC7C,0BAA0B,EAAE,mBAAmB;IAC/C,cAAc,EAAE,sBAAsB;IACtC,qBAAqB,EAAE,UAAU;IACjC,eAAe,EAAE,UAAU;IAC3B,cAAc,EAAE,OAAO;IACvB,eAAe,EAAE,aAAa;IAC9B,cAAc,EAAE,aAAa;IAC7B,SAAS,EAAE,YAAY;IACvB,aAAa,EAAE,aAAa;IAC5B,OAAO,EAAE,WAAW;IACpB,SAAS,EAAE,WAAW;IACtB,SAAS,EAAE,SAAS;IACpB,QAAQ,EAAE,QAAQ;IAClB,OAAO,EAAE,OAAO;CACjB,CAAC"}
+6
View File
@@ -0,0 +1,6 @@
import { OrderStatus } from '@prisma/client';
export declare const ORDER_TRANSITIONS: Record<OrderStatus, OrderStatus[]>;
export declare function isFinalStatus(status: OrderStatus): boolean;
export declare function allowedTransitions(from: OrderStatus): OrderStatus[];
export declare function canTransition(from: OrderStatus, to: OrderStatus): boolean;
export declare function assertTransition(from: OrderStatus, to: OrderStatus): void;
+83
View File
@@ -0,0 +1,83 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ORDER_TRANSITIONS = void 0;
exports.isFinalStatus = isFinalStatus;
exports.allowedTransitions = allowedTransitions;
exports.canTransition = canTransition;
exports.assertTransition = assertTransition;
const common_1 = require("@nestjs/common");
const client_1 = require("@prisma/client");
const order_status_1 = require("./order-status");
exports.ORDER_TRANSITIONS = {
awaiting_review: [client_1.OrderStatus.pricing],
pricing: [client_1.OrderStatus.awaiting_settlement],
awaiting_settlement: [
client_1.OrderStatus.awaiting_cash_payment,
client_1.OrderStatus.check_in_progress,
client_1.OrderStatus.pricing,
],
awaiting_cash_payment: [
client_1.OrderStatus.awaiting_cash_confirmation,
client_1.OrderStatus.expired,
],
awaiting_cash_confirmation: [
client_1.OrderStatus.cash_confirmed,
client_1.OrderStatus.awaiting_cash_payment,
],
cash_confirmed: [client_1.OrderStatus.final_approved],
check_in_progress: [client_1.OrderStatus.awaiting_check_review],
awaiting_check_review: [
client_1.OrderStatus.checks_approved,
client_1.OrderStatus.check_needs_fix,
],
check_needs_fix: [client_1.OrderStatus.awaiting_check_review],
checks_approved: [client_1.OrderStatus.final_approved],
final_approved: [client_1.OrderStatus.preparing],
preparing: [client_1.OrderStatus.ready_to_ship],
ready_to_ship: [client_1.OrderStatus.shipped],
shipped: [client_1.OrderStatus.delivered],
delivered: [],
cancelled: [],
rejected: [],
expired: [],
cart: [client_1.OrderStatus.awaiting_review],
submitted: [client_1.OrderStatus.awaiting_review],
needs_correction: [client_1.OrderStatus.awaiting_review, client_1.OrderStatus.pricing],
awaiting_settlement_fix: [client_1.OrderStatus.awaiting_settlement],
cash_in_progress: [client_1.OrderStatus.awaiting_cash_payment],
cash_invoice_issued: [client_1.OrderStatus.awaiting_cash_payment],
cash_payment_submitted: [client_1.OrderStatus.awaiting_cash_confirmation],
check_rejected: [client_1.OrderStatus.check_needs_fix],
};
const ADMIN_TERMINAL_TRANSITIONS = [
client_1.OrderStatus.cancelled,
client_1.OrderStatus.rejected,
];
const FINAL_STATUSES = [
client_1.OrderStatus.delivered,
client_1.OrderStatus.cancelled,
client_1.OrderStatus.rejected,
client_1.OrderStatus.expired,
];
function isFinalStatus(status) {
return FINAL_STATUSES.includes(status);
}
function allowedTransitions(from) {
const next = [...(exports.ORDER_TRANSITIONS[from] ?? [])];
if (!isFinalStatus(from)) {
for (const terminal of ADMIN_TERMINAL_TRANSITIONS) {
if (!next.includes(terminal))
next.push(terminal);
}
}
return next;
}
function canTransition(from, to) {
return allowedTransitions(from).includes(to);
}
function assertTransition(from, to) {
if (canTransition(from, to))
return;
throw new common_1.BadRequestException(`تغییر وضعیت از «${order_status_1.ORDER_STATUS_LABELS[from]}» به «${order_status_1.ORDER_STATUS_LABELS[to]}» مجاز نیست`);
}
//# sourceMappingURL=order-transitions.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"order-transitions.js","sourceRoot":"","sources":["../../../src/common/utils/order-transitions.ts"],"names":[],"mappings":";;;AA4EA,sCAEC;AAGD,gDAQC;AAED,sCAEC;AAED,4CAKC;AApGD,2CAAqD;AACrD,2CAA6C;AAC7C,iDAAqD;AASxC,QAAA,iBAAiB,GAAuC;IACnE,eAAe,EAAE,CAAC,oBAAW,CAAC,OAAO,CAAC;IACtC,OAAO,EAAE,CAAC,oBAAW,CAAC,mBAAmB,CAAC;IAC1C,mBAAmB,EAAE;QACnB,oBAAW,CAAC,qBAAqB;QACjC,oBAAW,CAAC,iBAAiB;QAC7B,oBAAW,CAAC,OAAO;KACpB;IAGD,qBAAqB,EAAE;QACrB,oBAAW,CAAC,0BAA0B;QACtC,oBAAW,CAAC,OAAO;KACpB;IACD,0BAA0B,EAAE;QAC1B,oBAAW,CAAC,cAAc;QAC1B,oBAAW,CAAC,qBAAqB;KAClC;IACD,cAAc,EAAE,CAAC,oBAAW,CAAC,cAAc,CAAC;IAG5C,iBAAiB,EAAE,CAAC,oBAAW,CAAC,qBAAqB,CAAC;IACtD,qBAAqB,EAAE;QACrB,oBAAW,CAAC,eAAe;QAC3B,oBAAW,CAAC,eAAe;KAC5B;IACD,eAAe,EAAE,CAAC,oBAAW,CAAC,qBAAqB,CAAC;IACpD,eAAe,EAAE,CAAC,oBAAW,CAAC,cAAc,CAAC;IAG7C,cAAc,EAAE,CAAC,oBAAW,CAAC,SAAS,CAAC;IACvC,SAAS,EAAE,CAAC,oBAAW,CAAC,aAAa,CAAC;IACtC,aAAa,EAAE,CAAC,oBAAW,CAAC,OAAO,CAAC;IACpC,OAAO,EAAE,CAAC,oBAAW,CAAC,SAAS,CAAC;IAChC,SAAS,EAAE,EAAE;IAGb,SAAS,EAAE,EAAE;IACb,QAAQ,EAAE,EAAE;IACZ,OAAO,EAAE,EAAE;IAGX,IAAI,EAAE,CAAC,oBAAW,CAAC,eAAe,CAAC;IACnC,SAAS,EAAE,CAAC,oBAAW,CAAC,eAAe,CAAC;IACxC,gBAAgB,EAAE,CAAC,oBAAW,CAAC,eAAe,EAAE,oBAAW,CAAC,OAAO,CAAC;IACpE,uBAAuB,EAAE,CAAC,oBAAW,CAAC,mBAAmB,CAAC;IAC1D,gBAAgB,EAAE,CAAC,oBAAW,CAAC,qBAAqB,CAAC;IACrD,mBAAmB,EAAE,CAAC,oBAAW,CAAC,qBAAqB,CAAC;IACxD,sBAAsB,EAAE,CAAC,oBAAW,CAAC,0BAA0B,CAAC;IAChE,cAAc,EAAE,CAAC,oBAAW,CAAC,eAAe,CAAC;CAC9C,CAAC;AAGF,MAAM,0BAA0B,GAAkB;IAChD,oBAAW,CAAC,SAAS;IACrB,oBAAW,CAAC,QAAQ;CACrB,CAAC;AAEF,MAAM,cAAc,GAAkB;IACpC,oBAAW,CAAC,SAAS;IACrB,oBAAW,CAAC,SAAS;IACrB,oBAAW,CAAC,QAAQ;IACpB,oBAAW,CAAC,OAAO;CACpB,CAAC;AAEF,SAAgB,aAAa,CAAC,MAAmB;IAC/C,OAAO,cAAc,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AACzC,CAAC;AAGD,SAAgB,kBAAkB,CAAC,IAAiB;IAClD,MAAM,IAAI,GAAG,CAAC,GAAG,CAAC,yBAAiB,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IAClD,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC;QACzB,KAAK,MAAM,QAAQ,IAAI,0BAA0B,EAAE,CAAC;YAClD,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC;gBAAE,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACpD,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAgB,aAAa,CAAC,IAAiB,EAAE,EAAe;IAC9D,OAAO,kBAAkB,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;AAC/C,CAAC;AAED,SAAgB,gBAAgB,CAAC,IAAiB,EAAE,EAAe;IACjE,IAAI,aAAa,CAAC,IAAI,EAAE,EAAE,CAAC;QAAE,OAAO;IACpC,MAAM,IAAI,4BAAmB,CAC3B,mBAAmB,kCAAmB,CAAC,IAAI,CAAC,SAAS,kCAAmB,CAAC,EAAE,CAAC,aAAa,CAC1F,CAAC;AACJ,CAAC"}
+6
View File
@@ -13,6 +13,7 @@ export declare class CompaniesController {
isActive: boolean;
displayOrder: number;
maxMeteragePerInvoice: number;
ownMaxMeteragePerInvoice: number | undefined;
cashInvoiceValidityHours: number;
cashInvoiceValidUntilHour: number | undefined;
catalogCount: number;
@@ -31,6 +32,7 @@ export declare class CompaniesController {
isActive: boolean;
displayOrder: number;
maxMeteragePerInvoice: number;
ownMaxMeteragePerInvoice: number | undefined;
cashInvoiceValidityHours: number;
cashInvoiceValidUntilHour: number | undefined;
catalogCount: number;
@@ -63,6 +65,7 @@ export declare class CompaniesController {
isActive: boolean;
displayOrder: number;
maxMeteragePerInvoice: number;
ownMaxMeteragePerInvoice: number | undefined;
cashInvoiceValidityHours: number;
cashInvoiceValidUntilHour: number | undefined;
catalogCount: number;
@@ -81,6 +84,7 @@ export declare class CompaniesController {
isActive: boolean;
displayOrder: number;
maxMeteragePerInvoice: number;
ownMaxMeteragePerInvoice: number | undefined;
cashInvoiceValidityHours: number;
cashInvoiceValidUntilHour: number | undefined;
catalogCount: number;
@@ -99,6 +103,7 @@ export declare class CompaniesController {
isActive: boolean;
displayOrder: number;
maxMeteragePerInvoice: number;
ownMaxMeteragePerInvoice: number | undefined;
cashInvoiceValidityHours: number;
cashInvoiceValidUntilHour: number | undefined;
catalogCount: number;
@@ -117,6 +122,7 @@ export declare class CompaniesController {
isActive: boolean;
displayOrder: number;
maxMeteragePerInvoice: number;
ownMaxMeteragePerInvoice: number | undefined;
cashInvoiceValidityHours: number;
cashInvoiceValidUntilHour: number | undefined;
catalogCount: number;
+6
View File
@@ -13,6 +13,7 @@ export declare class CompaniesService {
isActive: boolean;
displayOrder: number;
maxMeteragePerInvoice: number;
ownMaxMeteragePerInvoice: number | undefined;
cashInvoiceValidityHours: number;
cashInvoiceValidUntilHour: number | undefined;
catalogCount: number;
@@ -45,6 +46,7 @@ export declare class CompaniesService {
isActive: boolean;
displayOrder: number;
maxMeteragePerInvoice: number;
ownMaxMeteragePerInvoice: number | undefined;
cashInvoiceValidityHours: number;
cashInvoiceValidUntilHour: number | undefined;
catalogCount: number;
@@ -53,6 +55,7 @@ export declare class CompaniesService {
createdAt: string;
updatedAt: string;
}>;
private settingsMaxMeterage;
create(dto: CreateCompanyDto): Promise<{
id: string;
name: string;
@@ -63,6 +66,7 @@ export declare class CompaniesService {
isActive: boolean;
displayOrder: number;
maxMeteragePerInvoice: number;
ownMaxMeteragePerInvoice: number | undefined;
cashInvoiceValidityHours: number;
cashInvoiceValidUntilHour: number | undefined;
catalogCount: number;
@@ -81,6 +85,7 @@ export declare class CompaniesService {
isActive: boolean;
displayOrder: number;
maxMeteragePerInvoice: number;
ownMaxMeteragePerInvoice: number | undefined;
cashInvoiceValidityHours: number;
cashInvoiceValidUntilHour: number | undefined;
catalogCount: number;
@@ -99,6 +104,7 @@ export declare class CompaniesService {
isActive: boolean;
displayOrder: number;
maxMeteragePerInvoice: number;
ownMaxMeteragePerInvoice: number | undefined;
cashInvoiceValidityHours: number;
cashInvoiceValidUntilHour: number | undefined;
catalogCount: number;
+36 -21
View File
@@ -13,40 +13,54 @@ exports.CompaniesService = void 0;
const common_1 = require("@nestjs/common");
const client_1 = require("@prisma/client");
const prisma_service_1 = require("../prisma/prisma.service");
const meterage_1 = require("../common/utils/meterage");
let CompaniesService = class CompaniesService {
prisma;
constructor(prisma) {
this.prisma = prisma;
}
async findAll(activeOnly = false) {
const companies = await this.prisma.company.findMany({
where: activeOnly ? { isActive: true } : undefined,
orderBy: [{ displayOrder: 'asc' }, { name: 'asc' }],
include: {
_count: {
select: { catalogs: true, products: true, bankAccounts: true },
const [companies, fallbackMeterage] = await Promise.all([
this.prisma.company.findMany({
where: activeOnly ? { isActive: true } : undefined,
orderBy: [{ displayOrder: 'asc' }, { name: 'asc' }],
include: {
_count: {
select: { catalogs: true, products: true, bankAccounts: true },
},
},
},
});
return companies.map((c) => this.mapCompany(c));
}),
this.settingsMaxMeterage(),
]);
return companies.map((c) => this.mapCompany(c, fallbackMeterage));
}
async findOne(id) {
const company = await this.prisma.company.findUnique({
where: { id },
include: {
bankAccounts: { orderBy: { displayOrder: 'asc' } },
_count: {
select: { catalogs: true, products: true, bankAccounts: true },
const [company, fallbackMeterage] = await Promise.all([
this.prisma.company.findUnique({
where: { id },
include: {
bankAccounts: { orderBy: { displayOrder: 'asc' } },
_count: {
select: { catalogs: true, products: true, bankAccounts: true },
},
},
},
});
}),
this.settingsMaxMeterage(),
]);
if (!company)
throw new common_1.NotFoundException('شرکت یافت نشد');
return {
...this.mapCompany(company),
...this.mapCompany(company, fallbackMeterage),
bankAccounts: company.bankAccounts.map((b) => this.mapBankAccount(b)),
};
}
async settingsMaxMeterage() {
const settings = await this.prisma.systemSettings.findUnique({
where: { id: 'default' },
select: { maxMeteragePerInvoice: true },
});
return settings?.maxMeteragePerInvoice ?? null;
}
async create(dto) {
try {
const company = await this.prisma.company.create({
@@ -56,7 +70,7 @@ let CompaniesService = class CompaniesService {
description: dto.description?.trim() || null,
logoUrl: dto.logoUrl?.trim() || null,
displayOrder: dto.displayOrder ?? 0,
maxMeteragePerInvoice: dto.maxMeteragePerInvoice ?? 5000,
maxMeteragePerInvoice: dto.maxMeteragePerInvoice ?? null,
cashInvoiceValidityHours: dto.cashInvoiceValidityHours ?? 8,
cashInvoiceValidUntilHour: dto.cashInvoiceValidUntilHour ?? null,
isActive: dto.isActive ?? true,
@@ -186,7 +200,7 @@ let CompaniesService = class CompaniesService {
throw new common_1.NotFoundException('حساب بانکی یافت نشد');
return account;
}
mapCompany(company) {
mapCompany(company, fallbackMaxMeterage = null) {
return {
id: company.id,
name: company.name,
@@ -196,7 +210,8 @@ let CompaniesService = class CompaniesService {
logo: company.logoUrl ?? undefined,
isActive: company.isActive,
displayOrder: company.displayOrder,
maxMeteragePerInvoice: company.maxMeteragePerInvoice,
maxMeteragePerInvoice: (0, meterage_1.resolveMaxMeterage)(company.maxMeteragePerInvoice, fallbackMaxMeterage),
ownMaxMeteragePerInvoice: company.maxMeteragePerInvoice ?? undefined,
cashInvoiceValidityHours: company.cashInvoiceValidityHours,
cashInvoiceValidUntilHour: company.cashInvoiceValidUntilHour ?? undefined,
catalogCount: company._count?.catalogs ?? 0,
File diff suppressed because one or more lines are too long
+6 -4
View File
@@ -18,17 +18,19 @@ export declare class DashboardController {
userId: string;
companyId: string;
companyName: string;
settlementType: import("@prisma/client").$Enums.SettlementType;
settlementMethod: import("@prisma/client").$Enums.SettlementType;
settlementType: import("@prisma/client").$Enums.SettlementType | null;
settlementMethod: import("@prisma/client").$Enums.SettlementType | null;
checkMode: import("@prisma/client").$Enums.CheckRegistrationMode | undefined;
status: import("@prisma/client").$Enums.OrderStatus;
adminStatus: string;
allowedTransitions: import("@prisma/client").$Enums.OrderStatus[];
totalMeterage: number;
meterage: number;
totalAmount: number | null;
shippingCost: number | null;
totalDiscount: number | null;
finalAmount: number | null;
pricesVisible: boolean;
notes: string | undefined;
checkRassDays: number | undefined;
checkRassDate: string | undefined;
@@ -50,8 +52,8 @@ export declare class DashboardController {
confirmedMeterage: number | null;
unitPrice: number | null;
supplyPrice: number | null;
unitDiscount: number;
discount: number;
unitDiscount: number | null;
discount: number | null;
lineTotal: number | null;
totalAmount: number | null;
notes: string | undefined;
+6 -4
View File
@@ -20,17 +20,19 @@ export declare class DashboardService {
userId: string;
companyId: string;
companyName: string;
settlementType: import("@prisma/client").$Enums.SettlementType;
settlementMethod: import("@prisma/client").$Enums.SettlementType;
settlementType: import("@prisma/client").$Enums.SettlementType | null;
settlementMethod: import("@prisma/client").$Enums.SettlementType | null;
checkMode: import("@prisma/client").$Enums.CheckRegistrationMode | undefined;
status: import("@prisma/client").$Enums.OrderStatus;
adminStatus: string;
allowedTransitions: import("@prisma/client").$Enums.OrderStatus[];
totalMeterage: number;
meterage: number;
totalAmount: number | null;
shippingCost: number | null;
totalDiscount: number | null;
finalAmount: number | null;
pricesVisible: boolean;
notes: string | undefined;
checkRassDays: number | undefined;
checkRassDate: string | undefined;
@@ -52,8 +54,8 @@ export declare class DashboardService {
confirmedMeterage: number | null;
unitPrice: number | null;
supplyPrice: number | null;
unitDiscount: number;
discount: number;
unitDiscount: number | null;
discount: number | null;
lineTotal: number | null;
totalAmount: number | null;
notes: string | undefined;
+22
View File
@@ -0,0 +1,22 @@
import { NotificationTone, OrderStatus } from '@prisma/client';
export type CustomerEvent = 'order_submitted' | 'invoice_priced' | 'checks_approved' | 'checks_need_fix' | 'receipt_approved' | 'receipt_rejected' | 'cash_invoice_reminder' | 'cash_invoice_expired' | 'order_preparing' | 'order_ready_to_ship' | 'order_shipped' | 'order_delivered' | 'order_cancelled';
type SmsSwitch = 'smsOrderRegistered' | 'smsPricingReady' | 'smsPaymentReminder' | 'smsCheckReview' | 'smsShippingUpdate' | null;
export type CustomerEventTemplate = {
title: string;
body: (ctx: EventContext) => string;
sms?: (ctx: EventContext) => string;
tone: NotificationTone;
smsSwitch: SmsSwitch;
};
export type EventContext = {
orderNumber: string;
appName: string;
amount?: number | null;
note?: string | null;
deadline?: string | null;
carrier?: string | null;
trackingNumber?: string | null;
};
export declare function eventForStatus(status: OrderStatus): CustomerEvent | null;
export declare const CUSTOMER_EVENTS: Record<CustomerEvent, CustomerEventTemplate>;
export {};
+125
View File
@@ -0,0 +1,125 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CUSTOMER_EVENTS = void 0;
exports.eventForStatus = eventForStatus;
const client_1 = require("@prisma/client");
function money(amount) {
if (!amount)
return '—';
return `${Math.round(amount).toLocaleString('fa-IR')} تومان`;
}
function eventForStatus(status) {
switch (status) {
case client_1.OrderStatus.awaiting_settlement:
return 'invoice_priced';
case client_1.OrderStatus.preparing:
case client_1.OrderStatus.final_approved:
return 'order_preparing';
case client_1.OrderStatus.ready_to_ship:
return 'order_ready_to_ship';
case client_1.OrderStatus.shipped:
return 'order_shipped';
case client_1.OrderStatus.delivered:
return 'order_delivered';
case client_1.OrderStatus.cancelled:
case client_1.OrderStatus.rejected:
return 'order_cancelled';
case client_1.OrderStatus.expired:
return 'cash_invoice_expired';
default:
return null;
}
}
exports.CUSTOMER_EVENTS = {
order_submitted: {
title: 'سفارش شما ثبت شد',
body: (c) => `سفارش ${c.orderNumber} ثبت شد و برای بررسی و قیمت‌گذاری به کارشناسان ما رسید.`,
sms: (c) => `سفارش ${c.orderNumber} در ${c.appName} ثبت شد. پس از قیمت‌گذاری، فاکتور برای شما ارسال می‌شود.`,
tone: client_1.NotificationTone.info,
smsSwitch: 'smsOrderRegistered',
},
invoice_priced: {
title: 'فاکتور سفارش شما صادر شد',
body: (c) => `مبلغ فاکتور سفارش ${c.orderNumber} ${money(c.amount)} است. برای ادامه، روش تسویه را انتخاب کنید.`,
sms: (c) => `فاکتور سفارش ${c.orderNumber} به مبلغ ${money(c.amount)} صادر شد. برای انتخاب روش تسویه به پنل ${c.appName} مراجعه کنید.`,
tone: client_1.NotificationTone.success,
smsSwitch: 'smsPricingReady',
},
checks_approved: {
title: 'چک‌های شما تایید شد',
body: (c) => `همه چک‌های سفارش ${c.orderNumber} تایید شدند.`,
sms: (c) => `چک‌های سفارش ${c.orderNumber} تایید شد و سفارش به مرحله آماده‌سازی می‌رود.`,
tone: client_1.NotificationTone.success,
smsSwitch: 'smsCheckReview',
},
checks_need_fix: {
title: 'چک شما نیاز به اصلاح دارد',
body: (c) => `یک یا چند چک سفارش ${c.orderNumber} تایید نشد${c.note ? `: ${c.note}` : '.'} لطفاً چک‌ها را اصلاح و مجدداً ارسال کنید.`,
sms: (c) => `چک سفارش ${c.orderNumber} تایید نشد. برای اصلاح چک‌ها به پنل ${c.appName} مراجعه کنید.`,
tone: client_1.NotificationTone.danger,
smsSwitch: 'smsCheckReview',
},
receipt_approved: {
title: 'پرداخت شما تایید شد',
body: (c) => `رسید پرداخت سفارش ${c.orderNumber} تایید شد.`,
sms: (c) => `رسید پرداخت سفارش ${c.orderNumber} تایید شد. سفارش شما در حال آماده‌سازی است.`,
tone: client_1.NotificationTone.success,
smsSwitch: 'smsPaymentReminder',
},
receipt_rejected: {
title: 'رسید پرداخت تایید نشد',
body: (c) => `رسید پرداخت سفارش ${c.orderNumber} تایید نشد${c.note ? `: ${c.note}` : '.'} لطفاً رسید صحیح را ثبت کنید.`,
sms: (c) => `رسید پرداخت سفارش ${c.orderNumber} تایید نشد. لطفاً در پنل ${c.appName} رسید را اصلاح کنید.`,
tone: client_1.NotificationTone.danger,
smsSwitch: 'smsPaymentReminder',
},
cash_invoice_reminder: {
title: 'یادآوری مهلت پرداخت',
body: (c) => `مهلت پرداخت فاکتور سفارش ${c.orderNumber} به پایان نزدیک است. مبلغ قابل پرداخت ${money(c.amount)}.`,
sms: (c) => `مهلت پرداخت فاکتور سفارش ${c.orderNumber} در حال پایان است. مبلغ ${money(c.amount)}.`,
tone: client_1.NotificationTone.warning,
smsSwitch: 'smsPaymentReminder',
},
cash_invoice_expired: {
title: 'مهلت پرداخت فاکتور تمام شد',
body: (c) => `مهلت پرداخت فاکتور سفارش ${c.orderNumber} به پایان رسید. برای ثبت سفارش تازه با پشتیبانی تماس بگیرید.`,
sms: (c) => `مهلت پرداخت فاکتور سفارش ${c.orderNumber} به پایان رسید.`,
tone: client_1.NotificationTone.danger,
smsSwitch: 'smsPaymentReminder',
},
order_preparing: {
title: 'سفارش در حال آماده‌سازی',
body: (c) => `سفارش ${c.orderNumber} تایید نهایی شد و در حال آماده‌سازی است.`,
tone: client_1.NotificationTone.info,
smsSwitch: null,
},
order_ready_to_ship: {
title: 'سفارش آماده ارسال است',
body: (c) => `سفارش ${c.orderNumber} آماده ارسال شد.`,
sms: (c) => `سفارش ${c.orderNumber} آماده ارسال شد.`,
tone: client_1.NotificationTone.info,
smsSwitch: 'smsShippingUpdate',
},
order_shipped: {
title: 'سفارش ارسال شد',
body: (c) => `سفارش ${c.orderNumber} ارسال شد${c.carrier ? ` (${c.carrier})` : ''}${c.trackingNumber ? ` با کد رهگیری ${c.trackingNumber}` : ''}.`,
sms: (c) => `سفارش ${c.orderNumber} ارسال شد${c.trackingNumber ? `. کد رهگیری: ${c.trackingNumber}` : '.'}`,
tone: client_1.NotificationTone.success,
smsSwitch: 'smsShippingUpdate',
},
order_delivered: {
title: 'سفارش تحویل شد',
body: (c) => `سفارش ${c.orderNumber} تحویل داده شد. از خرید شما سپاسگزاریم.`,
sms: (c) => `سفارش ${c.orderNumber} تحویل داده شد. از خرید شما سپاسگزاریم.`,
tone: client_1.NotificationTone.success,
smsSwitch: 'smsShippingUpdate',
},
order_cancelled: {
title: 'سفارش لغو شد',
body: (c) => `سفارش ${c.orderNumber} لغو شد${c.note ? `: ${c.note}` : '.'} برای اطلاعات بیشتر با پشتیبانی تماس بگیرید.`,
sms: (c) => `سفارش ${c.orderNumber} لغو شد.`,
tone: client_1.NotificationTone.danger,
smsSwitch: null,
},
};
//# sourceMappingURL=notification-events.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"notification-events.js","sourceRoot":"","sources":["../../src/notifications/notification-events.ts"],"names":[],"mappings":"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"}
+36
View File
@@ -0,0 +1,36 @@
import { NotificationsService } from './notifications.service';
import type { AuthPrincipal } from '../common/decorators/current-user.decorator';
export declare class CustomerNotificationsController {
private readonly notifications;
constructor(notifications: NotificationsService);
list(user: AuthPrincipal): Promise<{
id: string;
orderId: string | undefined;
event: string;
title: string;
body: string;
tone: import("@prisma/client").$Enums.NotificationTone;
link: string | undefined;
isRead: boolean;
sentAt: string;
}[]>;
unreadCount(user: AuthPrincipal): Promise<{
count: number;
}>;
markAllRead(user: AuthPrincipal): Promise<{
count: number;
}>;
markRead(user: AuthPrincipal, id: string): Promise<{
count: number;
}>;
}
export declare class AdminNotificationsController {
private readonly notifications;
constructor(notifications: NotificationsService);
markAllRead(): Promise<{
ok: boolean;
}>;
markRead(id: string): Promise<{
ok: boolean;
}>;
}
+104
View File
@@ -0,0 +1,104 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AdminNotificationsController = exports.CustomerNotificationsController = void 0;
const common_1 = require("@nestjs/common");
const notifications_service_1 = require("./notifications.service");
const auth_decorators_1 = require("../common/decorators/auth.decorators");
const current_user_decorator_1 = require("../common/decorators/current-user.decorator");
let CustomerNotificationsController = class CustomerNotificationsController {
notifications;
constructor(notifications) {
this.notifications = notifications;
}
list(user) {
return this.notifications.listForCustomer(user.id);
}
unreadCount(user) {
return this.notifications.unreadCount(user.id);
}
markAllRead(user) {
return this.notifications.markAllRead(user.id);
}
markRead(user, id) {
return this.notifications.markRead(user.id, id);
}
};
exports.CustomerNotificationsController = CustomerNotificationsController;
__decorate([
(0, common_1.Get)(),
__param(0, (0, current_user_decorator_1.CurrentUser)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", void 0)
], CustomerNotificationsController.prototype, "list", null);
__decorate([
(0, common_1.Get)('unread-count'),
__param(0, (0, current_user_decorator_1.CurrentUser)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", void 0)
], CustomerNotificationsController.prototype, "unreadCount", null);
__decorate([
(0, common_1.Patch)('read-all'),
__param(0, (0, current_user_decorator_1.CurrentUser)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", void 0)
], CustomerNotificationsController.prototype, "markAllRead", null);
__decorate([
(0, common_1.Patch)(':id/read'),
__param(0, (0, current_user_decorator_1.CurrentUser)()),
__param(1, (0, common_1.Param)('id')),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, String]),
__metadata("design:returntype", void 0)
], CustomerNotificationsController.prototype, "markRead", null);
exports.CustomerNotificationsController = CustomerNotificationsController = __decorate([
(0, common_1.Controller)('notifications'),
(0, auth_decorators_1.AuthType)('customer'),
__metadata("design:paramtypes", [notifications_service_1.NotificationsService])
], CustomerNotificationsController);
let AdminNotificationsController = class AdminNotificationsController {
notifications;
constructor(notifications) {
this.notifications = notifications;
}
markAllRead() {
return this.notifications.markAllAdminRead();
}
markRead(id) {
return this.notifications.markAdminRead(id);
}
};
exports.AdminNotificationsController = AdminNotificationsController;
__decorate([
(0, common_1.Patch)('read-all'),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", void 0)
], AdminNotificationsController.prototype, "markAllRead", null);
__decorate([
(0, common_1.Patch)(':id/read'),
__param(0, (0, common_1.Param)('id')),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", void 0)
], AdminNotificationsController.prototype, "markRead", null);
exports.AdminNotificationsController = AdminNotificationsController = __decorate([
(0, common_1.Controller)('admin/notifications'),
(0, auth_decorators_1.AuthType)('admin'),
__metadata("design:paramtypes", [notifications_service_1.NotificationsService])
], AdminNotificationsController);
//# sourceMappingURL=notifications.controller.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"notifications.controller.js","sourceRoot":"","sources":["../../src/notifications/notifications.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAA+D;AAC/D,mEAA+D;AAC/D,0EAAgE;AAChE,wFAA0E;AAKnE,IAAM,+BAA+B,GAArC,MAAM,+BAA+B;IACb;IAA7B,YAA6B,aAAmC;QAAnC,kBAAa,GAAb,aAAa,CAAsB;IAAG,CAAC;IAGpE,IAAI,CAAgB,IAAmB;QACrC,OAAO,IAAI,CAAC,aAAa,CAAC,eAAe,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACrD,CAAC;IAGD,WAAW,CAAgB,IAAmB;QAC5C,OAAO,IAAI,CAAC,aAAa,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACjD,CAAC;IAGD,WAAW,CAAgB,IAAmB;QAC5C,OAAO,IAAI,CAAC,aAAa,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACjD,CAAC;IAGD,QAAQ,CAAgB,IAAmB,EAAe,EAAU;QAClE,OAAO,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;IAClD,CAAC;CACF,CAAA;AAtBY,0EAA+B;AAI1C;IADC,IAAA,YAAG,GAAE;IACA,WAAA,IAAA,oCAAW,GAAE,CAAA;;;;2DAElB;AAGD;IADC,IAAA,YAAG,EAAC,cAAc,CAAC;IACP,WAAA,IAAA,oCAAW,GAAE,CAAA;;;;kEAEzB;AAGD;IADC,IAAA,cAAK,EAAC,UAAU,CAAC;IACL,WAAA,IAAA,oCAAW,GAAE,CAAA;;;;kEAEzB;AAGD;IADC,IAAA,cAAK,EAAC,UAAU,CAAC;IACR,WAAA,IAAA,oCAAW,GAAE,CAAA;IAAuB,WAAA,IAAA,cAAK,EAAC,IAAI,CAAC,CAAA;;;;+DAExD;0CArBU,+BAA+B;IAF3C,IAAA,mBAAU,EAAC,eAAe,CAAC;IAC3B,IAAA,0BAAQ,EAAC,UAAU,CAAC;qCAEyB,4CAAoB;GADrD,+BAA+B,CAsB3C;AAIM,IAAM,4BAA4B,GAAlC,MAAM,4BAA4B;IACV;IAA7B,YAA6B,aAAmC;QAAnC,kBAAa,GAAb,aAAa,CAAsB;IAAG,CAAC;IAGpE,WAAW;QACT,OAAO,IAAI,CAAC,aAAa,CAAC,gBAAgB,EAAE,CAAC;IAC/C,CAAC;IAGD,QAAQ,CAAc,EAAU;QAC9B,OAAO,IAAI,CAAC,aAAa,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC;IAC9C,CAAC;CACF,CAAA;AAZY,oEAA4B;AAIvC;IADC,IAAA,cAAK,EAAC,UAAU,CAAC;;;;+DAGjB;AAGD;IADC,IAAA,cAAK,EAAC,UAAU,CAAC;IACR,WAAA,IAAA,cAAK,EAAC,IAAI,CAAC,CAAA;;;;4DAEpB;uCAXU,4BAA4B;IAFxC,IAAA,mBAAU,EAAC,qBAAqB,CAAC;IACjC,IAAA,0BAAQ,EAAC,OAAO,CAAC;qCAE4B,4CAAoB;GADrD,4BAA4B,CAYxC"}
+2
View File
@@ -0,0 +1,2 @@
export declare class NotificationsModule {
}
+25
View File
@@ -0,0 +1,25 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.NotificationsModule = void 0;
const common_1 = require("@nestjs/common");
const notifications_service_1 = require("./notifications.service");
const notifications_controller_1 = require("./notifications.controller");
const sms_module_1 = require("../sms/sms.module");
let NotificationsModule = class NotificationsModule {
};
exports.NotificationsModule = NotificationsModule;
exports.NotificationsModule = NotificationsModule = __decorate([
(0, common_1.Module)({
imports: [sms_module_1.SmsModule],
controllers: [notifications_controller_1.CustomerNotificationsController, notifications_controller_1.AdminNotificationsController],
providers: [notifications_service_1.NotificationsService],
exports: [notifications_service_1.NotificationsService],
})
], NotificationsModule);
//# sourceMappingURL=notifications.module.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"notifications.module.js","sourceRoot":"","sources":["../../src/notifications/notifications.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,mEAA+D;AAC/D,yEAGoC;AACpC,kDAA8C;AAQvC,IAAM,mBAAmB,GAAzB,MAAM,mBAAmB;CAAG,CAAA;AAAtB,kDAAmB;8BAAnB,mBAAmB;IAN/B,IAAA,eAAM,EAAC;QACN,OAAO,EAAE,CAAC,sBAAS,CAAC;QACpB,WAAW,EAAE,CAAC,0DAA+B,EAAE,uDAA4B,CAAC;QAC5E,SAAS,EAAE,CAAC,4CAAoB,CAAC;QACjC,OAAO,EAAE,CAAC,4CAAoB,CAAC;KAChC,CAAC;GACW,mBAAmB,CAAG"}
+61
View File
@@ -0,0 +1,61 @@
import { ConfigService } from '@nestjs/config';
import { NotificationTone } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { SmsService } from '../sms/sms.service';
import { type CustomerEvent } from './notification-events';
type NotifyOptions = {
amount?: number | null;
note?: string | null;
carrier?: string | null;
trackingNumber?: string | null;
};
export declare class NotificationsService {
private readonly prisma;
private readonly sms;
private readonly config;
private readonly logger;
constructor(prisma: PrismaService, sms: SmsService, config: ConfigService);
notifyOrderEvent(orderId: string, event: CustomerEvent, options?: NotifyOptions): Promise<void>;
dispatchOrderEvent(orderId: string, event: CustomerEvent, options?: NotifyOptions): void;
notifyAdmin(input: {
title: string;
body: string;
tone?: NotificationTone;
link?: string;
}): Promise<void>;
dispatchAdmin(input: {
title: string;
body: string;
tone?: NotificationTone;
link?: string;
}): void;
listForCustomer(customerId: string, limit?: number): Promise<{
id: string;
orderId: string | undefined;
event: string;
title: string;
body: string;
tone: import("@prisma/client").$Enums.NotificationTone;
link: string | undefined;
isRead: boolean;
sentAt: string;
}[]>;
unreadCount(customerId: string): Promise<{
count: number;
}>;
markRead(customerId: string, id: string): Promise<{
count: number;
}>;
markAllRead(customerId: string): Promise<{
count: number;
}>;
markAdminRead(id: string): Promise<{
ok: boolean;
}>;
markAllAdminRead(): Promise<{
ok: boolean;
}>;
private appName;
private isSmsEventEnabled;
}
export {};
+185
View File
@@ -0,0 +1,185 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var NotificationsService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.NotificationsService = void 0;
const common_1 = require("@nestjs/common");
const config_1 = require("@nestjs/config");
const client_1 = require("@prisma/client");
const prisma_service_1 = require("../prisma/prisma.service");
const sms_service_1 = require("../sms/sms.service");
const notification_events_1 = require("./notification-events");
let NotificationsService = NotificationsService_1 = class NotificationsService {
prisma;
sms;
config;
logger = new common_1.Logger(NotificationsService_1.name);
constructor(prisma, sms, config) {
this.prisma = prisma;
this.sms = sms;
this.config = config;
}
async notifyOrderEvent(orderId, event, options = {}) {
try {
const order = await this.prisma.order.findUnique({
where: { id: orderId },
select: {
id: true,
orderNumber: true,
userId: true,
finalAmount: true,
totalAmount: true,
user: { select: { phone: true } },
},
});
if (!order)
return;
const template = notification_events_1.CUSTOMER_EVENTS[event];
const ctx = {
orderNumber: order.orderNumber,
appName: this.appName(),
amount: options.amount ?? order.finalAmount ?? order.totalAmount ?? undefined,
note: options.note ?? null,
carrier: options.carrier ?? null,
trackingNumber: options.trackingNumber ?? null,
};
const notification = await this.prisma.customerNotification.create({
data: {
customerId: order.userId,
orderId: order.id,
event,
title: template.title,
body: template.body(ctx),
tone: template.tone,
link: `/orders/${order.id}`,
},
});
if (!template.sms || !template.smsSwitch)
return;
const enabled = await this.isSmsEventEnabled(template.smsSwitch);
if (!enabled || !this.sms.isEnabled())
return;
const result = await this.sms.send(order.user.phone, template.sms(ctx));
if (result.success) {
await this.prisma.customerNotification.update({
where: { id: notification.id },
data: { smsSent: true },
});
}
}
catch (err) {
this.logger.error(`notifyOrderEvent(${event}) failed for order ${orderId}`, err instanceof Error ? err.stack : err);
}
}
dispatchOrderEvent(orderId, event, options = {}) {
void this.notifyOrderEvent(orderId, event, options);
}
async notifyAdmin(input) {
try {
await this.prisma.adminNotification.create({
data: {
title: input.title,
body: input.body,
tone: input.tone ?? client_1.NotificationTone.info,
link: input.link ?? null,
},
});
}
catch (err) {
this.logger.error('notifyAdmin failed', err instanceof Error ? err.stack : err);
}
}
dispatchAdmin(input) {
void this.notifyAdmin(input);
}
async listForCustomer(customerId, limit = 50) {
const rows = await this.prisma.customerNotification.findMany({
where: { customerId },
orderBy: { sentAt: 'desc' },
take: limit,
});
return rows.map((n) => ({
id: n.id,
orderId: n.orderId ?? undefined,
event: n.event,
title: n.title,
body: n.body,
tone: n.tone ?? client_1.NotificationTone.info,
link: n.link ?? undefined,
isRead: n.isRead,
sentAt: n.sentAt.toISOString(),
}));
}
async unreadCount(customerId) {
const count = await this.prisma.customerNotification.count({
where: { customerId, isRead: false },
});
return { count };
}
async markRead(customerId, id) {
const existing = await this.prisma.customerNotification.findFirst({
where: { id, customerId },
select: { id: true },
});
if (!existing)
throw new common_1.NotFoundException('اعلان یافت نشد');
await this.prisma.customerNotification.update({
where: { id },
data: { isRead: true },
});
return this.unreadCount(customerId);
}
async markAllRead(customerId) {
await this.prisma.customerNotification.updateMany({
where: { customerId, isRead: false },
data: { isRead: true },
});
return { count: 0 };
}
async markAdminRead(id) {
await this.prisma.adminNotification.updateMany({
where: { id },
data: { isRead: true },
});
return { ok: true };
}
async markAllAdminRead() {
await this.prisma.adminNotification.updateMany({
where: { isRead: false },
data: { isRead: true },
});
return { ok: true };
}
appName() {
return this.config.get('SMS_APP_NAME', 'پارچه') ?? 'پارچه';
}
async isSmsEventEnabled(key) {
const settings = await this.prisma.systemSettings.findUnique({
where: { id: 'default' },
select: {
smsOrderRegistered: true,
smsPricingReady: true,
smsPaymentReminder: true,
smsCheckReview: true,
smsShippingUpdate: true,
},
});
return settings ? settings[key] : true;
}
};
exports.NotificationsService = NotificationsService;
exports.NotificationsService = NotificationsService = NotificationsService_1 = __decorate([
(0, common_1.Injectable)(),
__metadata("design:paramtypes", [prisma_service_1.PrismaService,
sms_service_1.SmsService,
config_1.ConfigService])
], NotificationsService);
//# sourceMappingURL=notifications.service.js.map
File diff suppressed because one or more lines are too long
+11
View File
@@ -0,0 +1,11 @@
import { PrismaService } from '../prisma/prisma.service';
import { NotificationsService } from '../notifications/notifications.service';
export declare class CashInvoiceSchedulerService {
private readonly prisma;
private readonly notifications;
private readonly logger;
constructor(prisma: PrismaService, notifications: NotificationsService);
handleCashInvoices(): Promise<void>;
sendReminders(): Promise<number>;
expireOverdueInvoices(): Promise<number>;
}
+118
View File
@@ -0,0 +1,118 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var CashInvoiceSchedulerService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.CashInvoiceSchedulerService = void 0;
const common_1 = require("@nestjs/common");
const schedule_1 = require("@nestjs/schedule");
const client_1 = require("@prisma/client");
const prisma_service_1 = require("../prisma/prisma.service");
const notifications_service_1 = require("../notifications/notifications.service");
const PAYABLE_STATUSES = [
client_1.OrderStatus.awaiting_cash_payment,
client_1.OrderStatus.cash_invoice_issued,
];
const REMINDER_WINDOW_MS = 60 * 60 * 1000;
let CashInvoiceSchedulerService = CashInvoiceSchedulerService_1 = class CashInvoiceSchedulerService {
prisma;
notifications;
logger = new common_1.Logger(CashInvoiceSchedulerService_1.name);
constructor(prisma, notifications) {
this.prisma = prisma;
this.notifications = notifications;
}
async handleCashInvoices() {
await this.sendReminders();
await this.expireOverdueInvoices();
}
async sendReminders() {
const now = new Date();
const soon = new Date(now.getTime() + REMINDER_WINDOW_MS);
try {
const orders = await this.prisma.order.findMany({
where: {
status: { in: PAYABLE_STATUSES },
cashReminderSentAt: null,
cashInvoiceExpiresAt: { gt: now, lte: soon },
},
select: { id: true, finalAmount: true },
});
for (const order of orders) {
await this.prisma.order.update({
where: { id: order.id },
data: { cashReminderSentAt: now },
});
await this.notifications.notifyOrderEvent(order.id, 'cash_invoice_reminder', { amount: order.finalAmount });
}
return orders.length;
}
catch (err) {
this.logger.error('sendReminders failed', err instanceof Error ? err.stack : err);
return 0;
}
}
async expireOverdueInvoices() {
const now = new Date();
try {
const orders = await this.prisma.order.findMany({
where: {
status: { in: PAYABLE_STATUSES },
cashInvoiceExpiresAt: { lt: now },
},
select: { id: true, orderNumber: true, status: true },
});
for (const order of orders) {
await this.prisma.$transaction([
this.prisma.order.update({
where: { id: order.id },
data: { status: client_1.OrderStatus.expired },
}),
this.prisma.orderStatusHistory.create({
data: {
orderId: order.id,
fromStatus: order.status,
toStatus: client_1.OrderStatus.expired,
changedBy: 'system',
note: 'پایان مهلت پرداخت فاکتور نقدی',
},
}),
]);
await this.notifications.notifyOrderEvent(order.id, 'cash_invoice_expired');
this.notifications.dispatchAdmin({
title: 'فاکتور نقدی منقضی شد',
body: `مهلت پرداخت سفارش ${order.orderNumber} به پایان رسید و سفارش منقضی شد.`,
link: '/orders',
});
}
if (orders.length) {
this.logger.log(`${orders.length} فاکتور نقدی منقضی شد`);
}
return orders.length;
}
catch (err) {
this.logger.error('expireOverdueInvoices failed', err instanceof Error ? err.stack : err);
return 0;
}
}
};
exports.CashInvoiceSchedulerService = CashInvoiceSchedulerService;
__decorate([
(0, schedule_1.Cron)(schedule_1.CronExpression.EVERY_10_MINUTES),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], CashInvoiceSchedulerService.prototype, "handleCashInvoices", null);
exports.CashInvoiceSchedulerService = CashInvoiceSchedulerService = CashInvoiceSchedulerService_1 = __decorate([
(0, common_1.Injectable)(),
__metadata("design:paramtypes", [prisma_service_1.PrismaService,
notifications_service_1.NotificationsService])
], CashInvoiceSchedulerService);
//# sourceMappingURL=cash-invoice-scheduler.service.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"cash-invoice-scheduler.service.js","sourceRoot":"","sources":["../../src/orders/cash-invoice-scheduler.service.ts"],"names":[],"mappings":"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"}
+6 -1
View File
@@ -22,13 +22,17 @@ export declare class SubmitOrderCheckDto {
}
export declare class SubmitOrderDto {
companyId: string;
settlementType: SettlementType;
settlementType?: SettlementType;
settlementMethod?: SettlementType;
checkMode?: CheckRegistrationMode;
notes?: string;
items: SubmitOrderItemDto[];
checks?: SubmitOrderCheckDto[];
}
export declare class ChooseSettlementDto {
settlementType: SettlementType;
checkMode?: CheckRegistrationMode;
}
export declare class AdminCreateOrderDto {
userId?: string;
customerPhone?: string;
@@ -57,6 +61,7 @@ export declare class UpdateOrderStatusDto {
status: OrderStatus;
note?: string;
reason?: string;
force?: boolean;
}
export declare class UpdateOrderChecksDto {
checks: SubmitOrderCheckDto[];
+23 -1
View File
@@ -9,7 +9,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.UpdateOrderChecksDto = exports.UpdateOrderStatusDto = exports.UpdateOrderPricingDto = exports.OrderItemPricingDto = exports.AdminCreateOrderDto = exports.SubmitOrderDto = exports.SubmitOrderCheckDto = exports.SubmitOrderItemDto = void 0;
exports.UpdateOrderChecksDto = exports.UpdateOrderStatusDto = exports.UpdateOrderPricingDto = exports.OrderItemPricingDto = exports.AdminCreateOrderDto = exports.ChooseSettlementDto = exports.SubmitOrderDto = exports.SubmitOrderCheckDto = exports.SubmitOrderItemDto = void 0;
const class_validator_1 = require("class-validator");
const class_transformer_1 = require("class-transformer");
const client_1 = require("@prisma/client");
@@ -137,6 +137,7 @@ __decorate([
__metadata("design:type", String)
], SubmitOrderDto.prototype, "companyId", void 0);
__decorate([
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsEnum)(client_1.SettlementType),
__metadata("design:type", String)
], SubmitOrderDto.prototype, "settlementType", void 0);
@@ -169,6 +170,20 @@ __decorate([
(0, class_transformer_1.Type)(() => SubmitOrderCheckDto),
__metadata("design:type", Array)
], SubmitOrderDto.prototype, "checks", void 0);
class ChooseSettlementDto {
settlementType;
checkMode;
}
exports.ChooseSettlementDto = ChooseSettlementDto;
__decorate([
(0, class_validator_1.IsEnum)(client_1.SettlementType),
__metadata("design:type", String)
], ChooseSettlementDto.prototype, "settlementType", void 0);
__decorate([
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsEnum)(client_1.CheckRegistrationMode),
__metadata("design:type", String)
], ChooseSettlementDto.prototype, "checkMode", void 0);
class AdminCreateOrderDto {
userId;
customerPhone;
@@ -305,6 +320,7 @@ class UpdateOrderStatusDto {
status;
note;
reason;
force;
}
exports.UpdateOrderStatusDto = UpdateOrderStatusDto;
__decorate([
@@ -321,6 +337,12 @@ __decorate([
(0, class_validator_1.IsString)(),
__metadata("design:type", String)
], UpdateOrderStatusDto.prototype, "reason", void 0);
__decorate([
(0, class_validator_1.IsOptional)(),
(0, class_transformer_1.Type)(() => Boolean),
(0, class_validator_1.IsBoolean)(),
__metadata("design:type", Boolean)
], UpdateOrderStatusDto.prototype, "force", void 0);
class UpdateOrderChecksDto {
checks;
checkMode;
+1 -1
View File
File diff suppressed because one or more lines are too long
+158 -37
View File
@@ -1,6 +1,6 @@
import { OrdersService } from './orders.service';
import type { AuthPrincipal } from '../common/decorators/current-user.decorator';
import { AdminCreateOrderDto, SubmitOrderDto, UpdateOrderChecksDto, UpdateOrderPricingDto, UpdateOrderStatusDto } from './dto/order.dto';
import { AdminCreateOrderDto, ChooseSettlementDto, SubmitOrderDto, UpdateOrderChecksDto, UpdateOrderPricingDto, UpdateOrderStatusDto } from './dto/order.dto';
export declare class CustomerOrdersController {
private readonly ordersService;
constructor(ordersService: OrdersService);
@@ -10,17 +10,19 @@ export declare class CustomerOrdersController {
userId: string;
companyId: string;
companyName: string;
settlementType: import("@prisma/client").$Enums.SettlementType;
settlementMethod: import("@prisma/client").$Enums.SettlementType;
settlementType: import("@prisma/client").$Enums.SettlementType | null;
settlementMethod: import("@prisma/client").$Enums.SettlementType | null;
checkMode: import("@prisma/client").$Enums.CheckRegistrationMode | undefined;
status: import("@prisma/client").$Enums.OrderStatus;
adminStatus: string;
allowedTransitions: import("@prisma/client").$Enums.OrderStatus[];
totalMeterage: number;
meterage: number;
totalAmount: number | null;
shippingCost: number | null;
totalDiscount: number | null;
finalAmount: number | null;
pricesVisible: boolean;
notes: string | undefined;
checkRassDays: number | undefined;
checkRassDate: string | undefined;
@@ -42,8 +44,8 @@ export declare class CustomerOrdersController {
confirmedMeterage: number | null;
unitPrice: number | null;
supplyPrice: number | null;
unitDiscount: number;
discount: number;
unitDiscount: number | null;
discount: number | null;
lineTotal: number | null;
totalAmount: number | null;
notes: string | undefined;
@@ -111,17 +113,19 @@ export declare class CustomerOrdersController {
userId: string;
companyId: string;
companyName: string;
settlementType: import("@prisma/client").$Enums.SettlementType;
settlementMethod: import("@prisma/client").$Enums.SettlementType;
settlementType: import("@prisma/client").$Enums.SettlementType | null;
settlementMethod: import("@prisma/client").$Enums.SettlementType | null;
checkMode: import("@prisma/client").$Enums.CheckRegistrationMode | undefined;
status: import("@prisma/client").$Enums.OrderStatus;
adminStatus: string;
allowedTransitions: import("@prisma/client").$Enums.OrderStatus[];
totalMeterage: number;
meterage: number;
totalAmount: number | null;
shippingCost: number | null;
totalDiscount: number | null;
finalAmount: number | null;
pricesVisible: boolean;
notes: string | undefined;
checkRassDays: number | undefined;
checkRassDate: string | undefined;
@@ -143,8 +147,8 @@ export declare class CustomerOrdersController {
confirmedMeterage: number | null;
unitPrice: number | null;
supplyPrice: number | null;
unitDiscount: number;
discount: number;
unitDiscount: number | null;
discount: number | null;
lineTotal: number | null;
totalAmount: number | null;
notes: string | undefined;
@@ -212,17 +216,19 @@ export declare class CustomerOrdersController {
userId: string;
companyId: string;
companyName: string;
settlementType: import("@prisma/client").$Enums.SettlementType;
settlementMethod: import("@prisma/client").$Enums.SettlementType;
settlementType: import("@prisma/client").$Enums.SettlementType | null;
settlementMethod: import("@prisma/client").$Enums.SettlementType | null;
checkMode: import("@prisma/client").$Enums.CheckRegistrationMode | undefined;
status: import("@prisma/client").$Enums.OrderStatus;
adminStatus: string;
allowedTransitions: import("@prisma/client").$Enums.OrderStatus[];
totalMeterage: number;
meterage: number;
totalAmount: number | null;
shippingCost: number | null;
totalDiscount: number | null;
finalAmount: number | null;
pricesVisible: boolean;
notes: string | undefined;
checkRassDays: number | undefined;
checkRassDate: string | undefined;
@@ -244,8 +250,111 @@ export declare class CustomerOrdersController {
confirmedMeterage: number | null;
unitPrice: number | null;
supplyPrice: number | null;
unitDiscount: number;
discount: number;
unitDiscount: number | null;
discount: number | null;
lineTotal: number | null;
totalAmount: number | null;
notes: string | undefined;
}[];
checks: {
id: string;
orderId: string;
row: number;
amount: number;
dueDate: string;
sayadId: string | undefined;
checkNumber: string | undefined;
ownerNationalId: string | undefined;
ownerFirstName: string | undefined;
ownerLastName: string | undefined;
bankName: string;
branchName: string | undefined;
frontImage: string | undefined;
backImage: string | undefined;
notes: string | undefined;
mode: import("@prisma/client").$Enums.CheckRegistrationMode;
status: import("@prisma/client").$Enums.CheckStatus;
createdAt: string;
updatedAt: string;
}[];
cashReceipt: {
id: string;
amount: number;
paidAmount: number | null;
transferDate: string | null;
transferTime: string | undefined;
trackingNumber: string | undefined;
trackingCode: string | undefined;
destinationAccountId: string | undefined;
receiptImage: string | undefined;
notes: string | undefined;
expiresAt: string | undefined;
status: import("@prisma/client").$Enums.CashPaymentStatus;
submittedAt: string | undefined;
createdAt: string;
} | undefined;
shipment: {
id: string;
orderId: string;
carrier: string;
trackingNumber: string;
meterage: number;
status: import("@prisma/client").$Enums.ShipmentStatus;
shippedAt: string | null;
deliveredAt: string | null;
} | undefined;
statusHistory: {
id: string;
fromStatus: import("@prisma/client").$Enums.OrderStatus | null;
toStatus: import("@prisma/client").$Enums.OrderStatus;
changedBy: string;
note: string | undefined;
reason: string | undefined;
changedAt: string;
}[];
}>;
chooseSettlement(user: AuthPrincipal, id: string, dto: ChooseSettlementDto): Promise<{
id: string;
orderNumber: string;
userId: string;
companyId: string;
companyName: string;
settlementType: import("@prisma/client").$Enums.SettlementType | null;
settlementMethod: import("@prisma/client").$Enums.SettlementType | null;
checkMode: import("@prisma/client").$Enums.CheckRegistrationMode | undefined;
status: import("@prisma/client").$Enums.OrderStatus;
adminStatus: string;
allowedTransitions: import("@prisma/client").$Enums.OrderStatus[];
totalMeterage: number;
meterage: number;
totalAmount: number | null;
shippingCost: number | null;
totalDiscount: number | null;
finalAmount: number | null;
pricesVisible: boolean;
notes: string | undefined;
checkRassDays: number | undefined;
checkRassDate: string | undefined;
cashInvoiceExpiresAt: string | undefined;
createdAt: string;
updatedAt: string;
customerName: string;
customerPhone: string;
customerNationalId: string | undefined;
customerType: import("@prisma/client").$Enums.CustomerType;
items: {
id: string;
productId: string | undefined;
catalogCode: string;
productName: string;
productCode: string | undefined;
color: string;
requestedMeterage: number;
confirmedMeterage: number | null;
unitPrice: number | null;
supplyPrice: number | null;
unitDiscount: number | null;
discount: number | null;
lineTotal: number | null;
totalAmount: number | null;
notes: string | undefined;
@@ -313,17 +422,19 @@ export declare class CustomerOrdersController {
userId: string;
companyId: string;
companyName: string;
settlementType: import("@prisma/client").$Enums.SettlementType;
settlementMethod: import("@prisma/client").$Enums.SettlementType;
settlementType: import("@prisma/client").$Enums.SettlementType | null;
settlementMethod: import("@prisma/client").$Enums.SettlementType | null;
checkMode: import("@prisma/client").$Enums.CheckRegistrationMode | undefined;
status: import("@prisma/client").$Enums.OrderStatus;
adminStatus: string;
allowedTransitions: import("@prisma/client").$Enums.OrderStatus[];
totalMeterage: number;
meterage: number;
totalAmount: number | null;
shippingCost: number | null;
totalDiscount: number | null;
finalAmount: number | null;
pricesVisible: boolean;
notes: string | undefined;
checkRassDays: number | undefined;
checkRassDate: string | undefined;
@@ -345,8 +456,8 @@ export declare class CustomerOrdersController {
confirmedMeterage: number | null;
unitPrice: number | null;
supplyPrice: number | null;
unitDiscount: number;
discount: number;
unitDiscount: number | null;
discount: number | null;
lineTotal: number | null;
totalAmount: number | null;
notes: string | undefined;
@@ -418,17 +529,19 @@ export declare class AdminOrdersController {
userId: string;
companyId: string;
companyName: string;
settlementType: import("@prisma/client").$Enums.SettlementType;
settlementMethod: import("@prisma/client").$Enums.SettlementType;
settlementType: import("@prisma/client").$Enums.SettlementType | null;
settlementMethod: import("@prisma/client").$Enums.SettlementType | null;
checkMode: import("@prisma/client").$Enums.CheckRegistrationMode | undefined;
status: import("@prisma/client").$Enums.OrderStatus;
adminStatus: string;
allowedTransitions: import("@prisma/client").$Enums.OrderStatus[];
totalMeterage: number;
meterage: number;
totalAmount: number | null;
shippingCost: number | null;
totalDiscount: number | null;
finalAmount: number | null;
pricesVisible: boolean;
notes: string | undefined;
checkRassDays: number | undefined;
checkRassDate: string | undefined;
@@ -450,8 +563,8 @@ export declare class AdminOrdersController {
confirmedMeterage: number | null;
unitPrice: number | null;
supplyPrice: number | null;
unitDiscount: number;
discount: number;
unitDiscount: number | null;
discount: number | null;
lineTotal: number | null;
totalAmount: number | null;
notes: string | undefined;
@@ -519,17 +632,19 @@ export declare class AdminOrdersController {
userId: string;
companyId: string;
companyName: string;
settlementType: import("@prisma/client").$Enums.SettlementType;
settlementMethod: import("@prisma/client").$Enums.SettlementType;
settlementType: import("@prisma/client").$Enums.SettlementType | null;
settlementMethod: import("@prisma/client").$Enums.SettlementType | null;
checkMode: import("@prisma/client").$Enums.CheckRegistrationMode | undefined;
status: import("@prisma/client").$Enums.OrderStatus;
adminStatus: string;
allowedTransitions: import("@prisma/client").$Enums.OrderStatus[];
totalMeterage: number;
meterage: number;
totalAmount: number | null;
shippingCost: number | null;
totalDiscount: number | null;
finalAmount: number | null;
pricesVisible: boolean;
notes: string | undefined;
checkRassDays: number | undefined;
checkRassDate: string | undefined;
@@ -551,8 +666,8 @@ export declare class AdminOrdersController {
confirmedMeterage: number | null;
unitPrice: number | null;
supplyPrice: number | null;
unitDiscount: number;
discount: number;
unitDiscount: number | null;
discount: number | null;
lineTotal: number | null;
totalAmount: number | null;
notes: string | undefined;
@@ -620,17 +735,19 @@ export declare class AdminOrdersController {
userId: string;
companyId: string;
companyName: string;
settlementType: import("@prisma/client").$Enums.SettlementType;
settlementMethod: import("@prisma/client").$Enums.SettlementType;
settlementType: import("@prisma/client").$Enums.SettlementType | null;
settlementMethod: import("@prisma/client").$Enums.SettlementType | null;
checkMode: import("@prisma/client").$Enums.CheckRegistrationMode | undefined;
status: import("@prisma/client").$Enums.OrderStatus;
adminStatus: string;
allowedTransitions: import("@prisma/client").$Enums.OrderStatus[];
totalMeterage: number;
meterage: number;
totalAmount: number | null;
shippingCost: number | null;
totalDiscount: number | null;
finalAmount: number | null;
pricesVisible: boolean;
notes: string | undefined;
checkRassDays: number | undefined;
checkRassDate: string | undefined;
@@ -652,8 +769,8 @@ export declare class AdminOrdersController {
confirmedMeterage: number | null;
unitPrice: number | null;
supplyPrice: number | null;
unitDiscount: number;
discount: number;
unitDiscount: number | null;
discount: number | null;
lineTotal: number | null;
totalAmount: number | null;
notes: string | undefined;
@@ -721,17 +838,19 @@ export declare class AdminOrdersController {
userId: string;
companyId: string;
companyName: string;
settlementType: import("@prisma/client").$Enums.SettlementType;
settlementMethod: import("@prisma/client").$Enums.SettlementType;
settlementType: import("@prisma/client").$Enums.SettlementType | null;
settlementMethod: import("@prisma/client").$Enums.SettlementType | null;
checkMode: import("@prisma/client").$Enums.CheckRegistrationMode | undefined;
status: import("@prisma/client").$Enums.OrderStatus;
adminStatus: string;
allowedTransitions: import("@prisma/client").$Enums.OrderStatus[];
totalMeterage: number;
meterage: number;
totalAmount: number | null;
shippingCost: number | null;
totalDiscount: number | null;
finalAmount: number | null;
pricesVisible: boolean;
notes: string | undefined;
checkRassDays: number | undefined;
checkRassDate: string | undefined;
@@ -753,8 +872,8 @@ export declare class AdminOrdersController {
confirmedMeterage: number | null;
unitPrice: number | null;
supplyPrice: number | null;
unitDiscount: number;
discount: number;
unitDiscount: number | null;
discount: number | null;
lineTotal: number | null;
totalAmount: number | null;
notes: string | undefined;
@@ -822,17 +941,19 @@ export declare class AdminOrdersController {
userId: string;
companyId: string;
companyName: string;
settlementType: import("@prisma/client").$Enums.SettlementType;
settlementMethod: import("@prisma/client").$Enums.SettlementType;
settlementType: import("@prisma/client").$Enums.SettlementType | null;
settlementMethod: import("@prisma/client").$Enums.SettlementType | null;
checkMode: import("@prisma/client").$Enums.CheckRegistrationMode | undefined;
status: import("@prisma/client").$Enums.OrderStatus;
adminStatus: string;
allowedTransitions: import("@prisma/client").$Enums.OrderStatus[];
totalMeterage: number;
meterage: number;
totalAmount: number | null;
shippingCost: number | null;
totalDiscount: number | null;
finalAmount: number | null;
pricesVisible: boolean;
notes: string | undefined;
checkRassDays: number | undefined;
checkRassDate: string | undefined;
@@ -854,8 +975,8 @@ export declare class AdminOrdersController {
confirmedMeterage: number | null;
unitPrice: number | null;
supplyPrice: number | null;
unitDiscount: number;
discount: number;
unitDiscount: number | null;
discount: number | null;
lineTotal: number | null;
totalAmount: number | null;
notes: string | undefined;
+13
View File
@@ -32,6 +32,9 @@ let CustomerOrdersController = class CustomerOrdersController {
submit(user, dto) {
return this.ordersService.submitOrder(user.id, dto);
}
chooseSettlement(user, id, dto) {
return this.ordersService.chooseSettlement(user.id, id, dto);
}
updateChecks(user, id, dto) {
return this.ordersService.updateChecks(user.id, id, dto);
}
@@ -63,6 +66,16 @@ __decorate([
__metadata("design:paramtypes", [Object, order_dto_1.SubmitOrderDto]),
__metadata("design:returntype", void 0)
], CustomerOrdersController.prototype, "submit", null);
__decorate([
(0, auth_decorators_1.AuthType)('customer'),
(0, common_1.Post)(':id/settlement'),
__param(0, (0, current_user_decorator_1.CurrentUser)()),
__param(1, (0, common_1.Param)('id')),
__param(2, (0, common_1.Body)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, String, order_dto_1.ChooseSettlementDto]),
__metadata("design:returntype", void 0)
], CustomerOrdersController.prototype, "chooseSettlement", null);
__decorate([
(0, auth_decorators_1.AuthType)('customer'),
(0, common_1.Patch)(':id/checks'),
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"orders.controller.js","sourceRoot":"","sources":["../../src/orders/orders.controller.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"orders.controller.js","sourceRoot":"","sources":["../../src/orders/orders.controller.ts"],"names":[],"mappings":"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"}
+4 -1
View File
@@ -10,13 +10,16 @@ exports.OrdersModule = void 0;
const common_1 = require("@nestjs/common");
const orders_controller_1 = require("./orders.controller");
const orders_service_1 = require("./orders.service");
const cash_invoice_scheduler_service_1 = require("./cash-invoice-scheduler.service");
const notifications_module_1 = require("../notifications/notifications.module");
let OrdersModule = class OrdersModule {
};
exports.OrdersModule = OrdersModule;
exports.OrdersModule = OrdersModule = __decorate([
(0, common_1.Module)({
imports: [notifications_module_1.NotificationsModule],
controllers: [orders_controller_1.CustomerOrdersController, orders_controller_1.AdminOrdersController],
providers: [orders_service_1.OrdersService],
providers: [orders_service_1.OrdersService, cash_invoice_scheduler_service_1.CashInvoiceSchedulerService],
exports: [orders_service_1.OrdersService],
})
], OrdersModule);
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"orders.module.js","sourceRoot":"","sources":["../../src/orders/orders.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,2DAG6B;AAC7B,qDAAiD;AAO1C,IAAM,YAAY,GAAlB,MAAM,YAAY;CAAG,CAAA;AAAf,oCAAY;uBAAZ,YAAY;IALxB,IAAA,eAAM,EAAC;QACN,WAAW,EAAE,CAAC,4CAAwB,EAAE,yCAAqB,CAAC;QAC9D,SAAS,EAAE,CAAC,8BAAa,CAAC;QAC1B,OAAO,EAAE,CAAC,8BAAa,CAAC;KACzB,CAAC;GACW,YAAY,CAAG"}
{"version":3,"file":"orders.module.js","sourceRoot":"","sources":["../../src/orders/orders.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,2DAG6B;AAC7B,qDAAiD;AACjD,qFAA+E;AAC/E,gFAA4E;AAQrE,IAAM,YAAY,GAAlB,MAAM,YAAY;CAAG,CAAA;AAAf,oCAAY;uBAAZ,YAAY;IANxB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE,CAAC,0CAAmB,CAAC;QAC9B,WAAW,EAAE,CAAC,4CAAwB,EAAE,yCAAqB,CAAC;QAC9D,SAAS,EAAE,CAAC,8BAAa,EAAE,4DAA2B,CAAC;QACvD,OAAO,EAAE,CAAC,8BAAa,CAAC;KACzB,CAAC;GACW,YAAY,CAAG"}
+167 -42
View File
@@ -1,6 +1,7 @@
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { AdminCreateOrderDto, SubmitOrderDto, UpdateOrderChecksDto, UpdateOrderPricingDto, UpdateOrderStatusDto } from './dto/order.dto';
import { AdminCreateOrderDto, ChooseSettlementDto, SubmitOrderDto, UpdateOrderChecksDto, UpdateOrderPricingDto, UpdateOrderStatusDto } from './dto/order.dto';
import { NotificationsService } from '../notifications/notifications.service';
declare const orderInclude: {
items: true;
checks: {
@@ -23,24 +24,27 @@ type OrderFull = Prisma.OrderGetPayload<{
}>;
export declare class OrdersService {
private readonly prisma;
constructor(prisma: PrismaService);
private readonly notifications;
constructor(prisma: PrismaService, notifications: NotificationsService);
findMyOrders(userId: string): Promise<{
id: string;
orderNumber: string;
userId: string;
companyId: string;
companyName: string;
settlementType: import("@prisma/client").$Enums.SettlementType;
settlementMethod: import("@prisma/client").$Enums.SettlementType;
settlementType: import("@prisma/client").$Enums.SettlementType | null;
settlementMethod: import("@prisma/client").$Enums.SettlementType | null;
checkMode: import("@prisma/client").$Enums.CheckRegistrationMode | undefined;
status: import("@prisma/client").$Enums.OrderStatus;
adminStatus: string;
allowedTransitions: import("@prisma/client").$Enums.OrderStatus[];
totalMeterage: number;
meterage: number;
totalAmount: number | null;
shippingCost: number | null;
totalDiscount: number | null;
finalAmount: number | null;
pricesVisible: boolean;
notes: string | undefined;
checkRassDays: number | undefined;
checkRassDate: string | undefined;
@@ -62,8 +66,8 @@ export declare class OrdersService {
confirmedMeterage: number | null;
unitPrice: number | null;
supplyPrice: number | null;
unitDiscount: number;
discount: number;
unitDiscount: number | null;
discount: number | null;
lineTotal: number | null;
totalAmount: number | null;
notes: string | undefined;
@@ -131,17 +135,19 @@ export declare class OrdersService {
userId: string;
companyId: string;
companyName: string;
settlementType: import("@prisma/client").$Enums.SettlementType;
settlementMethod: import("@prisma/client").$Enums.SettlementType;
settlementType: import("@prisma/client").$Enums.SettlementType | null;
settlementMethod: import("@prisma/client").$Enums.SettlementType | null;
checkMode: import("@prisma/client").$Enums.CheckRegistrationMode | undefined;
status: import("@prisma/client").$Enums.OrderStatus;
adminStatus: string;
allowedTransitions: import("@prisma/client").$Enums.OrderStatus[];
totalMeterage: number;
meterage: number;
totalAmount: number | null;
shippingCost: number | null;
totalDiscount: number | null;
finalAmount: number | null;
pricesVisible: boolean;
notes: string | undefined;
checkRassDays: number | undefined;
checkRassDate: string | undefined;
@@ -163,8 +169,8 @@ export declare class OrdersService {
confirmedMeterage: number | null;
unitPrice: number | null;
supplyPrice: number | null;
unitDiscount: number;
discount: number;
unitDiscount: number | null;
discount: number | null;
lineTotal: number | null;
totalAmount: number | null;
notes: string | undefined;
@@ -235,17 +241,19 @@ export declare class OrdersService {
userId: string;
companyId: string;
companyName: string;
settlementType: import("@prisma/client").$Enums.SettlementType;
settlementMethod: import("@prisma/client").$Enums.SettlementType;
settlementType: import("@prisma/client").$Enums.SettlementType | null;
settlementMethod: import("@prisma/client").$Enums.SettlementType | null;
checkMode: import("@prisma/client").$Enums.CheckRegistrationMode | undefined;
status: import("@prisma/client").$Enums.OrderStatus;
adminStatus: string;
allowedTransitions: import("@prisma/client").$Enums.OrderStatus[];
totalMeterage: number;
meterage: number;
totalAmount: number | null;
shippingCost: number | null;
totalDiscount: number | null;
finalAmount: number | null;
pricesVisible: boolean;
notes: string | undefined;
checkRassDays: number | undefined;
checkRassDate: string | undefined;
@@ -267,8 +275,8 @@ export declare class OrdersService {
confirmedMeterage: number | null;
unitPrice: number | null;
supplyPrice: number | null;
unitDiscount: number;
discount: number;
unitDiscount: number | null;
discount: number | null;
lineTotal: number | null;
totalAmount: number | null;
notes: string | undefined;
@@ -336,17 +344,19 @@ export declare class OrdersService {
userId: string;
companyId: string;
companyName: string;
settlementType: import("@prisma/client").$Enums.SettlementType;
settlementMethod: import("@prisma/client").$Enums.SettlementType;
settlementType: import("@prisma/client").$Enums.SettlementType | null;
settlementMethod: import("@prisma/client").$Enums.SettlementType | null;
checkMode: import("@prisma/client").$Enums.CheckRegistrationMode | undefined;
status: import("@prisma/client").$Enums.OrderStatus;
adminStatus: string;
allowedTransitions: import("@prisma/client").$Enums.OrderStatus[];
totalMeterage: number;
meterage: number;
totalAmount: number | null;
shippingCost: number | null;
totalDiscount: number | null;
finalAmount: number | null;
pricesVisible: boolean;
notes: string | undefined;
checkRassDays: number | undefined;
checkRassDate: string | undefined;
@@ -368,8 +378,8 @@ export declare class OrdersService {
confirmedMeterage: number | null;
unitPrice: number | null;
supplyPrice: number | null;
unitDiscount: number;
discount: number;
unitDiscount: number | null;
discount: number | null;
lineTotal: number | null;
totalAmount: number | null;
notes: string | undefined;
@@ -437,17 +447,19 @@ export declare class OrdersService {
userId: string;
companyId: string;
companyName: string;
settlementType: import("@prisma/client").$Enums.SettlementType;
settlementMethod: import("@prisma/client").$Enums.SettlementType;
settlementType: import("@prisma/client").$Enums.SettlementType | null;
settlementMethod: import("@prisma/client").$Enums.SettlementType | null;
checkMode: import("@prisma/client").$Enums.CheckRegistrationMode | undefined;
status: import("@prisma/client").$Enums.OrderStatus;
adminStatus: string;
allowedTransitions: import("@prisma/client").$Enums.OrderStatus[];
totalMeterage: number;
meterage: number;
totalAmount: number | null;
shippingCost: number | null;
totalDiscount: number | null;
finalAmount: number | null;
pricesVisible: boolean;
notes: string | undefined;
checkRassDays: number | undefined;
checkRassDate: string | undefined;
@@ -469,8 +481,111 @@ export declare class OrdersService {
confirmedMeterage: number | null;
unitPrice: number | null;
supplyPrice: number | null;
unitDiscount: number;
discount: number;
unitDiscount: number | null;
discount: number | null;
lineTotal: number | null;
totalAmount: number | null;
notes: string | undefined;
}[];
checks: {
id: string;
orderId: string;
row: number;
amount: number;
dueDate: string;
sayadId: string | undefined;
checkNumber: string | undefined;
ownerNationalId: string | undefined;
ownerFirstName: string | undefined;
ownerLastName: string | undefined;
bankName: string;
branchName: string | undefined;
frontImage: string | undefined;
backImage: string | undefined;
notes: string | undefined;
mode: import("@prisma/client").$Enums.CheckRegistrationMode;
status: import("@prisma/client").$Enums.CheckStatus;
createdAt: string;
updatedAt: string;
}[];
cashReceipt: {
id: string;
amount: number;
paidAmount: number | null;
transferDate: string | null;
transferTime: string | undefined;
trackingNumber: string | undefined;
trackingCode: string | undefined;
destinationAccountId: string | undefined;
receiptImage: string | undefined;
notes: string | undefined;
expiresAt: string | undefined;
status: import("@prisma/client").$Enums.CashPaymentStatus;
submittedAt: string | undefined;
createdAt: string;
} | undefined;
shipment: {
id: string;
orderId: string;
carrier: string;
trackingNumber: string;
meterage: number;
status: import("@prisma/client").$Enums.ShipmentStatus;
shippedAt: string | null;
deliveredAt: string | null;
} | undefined;
statusHistory: {
id: string;
fromStatus: import("@prisma/client").$Enums.OrderStatus | null;
toStatus: import("@prisma/client").$Enums.OrderStatus;
changedBy: string;
note: string | undefined;
reason: string | undefined;
changedAt: string;
}[];
}>;
chooseSettlement(userId: string, id: string, dto: ChooseSettlementDto): Promise<{
id: string;
orderNumber: string;
userId: string;
companyId: string;
companyName: string;
settlementType: import("@prisma/client").$Enums.SettlementType | null;
settlementMethod: import("@prisma/client").$Enums.SettlementType | null;
checkMode: import("@prisma/client").$Enums.CheckRegistrationMode | undefined;
status: import("@prisma/client").$Enums.OrderStatus;
adminStatus: string;
allowedTransitions: import("@prisma/client").$Enums.OrderStatus[];
totalMeterage: number;
meterage: number;
totalAmount: number | null;
shippingCost: number | null;
totalDiscount: number | null;
finalAmount: number | null;
pricesVisible: boolean;
notes: string | undefined;
checkRassDays: number | undefined;
checkRassDate: string | undefined;
cashInvoiceExpiresAt: string | undefined;
createdAt: string;
updatedAt: string;
customerName: string;
customerPhone: string;
customerNationalId: string | undefined;
customerType: import("@prisma/client").$Enums.CustomerType;
items: {
id: string;
productId: string | undefined;
catalogCode: string;
productName: string;
productCode: string | undefined;
color: string;
requestedMeterage: number;
confirmedMeterage: number | null;
unitPrice: number | null;
supplyPrice: number | null;
unitDiscount: number | null;
discount: number | null;
lineTotal: number | null;
totalAmount: number | null;
notes: string | undefined;
@@ -538,17 +653,19 @@ export declare class OrdersService {
userId: string;
companyId: string;
companyName: string;
settlementType: import("@prisma/client").$Enums.SettlementType;
settlementMethod: import("@prisma/client").$Enums.SettlementType;
settlementType: import("@prisma/client").$Enums.SettlementType | null;
settlementMethod: import("@prisma/client").$Enums.SettlementType | null;
checkMode: import("@prisma/client").$Enums.CheckRegistrationMode | undefined;
status: import("@prisma/client").$Enums.OrderStatus;
adminStatus: string;
allowedTransitions: import("@prisma/client").$Enums.OrderStatus[];
totalMeterage: number;
meterage: number;
totalAmount: number | null;
shippingCost: number | null;
totalDiscount: number | null;
finalAmount: number | null;
pricesVisible: boolean;
notes: string | undefined;
checkRassDays: number | undefined;
checkRassDate: string | undefined;
@@ -570,8 +687,8 @@ export declare class OrdersService {
confirmedMeterage: number | null;
unitPrice: number | null;
supplyPrice: number | null;
unitDiscount: number;
discount: number;
unitDiscount: number | null;
discount: number | null;
lineTotal: number | null;
totalAmount: number | null;
notes: string | undefined;
@@ -639,17 +756,19 @@ export declare class OrdersService {
userId: string;
companyId: string;
companyName: string;
settlementType: import("@prisma/client").$Enums.SettlementType;
settlementMethod: import("@prisma/client").$Enums.SettlementType;
settlementType: import("@prisma/client").$Enums.SettlementType | null;
settlementMethod: import("@prisma/client").$Enums.SettlementType | null;
checkMode: import("@prisma/client").$Enums.CheckRegistrationMode | undefined;
status: import("@prisma/client").$Enums.OrderStatus;
adminStatus: string;
allowedTransitions: import("@prisma/client").$Enums.OrderStatus[];
totalMeterage: number;
meterage: number;
totalAmount: number | null;
shippingCost: number | null;
totalDiscount: number | null;
finalAmount: number | null;
pricesVisible: boolean;
notes: string | undefined;
checkRassDays: number | undefined;
checkRassDate: string | undefined;
@@ -671,8 +790,8 @@ export declare class OrdersService {
confirmedMeterage: number | null;
unitPrice: number | null;
supplyPrice: number | null;
unitDiscount: number;
discount: number;
unitDiscount: number | null;
discount: number | null;
lineTotal: number | null;
totalAmount: number | null;
notes: string | undefined;
@@ -740,17 +859,19 @@ export declare class OrdersService {
userId: string;
companyId: string;
companyName: string;
settlementType: import("@prisma/client").$Enums.SettlementType;
settlementMethod: import("@prisma/client").$Enums.SettlementType;
settlementType: import("@prisma/client").$Enums.SettlementType | null;
settlementMethod: import("@prisma/client").$Enums.SettlementType | null;
checkMode: import("@prisma/client").$Enums.CheckRegistrationMode | undefined;
status: import("@prisma/client").$Enums.OrderStatus;
adminStatus: string;
allowedTransitions: import("@prisma/client").$Enums.OrderStatus[];
totalMeterage: number;
meterage: number;
totalAmount: number | null;
shippingCost: number | null;
totalDiscount: number | null;
finalAmount: number | null;
pricesVisible: boolean;
notes: string | undefined;
checkRassDays: number | undefined;
checkRassDate: string | undefined;
@@ -772,8 +893,8 @@ export declare class OrdersService {
confirmedMeterage: number | null;
unitPrice: number | null;
supplyPrice: number | null;
unitDiscount: number;
discount: number;
unitDiscount: number | null;
discount: number | null;
lineTotal: number | null;
totalAmount: number | null;
notes: string | undefined;
@@ -841,17 +962,19 @@ export declare class OrdersService {
userId: string;
companyId: string;
companyName: string;
settlementType: import("@prisma/client").$Enums.SettlementType;
settlementMethod: import("@prisma/client").$Enums.SettlementType;
settlementType: import("@prisma/client").$Enums.SettlementType | null;
settlementMethod: import("@prisma/client").$Enums.SettlementType | null;
checkMode: import("@prisma/client").$Enums.CheckRegistrationMode | undefined;
status: import("@prisma/client").$Enums.OrderStatus;
adminStatus: string;
allowedTransitions: import("@prisma/client").$Enums.OrderStatus[];
totalMeterage: number;
meterage: number;
totalAmount: number | null;
shippingCost: number | null;
totalDiscount: number | null;
finalAmount: number | null;
pricesVisible: boolean;
notes: string | undefined;
checkRassDays: number | undefined;
checkRassDate: string | undefined;
@@ -873,8 +996,8 @@ export declare class OrdersService {
confirmedMeterage: number | null;
unitPrice: number | null;
supplyPrice: number | null;
unitDiscount: number;
discount: number;
unitDiscount: number | null;
discount: number | null;
lineTotal: number | null;
totalAmount: number | null;
notes: string | undefined;
@@ -946,17 +1069,19 @@ export declare class OrdersService {
userId: string;
companyId: string;
companyName: string;
settlementType: import("@prisma/client").$Enums.SettlementType;
settlementMethod: import("@prisma/client").$Enums.SettlementType;
settlementType: import("@prisma/client").$Enums.SettlementType | null;
settlementMethod: import("@prisma/client").$Enums.SettlementType | null;
checkMode: import("@prisma/client").$Enums.CheckRegistrationMode | undefined;
status: import("@prisma/client").$Enums.OrderStatus;
adminStatus: string;
allowedTransitions: import("@prisma/client").$Enums.OrderStatus[];
totalMeterage: number;
meterage: number;
totalAmount: number | null;
shippingCost: number | null;
totalDiscount: number | null;
finalAmount: number | null;
pricesVisible: boolean;
notes: string | undefined;
checkRassDays: number | undefined;
checkRassDate: string | undefined;
@@ -978,8 +1103,8 @@ export declare class OrdersService {
confirmedMeterage: number | null;
unitPrice: number | null;
supplyPrice: number | null;
unitDiscount: number;
discount: number;
unitDiscount: number | null;
discount: number | null;
lineTotal: number | null;
totalAmount: number | null;
notes: string | undefined;
+132 -72
View File
@@ -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
+1 -1
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -4,6 +4,7 @@ export declare class UpdateSettingsDto {
defaultSettlementCheck?: boolean;
defaultSettlementCash?: boolean;
smsOrderRegistered?: boolean;
smsPricingReady?: boolean;
smsPaymentReminder?: boolean;
smsCheckReview?: boolean;
smsShippingUpdate?: boolean;
+6
View File
@@ -18,6 +18,7 @@ class UpdateSettingsDto {
defaultSettlementCheck;
defaultSettlementCash;
smsOrderRegistered;
smsPricingReady;
smsPaymentReminder;
smsCheckReview;
smsShippingUpdate;
@@ -54,6 +55,11 @@ __decorate([
(0, class_validator_1.IsBoolean)(),
__metadata("design:type", Boolean)
], UpdateSettingsDto.prototype, "smsOrderRegistered", void 0);
__decorate([
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsBoolean)(),
__metadata("design:type", Boolean)
], UpdateSettingsDto.prototype, "smsPricingReady", void 0);
__decorate([
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsBoolean)(),
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"settings.dto.js","sourceRoot":"","sources":["../../../src/settings/dto/settings.dto.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,qDAA8E;AAC9E,yDAAyC;AAEzC,MAAa,iBAAiB;IAK5B,qBAAqB,CAAU;IAM/B,wBAAwB,CAAU;IAIlC,sBAAsB,CAAW;IAIjC,qBAAqB,CAAW;IAIhC,kBAAkB,CAAW;IAI7B,kBAAkB,CAAW;IAI7B,cAAc,CAAW;IAIzB,iBAAiB,CAAW;IAI5B,YAAY,CAAiB;IAI7B,YAAY,CAAiB;CAC9B;AA5CD,8CA4CC;AAvCC;IAJC,IAAA,4BAAU,GAAE;IACZ,IAAA,wBAAI,EAAC,GAAG,EAAE,CAAC,MAAM,CAAC;IAClB,IAAA,uBAAK,GAAE;IACP,IAAA,qBAAG,EAAC,CAAC,CAAC;;gEACwB;AAM/B;IAJC,IAAA,4BAAU,GAAE;IACZ,IAAA,wBAAI,EAAC,GAAG,EAAE,CAAC,MAAM,CAAC;IAClB,IAAA,uBAAK,GAAE;IACP,IAAA,qBAAG,EAAC,CAAC,CAAC;;mEAC2B;AAIlC;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,2BAAS,GAAE;;iEACqB;AAIjC;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,2BAAS,GAAE;;gEACoB;AAIhC;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,2BAAS,GAAE;;6DACiB;AAI7B;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,2BAAS,GAAE;;6DACiB;AAI7B;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,2BAAS,GAAE;;yDACa;AAIzB;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,2BAAS,GAAE;;4DACgB;AAI5B;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;;uDACkB;AAI7B;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;;uDACkB"}
{"version":3,"file":"settings.dto.js","sourceRoot":"","sources":["../../../src/settings/dto/settings.dto.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,qDAA8E;AAC9E,yDAAyC;AAEzC,MAAa,iBAAiB;IAK5B,qBAAqB,CAAU;IAM/B,wBAAwB,CAAU;IAIlC,sBAAsB,CAAW;IAIjC,qBAAqB,CAAW;IAIhC,kBAAkB,CAAW;IAI7B,eAAe,CAAW;IAI1B,kBAAkB,CAAW;IAI7B,cAAc,CAAW;IAIzB,iBAAiB,CAAW;IAI5B,YAAY,CAAiB;IAI7B,YAAY,CAAiB;CAC9B;AAhDD,8CAgDC;AA3CC;IAJC,IAAA,4BAAU,GAAE;IACZ,IAAA,wBAAI,EAAC,GAAG,EAAE,CAAC,MAAM,CAAC;IAClB,IAAA,uBAAK,GAAE;IACP,IAAA,qBAAG,EAAC,CAAC,CAAC;;gEACwB;AAM/B;IAJC,IAAA,4BAAU,GAAE;IACZ,IAAA,wBAAI,EAAC,GAAG,EAAE,CAAC,MAAM,CAAC;IAClB,IAAA,uBAAK,GAAE;IACP,IAAA,qBAAG,EAAC,CAAC,CAAC;;mEAC2B;AAIlC;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,2BAAS,GAAE;;iEACqB;AAIjC;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,2BAAS,GAAE;;gEACoB;AAIhC;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,2BAAS,GAAE;;6DACiB;AAI7B;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,2BAAS,GAAE;;0DACc;AAI1B;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,2BAAS,GAAE;;6DACiB;AAI7B;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,2BAAS,GAAE;;yDACa;AAIzB;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,2BAAS,GAAE;;4DACgB;AAI5B;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;;uDACkB;AAI7B;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;;uDACkB"}
+2
View File
@@ -10,6 +10,7 @@ export declare class SettingsController {
defaultSettlementCheck: boolean;
defaultSettlementCash: boolean;
smsOrderRegistered: boolean;
smsPricingReady: boolean;
smsPaymentReminder: boolean;
smsCheckReview: boolean;
smsShippingUpdate: boolean;
@@ -24,6 +25,7 @@ export declare class SettingsController {
defaultSettlementCheck: boolean;
defaultSettlementCash: boolean;
smsOrderRegistered: boolean;
smsPricingReady: boolean;
smsPaymentReminder: boolean;
smsCheckReview: boolean;
smsShippingUpdate: boolean;
+2
View File
@@ -10,6 +10,7 @@ export declare class SettingsService {
defaultSettlementCheck: boolean;
defaultSettlementCash: boolean;
smsOrderRegistered: boolean;
smsPricingReady: boolean;
smsPaymentReminder: boolean;
smsCheckReview: boolean;
smsShippingUpdate: boolean;
@@ -24,6 +25,7 @@ export declare class SettingsService {
defaultSettlementCheck: boolean;
defaultSettlementCash: boolean;
smsOrderRegistered: boolean;
smsPricingReady: boolean;
smsPaymentReminder: boolean;
smsCheckReview: boolean;
smsShippingUpdate: boolean;
+2
View File
@@ -31,6 +31,7 @@ let SettingsService = class SettingsService {
defaultSettlementCheck: dto.defaultSettlementCheck,
defaultSettlementCash: dto.defaultSettlementCash,
smsOrderRegistered: dto.smsOrderRegistered,
smsPricingReady: dto.smsPricingReady,
smsPaymentReminder: dto.smsPaymentReminder,
smsCheckReview: dto.smsCheckReview,
smsShippingUpdate: dto.smsShippingUpdate,
@@ -59,6 +60,7 @@ let SettingsService = class SettingsService {
defaultSettlementCheck: settings.defaultSettlementCheck,
defaultSettlementCash: settings.defaultSettlementCash,
smsOrderRegistered: settings.smsOrderRegistered,
smsPricingReady: settings.smsPricingReady,
smsPaymentReminder: settings.smsPaymentReminder,
smsCheckReview: settings.smsCheckReview,
smsShippingUpdate: settings.smsShippingUpdate,
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"settings.service.js","sourceRoot":"","sources":["../../src/settings/settings.service.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAA4C;AAC5C,6DAAyD;AAIlD,IAAM,eAAe,GAArB,MAAM,eAAe;IACG;IAA7B,YAA6B,MAAqB;QAArB,WAAM,GAAN,MAAM,CAAe;IAAG,CAAC;IAEtD,KAAK,CAAC,GAAG;QACP,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,cAAc,EAAE,CAAC;QAC7C,OAAO,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC5B,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,GAAsB;QACjC,MAAM,IAAI,CAAC,cAAc,EAAE,CAAC;QAC5B,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC;YACvD,KAAK,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE;YACxB,IAAI,EAAE;gBACJ,qBAAqB,EAAE,GAAG,CAAC,qBAAqB;gBAChD,wBAAwB,EAAE,GAAG,CAAC,wBAAwB;gBACtD,sBAAsB,EAAE,GAAG,CAAC,sBAAsB;gBAClD,qBAAqB,EAAE,GAAG,CAAC,qBAAqB;gBAChD,kBAAkB,EAAE,GAAG,CAAC,kBAAkB;gBAC1C,kBAAkB,EAAE,GAAG,CAAC,kBAAkB;gBAC1C,cAAc,EAAE,GAAG,CAAC,cAAc;gBAClC,iBAAiB,EAAE,GAAG,CAAC,iBAAiB;gBACxC,YAAY,EACV,GAAG,CAAC,YAAY,KAAK,SAAS;oBAC5B,CAAC,CAAC,SAAS;oBACX,CAAC,CAAC,GAAG,CAAC,YAAY,EAAE,IAAI,EAAE,IAAI,IAAI;gBACtC,YAAY,EACV,GAAG,CAAC,YAAY,KAAK,SAAS;oBAC5B,CAAC,CAAC,SAAS;oBACX,CAAC,CAAC,GAAG,CAAC,YAAY,EAAE,IAAI,EAAE,IAAI,IAAI;aACvC;SACF,CAAC,CAAC;QACH,OAAO,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC5B,CAAC;IAEO,KAAK,CAAC,cAAc;QAC1B,OAAO,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC;YACvC,KAAK,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE;YACxB,MAAM,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE;YACzB,MAAM,EAAE,EAAE;SACX,CAAC,CAAC;IACL,CAAC;IAEO,GAAG,CAAC,QAaX;QACC,OAAO;YACL,EAAE,EAAE,QAAQ,CAAC,EAAE;YACf,qBAAqB,EAAE,QAAQ,CAAC,qBAAqB;YACrD,wBAAwB,EAAE,QAAQ,CAAC,wBAAwB;YAC3D,sBAAsB,EAAE,QAAQ,CAAC,sBAAsB;YACvD,qBAAqB,EAAE,QAAQ,CAAC,qBAAqB;YACrD,kBAAkB,EAAE,QAAQ,CAAC,kBAAkB;YAC/C,kBAAkB,EAAE,QAAQ,CAAC,kBAAkB;YAC/C,cAAc,EAAE,QAAQ,CAAC,cAAc;YACvC,iBAAiB,EAAE,QAAQ,CAAC,iBAAiB;YAC7C,YAAY,EAAE,QAAQ,CAAC,YAAY;YACnC,YAAY,EAAE,QAAQ,CAAC,YAAY;YACnC,SAAS,EAAE,QAAQ,CAAC,SAAS,CAAC,WAAW,EAAE;SAC5C,CAAC;IACJ,CAAC;CACF,CAAA;AAvEY,0CAAe;0BAAf,eAAe;IAD3B,IAAA,mBAAU,GAAE;qCAE0B,8BAAa;GADvC,eAAe,CAuE3B"}
{"version":3,"file":"settings.service.js","sourceRoot":"","sources":["../../src/settings/settings.service.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAA4C;AAC5C,6DAAyD;AAIlD,IAAM,eAAe,GAArB,MAAM,eAAe;IACG;IAA7B,YAA6B,MAAqB;QAArB,WAAM,GAAN,MAAM,CAAe;IAAG,CAAC;IAEtD,KAAK,CAAC,GAAG;QACP,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,cAAc,EAAE,CAAC;QAC7C,OAAO,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC5B,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,GAAsB;QACjC,MAAM,IAAI,CAAC,cAAc,EAAE,CAAC;QAC5B,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC;YACvD,KAAK,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE;YACxB,IAAI,EAAE;gBACJ,qBAAqB,EAAE,GAAG,CAAC,qBAAqB;gBAChD,wBAAwB,EAAE,GAAG,CAAC,wBAAwB;gBACtD,sBAAsB,EAAE,GAAG,CAAC,sBAAsB;gBAClD,qBAAqB,EAAE,GAAG,CAAC,qBAAqB;gBAChD,kBAAkB,EAAE,GAAG,CAAC,kBAAkB;gBAC1C,eAAe,EAAE,GAAG,CAAC,eAAe;gBACpC,kBAAkB,EAAE,GAAG,CAAC,kBAAkB;gBAC1C,cAAc,EAAE,GAAG,CAAC,cAAc;gBAClC,iBAAiB,EAAE,GAAG,CAAC,iBAAiB;gBACxC,YAAY,EACV,GAAG,CAAC,YAAY,KAAK,SAAS;oBAC5B,CAAC,CAAC,SAAS;oBACX,CAAC,CAAC,GAAG,CAAC,YAAY,EAAE,IAAI,EAAE,IAAI,IAAI;gBACtC,YAAY,EACV,GAAG,CAAC,YAAY,KAAK,SAAS;oBAC5B,CAAC,CAAC,SAAS;oBACX,CAAC,CAAC,GAAG,CAAC,YAAY,EAAE,IAAI,EAAE,IAAI,IAAI;aACvC;SACF,CAAC,CAAC;QACH,OAAO,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC5B,CAAC;IAEO,KAAK,CAAC,cAAc;QAC1B,OAAO,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC;YACvC,KAAK,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE;YACxB,MAAM,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE;YACzB,MAAM,EAAE,EAAE;SACX,CAAC,CAAC;IACL,CAAC;IAEO,GAAG,CAAC,QAcX;QACC,OAAO;YACL,EAAE,EAAE,QAAQ,CAAC,EAAE;YACf,qBAAqB,EAAE,QAAQ,CAAC,qBAAqB;YACrD,wBAAwB,EAAE,QAAQ,CAAC,wBAAwB;YAC3D,sBAAsB,EAAE,QAAQ,CAAC,sBAAsB;YACvD,qBAAqB,EAAE,QAAQ,CAAC,qBAAqB;YACrD,kBAAkB,EAAE,QAAQ,CAAC,kBAAkB;YAC/C,eAAe,EAAE,QAAQ,CAAC,eAAe;YACzC,kBAAkB,EAAE,QAAQ,CAAC,kBAAkB;YAC/C,cAAc,EAAE,QAAQ,CAAC,cAAc;YACvC,iBAAiB,EAAE,QAAQ,CAAC,iBAAiB;YAC7C,YAAY,EAAE,QAAQ,CAAC,YAAY;YACnC,YAAY,EAAE,QAAQ,CAAC,YAAY;YACnC,SAAS,EAAE,QAAQ,CAAC,SAAS,CAAC,WAAW,EAAE;SAC5C,CAAC;IACJ,CAAC;CACF,CAAA;AA1EY,0CAAe;0BAAf,eAAe;IAD3B,IAAA,mBAAU,GAAE;qCAE0B,8BAAa;GADvC,eAAe,CA0E3B"}
+2
View File
@@ -10,11 +10,13 @@ exports.ShipmentsModule = void 0;
const common_1 = require("@nestjs/common");
const shipments_controller_1 = require("./shipments.controller");
const shipments_service_1 = require("./shipments.service");
const notifications_module_1 = require("../notifications/notifications.module");
let ShipmentsModule = class ShipmentsModule {
};
exports.ShipmentsModule = ShipmentsModule;
exports.ShipmentsModule = ShipmentsModule = __decorate([
(0, common_1.Module)({
imports: [notifications_module_1.NotificationsModule],
controllers: [shipments_controller_1.ShipmentsController],
providers: [shipments_service_1.ShipmentsService],
exports: [shipments_service_1.ShipmentsService],
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"shipments.module.js","sourceRoot":"","sources":["../../src/shipments/shipments.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,iEAA6D;AAC7D,2DAAuD;AAOhD,IAAM,eAAe,GAArB,MAAM,eAAe;CAAG,CAAA;AAAlB,0CAAe;0BAAf,eAAe;IAL3B,IAAA,eAAM,EAAC;QACN,WAAW,EAAE,CAAC,0CAAmB,CAAC;QAClC,SAAS,EAAE,CAAC,oCAAgB,CAAC;QAC7B,OAAO,EAAE,CAAC,oCAAgB,CAAC;KAC5B,CAAC;GACW,eAAe,CAAG"}
{"version":3,"file":"shipments.module.js","sourceRoot":"","sources":["../../src/shipments/shipments.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,iEAA6D;AAC7D,2DAAuD;AACvD,gFAA4E;AAQrE,IAAM,eAAe,GAArB,MAAM,eAAe;CAAG,CAAA;AAAlB,0CAAe;0BAAf,eAAe;IAN3B,IAAA,eAAM,EAAC;QACN,OAAO,EAAE,CAAC,0CAAmB,CAAC;QAC9B,WAAW,EAAE,CAAC,0CAAmB,CAAC;QAClC,SAAS,EAAE,CAAC,oCAAgB,CAAC;QAC7B,OAAO,EAAE,CAAC,oCAAgB,CAAC;KAC5B,CAAC;GACW,eAAe,CAAG"}
+4 -1
View File
@@ -1,9 +1,11 @@
import { ShipmentStatus } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { AdvanceShipmentStatusDto, CreateShipmentDto, UpdateShipmentDto } from './dto/shipment.dto';
import { NotificationsService } from '../notifications/notifications.service';
export declare class ShipmentsService {
private readonly prisma;
constructor(prisma: PrismaService);
private readonly notifications;
constructor(prisma: PrismaService, notifications: NotificationsService);
findAll(status?: ShipmentStatus): Promise<{
id: string;
orderId: string;
@@ -74,6 +76,7 @@ export declare class ShipmentsService {
createdAt: string;
updatedAt: string;
}>;
private notifyOrderStatus;
private ensure;
private map;
}
+21 -2
View File
@@ -14,6 +14,8 @@ const common_1 = require("@nestjs/common");
const client_1 = require("@prisma/client");
const prisma_service_1 = require("../prisma/prisma.service");
const name_1 = require("../common/utils/name");
const notifications_service_1 = require("../notifications/notifications.service");
const notification_events_1 = require("../notifications/notification-events");
const STATUS_FLOW = [
client_1.ShipmentStatus.preparing,
client_1.ShipmentStatus.ready,
@@ -28,8 +30,10 @@ const SHIPMENT_TO_ORDER = {
};
let ShipmentsService = class ShipmentsService {
prisma;
constructor(prisma) {
notifications;
constructor(prisma, notifications) {
this.prisma = prisma;
this.notifications = notifications;
}
async findAll(status) {
const shipments = await this.prisma.shipment.findMany({
@@ -92,6 +96,10 @@ let ShipmentsService = class ShipmentsService {
}
return created;
});
this.notifyOrderStatus(order.id, SHIPMENT_TO_ORDER[status], {
carrier: shipment.carrier,
trackingNumber: shipment.trackingNumber,
});
return this.map(shipment);
}
async update(id, dto) {
@@ -160,8 +168,18 @@ let ShipmentsService = class ShipmentsService {
}
return result;
});
this.notifyOrderStatus(shipment.orderId, SHIPMENT_TO_ORDER[nextStatus], {
carrier: updated.carrier,
trackingNumber: updated.trackingNumber,
});
return this.map(updated);
}
notifyOrderStatus(orderId, status, context) {
const event = (0, notification_events_1.eventForStatus)(status);
if (event) {
this.notifications.dispatchOrderEvent(orderId, event, context);
}
}
async ensure(id) {
const shipment = await this.prisma.shipment.findUnique({ where: { id } });
if (!shipment)
@@ -188,6 +206,7 @@ let ShipmentsService = class ShipmentsService {
exports.ShipmentsService = ShipmentsService;
exports.ShipmentsService = ShipmentsService = __decorate([
(0, common_1.Injectable)(),
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
__metadata("design:paramtypes", [prisma_service_1.PrismaService,
notifications_service_1.NotificationsService])
], ShipmentsService);
//# sourceMappingURL=shipments.service.js.map
File diff suppressed because one or more lines are too long
+46
View File
@@ -15,6 +15,7 @@
"@nestjs/jwt": "^11.0.2",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/schedule": "^6.1.3",
"@nestjs/swagger": "^11.4.6",
"@prisma/client": "^6.19.3",
"bcryptjs": "^3.0.3",
@@ -2452,6 +2453,19 @@
"@nestjs/core": "^11.0.0"
}
},
"node_modules/@nestjs/schedule": {
"version": "6.1.3",
"resolved": "https://registry.npmmirror.com/@nestjs/schedule/-/schedule-6.1.3.tgz",
"integrity": "sha512-RflMFOpR16Dwd1jAUbeB4mfGTCh65fvEdL4mSjQPJChpkRGRjIXjb+6YQcK2faQrVT60c9DmLmoVR7/ONCtuYQ==",
"license": "MIT",
"dependencies": {
"cron": "4.4.0"
},
"peerDependencies": {
"@nestjs/common": "^10.0.0 || ^11.0.0",
"@nestjs/core": "^10.0.0 || ^11.0.0"
}
},
"node_modules/@nestjs/schematics": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.1.0.tgz",
@@ -3073,6 +3087,12 @@
"@types/node": "*"
}
},
"node_modules/@types/luxon": {
"version": "3.7.4",
"resolved": "https://registry.npmmirror.com/@types/luxon/-/luxon-3.7.4.tgz",
"integrity": "sha512-V536ZAd6ZJztrrBlLcDFaaZrXNAL2E5uGmssWf/dpSiLkmkLScXUYhUBnWPmtW+cIqnNHzf6//TCMpIc9SCRRQ==",
"license": "MIT"
},
"node_modules/@types/methods": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
@@ -5146,6 +5166,23 @@
"dev": true,
"license": "MIT"
},
"node_modules/cron": {
"version": "4.4.0",
"resolved": "https://registry.npmmirror.com/cron/-/cron-4.4.0.tgz",
"integrity": "sha512-fkdfq+b+AHI4cKdhZlppHveI/mgz2qpiYxcm+t5E5TsxX7QrLS1VE0+7GENEk9z0EeGPcpSciGv6ez24duWhwQ==",
"license": "MIT",
"dependencies": {
"@types/luxon": "~3.7.0",
"luxon": "~3.7.0"
},
"engines": {
"node": ">=18.x"
},
"funding": {
"type": "ko-fi",
"url": "https://ko-fi.com/intcreator"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -7979,6 +8016,15 @@
"yallist": "^3.0.2"
}
},
"node_modules/luxon": {
"version": "3.7.2",
"resolved": "https://registry.npmmirror.com/luxon/-/luxon-3.7.2.tgz",
"integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==",
"license": "MIT",
"engines": {
"node": ">=12"
}
},
"node_modules/magic-string": {
"version": "0.30.17",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
+1
View File
@@ -43,6 +43,7 @@
"@nestjs/jwt": "^11.0.2",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/schedule": "^6.1.3",
"@nestjs/swagger": "^11.4.6",
"@prisma/client": "^6.19.3",
"bcryptjs": "^3.0.3",
Binary file not shown.
@@ -0,0 +1,2 @@
-- روش تسویه دیگر در لحظه ثبت سفارش گرفته نمی‌شود؛ مشتری بعد از قیمت‌گذاری انتخاب می‌کند.
ALTER TABLE "Order" ALTER COLUMN "settlementType" DROP NOT NULL;
@@ -0,0 +1,24 @@
-- اعلان درون‌برنامه‌ای مشتری به‌همراه سوئیچ پیامک «فاکتور آماده شد»
ALTER TABLE "SystemSettings" ADD COLUMN "smsPricingReady" BOOLEAN NOT NULL DEFAULT true;
CREATE TABLE "CustomerNotification" (
"id" TEXT NOT NULL,
"customerId" TEXT NOT NULL,
"orderId" TEXT,
"event" TEXT NOT NULL,
"title" TEXT NOT NULL,
"body" TEXT NOT NULL,
"tone" "NotificationTone" DEFAULT 'info',
"link" TEXT,
"isRead" BOOLEAN NOT NULL DEFAULT false,
"smsSent" BOOLEAN NOT NULL DEFAULT false,
"sentAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "CustomerNotification_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "CustomerNotification_customerId_isRead_idx" ON "CustomerNotification"("customerId", "isRead");
CREATE INDEX "CustomerNotification_sentAt_idx" ON "CustomerNotification"("sentAt");
ALTER TABLE "CustomerNotification" ADD CONSTRAINT "CustomerNotification_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "CustomerNotification" ADD CONSTRAINT "CustomerNotification_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1,3 @@
-- سقف متراژ شرکت اختیاری می‌شود؛ خالی یعنی از SystemSettings ارث می‌برد.
ALTER TABLE "Company" ALTER COLUMN "maxMeteragePerInvoice" DROP NOT NULL;
ALTER TABLE "Company" ALTER COLUMN "maxMeteragePerInvoice" DROP DEFAULT;
@@ -0,0 +1,4 @@
-- یادآوری پایان مهلت پرداخت فقط یک‌بار برای هر فاکتور ارسال می‌شود.
ALTER TABLE "Order" ADD COLUMN "cashReminderSentAt" TIMESTAMP(3);
CREATE INDEX "Order_status_cashInvoiceExpiresAt_idx" ON "Order"("status", "cashInvoiceExpiresAt");
+35 -7
View File
@@ -94,7 +94,8 @@ model Company {
logoUrl String?
isActive Boolean @default(true)
displayOrder Int @default(0)
maxMeteragePerInvoice Int @default(5000)
/// خالی یعنی از تنظیمات سیستم ارث می‌برد
maxMeteragePerInvoice Int?
cashInvoiceValidityHours Int @default(8)
cashInvoiceValidUntilHour Int?
createdAt DateTime @default(now())
@@ -183,9 +184,10 @@ model Customer {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
company Company? @relation(fields: [companyId], references: [id], onDelete: SetNull)
carts Cart[]
orders Order[]
company Company? @relation(fields: [companyId], references: [id], onDelete: SetNull)
carts Cart[]
orders Order[]
notifications CustomerNotification[]
@@index([companyId])
}
@@ -258,7 +260,8 @@ model Order {
userId String
companyId String
companyName String
settlementType SettlementType
// روش تسویه بعد از قیمت‌گذاری ادمین توسط مشتری انتخاب می‌شود
settlementType SettlementType?
checkMode CheckRegistrationMode?
status OrderStatus @default(awaiting_review)
totalMeterage Float @default(0)
@@ -270,21 +273,25 @@ model Order {
checkRassDays Int?
checkRassDate DateTime?
cashInvoiceExpiresAt DateTime?
/// زمان ارسال یادآوری پایان مهلت پرداخت تا دوباره ارسال نشود
cashReminderSentAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user Customer @relation(fields: [userId], references: [id], onDelete: Restrict)
company Company @relation(fields: [companyId], references: [id], onDelete: Restrict)
user Customer @relation(fields: [userId], references: [id], onDelete: Restrict)
company Company @relation(fields: [companyId], references: [id], onDelete: Restrict)
items OrderItem[]
checks Check[]
cashReceipt CashPaymentReceipt?
shipment Shipment?
statusHistory OrderStatusHistory[]
notifications CustomerNotification[]
@@index([userId])
@@index([companyId])
@@index([status])
@@index([createdAt])
@@index([status, cashInvoiceExpiresAt])
}
model OrderItem {
@@ -393,6 +400,7 @@ model SystemSettings {
defaultSettlementCheck Boolean @default(true)
defaultSettlementCash Boolean @default(true)
smsOrderRegistered Boolean @default(true)
smsPricingReady Boolean @default(true)
smsPaymentReminder Boolean @default(true)
smsCheckReview Boolean @default(true)
smsShippingUpdate Boolean @default(true)
@@ -411,6 +419,26 @@ model AdminNotification {
sentAt DateTime @default(now())
}
model CustomerNotification {
id String @id @default(cuid())
customerId String
orderId String?
event String
title String
body String
tone NotificationTone? @default(info)
link String?
isRead Boolean @default(false)
smsSent Boolean @default(false)
sentAt DateTime @default(now())
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
order Order? @relation(fields: [orderId], references: [id], onDelete: SetNull)
@@index([customerId, isRead])
@@index([sentAt])
}
model InternalMessage {
id String @id @default(cuid())
senderName String
+2 -2
View File
@@ -91,7 +91,7 @@ async function main() {
description: 'تولیدکننده پارچه‌های باکیفیت',
isActive: true,
displayOrder: 1,
maxMeteragePerInvoice: 5000,
maxMeteragePerInvoice: null,
cashInvoiceValidityHours: 8,
cashInvoiceValidUntilHour: 17,
},
@@ -102,7 +102,7 @@ async function main() {
description: 'عرضه پارچه‌های تخصصی',
isActive: true,
displayOrder: 2,
maxMeteragePerInvoice: 5000,
maxMeteragePerInvoice: null,
cashInvoiceValidityHours: 8,
cashInvoiceValidUntilHour: 17,
},
+308 -113
View File
@@ -1,6 +1,7 @@
/**
* تست دود جریان کامل سفارش: ورود مشتری، سبد، ثبت چکی و نقدی،
* قیمت‌گذاری ادمین، رسید نقدی و بررسی وضعیت‌ها.
* تست دود گردش کامل سفارش مطابق روال کارفرما:
* مشتری بدون دیدن قیمت سفارش می‌دهد، ادمین قیمت می‌گذارد و فاکتور می‌فرستد،
* مشتری روش تسویه را انتخاب می‌کند و مسیر نقدی/چکی تا تایید نهایی پیش می‌رود.
* اجرا: node scripts/smoke-order-flow.mjs
*/
const BASE = process.env.SMOKE_BASE_URL || 'http://127.0.0.1:3999/api';
@@ -41,9 +42,8 @@ async function api(path, { method = 'GET', body, token } = {}) {
return { status: res.status, data };
}
function jalaliIsoInDays(days) {
const d = new Date(Date.now() + days * 86400000);
return d.toISOString().slice(0, 10);
function isoInDays(days) {
return new Date(Date.now() + days * 86400000).toISOString().slice(0, 10);
}
/** تصویر base64 بزرگ‌تر از سقف پیش‌فرض ۱۰۰KB اکسپرس */
@@ -51,6 +51,22 @@ function bigBase64(kb) {
return `data:image/jpeg;base64,${'A'.repeat(kb * 1024)}`;
}
function checkPayload(amount, days, extra = {}) {
return {
amount,
dueDate: isoInDays(days),
sayadId: '1234567890123456',
checkNumber: '123456',
ownerNationalId: '0012345678',
ownerFirstName: 'علی',
ownerLastName: 'رضایی',
bankName: 'ملی',
branchName: 'مرکزی',
mode: 'full',
...extra,
};
}
async function main() {
console.log(`\n== Smoke test @ ${BASE} ==\n`);
@@ -65,7 +81,7 @@ async function main() {
const badVerify = await api('/auth/otp/verify', {
method: 'POST',
body: { phone: CUSTOMER_PHONE, code: '0000' === code ? '1111' : '0000' },
body: { phone: CUSTOMER_PHONE, code: String(code) === '0000' ? '1111' : '0000' },
});
check('کد اشتباه رد می‌شود', badVerify.status === 401, `status=${badVerify.status}`);
@@ -91,10 +107,15 @@ async function main() {
const noToken = await api('/orders');
check('بدون توکن ۴۰۱', noToken.status === 401, `status=${noToken.status}`);
console.log('\n[3] سبد خرید');
console.log('\n[3] سبد خرید بدون قیمت');
const companies = await api('/companies/public');
const company = companies.data?.[0];
check('لیست شرکت‌ها', !!company, JSON.stringify(companies.data).slice(0, 120));
check(
'سقف متراژ موثر برگردانده می‌شود',
typeof company?.maxMeteragePerInvoice === 'number' && company.maxMeteragePerInvoice > 0,
`max=${company?.maxMeteragePerInvoice}`,
);
const products = await api(`/products?companyId=${company.id}`, { token: userToken });
const product = (Array.isArray(products.data) ? products.data : products.data?.items)?.find(
@@ -110,89 +131,63 @@ async function main() {
body: { productId: product.id, meterage },
});
check('افزودن به سبد', addItem.status < 300, `status=${addItem.status} ${JSON.stringify(addItem.data).slice(0, 120)}`);
const cartItem = addItem.data?.items?.[0];
check('سبد قیمت واحد ندارد', cartItem?.unitPrice === undefined, `unitPrice=${cartItem?.unitPrice}`);
console.log('\n[4] ثبت سفارش چکی با تصویر بزرگ');
const checkOrder = await api('/orders', {
method: 'POST',
token: userToken,
body: {
companyId: company.id,
settlementType: 'check',
checkMode: 'full',
items: [{ productId: product.id, meterage }],
checks: [
{
amount: 2000000,
dueDate: jalaliIsoInDays(30),
sayadId: '1234567890123456',
checkNumber: '123456',
ownerNationalId: '0012345678',
ownerFirstName: 'علی',
ownerLastName: 'رضایی',
bankName: 'ملی',
branchName: 'مرکزی',
frontImage: bigBase64(300),
backImage: bigBase64(300),
mode: 'full',
},
],
},
});
check(
'ثبت سفارش چکی (بدنه ~۶۰۰KB)',
checkOrder.status < 300,
`status=${checkOrder.status} ${JSON.stringify(checkOrder.data).slice(0, 200)}`,
);
const checkOrderId = checkOrder.data?.id;
check('راس چک محاسبه شد', checkOrder.data?.checkRassDays === 30, `rass=${checkOrder.data?.checkRassDays}`);
console.log('\n[5] فیلدهای اضافه باید رد شوند');
const extraField = await api('/orders', {
method: 'POST',
token: userToken,
body: {
companyId: company.id,
settlementType: 'cash',
items: [{ productId: product.id, meterage }],
bogusField: 'x',
},
});
check('فیلد ناشناخته ۴۰۰ می‌دهد', extraField.status === 400, `status=${extraField.status}`);
console.log('\n[6] ثبت سفارش نقدی');
await api('/carts/items', {
method: 'POST',
token: userToken,
body: { productId: product.id, meterage },
});
console.log('\n[4] ثبت سفارش بدون روش تسویه و بدون قیمت');
const cashOrder = await api('/orders', {
method: 'POST',
token: userToken,
body: { companyId: company.id, items: [{ productId: product.id, meterage }] },
});
check('ثبت سفارش', cashOrder.status < 300, `status=${cashOrder.status} ${JSON.stringify(cashOrder.data).slice(0, 200)}`);
const cashOrderId = cashOrder.data?.id;
check('وضعیت اولیه در انتظار بررسی', cashOrder.data?.status === 'awaiting_review', `status=${cashOrder.data?.status}`);
check('روش تسویه خالی است', cashOrder.data?.settlementType == null, `settlement=${cashOrder.data?.settlementType}`);
check('مبلغ برای مشتری پنهان است', cashOrder.data?.finalAmount == null && cashOrder.data?.totalAmount == null);
check('پرچم pricesVisible خاموش است', cashOrder.data?.pricesVisible === false, `flag=${cashOrder.data?.pricesVisible}`);
check(
'قیمت واحد آیتم‌ها پنهان است',
(cashOrder.data?.items ?? []).every((i) => i.unitPrice == null),
);
console.log('\n[5] اعتبارسنجی ورودی');
const extraField = await api('/orders', {
method: 'POST',
token: userToken,
body: { companyId: company.id, items: [{ productId: product.id, meterage }], bogusField: 'x' },
});
check('فیلد ناشناخته ۴۰۰ می‌دهد', extraField.status === 400, `status=${extraField.status}`);
const earlySettlement = await api(`/orders/${cashOrderId}/settlement`, {
method: 'POST',
token: userToken,
body: { settlementType: 'cash' },
});
check('انتخاب تسویه قبل از فاکتور رد می‌شود', earlySettlement.status === 400, `status=${earlySettlement.status}`);
const earlyReceipt = await api(`/orders/${cashOrderId}/cash-receipt`, {
method: 'POST',
token: userToken,
body: {
companyId: company.id,
settlementType: 'cash',
items: [{ productId: product.id, meterage }],
amount: 1000,
transferDate: isoInDays(0),
transferTime: '12:00',
trackingNumber: '1',
destinationAccountId: 'x',
},
});
check('ثبت سفارش نقدی', cashOrder.status < 300, `status=${cashOrder.status} ${JSON.stringify(cashOrder.data).slice(0, 200)}`);
const cashOrderId = cashOrder.data?.id;
const expiry = cashOrder.data?.cashInvoiceExpiresAt;
check('انقضای فاکتور در آینده است', !!expiry && new Date(expiry) > new Date(), `expiry=${expiry}`);
check('رسید قبل از فاکتور رد می‌شود', earlyReceipt.status === 400, `status=${earlyReceipt.status}`);
console.log('\n[7] سفارش در لیست مشتری دیده می‌شود');
const myOrders = await api('/orders', { token: userToken });
const ids = (myOrders.data ?? []).map((o) => o.id);
check('سفارش چکی در لیست', ids.includes(checkOrderId));
check('سفارش نقدی در لیست', ids.includes(cashOrderId));
console.log('\n[8] لیست و فیلتر ادمین');
console.log('\n[6] سطل‌های وضعیت ادمین');
const adminOrders = await api('/admin/orders', { token: adminToken });
const adminIds = (adminOrders.data ?? []).map((o) => o.id);
check('هر دو سفارش برای ادمین', adminIds.includes(checkOrderId) && adminIds.includes(cashOrderId));
const adminStatuses = new Set((adminOrders.data ?? []).map((o) => o.adminStatus));
check('سفارش برای ادمین دیده می‌شود', adminIds.includes(cashOrderId));
const allowedTabs = new Set([
'pending_review',
'awaiting_customer',
'pending_check',
'pending_payment',
'needs_correction',
'check_correction',
'final_approval',
@@ -202,20 +197,35 @@ async function main() {
'rejected',
'expired',
]);
const unknown = [...adminStatuses].filter((s) => !allowedTabs.has(s));
const unknown = [...new Set((adminOrders.data ?? []).map((o) => o.adminStatus))].filter(
(s) => !allowedTabs.has(s),
);
check('همه وضعیت‌ها به تب معتبر نگاشت شده‌اند', unknown.length === 0, `unknown=${unknown.join(',')}`);
const filtered = await api('/admin/orders?status=pending_review', { token: adminToken });
check(
'فیلتر «در انتظار بررسی» سفارش‌ها را برمی‌گرداند',
'فیلتر «در انتظار بررسی» سفارش را برمی‌گرداند',
(filtered.data ?? []).some((o) => o.id === cashOrderId),
`count=${(filtered.data ?? []).length}`,
);
console.log('\n[9] قیمت‌گذاری ادمین');
console.log('\n[7] قیمت‌گذاری ادمین');
const detail = await api(`/admin/orders/${cashOrderId}`, { token: adminToken });
const items = detail.data?.items ?? [];
check('سفارش آیتم دارد', items.length > 0);
check(
'ادمین گذرهای مجاز را می‌بیند',
Array.isArray(detail.data?.allowedTransitions) && detail.data.allowedTransitions.includes('pricing'),
`transitions=${JSON.stringify(detail.data?.allowedTransitions)}`,
);
const earlyInvoice = await api(`/admin/orders/${cashOrderId}/status`, {
method: 'PATCH',
token: adminToken,
body: { status: 'awaiting_settlement' },
});
check('ارسال فاکتور بدون قیمت‌گذاری رد می‌شود', earlyInvoice.status === 400, `status=${earlyInvoice.status}`);
const pricing = await api(`/admin/orders/${cashOrderId}/pricing`, {
method: 'PATCH',
token: adminToken,
@@ -225,17 +235,13 @@ async function main() {
},
});
const expectedTotal = 100000 * meterage;
const expectedFinal = expectedTotal + 50000;
check('قیمت‌گذاری موفق', pricing.status < 300, `status=${pricing.status} ${JSON.stringify(pricing.data).slice(0, 150)}`);
check(
'جمع کل درست است',
Math.abs((pricing.data?.totalAmount ?? 0) - expectedTotal) < 0.01,
`total=${pricing.data?.totalAmount} expected=${expectedTotal}`,
);
check(
'مبلغ نهایی = جمع + حمل',
Math.abs((pricing.data?.finalAmount ?? 0) - (expectedTotal + 50000)) < 0.01,
`final=${pricing.data?.finalAmount}`,
);
check('جمع کل درست است', Math.abs((pricing.data?.totalAmount ?? 0) - expectedTotal) < 0.01, `total=${pricing.data?.totalAmount}`);
check('مبلغ نهایی = جمع + حمل', Math.abs((pricing.data?.finalAmount ?? 0) - expectedFinal) < 0.01, `final=${pricing.data?.finalAmount}`);
const stillHidden = await api(`/orders/${cashOrderId}`, { token: userToken });
check('قیمت تا ارسال فاکتور برای مشتری پنهان است', stillHidden.data?.finalAmount == null, `final=${stillHidden.data?.finalAmount}`);
const badDiscount = await api(`/admin/orders/${cashOrderId}/pricing`, {
method: 'PATCH',
@@ -244,7 +250,64 @@ async function main() {
});
check('تخفیف بیشتر از قیمت رد می‌شود', badDiscount.status === 400, `status=${badDiscount.status}`);
console.log('\n[10] رسید پرداخت نقدی');
console.log('\n[8] ارسال فاکتور برای مشتری');
await api(`/admin/orders/${cashOrderId}/pricing`, {
method: 'PATCH',
token: adminToken,
body: {
items: [{ id: items[0].id, confirmedMeterage: meterage, unitPrice: 100000, unitDiscount: 0 }],
shippingCost: 50000,
},
});
const invoice = await api(`/admin/orders/${cashOrderId}/status`, {
method: 'PATCH',
token: adminToken,
body: { status: 'awaiting_settlement', note: 'فاکتور نهایی شد' },
});
check('ارسال فاکتور', invoice.status < 300, `status=${invoice.status} ${JSON.stringify(invoice.data).slice(0, 150)}`);
const priced = await api(`/orders/${cashOrderId}`, { token: userToken });
check('مشتری مبلغ فاکتور را می‌بیند', Math.abs((priced.data?.finalAmount ?? 0) - expectedFinal) < 0.01, `final=${priced.data?.finalAmount}`);
check('pricesVisible روشن شد', priced.data?.pricesVisible !== false);
const illegal = await api(`/admin/orders/${cashOrderId}/status`, {
method: 'PATCH',
token: adminToken,
body: { status: 'delivered' },
});
check('گذر غیرمجاز بدون force رد می‌شود', illegal.status === 400, `status=${illegal.status}`);
const forceNoReason = await api(`/admin/orders/${cashOrderId}/status`, {
method: 'PATCH',
token: adminToken,
body: { status: 'delivered', force: true },
});
check('force بدون دلیل رد می‌شود', forceNoReason.status === 400, `status=${forceNoReason.status}`);
console.log('\n[9] اعلان مشتری');
const notifs = await api('/notifications', { token: userToken });
const invoiceNotif = (notifs.data ?? []).find((n) => n.event === 'invoice_priced');
check('اعلان صدور فاکتور ساخته شد', !!invoiceNotif, JSON.stringify(notifs.data)?.slice(0, 200));
check('اعلان به سفارش لینک دارد', invoiceNotif?.link === `/orders/${cashOrderId}`, `link=${invoiceNotif?.link}`);
const unread = await api('/notifications/unread-count', { token: userToken });
check('شمارنده خوانده‌نشده مثبت است', (unread.data?.count ?? 0) > 0, `count=${unread.data?.count}`);
if (invoiceNotif) {
const marked = await api(`/notifications/${invoiceNotif.id}/read`, { method: 'PATCH', token: userToken });
check('علامت خوانده‌شده', marked.status < 300 && marked.data?.count < (unread.data?.count ?? 0), JSON.stringify(marked.data));
}
const readAll = await api('/notifications/read-all', { method: 'PATCH', token: userToken });
check('خواندن همه', readAll.status < 300 && (readAll.data?.count ?? -1) === 0, JSON.stringify(readAll.data));
console.log('\n[10] مسیر نقدی');
const chooseCash = await api(`/orders/${cashOrderId}/settlement`, {
method: 'POST',
token: userToken,
body: { settlementType: 'cash' },
});
check('انتخاب تسویه نقدی', chooseCash.status < 300, `status=${chooseCash.status} ${JSON.stringify(chooseCash.data).slice(0, 150)}`);
check('وضعیت در انتظار پرداخت', chooseCash.data?.status === 'awaiting_cash_payment', `status=${chooseCash.data?.status}`);
const expiry = chooseCash.data?.cashInvoiceExpiresAt;
check('انقضای فاکتور در آینده است', !!expiry && new Date(expiry) > new Date(), `expiry=${expiry}`);
const accounts = await api(`/companies/${company.id}/bank-accounts`, { token: userToken });
const account = (accounts.data ?? [])[0];
check('حساب بانکی موجود', !!account, JSON.stringify(accounts.data).slice(0, 120));
@@ -253,8 +316,8 @@ async function main() {
method: 'POST',
token: userToken,
body: {
amount: expectedTotal + 50000,
transferDate: jalaliIsoInDays(0),
amount: expectedFinal,
transferDate: isoInDays(0),
transferTime: '12:00',
trackingNumber: '987654321',
destinationAccountId: account.id,
@@ -262,15 +325,151 @@ async function main() {
},
});
check('ثبت رسید (تصویر ۴۰۰KB)', receipt.status < 300, `status=${receipt.status} ${JSON.stringify(receipt.data).slice(0, 200)}`);
const afterReceipt = await api(`/orders/${cashOrderId}`, { token: userToken });
check('وضعیت به تأیید پرداخت رفت', afterReceipt.data?.status === 'awaiting_cash_confirmation', `status=${afterReceipt.data?.status}`);
const receipts = await api('/admin/cash-payments', { token: adminToken });
const pendingReceipt = (receipts.data ?? []).find((r) => r.orderId === cashOrderId);
check('رسید در صف ادمین است', !!pendingReceipt, JSON.stringify(receipts.data)?.slice(0, 160));
const rejectReceipt = await api(`/admin/cash-payments/${pendingReceipt.id}/status`, {
method: 'PATCH',
token: adminToken,
body: { status: 'rejected', note: 'مبلغ نامطابق' },
});
check('رد رسید', rejectReceipt.status < 300, `status=${rejectReceipt.status}`);
const afterReject = await api(`/orders/${cashOrderId}`, { token: userToken });
check(
'وضعیت به تأیید پرداخت رفت',
afterReceipt.data?.status === 'awaiting_cash_confirmation',
`status=${afterReceipt.data?.status}`,
'رد رسید مشتری را به مرحله پرداخت برمی‌گرداند',
afterReject.data?.status === 'awaiting_cash_payment',
`status=${afterReject.data?.status}`,
);
console.log('\n[11] دسترسی متقاطع بین مشتری‌ها');
const approveReceipt = await api(`/admin/cash-payments/${pendingReceipt.id}/status`, {
method: 'PATCH',
token: adminToken,
body: { status: 'approved' },
});
check('تأیید رسید', approveReceipt.status < 300, `status=${approveReceipt.status}`);
const afterApprove = await api(`/orders/${cashOrderId}`, { token: userToken });
check('وضعیت پرداخت تأییدشده', afterApprove.data?.status === 'cash_confirmed', `status=${afterApprove.data?.status}`);
const finalApprove = await api(`/admin/orders/${cashOrderId}/status`, {
method: 'PATCH',
token: adminToken,
body: { status: 'final_approved' },
});
check('تأیید نهایی سفارش نقدی', finalApprove.status < 300, `status=${finalApprove.status}`);
console.log('\n[11] مسیر چکی با حلقه اصلاح');
await api('/carts', { method: 'DELETE', token: userToken });
await api('/carts/items', {
method: 'POST',
token: userToken,
body: { productId: product.id, meterage },
});
const checkOrder = await api('/orders', {
method: 'POST',
token: userToken,
body: { companyId: company.id, items: [{ productId: product.id, meterage }] },
});
const checkOrderId = checkOrder.data?.id;
check('ثبت سفارش دوم', checkOrder.status < 300, `status=${checkOrder.status}`);
const checkDetail = await api(`/admin/orders/${checkOrderId}`, { token: adminToken });
const checkItem = checkDetail.data?.items?.[0];
await api(`/admin/orders/${checkOrderId}/pricing`, {
method: 'PATCH',
token: adminToken,
body: { items: [{ id: checkItem.id, confirmedMeterage: meterage, unitPrice: 100000 }] },
});
await api(`/admin/orders/${checkOrderId}/status`, {
method: 'PATCH',
token: adminToken,
body: { status: 'awaiting_settlement' },
});
const chooseCheck = await api(`/orders/${checkOrderId}/settlement`, {
method: 'POST',
token: userToken,
body: { settlementType: 'check', checkMode: 'full' },
});
check('انتخاب تسویه چکی', chooseCheck.status < 300, `status=${chooseCheck.status} ${JSON.stringify(chooseCheck.data).slice(0, 150)}`);
check('وضعیت در حال ثبت چک', chooseCheck.data?.status === 'check_in_progress', `status=${chooseCheck.data?.status}`);
const submitChecks = await api(`/orders/${checkOrderId}/checks`, {
method: 'PATCH',
token: userToken,
body: {
checkMode: 'full',
checks: [
checkPayload(expectedTotal, 30, {
frontImage: bigBase64(300),
backImage: bigBase64(300),
}),
],
},
});
check('ارسال چک‌ها (بدنه ~۶۰۰KB)', submitChecks.status < 300, `status=${submitChecks.status} ${JSON.stringify(submitChecks.data).slice(0, 200)}`);
check('وضعیت بررسی چک', submitChecks.data?.status === 'awaiting_check_review', `status=${submitChecks.data?.status}`);
check('راس چک محاسبه شد', submitChecks.data?.checkRassDays === 30, `rass=${submitChecks.data?.checkRassDays}`);
const orderChecks = await api(`/admin/orders/${checkOrderId}/checks`, { token: adminToken });
const firstCheck = (orderChecks.data ?? [])[0];
check('چک برای ادمین دیده می‌شود', !!firstCheck, JSON.stringify(orderChecks.data)?.slice(0, 160));
const rejectCheck = await api(`/admin/checks/${firstCheck.id}/status`, {
method: 'PATCH',
token: adminToken,
body: { status: 'rejected', note: 'تصویر ناخوانا' },
});
check('رد چک', rejectCheck.status < 300, `status=${rejectCheck.status}`);
const afterCheckReject = await api(`/orders/${checkOrderId}`, { token: userToken });
check(
'رد چک به اصلاح می‌رود نه بن‌بست',
afterCheckReject.data?.status === 'check_needs_fix',
`status=${afterCheckReject.data?.status}`,
);
const fixChecks = await api(`/orders/${checkOrderId}/checks`, {
method: 'PATCH',
token: userToken,
body: { checkMode: 'full', checks: [checkPayload(expectedTotal, 45)] },
});
check('اصلاح چک‌ها', fixChecks.status < 300, `status=${fixChecks.status} ${JSON.stringify(fixChecks.data).slice(0, 200)}`);
check('بازگشت به بررسی چک', fixChecks.data?.status === 'awaiting_check_review', `status=${fixChecks.data?.status}`);
const fixedChecks = await api(`/admin/orders/${checkOrderId}/checks`, { token: adminToken });
const approveCheck = await api(`/admin/checks/${fixedChecks.data[0].id}/status`, {
method: 'PATCH',
token: adminToken,
body: { status: 'approved' },
});
check('تأیید چک', approveCheck.status < 300, `status=${approveCheck.status}`);
const afterCheckApprove = await api(`/orders/${checkOrderId}`, { token: userToken });
check('وضعیت تأیید چک‌ها', afterCheckApprove.data?.status === 'checks_approved', `status=${afterCheckApprove.data?.status}`);
console.log('\n[12] تنظیمات سیستم');
const settings = await api('/admin/settings', { token: adminToken });
check('خواندن تنظیمات', settings.status < 300 && typeof settings.data?.maxMeteragePerInvoice === 'number', JSON.stringify(settings.data)?.slice(0, 160));
const savedSettings = await api('/admin/settings', {
method: 'PATCH',
token: adminToken,
body: { smsPricingReady: false, maxMeteragePerInvoice: 6000 },
});
check('ذخیره تنظیمات', savedSettings.data?.maxMeteragePerInvoice === 6000 && savedSettings.data?.smsPricingReady === false, JSON.stringify(savedSettings.data)?.slice(0, 200));
const companyAfter = await api('/companies/public');
check(
'سقف متراژ شرکت از تنظیمات ارث می‌برد',
companyAfter.data?.[0]?.maxMeteragePerInvoice === 6000,
`max=${companyAfter.data?.[0]?.maxMeteragePerInvoice}`,
);
await api('/admin/settings', {
method: 'PATCH',
token: adminToken,
body: { smsPricingReady: true, maxMeteragePerInvoice: 5000 },
});
console.log('\n[13] دسترسی متقاطع بین مشتری‌ها');
const otherPhone = '09129999001';
await api('/auth/otp/request', {
method: 'POST',
@@ -285,31 +484,27 @@ async function main() {
if (otherToken) {
const foreign = await api(`/orders/${cashOrderId}`, { token: otherToken });
check('سفارش دیگری قابل خواندن نیست', foreign.status === 403, `status=${foreign.status}`);
const foreignReceipt = await api(`/orders/${cashOrderId}/cash-receipt`, {
const foreignSettlement = await api(`/orders/${checkOrderId}/settlement`, {
method: 'POST',
token: otherToken,
body: {
amount: 1000,
transferDate: jalaliIsoInDays(0),
transferTime: '12:00',
trackingNumber: '1',
destinationAccountId: account.id,
},
body: { settlementType: 'cash' },
});
check('رسید روی سفارش دیگری رد می‌شود', foreignReceipt.status === 403, `status=${foreignReceipt.status}`);
check('انتخاب تسویه روی سفارش دیگری رد می‌شود', foreignSettlement.status === 403, `status=${foreignSettlement.status}`);
const foreignNotifs = await api('/notifications', { token: otherToken });
check('اعلان‌ها بین مشتری‌ها جدا است', (foreignNotifs.data ?? []).length === 0, `count=${(foreignNotifs.data ?? []).length}`);
}
console.log('\n[12] محافظت از ویرایش چک بعد از تأیید');
const approve = await api(`/admin/orders/${checkOrderId}/status`, {
console.log('\n[14] محافظت از ویرایش بعد از تحویل');
const forceDeliver = await api(`/admin/orders/${checkOrderId}/status`, {
method: 'PATCH',
token: adminToken,
body: { status: 'delivered' },
body: { status: 'delivered', force: true, reason: 'تحویل دستی برای تست' },
});
check('تغییر وضعیت توسط ادمین', approve.status < 300, `status=${approve.status}`);
check('گذر دستی با دلیل پذیرفته می‌شود', forceDeliver.status < 300, `status=${forceDeliver.status} ${JSON.stringify(forceDeliver.data).slice(0, 150)}`);
const lateEdit = await api(`/orders/${checkOrderId}/checks`, {
method: 'PATCH',
token: userToken,
body: { checks: [{ amount: 1, dueDate: jalaliIsoInDays(10), mode: 'brief' }] },
body: { checks: [checkPayload(1000, 10)] },
});
check('ویرایش چک بعد از تحویل رد می‌شود', lateEdit.status === 400, `status=${lateEdit.status}`);
const lateReceipt = await api(`/orders/${checkOrderId}/cash-receipt`, {
@@ -317,7 +512,7 @@ async function main() {
token: userToken,
body: {
amount: 1000,
transferDate: jalaliIsoInDays(0),
transferDate: isoInDays(0),
transferTime: '12:00',
trackingNumber: '1',
destinationAccountId: account.id,
+4
View File
@@ -1,6 +1,7 @@
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { ConfigModule } from '@nestjs/config';
import { ScheduleModule } from '@nestjs/schedule';
import { PrismaModule } from './prisma/prisma.module';
import { AuthModule } from './auth/auth.module';
import { JwtAuthGuard } from './common/guards/jwt-auth.guard';
@@ -16,10 +17,12 @@ import { ShipmentsModule } from './shipments/shipments.module';
import { DashboardModule } from './dashboard/dashboard.module';
import { SettingsModule } from './settings/settings.module';
import { CartsModule } from './carts/carts.module';
import { NotificationsModule } from './notifications/notifications.module';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
ScheduleModule.forRoot(),
PrismaModule,
AuthModule,
CompaniesModule,
@@ -34,6 +37,7 @@ import { CartsModule } from './carts/carts.module';
DashboardModule,
SettingsModule,
CartsModule,
NotificationsModule,
],
providers: [
{
-4
View File
@@ -47,7 +47,6 @@ export class CartsService {
data: {
meterage: newMeterage,
notes: dto.notes?.trim() || existing.notes,
unitPrice: product.basePrice,
},
});
} else {
@@ -60,7 +59,6 @@ export class CartsService {
productCode: product.code,
color: product.color,
meterage: dto.meterage,
unitPrice: product.basePrice,
notes: dto.notes?.trim() || null,
companyId: product.companyId,
companyName: product.company.name,
@@ -188,7 +186,6 @@ export class CartsService {
productCode: string;
color: string;
meterage: number;
unitPrice: number | null;
notes: string | null;
companyId: string;
companyName: string;
@@ -209,7 +206,6 @@ export class CartsService {
productCode: item.productCode,
color: item.color,
meterage: item.meterage,
unitPrice: item.unitPrice ?? undefined,
notes: item.notes ?? undefined,
companyId: item.companyId,
companyName: item.companyName,
@@ -1,8 +1,10 @@
import { Module } from '@nestjs/common';
import { CashPaymentsController } from './cash-payments.controller';
import { CashPaymentsService } from './cash-payments.service';
import { NotificationsModule } from '../notifications/notifications.module';
@Module({
imports: [NotificationsModule],
controllers: [CashPaymentsController],
providers: [CashPaymentsService],
exports: [CashPaymentsService],
+33 -7
View File
@@ -11,14 +11,10 @@ import {
UpdateCashPaymentStatusDto,
} from './dto/cash-payment.dto';
import { joinFullName } from '../common/utils/name';
import { NotificationsService } from '../notifications/notifications.service';
/** بعد از تأیید پرداخت یا ارسال، رسید جدید نباید وضعیت را به عقب برگرداند. */
const RECEIPT_SUBMITTABLE_STATUSES: OrderStatus[] = [
OrderStatus.awaiting_review,
OrderStatus.pricing,
OrderStatus.needs_correction,
OrderStatus.awaiting_settlement,
OrderStatus.awaiting_settlement_fix,
OrderStatus.cash_in_progress,
OrderStatus.cash_invoice_issued,
OrderStatus.awaiting_cash_payment,
@@ -28,7 +24,10 @@ const RECEIPT_SUBMITTABLE_STATUSES: OrderStatus[] = [
@Injectable()
export class CashPaymentsService {
constructor(private readonly prisma: PrismaService) {}
constructor(
private readonly prisma: PrismaService,
private readonly notifications: NotificationsService,
) {}
async submitReceipt(
userId: string,
@@ -112,6 +111,14 @@ export class CashPaymentsService {
return saved;
});
this.notifications.dispatchAdmin({
title: 'رسید پرداخت نقدی ثبت شد',
body: `مشتری برای سفارش ${order.orderNumber} رسید به مبلغ ${Math.round(
dto.amount,
).toLocaleString('fa-IR')} تومان ثبت کرد.`,
link: `/orders/${orderId}`,
});
return this.mapReceipt(receipt, order.orderNumber, '');
}
@@ -157,11 +164,12 @@ export class CashPaymentsService {
include: { order: { include: { user: true } } },
});
// رد رسید باید مشتری را به مرحله پرداخت برگرداند تا بتواند رسید تازه بفرستد.
let nextStatus: OrderStatus | null = null;
if (dto.status === CashPaymentStatus.approved) {
nextStatus = OrderStatus.cash_confirmed;
} else if (dto.status === CashPaymentStatus.rejected) {
nextStatus = OrderStatus.needs_correction;
nextStatus = OrderStatus.awaiting_cash_payment;
} else if (dto.status === CashPaymentStatus.expired) {
nextStatus = OrderStatus.expired;
}
@@ -185,6 +193,24 @@ export class CashPaymentsService {
return result;
});
if (dto.status === CashPaymentStatus.approved) {
this.notifications.dispatchOrderEvent(
receipt.orderId,
'receipt_approved',
);
} else if (dto.status === CashPaymentStatus.rejected) {
this.notifications.dispatchOrderEvent(
receipt.orderId,
'receipt_rejected',
{ note: dto.note ?? null },
);
} else if (dto.status === CashPaymentStatus.expired) {
this.notifications.dispatchOrderEvent(
receipt.orderId,
'cash_invoice_expired',
);
}
return this.mapReceipt(
updated,
updated.order.orderNumber,
+2
View File
@@ -1,8 +1,10 @@
import { Module } from '@nestjs/common';
import { ChecksController } from './checks.controller';
import { ChecksService } from './checks.service';
import { NotificationsModule } from '../notifications/notifications.module';
@Module({
imports: [NotificationsModule],
controllers: [ChecksController],
providers: [ChecksService],
exports: [ChecksService],
+17 -4
View File
@@ -2,10 +2,14 @@ import { Injectable, NotFoundException } from '@nestjs/common';
import { CheckStatus, OrderStatus } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { UpdateCheckStatusDto } from './dto/check.dto';
import { NotificationsService } from '../notifications/notifications.service';
@Injectable()
export class ChecksService {
constructor(private readonly prisma: PrismaService) {}
constructor(
private readonly prisma: PrismaService,
private readonly notifications: NotificationsService,
) {}
async findByOrder(orderId: string) {
const order = await this.prisma.order.findUnique({
@@ -27,6 +31,7 @@ export class ChecksService {
});
if (!check) throw new NotFoundException('چک یافت نشد');
let notifyStatus: OrderStatus | null = null;
const updated = await this.prisma.$transaction(async (tx) => {
const result = await tx.check.update({
where: { id },
@@ -48,13 +53,12 @@ export class ChecksService {
(c) => c.status === CheckStatus.rejected,
);
// رد چک باید قابل اصلاح بماند؛ check_rejected راه برگشتی برای مشتری ندارد.
let nextStatus: OrderStatus | null = null;
if (anyRejected) {
nextStatus = OrderStatus.check_rejected;
nextStatus = OrderStatus.check_needs_fix;
} else if (allApproved) {
nextStatus = OrderStatus.checks_approved;
} else if (dto.status === CheckStatus.rejected) {
nextStatus = OrderStatus.check_needs_fix;
}
if (nextStatus && nextStatus !== check.order.status) {
@@ -71,11 +75,20 @@ export class ChecksService {
note: dto.note || `وضعیت چک به ${dto.status} تغییر کرد`,
},
});
notifyStatus = nextStatus;
}
return result;
});
if (notifyStatus === OrderStatus.checks_approved) {
this.notifications.dispatchOrderEvent(check.orderId, 'checks_approved');
} else if (notifyStatus === OrderStatus.check_needs_fix) {
this.notifications.dispatchOrderEvent(check.orderId, 'checks_need_fix', {
note: dto.note ?? null,
});
}
return this.map(updated);
}
+14
View File
@@ -0,0 +1,14 @@
/** وقتی نه شرکت و نه تنظیمات سیستم مقداری ندارند. */
export const FALLBACK_MAX_METERAGE = 5000;
/**
* سقف متراژ موثر هر فاکتور: مقدار شرکت، در نبودش تنظیمات سیستم.
*/
export function resolveMaxMeterage(
companyValue: number | null | undefined,
settingsValue: number | null | undefined,
): number {
if (companyValue != null && companyValue > 0) return companyValue;
if (settingsValue != null && settingsValue > 0) return settingsValue;
return FALLBACK_MAX_METERAGE;
}
+10 -8
View File
@@ -4,22 +4,24 @@ import { OrderStatus } from '@prisma/client';
export const ADMIN_STATUS_MAP: Record<string, OrderStatus[]> = {
pending_review: [
OrderStatus.cart,
OrderStatus.awaiting_settlement,
OrderStatus.cash_in_progress,
OrderStatus.check_in_progress,
OrderStatus.submitted,
OrderStatus.awaiting_review,
OrderStatus.pricing,
],
awaiting_customer: [
OrderStatus.awaiting_settlement,
OrderStatus.awaiting_settlement_fix,
OrderStatus.check_in_progress,
OrderStatus.cash_in_progress,
OrderStatus.cash_invoice_issued,
OrderStatus.awaiting_cash_payment,
],
pending_check: [OrderStatus.awaiting_check_review],
pending_payment: [
OrderStatus.cash_payment_submitted,
OrderStatus.awaiting_cash_confirmation,
],
pending_check: [OrderStatus.awaiting_check_review],
needs_correction: [
OrderStatus.needs_correction,
OrderStatus.awaiting_settlement_fix,
],
needs_correction: [OrderStatus.needs_correction],
check_correction: [OrderStatus.check_needs_fix, OrderStatus.check_rejected],
final_approval: [
OrderStatus.final_approved,
+101
View File
@@ -0,0 +1,101 @@
import { BadRequestException } from '@nestjs/common';
import { OrderStatus } from '@prisma/client';
import { ORDER_STATUS_LABELS } from './order-status';
/**
* گذرهای مجاز گردش سفارش. enum دیتابیس وضعیت‌های بیشتری دارد که از نسخه‌های
* قبلی مانده‌اند؛ فقط همین‌ها در جریان واقعی استفاده می‌شوند.
*
* ترتیب کار: ثبت سفارش ← قیمت‌گذاری ادمین ← ارسال فاکتور ← انتخاب تسویه توسط
* مشتری ← شاخه نقدی یا چکی ← تأیید نهایی ← ارسال.
*/
export const ORDER_TRANSITIONS: Record<OrderStatus, OrderStatus[]> = {
awaiting_review: [OrderStatus.pricing],
pricing: [OrderStatus.awaiting_settlement],
awaiting_settlement: [
OrderStatus.awaiting_cash_payment,
OrderStatus.check_in_progress,
OrderStatus.pricing,
],
// شاخه نقدی
awaiting_cash_payment: [
OrderStatus.awaiting_cash_confirmation,
OrderStatus.expired,
],
awaiting_cash_confirmation: [
OrderStatus.cash_confirmed,
OrderStatus.awaiting_cash_payment,
],
cash_confirmed: [OrderStatus.final_approved],
// شاخه چکی
check_in_progress: [OrderStatus.awaiting_check_review],
awaiting_check_review: [
OrderStatus.checks_approved,
OrderStatus.check_needs_fix,
],
check_needs_fix: [OrderStatus.awaiting_check_review],
checks_approved: [OrderStatus.final_approved],
// مسیر مشترک تا تحویل
final_approved: [OrderStatus.preparing],
preparing: [OrderStatus.ready_to_ship],
ready_to_ship: [OrderStatus.shipped],
shipped: [OrderStatus.delivered],
delivered: [],
// پایانی‌ها
cancelled: [],
rejected: [],
expired: [],
// وضعیت‌های قدیمی: فقط راه خروج دارند تا رکوردهای موجود قفل نشوند
cart: [OrderStatus.awaiting_review],
submitted: [OrderStatus.awaiting_review],
needs_correction: [OrderStatus.awaiting_review, OrderStatus.pricing],
awaiting_settlement_fix: [OrderStatus.awaiting_settlement],
cash_in_progress: [OrderStatus.awaiting_cash_payment],
cash_invoice_issued: [OrderStatus.awaiting_cash_payment],
cash_payment_submitted: [OrderStatus.awaiting_cash_confirmation],
check_rejected: [OrderStatus.check_needs_fix],
};
/** ادمین در هر مرحله‌ای می‌تواند سفارش را ببندد. */
const ADMIN_TERMINAL_TRANSITIONS: OrderStatus[] = [
OrderStatus.cancelled,
OrderStatus.rejected,
];
const FINAL_STATUSES: OrderStatus[] = [
OrderStatus.delivered,
OrderStatus.cancelled,
OrderStatus.rejected,
OrderStatus.expired,
];
export function isFinalStatus(status: OrderStatus): boolean {
return FINAL_STATUSES.includes(status);
}
/** گذرهایی که ادمین بدون حالت اجباری می‌تواند انتخاب کند. */
export function allowedTransitions(from: OrderStatus): OrderStatus[] {
const next = [...(ORDER_TRANSITIONS[from] ?? [])];
if (!isFinalStatus(from)) {
for (const terminal of ADMIN_TERMINAL_TRANSITIONS) {
if (!next.includes(terminal)) next.push(terminal);
}
}
return next;
}
export function canTransition(from: OrderStatus, to: OrderStatus): boolean {
return allowedTransitions(from).includes(to);
}
export function assertTransition(from: OrderStatus, to: OrderStatus): void {
if (canTransition(from, to)) return;
throw new BadRequestException(
`تغییر وضعیت از «${ORDER_STATUS_LABELS[from]}» به «${ORDER_STATUS_LABELS[to]}» مجاز نیست`,
);
}
+59 -35
View File
@@ -5,6 +5,7 @@ import {
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { resolveMaxMeterage } from '../common/utils/meterage';
import {
CreateBankAccountDto,
CreateCompanyDto,
@@ -17,35 +18,50 @@ export class CompaniesService {
constructor(private readonly prisma: PrismaService) {}
async findAll(activeOnly = false) {
const companies = await this.prisma.company.findMany({
where: activeOnly ? { isActive: true } : undefined,
orderBy: [{ displayOrder: 'asc' }, { name: 'asc' }],
include: {
_count: {
select: { catalogs: true, products: true, bankAccounts: true },
const [companies, fallbackMeterage] = await Promise.all([
this.prisma.company.findMany({
where: activeOnly ? { isActive: true } : undefined,
orderBy: [{ displayOrder: 'asc' }, { name: 'asc' }],
include: {
_count: {
select: { catalogs: true, products: true, bankAccounts: true },
},
},
},
});
return companies.map((c) => this.mapCompany(c));
}),
this.settingsMaxMeterage(),
]);
return companies.map((c) => this.mapCompany(c, fallbackMeterage));
}
async findOne(id: string) {
const company = await this.prisma.company.findUnique({
where: { id },
include: {
bankAccounts: { orderBy: { displayOrder: 'asc' } },
_count: {
select: { catalogs: true, products: true, bankAccounts: true },
const [company, fallbackMeterage] = await Promise.all([
this.prisma.company.findUnique({
where: { id },
include: {
bankAccounts: { orderBy: { displayOrder: 'asc' } },
_count: {
select: { catalogs: true, products: true, bankAccounts: true },
},
},
},
});
}),
this.settingsMaxMeterage(),
]);
if (!company) throw new NotFoundException('شرکت یافت نشد');
return {
...this.mapCompany(company),
...this.mapCompany(company, fallbackMeterage),
bankAccounts: company.bankAccounts.map((b) => this.mapBankAccount(b)),
};
}
/** سقف متراژ تنظیمات سیستم که شرکت‌های بدون سقف اختصاصی از آن ارث می‌برند. */
private async settingsMaxMeterage(): Promise<number | null> {
const settings = await this.prisma.systemSettings.findUnique({
where: { id: 'default' },
select: { maxMeteragePerInvoice: true },
});
return settings?.maxMeteragePerInvoice ?? null;
}
async create(dto: CreateCompanyDto) {
try {
const company = await this.prisma.company.create({
@@ -55,7 +71,7 @@ export class CompaniesService {
description: dto.description?.trim() || null,
logoUrl: dto.logoUrl?.trim() || null,
displayOrder: dto.displayOrder ?? 0,
maxMeteragePerInvoice: dto.maxMeteragePerInvoice ?? 5000,
maxMeteragePerInvoice: dto.maxMeteragePerInvoice ?? null,
cashInvoiceValidityHours: dto.cashInvoiceValidityHours ?? 8,
cashInvoiceValidUntilHour: dto.cashInvoiceValidUntilHour ?? null,
isActive: dto.isActive ?? true,
@@ -196,21 +212,24 @@ export class CompaniesService {
return account;
}
private mapCompany(company: {
id: string;
name: string;
slug: string;
description: string | null;
logoUrl: string | null;
isActive: boolean;
displayOrder: number;
maxMeteragePerInvoice: number;
cashInvoiceValidityHours: number;
cashInvoiceValidUntilHour: number | null;
createdAt: Date;
updatedAt: Date;
_count?: { catalogs: number; products: number; bankAccounts: number };
}) {
private mapCompany(
company: {
id: string;
name: string;
slug: string;
description: string | null;
logoUrl: string | null;
isActive: boolean;
displayOrder: number;
maxMeteragePerInvoice: number | null;
cashInvoiceValidityHours: number;
cashInvoiceValidUntilHour: number | null;
createdAt: Date;
updatedAt: Date;
_count?: { catalogs: number; products: number; bankAccounts: number };
},
fallbackMaxMeterage: number | null = null,
) {
return {
id: company.id,
name: company.name,
@@ -220,7 +239,12 @@ export class CompaniesService {
logo: company.logoUrl ?? undefined,
isActive: company.isActive,
displayOrder: company.displayOrder,
maxMeteragePerInvoice: company.maxMeteragePerInvoice,
maxMeteragePerInvoice: resolveMaxMeterage(
company.maxMeteragePerInvoice,
fallbackMaxMeterage,
),
/** مقدار خام شرکت؛ خالی یعنی از تنظیمات سیستم ارث می‌برد */
ownMaxMeteragePerInvoice: company.maxMeteragePerInvoice ?? undefined,
cashInvoiceValidityHours: company.cashInvoiceValidityHours,
cashInvoiceValidUntilHour: company.cashInvoiceValidUntilHour ?? undefined,
catalogCount: company._count?.catalogs ?? 0,
+193
View File
@@ -0,0 +1,193 @@
import { NotificationTone, OrderStatus } from '@prisma/client';
/** رویدادهایی که برای مشتری اعلان و پیامک تولید می‌کنند. */
export type CustomerEvent =
| 'order_submitted'
| 'invoice_priced'
| 'checks_approved'
| 'checks_need_fix'
| 'receipt_approved'
| 'receipt_rejected'
| 'cash_invoice_reminder'
| 'cash_invoice_expired'
| 'order_preparing'
| 'order_ready_to_ship'
| 'order_shipped'
| 'order_delivered'
| 'order_cancelled';
/** کلید سوئیچ‌های پیامک در SystemSettings؛ null یعنی پیامک ندارد. */
type SmsSwitch =
| 'smsOrderRegistered'
| 'smsPricingReady'
| 'smsPaymentReminder'
| 'smsCheckReview'
| 'smsShippingUpdate'
| null;
export type CustomerEventTemplate = {
title: string;
/** متن اعلان درون‌برنامه‌ای */
body: (ctx: EventContext) => string;
/** متن پیامک؛ نبودنش یعنی فقط اعلان درون‌برنامه‌ای */
sms?: (ctx: EventContext) => string;
tone: NotificationTone;
smsSwitch: SmsSwitch;
};
export type EventContext = {
orderNumber: string;
appName: string;
amount?: number | null;
note?: string | null;
deadline?: string | null;
carrier?: string | null;
trackingNumber?: string | null;
};
function money(amount?: number | null): string {
if (!amount) return '—';
return `${Math.round(amount).toLocaleString('fa-IR')} تومان`;
}
/**
* وضعیت‌هایی که به‌تنهایی برای مشتری خبر دارند. وضعیت‌های میانی مثل
* awaiting_check_review خبری برای مشتری ندارند و اینجا نمی‌آیند.
*/
export function eventForStatus(status: OrderStatus): CustomerEvent | null {
switch (status) {
case OrderStatus.awaiting_settlement:
return 'invoice_priced';
case OrderStatus.preparing:
case OrderStatus.final_approved:
return 'order_preparing';
case OrderStatus.ready_to_ship:
return 'order_ready_to_ship';
case OrderStatus.shipped:
return 'order_shipped';
case OrderStatus.delivered:
return 'order_delivered';
case OrderStatus.cancelled:
case OrderStatus.rejected:
return 'order_cancelled';
case OrderStatus.expired:
return 'cash_invoice_expired';
default:
return null;
}
}
export const CUSTOMER_EVENTS: Record<CustomerEvent, CustomerEventTemplate> = {
order_submitted: {
title: 'سفارش شما ثبت شد',
body: (c) =>
`سفارش ${c.orderNumber} ثبت شد و برای بررسی و قیمت‌گذاری به کارشناسان ما رسید.`,
sms: (c) =>
`سفارش ${c.orderNumber} در ${c.appName} ثبت شد. پس از قیمت‌گذاری، فاکتور برای شما ارسال می‌شود.`,
tone: NotificationTone.info,
smsSwitch: 'smsOrderRegistered',
},
invoice_priced: {
title: 'فاکتور سفارش شما صادر شد',
body: (c) =>
`مبلغ فاکتور سفارش ${c.orderNumber} ${money(c.amount)} است. برای ادامه، روش تسویه را انتخاب کنید.`,
sms: (c) =>
`فاکتور سفارش ${c.orderNumber} به مبلغ ${money(c.amount)} صادر شد. برای انتخاب روش تسویه به پنل ${c.appName} مراجعه کنید.`,
tone: NotificationTone.success,
smsSwitch: 'smsPricingReady',
},
checks_approved: {
title: 'چک‌های شما تایید شد',
body: (c) => `همه چک‌های سفارش ${c.orderNumber} تایید شدند.`,
sms: (c) =>
`چک‌های سفارش ${c.orderNumber} تایید شد و سفارش به مرحله آماده‌سازی می‌رود.`,
tone: NotificationTone.success,
smsSwitch: 'smsCheckReview',
},
checks_need_fix: {
title: 'چک شما نیاز به اصلاح دارد',
body: (c) =>
`یک یا چند چک سفارش ${c.orderNumber} تایید نشد${c.note ? `: ${c.note}` : '.'} لطفاً چک‌ها را اصلاح و مجدداً ارسال کنید.`,
sms: (c) =>
`چک سفارش ${c.orderNumber} تایید نشد. برای اصلاح چک‌ها به پنل ${c.appName} مراجعه کنید.`,
tone: NotificationTone.danger,
smsSwitch: 'smsCheckReview',
},
receipt_approved: {
title: 'پرداخت شما تایید شد',
body: (c) => `رسید پرداخت سفارش ${c.orderNumber} تایید شد.`,
sms: (c) =>
`رسید پرداخت سفارش ${c.orderNumber} تایید شد. سفارش شما در حال آماده‌سازی است.`,
tone: NotificationTone.success,
smsSwitch: 'smsPaymentReminder',
},
receipt_rejected: {
title: 'رسید پرداخت تایید نشد',
body: (c) =>
`رسید پرداخت سفارش ${c.orderNumber} تایید نشد${c.note ? `: ${c.note}` : '.'} لطفاً رسید صحیح را ثبت کنید.`,
sms: (c) =>
`رسید پرداخت سفارش ${c.orderNumber} تایید نشد. لطفاً در پنل ${c.appName} رسید را اصلاح کنید.`,
tone: NotificationTone.danger,
smsSwitch: 'smsPaymentReminder',
},
cash_invoice_reminder: {
title: 'یادآوری مهلت پرداخت',
body: (c) =>
`مهلت پرداخت فاکتور سفارش ${c.orderNumber} به پایان نزدیک است. مبلغ قابل پرداخت ${money(c.amount)}.`,
sms: (c) =>
`مهلت پرداخت فاکتور سفارش ${c.orderNumber} در حال پایان است. مبلغ ${money(c.amount)}.`,
tone: NotificationTone.warning,
smsSwitch: 'smsPaymentReminder',
},
cash_invoice_expired: {
title: 'مهلت پرداخت فاکتور تمام شد',
body: (c) =>
`مهلت پرداخت فاکتور سفارش ${c.orderNumber} به پایان رسید. برای ثبت سفارش تازه با پشتیبانی تماس بگیرید.`,
sms: (c) => `مهلت پرداخت فاکتور سفارش ${c.orderNumber} به پایان رسید.`,
tone: NotificationTone.danger,
smsSwitch: 'smsPaymentReminder',
},
order_preparing: {
title: 'سفارش در حال آماده‌سازی',
body: (c) =>
`سفارش ${c.orderNumber} تایید نهایی شد و در حال آماده‌سازی است.`,
tone: NotificationTone.info,
smsSwitch: null,
},
order_ready_to_ship: {
title: 'سفارش آماده ارسال است',
body: (c) => `سفارش ${c.orderNumber} آماده ارسال شد.`,
sms: (c) => `سفارش ${c.orderNumber} آماده ارسال شد.`,
tone: NotificationTone.info,
smsSwitch: 'smsShippingUpdate',
},
order_shipped: {
title: 'سفارش ارسال شد',
body: (c) =>
`سفارش ${c.orderNumber} ارسال شد${c.carrier ? ` (${c.carrier})` : ''}${
c.trackingNumber ? ` با کد رهگیری ${c.trackingNumber}` : ''
}.`,
sms: (c) =>
`سفارش ${c.orderNumber} ارسال شد${
c.trackingNumber ? `. کد رهگیری: ${c.trackingNumber}` : '.'
}`,
tone: NotificationTone.success,
smsSwitch: 'smsShippingUpdate',
},
order_delivered: {
title: 'سفارش تحویل شد',
body: (c) =>
`سفارش ${c.orderNumber} تحویل داده شد. از خرید شما سپاسگزاریم.`,
sms: (c) => `سفارش ${c.orderNumber} تحویل داده شد. از خرید شما سپاسگزاریم.`,
tone: NotificationTone.success,
smsSwitch: 'smsShippingUpdate',
},
order_cancelled: {
title: 'سفارش لغو شد',
body: (c) =>
`سفارش ${c.orderNumber} لغو شد${c.note ? `: ${c.note}` : '.'} برای اطلاعات بیشتر با پشتیبانی تماس بگیرید.`,
sms: (c) => `سفارش ${c.orderNumber} لغو شد.`,
tone: NotificationTone.danger,
smsSwitch: null,
},
};
@@ -0,0 +1,47 @@
import { Controller, Get, Param, Patch } from '@nestjs/common';
import { NotificationsService } from './notifications.service';
import { AuthType } from '../common/decorators/auth.decorators';
import { CurrentUser } from '../common/decorators/current-user.decorator';
import type { AuthPrincipal } from '../common/decorators/current-user.decorator';
@Controller('notifications')
@AuthType('customer')
export class CustomerNotificationsController {
constructor(private readonly notifications: NotificationsService) {}
@Get()
list(@CurrentUser() user: AuthPrincipal) {
return this.notifications.listForCustomer(user.id);
}
@Get('unread-count')
unreadCount(@CurrentUser() user: AuthPrincipal) {
return this.notifications.unreadCount(user.id);
}
@Patch('read-all')
markAllRead(@CurrentUser() user: AuthPrincipal) {
return this.notifications.markAllRead(user.id);
}
@Patch(':id/read')
markRead(@CurrentUser() user: AuthPrincipal, @Param('id') id: string) {
return this.notifications.markRead(user.id, id);
}
}
@Controller('admin/notifications')
@AuthType('admin')
export class AdminNotificationsController {
constructor(private readonly notifications: NotificationsService) {}
@Patch('read-all')
markAllRead() {
return this.notifications.markAllAdminRead();
}
@Patch(':id/read')
markRead(@Param('id') id: string) {
return this.notifications.markAdminRead(id);
}
}
+15
View File
@@ -0,0 +1,15 @@
import { Module } from '@nestjs/common';
import { NotificationsService } from './notifications.service';
import {
AdminNotificationsController,
CustomerNotificationsController,
} from './notifications.controller';
import { SmsModule } from '../sms/sms.module';
@Module({
imports: [SmsModule],
controllers: [CustomerNotificationsController, AdminNotificationsController],
providers: [NotificationsService],
exports: [NotificationsService],
})
export class NotificationsModule {}
+224
View File
@@ -0,0 +1,224 @@
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { NotificationTone } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { SmsService } from '../sms/sms.service';
import {
CUSTOMER_EVENTS,
type CustomerEvent,
type EventContext,
} from './notification-events';
type NotifyOptions = {
amount?: number | null;
note?: string | null;
carrier?: string | null;
trackingNumber?: string | null;
};
@Injectable()
export class NotificationsService {
private readonly logger = new Logger(NotificationsService.name);
constructor(
private readonly prisma: PrismaService,
private readonly sms: SmsService,
private readonly config: ConfigService,
) {}
/**
* باید **بعد از commit تراکنش** صدا زده شود؛ وگرنه با rollback هم پیامک رفته
* است و هم اعلانی برای سفارشی ساخته می‌شود که وجود ندارد.
*/
async notifyOrderEvent(
orderId: string,
event: CustomerEvent,
options: NotifyOptions = {},
): Promise<void> {
try {
const order = await this.prisma.order.findUnique({
where: { id: orderId },
select: {
id: true,
orderNumber: true,
userId: true,
finalAmount: true,
totalAmount: true,
user: { select: { phone: true } },
},
});
if (!order) return;
const template = CUSTOMER_EVENTS[event];
const ctx: EventContext = {
orderNumber: order.orderNumber,
appName: this.appName(),
amount:
options.amount ?? order.finalAmount ?? order.totalAmount ?? undefined,
note: options.note ?? null,
carrier: options.carrier ?? null,
trackingNumber: options.trackingNumber ?? null,
};
const notification = await this.prisma.customerNotification.create({
data: {
customerId: order.userId,
orderId: order.id,
event,
title: template.title,
body: template.body(ctx),
tone: template.tone,
link: `/orders/${order.id}`,
},
});
if (!template.sms || !template.smsSwitch) return;
const enabled = await this.isSmsEventEnabled(template.smsSwitch);
if (!enabled || !this.sms.isEnabled()) return;
const result = await this.sms.send(order.user.phone, template.sms(ctx));
if (result.success) {
await this.prisma.customerNotification.update({
where: { id: notification.id },
data: { smsSent: true },
});
}
} catch (err) {
// اعلان هیچ‌وقت نباید عملیات اصلی را بشکند
this.logger.error(
`notifyOrderEvent(${event}) failed for order ${orderId}`,
err instanceof Error ? err.stack : err,
);
}
}
/** نسخه بدون await برای مسیرهایی که نباید منتظر پیامک بمانند. */
dispatchOrderEvent(
orderId: string,
event: CustomerEvent,
options: NotifyOptions = {},
): void {
void this.notifyOrderEvent(orderId, event, options);
}
async notifyAdmin(input: {
title: string;
body: string;
tone?: NotificationTone;
link?: string;
}): Promise<void> {
try {
await this.prisma.adminNotification.create({
data: {
title: input.title,
body: input.body,
tone: input.tone ?? NotificationTone.info,
link: input.link ?? null,
},
});
} catch (err) {
this.logger.error(
'notifyAdmin failed',
err instanceof Error ? err.stack : err,
);
}
}
dispatchAdmin(input: {
title: string;
body: string;
tone?: NotificationTone;
link?: string;
}): void {
void this.notifyAdmin(input);
}
async listForCustomer(customerId: string, limit = 50) {
const rows = await this.prisma.customerNotification.findMany({
where: { customerId },
orderBy: { sentAt: 'desc' },
take: limit,
});
return rows.map((n) => ({
id: n.id,
orderId: n.orderId ?? undefined,
event: n.event,
title: n.title,
body: n.body,
tone: n.tone ?? NotificationTone.info,
link: n.link ?? undefined,
isRead: n.isRead,
sentAt: n.sentAt.toISOString(),
}));
}
async unreadCount(customerId: string) {
const count = await this.prisma.customerNotification.count({
where: { customerId, isRead: false },
});
return { count };
}
async markRead(customerId: string, id: string) {
const existing = await this.prisma.customerNotification.findFirst({
where: { id, customerId },
select: { id: true },
});
if (!existing) throw new NotFoundException('اعلان یافت نشد');
await this.prisma.customerNotification.update({
where: { id },
data: { isRead: true },
});
return this.unreadCount(customerId);
}
async markAllRead(customerId: string) {
await this.prisma.customerNotification.updateMany({
where: { customerId, isRead: false },
data: { isRead: true },
});
return { count: 0 };
}
async markAdminRead(id: string) {
await this.prisma.adminNotification.updateMany({
where: { id },
data: { isRead: true },
});
return { ok: true };
}
async markAllAdminRead() {
await this.prisma.adminNotification.updateMany({
where: { isRead: false },
data: { isRead: true },
});
return { ok: true };
}
private appName(): string {
return this.config.get<string>('SMS_APP_NAME', 'پارچه') ?? 'پارچه';
}
private async isSmsEventEnabled(
key:
| 'smsOrderRegistered'
| 'smsPricingReady'
| 'smsPaymentReminder'
| 'smsCheckReview'
| 'smsShippingUpdate',
): Promise<boolean> {
const settings = await this.prisma.systemSettings.findUnique({
where: { id: 'default' },
select: {
smsOrderRegistered: true,
smsPricingReady: true,
smsPaymentReminder: true,
smsCheckReview: true,
smsShippingUpdate: true,
},
});
// در نبود ردیف تنظیمات، پیش‌فرض‌های اسکیمای فعال در نظر گرفته می‌شوند
return settings ? settings[key] : true;
}
}
@@ -0,0 +1,120 @@
import { Injectable, Logger } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { OrderStatus } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { NotificationsService } from '../notifications/notifications.service';
/** وضعیت‌هایی که فاکتور نقدی در آن‌ها منتظر پرداخت مشتری است. */
const PAYABLE_STATUSES: OrderStatus[] = [
OrderStatus.awaiting_cash_payment,
OrderStatus.cash_invoice_issued,
];
/** چقدر پیش از پایان مهلت، یادآوری فرستاده شود. */
const REMINDER_WINDOW_MS = 60 * 60 * 1000;
@Injectable()
export class CashInvoiceSchedulerService {
private readonly logger = new Logger(CashInvoiceSchedulerService.name);
constructor(
private readonly prisma: PrismaService,
private readonly notifications: NotificationsService,
) {}
@Cron(CronExpression.EVERY_10_MINUTES)
async handleCashInvoices(): Promise<void> {
await this.sendReminders();
await this.expireOverdueInvoices();
}
/** یادآوری یک‌بار برای هر فاکتوری که کمتر از یک ساعت به پایان مهلتش مانده. */
async sendReminders(): Promise<number> {
const now = new Date();
const soon = new Date(now.getTime() + REMINDER_WINDOW_MS);
try {
const orders = await this.prisma.order.findMany({
where: {
status: { in: PAYABLE_STATUSES },
cashReminderSentAt: null,
cashInvoiceExpiresAt: { gt: now, lte: soon },
},
select: { id: true, finalAmount: true },
});
for (const order of orders) {
await this.prisma.order.update({
where: { id: order.id },
data: { cashReminderSentAt: now },
});
await this.notifications.notifyOrderEvent(
order.id,
'cash_invoice_reminder',
{ amount: order.finalAmount },
);
}
return orders.length;
} catch (err) {
this.logger.error(
'sendReminders failed',
err instanceof Error ? err.stack : err,
);
return 0;
}
}
/** فاکتورهای نقدی که مهلتشان تمام شده به وضعیت منقضی می‌روند. */
async expireOverdueInvoices(): Promise<number> {
const now = new Date();
try {
const orders = await this.prisma.order.findMany({
where: {
status: { in: PAYABLE_STATUSES },
cashInvoiceExpiresAt: { lt: now },
},
select: { id: true, orderNumber: true, status: true },
});
for (const order of orders) {
await this.prisma.$transaction([
this.prisma.order.update({
where: { id: order.id },
data: { status: OrderStatus.expired },
}),
this.prisma.orderStatusHistory.create({
data: {
orderId: order.id,
fromStatus: order.status,
toStatus: OrderStatus.expired,
changedBy: 'system',
note: 'پایان مهلت پرداخت فاکتور نقدی',
},
}),
]);
await this.notifications.notifyOrderEvent(
order.id,
'cash_invoice_expired',
);
this.notifications.dispatchAdmin({
title: 'فاکتور نقدی منقضی شد',
body: `مهلت پرداخت سفارش ${order.orderNumber} به پایان رسید و سفارش منقضی شد.`,
link: '/orders',
});
}
if (orders.length) {
this.logger.log(`${orders.length} فاکتور نقدی منقضی شد`);
}
return orders.length;
} catch (err) {
this.logger.error(
'expireOverdueInvoices failed',
err instanceof Error ? err.stack : err,
);
return 0;
}
}
}
+22 -2
View File
@@ -1,6 +1,7 @@
import {
ArrayMinSize,
IsArray,
IsBoolean,
IsDateString,
IsEnum,
IsNumber,
@@ -94,10 +95,14 @@ export class SubmitOrderDto {
@IsString()
companyId!: string;
/**
* روش تسویه بعد از قیمت‌گذاری ادمین انتخاب می‌شود، نه اینجا.
* برای سازگاری با کلاینت‌های قدیمی پذیرفته و نادیده گرفته می‌شود.
*/
@IsOptional()
@IsEnum(SettlementType)
settlementType!: SettlementType;
settlementType?: SettlementType;
/** Alias پذیرفته‌شده از کلاینت‌های قدیمی؛ نادیده گرفته می‌شود */
@IsOptional()
@IsEnum(SettlementType)
settlementMethod?: SettlementType;
@@ -123,6 +128,15 @@ export class SubmitOrderDto {
checks?: SubmitOrderCheckDto[];
}
export class ChooseSettlementDto {
@IsEnum(SettlementType)
settlementType!: SettlementType;
@IsOptional()
@IsEnum(CheckRegistrationMode)
checkMode?: CheckRegistrationMode;
}
export class AdminCreateOrderDto {
@IsOptional()
@IsString()
@@ -229,6 +243,12 @@ export class UpdateOrderStatusDto {
@IsOptional()
@IsString()
reason?: string;
/** عبور از ماشین وضعیت؛ فقط با ذکر دلیل و با ثبت در تاریخچه. */
@IsOptional()
@Type(() => Boolean)
@IsBoolean()
force?: boolean;
}
export class UpdateOrderChecksDto {
+11
View File
@@ -13,6 +13,7 @@ import { CurrentUser } from '../common/decorators/current-user.decorator';
import type { AuthPrincipal } from '../common/decorators/current-user.decorator';
import {
AdminCreateOrderDto,
ChooseSettlementDto,
SubmitOrderDto,
UpdateOrderChecksDto,
UpdateOrderPricingDto,
@@ -41,6 +42,16 @@ export class CustomerOrdersController {
return this.ordersService.submitOrder(user.id, dto);
}
@AuthType('customer')
@Post(':id/settlement')
chooseSettlement(
@CurrentUser() user: AuthPrincipal,
@Param('id') id: string,
@Body() dto: ChooseSettlementDto,
) {
return this.ordersService.chooseSettlement(user.id, id, dto);
}
@AuthType('customer')
@Patch(':id/checks')
updateChecks(
+4 -1
View File
@@ -4,10 +4,13 @@ import {
CustomerOrdersController,
} from './orders.controller';
import { OrdersService } from './orders.service';
import { CashInvoiceSchedulerService } from './cash-invoice-scheduler.service';
import { NotificationsModule } from '../notifications/notifications.module';
@Module({
imports: [NotificationsModule],
controllers: [CustomerOrdersController, AdminOrdersController],
providers: [OrdersService],
providers: [OrdersService, CashInvoiceSchedulerService],
exports: [OrdersService],
})
export class OrdersModule {}
+169 -84
View File
@@ -13,6 +13,7 @@ import {
import { PrismaService } from '../prisma/prisma.service';
import {
AdminCreateOrderDto,
ChooseSettlementDto,
SubmitOrderCheckDto,
SubmitOrderDto,
UpdateOrderChecksDto,
@@ -26,6 +27,13 @@ import {
resolveAdminStatusFilter,
toAdminStatus,
} from '../common/utils/order-status';
import {
allowedTransitions,
assertTransition,
} from '../common/utils/order-transitions';
import { resolveMaxMeterage } from '../common/utils/meterage';
import { NotificationsService } from '../notifications/notifications.service';
import { eventForStatus } from '../notifications/notification-events';
import { joinFullName, splitFullName } from '../common/utils/name';
import {
isValidPhone,
@@ -52,19 +60,27 @@ function roundMoney(value: number): number {
/** بعد از تأیید چک‌ها یا ارسال سفارش، مشتری نباید بتواند چک‌ها را بازنویسی کند. */
const CHECK_EDITABLE_STATUSES: OrderStatus[] = [
OrderStatus.awaiting_review,
OrderStatus.pricing,
OrderStatus.needs_correction,
OrderStatus.awaiting_settlement_fix,
OrderStatus.check_in_progress,
OrderStatus.awaiting_check_review,
OrderStatus.check_needs_fix,
OrderStatus.check_rejected,
];
/**
* تا وقتی ادمین فاکتور را نفرستاده، مشتری نباید هیچ مبلغی ببیند.
* پنهان‌سازی روی خود پاسخ API انجام می‌شود، نه در کلاینت.
*/
const PRICE_HIDDEN_STATUSES: OrderStatus[] = [
OrderStatus.awaiting_review,
OrderStatus.pricing,
];
@Injectable()
export class OrdersService {
constructor(private readonly prisma: PrismaService) {}
constructor(
private readonly prisma: PrismaService,
private readonly notifications: NotificationsService,
) {}
async findMyOrders(userId: string) {
const orders = await this.prisma.order.findMany({
@@ -119,12 +135,20 @@ export class OrdersService {
}
async submitOrder(userId: string, dto: SubmitOrderDto) {
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 NotFoundException('شرکت یافت نشد یا غیرفعال است');
}
const maxMeterage = resolveMaxMeterage(
company.maxMeteragePerInvoice,
settings?.maxMeteragePerInvoice,
);
const productIds = dto.items.map((i) => i.productId);
const products = await this.prisma.product.findMany({
@@ -159,44 +183,17 @@ export class OrdersService {
productCode: product.code,
color: product.color,
requestedMeterage: item.meterage,
unitPrice: product.basePrice,
notes: item.notes?.trim() || null,
};
});
if (totalMeterage > company.maxMeteragePerInvoice) {
if (totalMeterage > maxMeterage) {
throw new BadRequestException(
`حداکثر متراژ مجاز هر فاکتور ${company.maxMeteragePerInvoice} متر است`,
`حداکثر متراژ مجاز هر فاکتور ${maxMeterage} متر است`,
);
}
const settlementType = dto.settlementType;
if (settlementType === SettlementType.check) {
if (!dto.checks?.length) {
throw new BadRequestException('برای تسویه چکی حداقل یک چک لازم است');
}
}
const checkMode =
settlementType === SettlementType.check
? (dto.checkMode ?? CheckRegistrationMode.full)
: null;
const rass =
settlementType === SettlementType.check && dto.checks?.length
? calculateCheckRass(
dto.checks.map((c) => ({
amount: c.amount,
dueDate: c.dueDate,
})),
)
: null;
const cashInvoiceExpiresAt =
settlementType === SettlementType.cash
? computeCashInvoiceExpiry(company)
: null;
// روش تسویه و چک‌ها بعد از قیمت‌گذاری ادمین گرفته می‌شوند، نه اینجا.
const order = await this.createWithUniqueOrderNumber((orderNumber) =>
this.prisma.$transaction(async (tx) => {
const created = await tx.order.create({
@@ -205,36 +202,19 @@ export class OrdersService {
userId,
companyId: company.id,
companyName: company.name,
settlementType,
checkMode,
settlementType: null,
checkMode: null,
status: 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 === SettlementType.check && dto.checks
? {
create: dto.checks.map((c, index) =>
this.mapCheckCreate(c, index + 1, checkMode!),
),
}
: undefined,
statusHistory: {
create: [
{
fromStatus: null,
toStatus: OrderStatus.submitted,
changedBy: 'customer',
note: 'سفارش ثبت شد',
},
{
fromStatus: OrderStatus.submitted,
toStatus: OrderStatus.awaiting_review,
changedBy: 'customer',
note: 'ارسال برای بررسی مدیریت',
note: 'سفارش ثبت شد و برای بررسی مدیریت ارسال شد',
},
],
},
@@ -254,15 +234,69 @@ export class OrdersService {
}),
);
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: string, dto: AdminCreateOrderDto) {
const settlementType = dto.settlementType ?? dto.settlementMethod;
if (!settlementType) {
throw new BadRequestException('نوع تسویه الزامی است');
/**
* مشتری بعد از دیدن جمع فاکتور روش تسویه را انتخاب می‌کند.
* نقدی مستقیم به مرحله پرداخت می‌رود، چکی به مرحله ثبت چک‌ها.
*/
async chooseSettlement(userId: string, id: string, dto: ChooseSettlementDto) {
const order = await this.getOrder(id);
if (order.userId !== userId) {
throw new ForbiddenException('دسترسی به این سفارش مجاز نیست');
}
if (order.status !== OrderStatus.awaiting_settlement) {
throw new BadRequestException(
'انتخاب روش تسویه فقط پس از صدور فاکتور توسط مدیریت امکان‌پذیر است',
);
}
const isCash = dto.settlementType === SettlementType.cash;
const nextStatus = isCash
? OrderStatus.awaiting_cash_payment
: OrderStatus.check_in_progress;
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 ?? CheckRegistrationMode.full),
cashInvoiceExpiresAt: isCash
? 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: string, dto: AdminCreateOrderDto) {
const company = await this.prisma.company.findUnique({
where: { id: dto.companyId },
});
@@ -303,7 +337,7 @@ export class OrdersService {
userId: ownerId,
companyId: company.id,
companyName: company.name,
settlementType,
settlementType: null,
status: OrderStatus.awaiting_review,
totalMeterage,
notes: dto.notes?.trim() || null,
@@ -422,6 +456,26 @@ export class OrdersService {
return this.mapOrder(order, true);
}
if (dto.force) {
if (!dto.reason?.trim()) {
throw new BadRequestException(
'برای تغییر دستی وضعیت، ذکر دلیل الزامی است',
);
}
} else {
assertTransition(order.status, dto.status);
}
// فاکتور بدون مبلغ نباید برای مشتری ارسال شود
if (
dto.status === OrderStatus.awaiting_settlement &&
(order.finalAmount ?? 0) <= 0
) {
throw new BadRequestException(
'ابتدا سفارش را قیمت‌گذاری کنید تا فاکتور قابل ارسال باشد',
);
}
await this.prisma.$transaction(async (tx) => {
await tx.order.update({
where: { id },
@@ -433,7 +487,9 @@ export 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,
},
});
@@ -454,6 +510,13 @@ export class OrdersService {
}
});
const event = eventForStatus(dto.status);
if (event) {
this.notifications.dispatchOrderEvent(id, event, {
note: dto.reason ?? dto.note ?? null,
});
}
return this.findAdminOrder(id);
}
@@ -471,6 +534,10 @@ export class OrdersService {
);
}
if (!dto.checks.length) {
throw new BadRequestException('حداقل یک چک لازم است');
}
const checkMode =
dto.checkMode ?? order.checkMode ?? CheckRegistrationMode.full;
const rass = calculateCheckRass(
@@ -478,6 +545,16 @@ export class OrdersService {
order.createdAt,
);
// ثبت اولیه و اصلاح، هر دو سفارش را به صف بررسی چک می‌برند.
const goesToReview =
order.status === OrderStatus.check_in_progress ||
order.status === OrderStatus.check_needs_fix ||
order.status === OrderStatus.check_rejected;
const nextStatus = goesToReview
? OrderStatus.awaiting_check_review
: order.status;
const isFix = order.status !== OrderStatus.check_in_progress;
await this.prisma.$transaction(async (tx) => {
await tx.check.deleteMany({ where: { orderId: id } });
await tx.check.createMany({
@@ -492,25 +569,32 @@ export class OrdersService {
checkMode,
checkRassDays: rass.rassDays,
checkRassDate: rass.rassDate ? new Date(rass.rassDate) : null,
status:
order.status === OrderStatus.check_needs_fix
? OrderStatus.awaiting_check_review
: order.status,
status: nextStatus,
},
});
if (order.status === OrderStatus.check_needs_fix) {
if (nextStatus !== order.status) {
await tx.orderStatusHistory.create({
data: {
orderId: id,
fromStatus: order.status,
toStatus: 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);
}
@@ -579,6 +663,8 @@ export class OrdersService {
order.user.firstName,
order.user.lastName,
);
const hidePrices =
!adminView && PRICE_HIDDEN_STATUSES.includes(order.status);
const base = {
id: order.id,
orderNumber: order.orderNumber,
@@ -590,12 +676,14 @@ export class OrdersService {
checkMode: order.checkMode ?? undefined,
status: order.status,
adminStatus: toAdminStatus(order.status),
allowedTransitions: adminView ? 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(),
@@ -615,12 +703,12 @@ export 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) => ({
@@ -688,9 +776,6 @@ export class OrdersService {
})),
};
if (adminView) {
return base;
}
return base;
}
}
+4
View File
@@ -26,6 +26,10 @@ export class UpdateSettingsDto {
@IsBoolean()
smsOrderRegistered?: boolean;
@IsOptional()
@IsBoolean()
smsPricingReady?: boolean;
@IsOptional()
@IsBoolean()
smsPaymentReminder?: boolean;
+3
View File
@@ -21,6 +21,7 @@ export class SettingsService {
defaultSettlementCheck: dto.defaultSettlementCheck,
defaultSettlementCash: dto.defaultSettlementCash,
smsOrderRegistered: dto.smsOrderRegistered,
smsPricingReady: dto.smsPricingReady,
smsPaymentReminder: dto.smsPaymentReminder,
smsCheckReview: dto.smsCheckReview,
smsShippingUpdate: dto.smsShippingUpdate,
@@ -52,6 +53,7 @@ export class SettingsService {
defaultSettlementCheck: boolean;
defaultSettlementCash: boolean;
smsOrderRegistered: boolean;
smsPricingReady: boolean;
smsPaymentReminder: boolean;
smsCheckReview: boolean;
smsShippingUpdate: boolean;
@@ -66,6 +68,7 @@ export class SettingsService {
defaultSettlementCheck: settings.defaultSettlementCheck,
defaultSettlementCash: settings.defaultSettlementCash,
smsOrderRegistered: settings.smsOrderRegistered,
smsPricingReady: settings.smsPricingReady,
smsPaymentReminder: settings.smsPaymentReminder,
smsCheckReview: settings.smsCheckReview,
smsShippingUpdate: settings.smsShippingUpdate,

Some files were not shown because too many files have changed in this diff Show More