diff --git a/prisma/seed.ts b/prisma/seed.ts index 9d54aa2..ce0b05c 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -42,6 +42,8 @@ async function main() { await prisma.product.deleteMany(); await prisma.catalog.deleteMany(); await prisma.bankAccount.deleteMany(); + await prisma.levelBenefit.deleteMany(); + await prisma.customerLevelDef.deleteMany(); await prisma.customer.deleteMany(); await prisma.company.deleteMany(); await prisma.adminUser.deleteMany(); @@ -332,6 +334,51 @@ async function main() { ], }); + await prisma.customerLevelDef.create({ + data: { + code: 'bronze', + label: 'برنزی', + sortOrder: 1, + criteriaJson: { minSalesToman: 0 }, + benefits: { + create: [{ title: 'دسترسی به کاتالوگ پایه' }], + }, + }, + }); + await prisma.customerLevelDef.create({ + data: { + code: 'silver', + label: 'نقره‌ای', + sortOrder: 2, + criteriaJson: { minSalesToman: 500_000_000 }, + benefits: { + create: [{ title: 'پشتیبانی اولویت‌دار' }], + }, + }, + }); + const goldLevel = await prisma.customerLevelDef.create({ + data: { + code: 'gold', + label: 'طلایی', + sortOrder: 3, + criteriaJson: { minSalesToman: 1_500_000_000 }, + benefits: { + create: [{ title: 'تخفیف ویژه سفارش‌های عمده' }], + }, + }, + }); + await prisma.customerLevelDef.create({ + data: { + code: 'vip', + label: 'VIP', + sortOrder: 4, + criteriaJson: { minSalesToman: 3_000_000_000 }, + benefits: { + create: [{ title: 'مشاوره تخصصی و ارسال سریع' }], + }, + }, + }); + await prisma.customer.create({ data: { id: 'agent-1', @@ -342,10 +389,11 @@ async function main() { customerType: CustomerType.wholesale, appRole: AppUserRole.agent, agentCode: 'W-1247', + levelDefId: goldLevel.id, levelLabel: 'طلایی', nextLevelLabel: 'VIP', - pointsToNext: 550_000_000, - levelProgress: 72, + pointsToNext: 0, + levelProgress: 0, companyId: 'company-par', isActive: true, avatarLetter: 'ع', diff --git a/runtime/server.cjs b/runtime/server.cjs index 99b6ef9..fd8561d 100644 --- a/runtime/server.cjs +++ b/runtime/server.cjs @@ -133240,6 +133240,25 @@ var require_notifications_service = __commonJS({ dispatchAdmin(input) { void this.notifyAdmin(input); } + async notifyCustomer(input) { + try { + await this.prisma.customerNotification.create({ + data: { + customerId: input.customerId, + event: input.event, + title: input.title.trim(), + body: input.body.trim(), + tone: input.tone ?? client_1.NotificationTone.info, + link: input.link ?? null + } + }); + } catch (err) { + this.logger.error(`notifyCustomer(${input.event}) failed for customer ${input.customerId}`, err instanceof Error ? err.stack : err); + } + } + dispatchCustomer(input) { + void this.notifyCustomer(input); + } async listForCustomer(customerId, limit = 50) { const rows = await this.prisma.customerNotification.findMany({ where: { customerId }, @@ -139500,14 +139519,18 @@ var require_suggestions_service = __commonJS({ Object.defineProperty(exports2, "__esModule", { value: true }); exports2.SuggestionsService = void 0; var common_12 = require_common(); + var client_1 = require_default2(); + var notifications_service_1 = require_notifications_service(); var prisma_service_1 = require_prisma_service(); var SuggestionsService = class SuggestionsService { static { __name(this, "SuggestionsService"); } prisma; - constructor(prisma) { + notifications; + constructor(prisma, notifications) { this.prisma = prisma; + this.notifications = notifications; } async listAdmin(customerId) { const rows = await this.prisma.productSuggestion.findMany({ @@ -139549,6 +139572,15 @@ var require_suggestions_service = __commonJS({ product: { select: { name: true, code: true, color: true } } } }); + const note = row.note?.trim(); + this.notifications.dispatchCustomer({ + customerId: input.customerId, + event: "product_suggestion", + title: "\u067E\u06CC\u0634\u0646\u0647\u0627\u062F \u0645\u062D\u0635\u0648\u0644 \u0628\u0631\u0627\u06CC \u0634\u0645\u0627", + body: note ? `\u06A9\u0627\u0631\u0634\u0646\u0627\u0633\u0627\u0646 \u0645\u0627 \u0645\u062D\u0635\u0648\u0644 \xAB${row.product.name}\xBB \u0631\u0627 \u0628\u0647 \u0634\u0645\u0627 \u067E\u06CC\u0634\u0646\u0647\u0627\u062F \u062F\u0627\u062F\u0647\u200C\u0627\u0646\u062F: ${note}` : `\u06A9\u0627\u0631\u0634\u0646\u0627\u0633\u0627\u0646 \u0645\u0627 \u0645\u062D\u0635\u0648\u0644 \xAB${row.product.name}\xBB \u0631\u0627 \u0628\u0647 \u0634\u0645\u0627 \u067E\u06CC\u0634\u0646\u0647\u0627\u062F \u062F\u0627\u062F\u0647\u200C\u0627\u0646\u062F.`, + tone: client_1.NotificationTone.info, + link: `/product?id=${input.productId}` + }); return this.map(row); } async setActive(id, isActive) { @@ -139596,7 +139628,10 @@ var require_suggestions_service = __commonJS({ exports2.SuggestionsService = SuggestionsService; exports2.SuggestionsService = SuggestionsService = __decorate2([ (0, common_12.Injectable)(), - __metadata2("design:paramtypes", [prisma_service_1.PrismaService]) + __metadata2("design:paramtypes", [ + prisma_service_1.PrismaService, + notifications_service_1.NotificationsService + ]) ], SuggestionsService); } }); @@ -139738,6 +139773,7 @@ var require_suggestions_module = __commonJS({ Object.defineProperty(exports2, "__esModule", { value: true }); exports2.SuggestionsModule = void 0; var common_12 = require_common(); + var notifications_module_1 = require_notifications_module(); var suggestions_controller_1 = require_suggestions_controller(); var suggestions_service_1 = require_suggestions_service(); var SuggestionsModule = class SuggestionsModule { @@ -139748,6 +139784,7 @@ var require_suggestions_module = __commonJS({ exports2.SuggestionsModule = SuggestionsModule; exports2.SuggestionsModule = SuggestionsModule = __decorate2([ (0, common_12.Module)({ + imports: [notifications_module_1.NotificationsModule], controllers: [suggestions_controller_1.SuggestionsController], providers: [suggestions_service_1.SuggestionsService], exports: [suggestions_service_1.SuggestionsService] diff --git a/src/agent/agent-level.service.ts b/src/agent/agent-level.service.ts new file mode 100644 index 0000000..bab76d2 --- /dev/null +++ b/src/agent/agent-level.service.ts @@ -0,0 +1,103 @@ +import { Injectable } from '@nestjs/common'; +import { AppUserRole } from '@prisma/client'; +import { PrismaService } from '../prisma/prisma.service'; +import { + AgentLevelSnapshot, + DEFAULT_AGENT_LEVEL_TIERS, + LevelTier, + computeAgentLevel, + parseLevelMinSales, +} from './agent-level.util'; + +@Injectable() +export class AgentLevelService { + constructor(private readonly prisma: PrismaService) {} + + async getLevelTiers(): Promise { + const rows = await this.prisma.customerLevelDef.findMany({ + where: { isActive: true }, + orderBy: [{ sortOrder: 'asc' }, { label: 'asc' }], + }); + + if (!rows.length) { + return DEFAULT_AGENT_LEVEL_TIERS; + } + + return rows.map((row) => ({ + id: row.id, + code: row.code, + label: row.label, + sortOrder: row.sortOrder, + minSalesToman: parseLevelMinSales(row.criteriaJson), + })); + } + + async getAgentSalesVolume(agentId: string): Promise { + const sales = await this.prisma.order.aggregate({ + where: { + user: { assignedAgentId: agentId }, + finalAmount: { not: null }, + }, + _sum: { finalAmount: true }, + }); + + return sales._sum.finalAmount ?? 0; + } + + async syncAgentLevel(agentId: string): Promise { + const agent = await this.prisma.customer.findUnique({ + where: { id: agentId }, + select: { id: true, appRole: true }, + }); + + if (!agent || agent.appRole !== AppUserRole.agent) { + return { + levelLabel: 'برنزی', + nextLevelLabel: 'نقره‌ای', + pointsToNext: 0, + levelProgress: 0, + levelDefId: null, + salesVolumeToman: 0, + }; + } + + const [salesVolumeToman, tiers] = await Promise.all([ + this.getAgentSalesVolume(agentId), + this.getLevelTiers(), + ]); + + const computed = computeAgentLevel(salesVolumeToman, tiers); + + await this.prisma.customer.update({ + where: { id: agentId }, + data: { + levelLabel: computed.levelLabel, + nextLevelLabel: computed.nextLevelLabel, + pointsToNext: computed.pointsToNext, + levelProgress: computed.levelProgress, + levelDefId: computed.levelDefId, + }, + }); + + return { + ...computed, + salesVolumeToman, + }; + } + + async syncAgentForOrder(orderId: string): Promise { + const order = await this.prisma.order.findUnique({ + where: { id: orderId }, + select: { + user: { + select: { assignedAgentId: true }, + }, + }, + }); + + const agentId = order?.user?.assignedAgentId; + if (!agentId) return; + + await this.syncAgentLevel(agentId); + } +} diff --git a/src/agent/agent-level.util.spec.ts b/src/agent/agent-level.util.spec.ts new file mode 100644 index 0000000..008aebd --- /dev/null +++ b/src/agent/agent-level.util.spec.ts @@ -0,0 +1,27 @@ +import { computeAgentLevel, DEFAULT_AGENT_LEVEL_TIERS } from './agent-level.util'; + +describe('computeAgentLevel', () => { + it('starts at bronze with empty progress', () => { + const result = computeAgentLevel(0, DEFAULT_AGENT_LEVEL_TIERS); + expect(result.levelLabel).toBe('برنزی'); + expect(result.nextLevelLabel).toBe('نقره‌ای'); + expect(result.pointsToNext).toBe(500_000_000); + expect(result.levelProgress).toBe(0); + }); + + it('calculates progress between silver and gold', () => { + const result = computeAgentLevel(1_000_000_000, DEFAULT_AGENT_LEVEL_TIERS); + expect(result.levelLabel).toBe('نقره‌ای'); + expect(result.nextLevelLabel).toBe('طلایی'); + expect(result.pointsToNext).toBe(500_000_000); + expect(result.levelProgress).toBe(50); + }); + + it('caps at vip with full progress', () => { + const result = computeAgentLevel(4_000_000_000, DEFAULT_AGENT_LEVEL_TIERS); + expect(result.levelLabel).toBe('VIP'); + expect(result.nextLevelLabel).toBe('VIP'); + expect(result.pointsToNext).toBe(0); + expect(result.levelProgress).toBe(100); + }); +}); diff --git a/src/agent/agent-level.util.ts b/src/agent/agent-level.util.ts new file mode 100644 index 0000000..4f73733 --- /dev/null +++ b/src/agent/agent-level.util.ts @@ -0,0 +1,93 @@ +export type LevelTier = { + id?: string; + code: string; + label: string; + sortOrder: number; + minSalesToman: number; +}; + +export type AgentLevelSnapshot = { + levelLabel: string; + nextLevelLabel: string; + pointsToNext: number; + levelProgress: number; + levelDefId: string | null; + salesVolumeToman: number; +}; + +export const DEFAULT_AGENT_LEVEL_TIERS: LevelTier[] = [ + { code: 'bronze', label: 'برنزی', sortOrder: 1, minSalesToman: 0 }, + { code: 'silver', label: 'نقره‌ای', sortOrder: 2, minSalesToman: 500_000_000 }, + { code: 'gold', label: 'طلایی', sortOrder: 3, minSalesToman: 1_500_000_000 }, + { code: 'vip', label: 'VIP', sortOrder: 4, minSalesToman: 3_000_000_000 }, +]; + +export function parseLevelMinSales(criteriaJson: unknown): number { + if (!criteriaJson || typeof criteriaJson !== 'object') return 0; + const value = + (criteriaJson as Record).minSalesToman ?? + (criteriaJson as Record).minSales ?? + (criteriaJson as Record).salesVolumeToman ?? + 0; + const parsed = Number(value); + return Number.isFinite(parsed) && parsed >= 0 ? Math.round(parsed) : 0; +} + +export function sortLevelTiers(tiers: LevelTier[]): LevelTier[] { + return [...tiers].sort( + (a, b) => a.minSalesToman - b.minSalesToman || a.sortOrder - b.sortOrder, + ); +} + +export function computeAgentLevel( + salesVolumeToman: number, + tiers: LevelTier[], +): Omit { + const sorted = sortLevelTiers(tiers); + const safeSales = Math.max(0, Math.round(salesVolumeToman)); + + if (!sorted.length) { + return { + levelLabel: 'برنزی', + nextLevelLabel: 'نقره‌ای', + pointsToNext: 0, + levelProgress: 0, + levelDefId: null, + }; + } + + let currentIndex = 0; + for (let index = 0; index < sorted.length; index += 1) { + if (safeSales >= sorted[index].minSalesToman) { + currentIndex = index; + } + } + + const current = sorted[currentIndex]; + const next = sorted[currentIndex + 1]; + + if (!next) { + return { + levelLabel: current.label, + nextLevelLabel: current.label, + pointsToNext: 0, + levelProgress: 100, + levelDefId: current.id ?? null, + }; + } + + const range = next.minSalesToman - current.minSalesToman; + const progressInRange = safeSales - current.minSalesToman; + const levelProgress = + range <= 0 + ? 100 + : Math.min(100, Math.max(0, Math.round((progressInRange / range) * 100))); + + return { + levelLabel: current.label, + nextLevelLabel: next.label, + pointsToNext: Math.max(0, next.minSalesToman - safeSales), + levelProgress, + levelDefId: current.id ?? null, + }; +} diff --git a/src/agent/agent.module.ts b/src/agent/agent.module.ts index 4a8524a..b240100 100644 --- a/src/agent/agent.module.ts +++ b/src/agent/agent.module.ts @@ -1,12 +1,13 @@ import { Module } from '@nestjs/common'; import { AgentController } from './agent.controller'; +import { AgentLevelService } from './agent-level.service'; import { AgentService } from './agent.service'; import { PrismaModule } from '../prisma/prisma.module'; @Module({ imports: [PrismaModule], controllers: [AgentController], - providers: [AgentService], - exports: [AgentService], + providers: [AgentService, AgentLevelService], + exports: [AgentService, AgentLevelService], }) export class AgentModule {} diff --git a/src/agent/agent.service.ts b/src/agent/agent.service.ts index 8418b1e..2ff4684 100644 --- a/src/agent/agent.service.ts +++ b/src/agent/agent.service.ts @@ -19,12 +19,16 @@ import { CreateReminderDto, } from './dto/agent.dto'; import { loadCatalogCategories } from '../catalog/category-defs'; +import { AgentLevelService } from './agent-level.service'; const AVATAR_COLORS = ['#f9a8d4', '#c4b5fd', '#93c5fd', '#86efac', '#fcd34d', '#fdba74']; @Injectable() export class AgentService { - constructor(private readonly prisma: PrismaService) {} + constructor( + private readonly prisma: PrismaService, + private readonly agentLevelService: AgentLevelService, + ) {} private async requireAgent(agentId: string) { const agent = await this.prisma.customer.findUnique({ @@ -38,7 +42,7 @@ export class AgentService { async getProfile(agentId: string) { const agent = await this.requireAgent(agentId); - const [customersCount, ordersCount] = await Promise.all([ + const [customersCount, ordersCount, levelSnapshot] = await Promise.all([ this.prisma.customer.count({ where: { assignedAgentId: agentId, isActive: true }, }), @@ -47,27 +51,20 @@ export class AgentService { user: { assignedAgentId: agentId }, }, }), + this.agentLevelService.syncAgentLevel(agentId), ]); - const sales = await this.prisma.order.aggregate({ - where: { - user: { assignedAgentId: agentId }, - finalAmount: { not: null }, - }, - _sum: { finalAmount: true }, - }); - - const salesVolume = sales._sum.finalAmount ?? 0; + const salesVolume = levelSnapshot.salesVolumeToman; return { id: agent.id, firstName: agent.firstName, lastName: agent.lastName, code: agent.agentCode ?? '', - levelLabel: agent.levelLabel ?? 'برنزی', - nextLevelLabel: agent.nextLevelLabel ?? 'نقره‌ای', - pointsToNext: agent.pointsToNext ?? 0, - levelProgress: agent.levelProgress ?? 0, + levelLabel: levelSnapshot.levelLabel, + nextLevelLabel: levelSnapshot.nextLevelLabel, + pointsToNext: levelSnapshot.pointsToNext, + levelProgress: levelSnapshot.levelProgress, phone: agent.phone, customersCount, ordersCount, diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index 667ee01..3cd5f36 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -4,11 +4,13 @@ import { ConfigModule, ConfigService } from '@nestjs/config'; import { AuthService } from './auth.service'; import { AuthController } from './auth.controller'; import { SmsModule } from '../sms/sms.module'; +import { AgentModule } from '../agent/agent.module'; import { resolveJwtSecret } from '../common/utils/jwt-secret'; @Module({ imports: [ SmsModule, + AgentModule, JwtModule.registerAsync({ imports: [ConfigModule], inject: [ConfigService], diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index d7a5958..43c37fb 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -8,6 +8,8 @@ import { ConfigService } from '@nestjs/config'; import { JwtService } from '@nestjs/jwt'; import { randomInt } from 'node:crypto'; import * as bcrypt from 'bcryptjs'; +import { AppUserRole } from '@prisma/client'; +import { AgentLevelService } from '../agent/agent-level.service'; import { PrismaService } from '../prisma/prisma.service'; import { SmsService } from '../sms/sms.service'; import { @@ -31,6 +33,7 @@ export class AuthService { private readonly jwt: JwtService, private readonly config: ConfigService, private readonly sms: SmsService, + private readonly agentLevelService: AgentLevelService, ) {} async requestOtp(dto: RequestOtpDto) { @@ -153,6 +156,13 @@ export class AuthService { await this.prisma.otpChallenge.deleteMany({ where: { phone } }); + if (user.appRole === AppUserRole.agent) { + await this.agentLevelService.syncAgentLevel(user.id); + user = + (await this.prisma.customer.findUnique({ where: { id: user.id } })) ?? + user; + } + const accessToken = await this.signCustomerToken( user.id, user.phone, @@ -215,10 +225,18 @@ export class AuthService { }; } - const user = await this.prisma.customer.findUnique({ + let user = await this.prisma.customer.findUnique({ where: { id: userId }, }); if (!user) throw new UnauthorizedException(); + + if (user.appRole === AppUserRole.agent) { + await this.agentLevelService.syncAgentLevel(user.id); + user = + (await this.prisma.customer.findUnique({ where: { id: userId } })) ?? + user; + } + return this.mapCustomer(user); } diff --git a/src/catalog/category-defs.ts b/src/catalog/category-defs.ts index b2cec3f..92adef2 100644 --- a/src/catalog/category-defs.ts +++ b/src/catalog/category-defs.ts @@ -10,6 +10,9 @@ export const CATEGORY_ICONS = [ 'catalog', 'headset', 'home', + 'shirt', + 'suit', + 'trousers', ] as const export type CategoryIconId = (typeof CATEGORY_ICONS)[number] @@ -46,7 +49,7 @@ export const USAGE_CATEGORIES: UsageCategoryDef[] = [ id: 'trousers', title: 'پارچه‌های شلواری', subtitle: 'مشاهده محصولات', - icon: 'grid', + icon: 'trousers', showTitle: true, showSubtitle: true, showIcon: true, @@ -55,7 +58,7 @@ export const USAGE_CATEGORIES: UsageCategoryDef[] = [ id: 'suit', title: 'پارچه‌های کت‌شلواری', subtitle: 'مشاهده محصولات', - icon: 'bag', + icon: 'suit', showTitle: true, showSubtitle: true, showIcon: true, @@ -64,7 +67,7 @@ export const USAGE_CATEGORIES: UsageCategoryDef[] = [ id: 'shirt', title: 'پارچه‌های پیراهنی', subtitle: 'مشاهده محصولات', - icon: 'id-card', + icon: 'shirt', showTitle: true, showSubtitle: true, showIcon: true, @@ -140,11 +143,19 @@ function parseIcon(value: unknown, fallback: CategoryIconId): CategoryIconId { } function defaultUsageIcon(id: string): CategoryIconId { - if (id === 'shirt') return 'id-card' - if (id === 'suit') return 'bag' + if (id === 'shirt') return 'shirt' + if (id === 'suit') return 'suit' + if (id === 'trousers') return 'trousers' return 'grid' } +function resolveUsageIconForApi(item: UsageCategoryDef): CategoryIconId { + if (item.id === 'shirt' || item.id === 'suit' || item.id === 'trousers') { + return defaultUsageIcon(item.id) + } + return item.icon +} + function defaultSeasonIcon(id: string): CategoryIconId { if (id === 'summer') return 'bolt' if (id === 'winter') return 'feather' @@ -229,6 +240,7 @@ export async function loadCatalogCategories(prisma: PrismaService) { return { usage: defs.usage.map((item) => ({ ...item, + icon: resolveUsageIconForApi(item), imageUrl: images[item.id] || undefined, })), seasons: defs.seasons.map((item) => ({ diff --git a/src/notifications/notifications.service.ts b/src/notifications/notifications.service.ts index 6bbd3d7..b5bd3f9 100644 --- a/src/notifications/notifications.service.ts +++ b/src/notifications/notifications.service.ts @@ -133,6 +133,44 @@ export class NotificationsService { void this.notifyAdmin(input); } + async notifyCustomer(input: { + customerId: string; + event: string; + title: string; + body: string; + tone?: NotificationTone; + link?: string; + }): Promise { + try { + await this.prisma.customerNotification.create({ + data: { + customerId: input.customerId, + event: input.event, + title: input.title.trim(), + body: input.body.trim(), + tone: input.tone ?? NotificationTone.info, + link: input.link ?? null, + }, + }); + } catch (err) { + this.logger.error( + `notifyCustomer(${input.event}) failed for customer ${input.customerId}`, + err instanceof Error ? err.stack : err, + ); + } + } + + dispatchCustomer(input: { + customerId: string; + event: string; + title: string; + body: string; + tone?: NotificationTone; + link?: string; + }): void { + void this.notifyCustomer(input); + } + async listForCustomer(customerId: string, limit = 50) { const rows = await this.prisma.customerNotification.findMany({ where: { customerId }, diff --git a/src/orders/orders.module.ts b/src/orders/orders.module.ts index 6af1a26..9b998be 100644 --- a/src/orders/orders.module.ts +++ b/src/orders/orders.module.ts @@ -6,9 +6,10 @@ import { import { OrdersService } from './orders.service'; import { CashInvoiceSchedulerService } from './cash-invoice-scheduler.service'; import { NotificationsModule } from '../notifications/notifications.module'; +import { AgentModule } from '../agent/agent.module'; @Module({ - imports: [NotificationsModule], + imports: [NotificationsModule, AgentModule], controllers: [CustomerOrdersController, AdminOrdersController], providers: [OrdersService, CashInvoiceSchedulerService], exports: [OrdersService], diff --git a/src/orders/orders.service.ts b/src/orders/orders.service.ts index c5c8d27..e8bc466 100644 --- a/src/orders/orders.service.ts +++ b/src/orders/orders.service.ts @@ -33,6 +33,7 @@ import { assertTransition, } from '../common/utils/order-transitions'; import { resolveMaxMeterage } from '../common/utils/meterage'; +import { AgentLevelService } from '../agent/agent-level.service'; import { NotificationsService } from '../notifications/notifications.service'; import { eventForStatus } from '../notifications/notification-events'; import { joinFullName, splitFullName } from '../common/utils/name'; @@ -81,6 +82,7 @@ export class OrdersService { constructor( private readonly prisma: PrismaService, private readonly notifications: NotificationsService, + private readonly agentLevelService: AgentLevelService, ) {} async findMyOrders(userId: string) { @@ -498,6 +500,8 @@ export class OrdersService { } }); + await this.agentLevelService.syncAgentForOrder(id); + return this.findAdminOrder(id); } diff --git a/src/suggestions/suggestions.module.ts b/src/suggestions/suggestions.module.ts index 687d525..01044ed 100644 --- a/src/suggestions/suggestions.module.ts +++ b/src/suggestions/suggestions.module.ts @@ -1,8 +1,10 @@ import { Module } from '@nestjs/common'; +import { NotificationsModule } from '../notifications/notifications.module'; import { SuggestionsController } from './suggestions.controller'; import { SuggestionsService } from './suggestions.service'; @Module({ + imports: [NotificationsModule], controllers: [SuggestionsController], providers: [SuggestionsService], exports: [SuggestionsService], diff --git a/src/suggestions/suggestions.service.ts b/src/suggestions/suggestions.service.ts index 7be675e..bdaf23e 100644 --- a/src/suggestions/suggestions.service.ts +++ b/src/suggestions/suggestions.service.ts @@ -1,9 +1,14 @@ import { Injectable, NotFoundException } from '@nestjs/common'; +import { NotificationTone } from '@prisma/client'; +import { NotificationsService } from '../notifications/notifications.service'; import { PrismaService } from '../prisma/prisma.service'; @Injectable() export class SuggestionsService { - constructor(private readonly prisma: PrismaService) {} + constructor( + private readonly prisma: PrismaService, + private readonly notifications: NotificationsService, + ) {} async listAdmin(customerId?: string) { const rows = await this.prisma.productSuggestion.findMany({ @@ -52,6 +57,19 @@ export class SuggestionsService { product: { select: { name: true, code: true, color: true } }, }, }); + + const note = row.note?.trim(); + this.notifications.dispatchCustomer({ + customerId: input.customerId, + event: 'product_suggestion', + title: 'پیشنهاد محصول برای شما', + body: note + ? `کارشناسان ما محصول «${row.product.name}» را به شما پیشنهاد داده‌اند: ${note}` + : `کارشناسان ما محصول «${row.product.name}» را به شما پیشنهاد داده‌اند.`, + tone: NotificationTone.info, + link: `/product?id=${input.productId}`, + }); + return this.map(row); }