import { BadRequestException, Injectable, NotFoundException, } from '@nestjs/common'; import type { CreateLoyaltyTierRequest, LoyaltyMemberDto, LoyaltyTierDto, UpdateLoyaltyTierRequest, } from '@mento/shared'; import { PrismaService } from '../prisma/prisma.service'; import { type Actor, ownerWhere } from '../common/record-scope'; const DEFAULT_TIERS = [ { name: 'برنزی', minPoints: 0, color: '#b87333' }, { name: 'نقره‌ای', minPoints: 500, color: '#9aa0a6' }, { name: 'طلایی', minPoints: 2000, color: '#d4af37' }, ] as const; @Injectable() export class LoyaltyService { constructor(private readonly prisma: PrismaService) {} async ensureDefaultTiers(organizationId: string): Promise { const count = await this.prisma.loyaltyTier.count({ where: { organizationId }, }); if (count > 0) return; await this.prisma.loyaltyTier.createMany({ data: DEFAULT_TIERS.map((t, position) => ({ organizationId, name: t.name, minPoints: t.minPoints, color: t.color, position, })), }); } async listTiers(organizationId: string): Promise { await this.ensureDefaultTiers(organizationId); const rows = await this.prisma.loyaltyTier.findMany({ where: { organizationId }, orderBy: [{ minPoints: 'asc' }, { position: 'asc' }], include: { _count: { select: { people: true } } }, }); return rows.map((r) => ({ id: r.id, name: r.name, minPoints: r.minPoints, position: r.position, color: r.color, memberCount: r._count.people, })); } async createTier( organizationId: string, dto: CreateLoyaltyTierRequest, ): Promise { await this.ensureDefaultTiers(organizationId); const maxPos = await this.prisma.loyaltyTier.aggregate({ where: { organizationId }, _max: { position: true }, }); const row = await this.prisma.loyaltyTier.create({ data: { organizationId, name: dto.name.trim(), minPoints: Math.max(0, Math.floor(dto.minPoints)), color: emptyToNull(dto.color), position: (maxPos._max.position ?? -1) + 1, }, }); await this.recomputeAllTiers(organizationId); return { id: row.id, name: row.name, minPoints: row.minPoints, position: row.position, color: row.color, memberCount: 0, }; } async updateTier( organizationId: string, id: string, dto: UpdateLoyaltyTierRequest, ): Promise { const existing = await this.prisma.loyaltyTier.findFirst({ where: { id, organizationId }, }); if (!existing) throw new NotFoundException('سطح پیدا نشد'); const row = await this.prisma.loyaltyTier.update({ where: { id }, data: { ...(dto.name !== undefined ? { name: dto.name.trim() } : {}), ...(dto.minPoints !== undefined ? { minPoints: Math.max(0, Math.floor(dto.minPoints)) } : {}), ...('color' in dto ? { color: emptyToNull(dto.color ?? null) } : {}), }, include: { _count: { select: { people: true } } }, }); await this.recomputeAllTiers(organizationId); return { id: row.id, name: row.name, minPoints: row.minPoints, position: row.position, color: row.color, memberCount: row._count.people, }; } async deleteTier(organizationId: string, id: string): Promise { const existing = await this.prisma.loyaltyTier.findFirst({ where: { id, organizationId }, }); if (!existing) throw new NotFoundException('سطح پیدا نشد'); await this.prisma.person.updateMany({ where: { organizationId, loyaltyTierId: id }, data: { loyaltyTierId: null }, }); await this.prisma.loyaltyTier.delete({ where: { id } }); await this.recomputeAllTiers(organizationId); } async listMembers( actor: Actor, q?: string, ): Promise { await this.ensureDefaultTiers(actor.organizationId); const rows = await this.prisma.person.findMany({ where: { organizationId: actor.organizationId, ...ownerWhere(actor), ...(q?.trim() ? { OR: [ { firstName: { contains: q.trim(), mode: 'insensitive' } }, { lastName: { contains: q.trim(), mode: 'insensitive' } }, { mobile: { contains: q.trim() } }, { email: { contains: q.trim(), mode: 'insensitive' } }, ], } : {}), }, include: { company: { select: { id: true, name: true } }, loyaltyTier: { select: { id: true, name: true, minPoints: true, color: true }, }, }, orderBy: [{ loyaltyPoints: 'desc' }, { lastName: 'asc' }], take: 200, }); return rows.map((r) => ({ id: r.id, fullName: `${r.firstName} ${r.lastName}`.trim(), mobile: r.mobile, email: r.email, loyaltyPoints: r.loyaltyPoints, loyaltyTier: r.loyaltyTier, company: r.company, })); } async adjustPoints( actor: Actor, personId: string, delta: number, ): Promise { if (!Number.isFinite(delta) || delta === 0) { throw new BadRequestException('مقدار امتیاز نامعتبر است'); } const person = await this.prisma.person.findFirst({ where: { id: personId, organizationId: actor.organizationId, ...ownerWhere(actor), }, }); if (!person) throw new NotFoundException('شخص پیدا نشد'); const next = Math.max(0, person.loyaltyPoints + Math.trunc(delta)); const tierId = await this.resolveTierId(actor.organizationId, next); const updated = await this.prisma.person.update({ where: { id: personId }, data: { loyaltyPoints: next, loyaltyTierId: tierId }, include: { company: { select: { id: true, name: true } }, loyaltyTier: { select: { id: true, name: true, minPoints: true, color: true }, }, }, }); return { id: updated.id, fullName: `${updated.firstName} ${updated.lastName}`.trim(), mobile: updated.mobile, email: updated.email, loyaltyPoints: updated.loyaltyPoints, loyaltyTier: updated.loyaltyTier, company: updated.company, }; } /** برای اتوماسیون — بدون ACL مالکیت */ async addPointsByOrg( organizationId: string, personId: string, points: number, ): Promise { if (!personId || !points) return; const person = await this.prisma.person.findFirst({ where: { id: personId, organizationId }, }); if (!person) return; const next = Math.max(0, person.loyaltyPoints + Math.trunc(points)); const tierId = await this.resolveTierId(organizationId, next); await this.prisma.person.update({ where: { id: personId }, data: { loyaltyPoints: next, loyaltyTierId: tierId }, }); } async resolveTierId( organizationId: string, points: number, ): Promise { await this.ensureDefaultTiers(organizationId); const tier = await this.prisma.loyaltyTier.findFirst({ where: { organizationId, minPoints: { lte: points } }, orderBy: { minPoints: 'desc' }, }); return tier?.id ?? null; } private async recomputeAllTiers(organizationId: string) { const people = await this.prisma.person.findMany({ where: { organizationId }, select: { id: true, loyaltyPoints: true }, }); const tiers = await this.prisma.loyaltyTier.findMany({ where: { organizationId }, orderBy: { minPoints: 'desc' }, }); for (const p of people) { const tier = tiers.find((t) => t.minPoints <= p.loyaltyPoints) ?? null; await this.prisma.person.update({ where: { id: p.id }, data: { loyaltyTierId: tier?.id ?? null }, }); } } } function emptyToNull(v?: string | null) { if (v == null) return null; const t = v.trim(); return t ? t : null; }