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
+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