configRepo->get('sms_price_rials') ?: self::SMS_PRICE_RIALS)); } #[Route('/api/v1/sms/wallet/balance', methods: ['GET'])] public function balance(#[CurrentUser] User $user): JsonResponse { [$entityType, $entityId] = $this->resolveEntity($user); if ($entityId === null) { return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403); } $balanceRials = $this->walletService->getBalance($entityType, $entityId); $smsPriceRials = $this->smsPriceRials(); $estimatedSms = (int) floor($balanceRials / $smsPriceRials); return $this->success([ 'balance_rials' => $balanceRials, 'sms_price_rials' => $smsPriceRials, 'estimated_sms_count' => $estimatedSms, ]); } #[Route('/api/v1/sms/wallet/charge', methods: ['POST'])] public function charge(Request $request, #[CurrentUser] User $user): JsonResponse { [$entityType, $entityId] = $this->resolveEntity($user); if ($entityId === null) { return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403); } $data = json_decode($request->getContent(), true) ?? []; $gatewayName = trim($data['gateway'] ?? 'mellat'); $amountRials = (int) ($data['amount_rials'] ?? 0); if ($amountRials <= 0) { return $this->error(ErrorCodes::ERR_PAYMENT_002, 'مبلغ شارژ نامعتبر است', 422); } // فقط اعتبارسنجی درگاه؛ ارتباط با بانک در flow واحد (GET /payment/pay) انجام می‌شود. if ($this->gateways->resolve($gatewayName) === null) { return $this->error(ErrorCodes::ERR_VALIDATION_001, 'درگاه پرداخت نامعتبر است', 422); } $frontendAddress = trim($data['frontend_address'] ?? ''); $payment = new Payment($user, $amountRials, $gatewayName, Payment::TYPE_SMS_WALLET, $frontendAddress); $payment->setMetadata(['entity_type' => $entityType, 'entity_id' => $entityId]); $this->paymentRepo->save($payment); return $this->success([ 'payment_uuid' => $payment->getUuid(), 'pay_url' => $this->appBaseUrl . '/api/v1/payment/pay/' . $payment->getOrderId(), 'order_id' => $payment->getOrderId(), ]); } #[Route('/api/v1/sms/wallet/logs', methods: ['GET'])] public function logs(Request $request, #[CurrentUser] User $user): JsonResponse { [$entityType, $entityId] = $this->resolveEntity($user); if ($entityId === null) { return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403); } $page = max(1, (int) $request->query->get('page', 1)); $limit = min(50, max(10, (int) $request->query->get('limit', 20))); $wallet = $this->walletService->getOrCreate($entityType, $entityId); $txs = $this->txRepo->findByWallet($wallet, $page, $limit); $total = $this->txRepo->countByWallet($wallet); return $this->paginated( array_map(fn($tx) => $tx->toArray(), $txs), $total, $page, $limit ); } #[Route('/api/v1/sms/settings', methods: ['GET'])] public function getSettings(#[CurrentUser] User $user): JsonResponse { [$entityType, $entityId] = $this->resolveEntity($user); if ($entityId === null) { return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403); } $settings = $this->settingsRepo->findByEntity($entityType, $entityId); if ($settings === null) { return $this->success([ 'entity_type' => $entityType, 'entity_id' => $entityId, 'reminder_enabled' => false, 'reminder_hours_before' => 2, 'post_visit_enabled' => false, 'post_visit_text' => null, ]); } return $this->success($settings->toArray()); } #[Route('/api/v1/sms/settings', methods: ['PATCH'])] public function updateSettings(Request $request, #[CurrentUser] User $user): JsonResponse { [$entityType, $entityId] = $this->resolveEntity($user); if ($entityId === null) { return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403); } $settings = $this->settingsRepo->findByEntity($entityType, $entityId); if ($settings === null) { $settings = new SmsSettings($entityType, $entityId); } $data = json_decode($request->getContent(), true) ?? []; if (isset($data['reminder_enabled'])) { $settings->setReminderEnabled((bool) $data['reminder_enabled']); } if (isset($data['reminder_hours_before'])) { $settings->setReminderHoursBefore((int) $data['reminder_hours_before']); } if (isset($data['post_visit_enabled'])) { $settings->setPostVisitEnabled((bool) $data['post_visit_enabled']); } if (array_key_exists('post_visit_text', $data) && $data['post_visit_text'] !== null) { $text = trim((string) $data['post_visit_text']); if ($text !== '') { $settings->submitPostVisitText($text); } } $this->settingsRepo->save($settings); return $this->success($settings->toArray()); } #[Route('/api/v1/admin/sms/settings/review', methods: ['GET'])] #[IsGranted('ROLE_ADMIN')] public function adminReviewList(Request $request): JsonResponse { $status = $request->query->get('status', SmsSettings::TEXT_STATUS_PENDING); if (!in_array($status, [SmsSettings::TEXT_STATUS_PENDING, SmsSettings::TEXT_STATUS_APPROVED], true)) { $status = SmsSettings::TEXT_STATUS_PENDING; } $pending = $this->settingsRepo->createQueryBuilder('s') ->where('s.postVisitTextStatus = :status') ->setParameter('status', $status) ->getQuery() ->getResult(); $data = array_map(function (SmsSettings $s) { $row = $s->toArray(); $row['entity_name'] = $this->resolveEntityName($s->getEntityType(), $s->getEntityId()); return $row; }, $pending); return $this->success(['data' => $data]); } #[Route('/api/v1/admin/sms/settings/{id}/approve', methods: ['POST'])] #[IsGranted('ROLE_ADMIN')] public function adminApprove(int $id): JsonResponse { $settings = $this->settingsRepo->find($id); if ($settings === null) { return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'تنظیمات یافت نشد', 404); } $settings->approvePostVisitText(); $this->settingsRepo->save($settings); return $this->success($settings->toArray()); } #[Route('/api/v1/admin/sms/settings/{id}/reject', methods: ['POST'])] #[IsGranted('ROLE_ADMIN')] public function adminReject(int $id, Request $request): JsonResponse { $settings = $this->settingsRepo->find($id); if ($settings === null) { return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'تنظیمات یافت نشد', 404); } $data = json_decode($request->getContent(), true) ?? []; $reason = trim($data['reason'] ?? ''); if ($reason === '') { return $this->error(ErrorCodes::ERR_VALIDATION_001, 'دلیل رد الزامی است', 422); } $settings->rejectPostVisitText($reason); $this->settingsRepo->save($settings); return $this->success($settings->toArray()); } private function resolveEntityName(string $type, int $id): ?string { if ($type === 'doctor') { return $this->doctorRepo->find($id)?->getName(); } if ($type === 'clinic') { return $this->clinicRepo->find($id)?->getName(); } return null; } #[Route('/api/v1/admin/sms/wallet-report', methods: ['GET'])] #[IsGranted('ROLE_ADMIN')] public function adminReport(Request $request): JsonResponse { $page = max(1, (int) $request->query->get('page', 1)); $limit = min(100, max(10, (int) $request->query->get('limit', 20))); $total = (int) $this->walletRepo->createQueryBuilder('w') ->select('COUNT(w.id)') ->getQuery() ->getSingleScalarResult(); $wallets = $this->walletRepo->createQueryBuilder('w') ->orderBy('w.balanceRials', 'DESC') ->setFirstResult(($page - 1) * $limit) ->setMaxResults($limit) ->getQuery() ->getArrayResult(); return $this->paginated($wallets, $total, $page, $limit); } private function resolveEntity(User $user): array { if ($user->hasRole('ROLE_DOCTOR')) { $doctor = $this->doctorRepo->findByUser($user); return $doctor !== null ? ['doctor', $doctor->getId()] : ['doctor', null]; } if ($user->hasRole('ROLE_CLINIC')) { $clinic = $this->clinicRepo->findByUser($user); return $clinic !== null ? ['clinic', $clinic->getId()] : ['clinic', null]; } return ['unknown', null]; } }