import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common'; import { PrismaService } from '../../prisma/prisma.service'; import { SmsService } from '../sms/sms.service'; import { CompleteProfileDto } from './dto/complete-profile.dto'; import { brandName } from '../../common/brand'; /** * "تکمیل پروفایل" — the customer uploads their three documents. * * Ports henza_server Major/CustomerController::complete * (app/Http/Controllers/Major/CustomerController.php:73-108), reached from the * PWA at POST /major/complete (completeProfile.vue:193). * * Reference behaviour kept: * - all three document ids are required (:75-79); a miss is a 422 * - the customer is taken from the session (`auth('api-customer')->id()`, :86) * - the verification step is set to the reference's post-change value 2, * i.e. approved outright, NOT "waiting for an expert" — the reference's * own `$input['sts_verify_step_two'] = 1;` line is commented out at :83 * in favour of `= 2` at :85. Here that is VerificationStep.VERIFIED. * - the customer SMS and the admin SMS, both byte-identical (:97-105) * * Reference behaviour deliberately NOT ported, both recorded in ISSUES.md: * - SEC-14: `$customer->update($request->all())` mass-assigned the entire * request body onto the customer row, so the app could set `gram_level`, * `percent`, `active` or `remain` on itself. Only the three document ids * are read here. * - SEC-15: the four admin mobiles were hardcoded in the source * (sendSmsToAdmin, :110-112). They come from PROFILE_ADMIN_MOBILES here, * so no personal number is committed to this repository. */ @Injectable() export class CustomerProfileService { private readonly logger = new Logger(CustomerProfileService.name); constructor( private readonly prisma: PrismaService, private readonly sms: SmsService, ) {} /** * The admins who are told a customer uploaded their documents. * * The reference hardcoded four numbers; this reads a comma-separated env var * instead. Unset means nobody is notified, and the customer's own SMS and the * upload itself still go through — consistent with D20, where SMS never * blocks an operation. */ private adminMobiles(): string[] { return (process.env.PROFILE_ADMIN_MOBILES ?? '') .split(',') .map((m) => m.trim()) .filter(Boolean); } /** * Resolves an uploaded-file id to its stored URL. * * The reference stored the raw id in the customer row and joined to `files` * on read; the target's Customer columns hold URLs * (`nationalIdImageUrl`, `businessLicenseUrl`, `shopImageUrl`), so the id is * resolved here. An id that names no file is a 400 rather than a silent * null, so a half-completed profile can never be marked verified. */ private async urlOf(id: string, label: string): Promise { const file = await this.prisma.uploadedFile.findUnique({ where: { id } }); if (!file) throw new BadRequestException(`${label} یافت نشد`); return file.url; } async complete(customerId: string, dto: CompleteProfileDto) { const customer = await this.prisma.customer.findUnique({ where: { id: customerId } }); if (!customer) throw new NotFoundException('مشتری یافت نشد'); // Resolved before the write, so a bad id fails the whole call. const [businessLicenseUrl, shopImageUrl, nationalIdImageUrl] = await Promise.all([ this.urlOf(dto.business_license_image_id, 'تصویر جواز کسب'), this.urlOf(dto.shop_image_id, 'تصویر سردر مغازه'), this.urlOf(dto.ncode_image_id, 'تصویر کارت ملی'), ]); const updated = await this.prisma.customer.update({ where: { id: customerId }, data: { businessLicenseUrl, shopImageUrl, nationalIdImageUrl, // The reference's live value: 2, approved outright (:85). verificationStep: 'VERIFIED', }, }); this.notify(updated.mobile, updated.firstName ?? '', updated.lastName ?? ''); // The reference answered a bare `true` (:107); the app only checks that the // call succeeded (completeProfile.vue:193-194). return { ok: true }; } /** * Both messages, fire-and-forget. * * D20: a failed or unconfigured SMS must never fail the upload, so nothing * here is awaited and every rejection is swallowed into the log. The * reference likewise queued them (SendSmsAfterCreate::dispatch, :100). */ private notify(mobile: string, firstName: string, lastName: string) { // Major/CustomerController::complete:97-99, brand read from BRAND_NAME. const message = `${brandName()}\n` + `همکار گرامی ${firstName} ${lastName}\n` + 'تکمیل پروفایل شما با موفقیت انجام شد.'; // :103-105, brand read from BRAND_NAME. const adminMessage = `${brandName()}\n` + `مشتری با نام ${firstName} ${lastName}` + ' مدارک خود را بارگذاری کرد.'; const fire = (to: string, body: string) => { void this.sms.send(to, body).catch((e: unknown) => { this.logger.error(`Complete-profile SMS to ${to} failed: ${String(e)}`); }); }; fire(mobile, message); for (const admin of this.adminMobiles()) fire(admin, adminMessage); } }