breakdownRepo->existsForPayment($payment)) return; $rep = $this->eligibleAppointmentRep($doctorRepId, $bookingRepId); $secretaries = $this->secretaryShares->for($payment); if ($rep === null && $secretaries === []) return; $this->settle( $payment, FinancialBreakdown::SOURCE_APPOINTMENT, $rep !== null ? (float) $rep->getCommissionPercent() : 0.0, $rep, $doctorId, null, $secretaries, ); } /** * نمایندهٔ واجد شرط برای پورسانت نوبت. گاردِ دامنه: پزشک باید متعلق به نماینده * باشد و نوبت هم از دامنهٔ همان نماینده ثبت شده باشد. */ private function eligibleAppointmentRep(?int $doctorRepId, ?int $bookingRepId): ?Representation { if ($this->configRepo->get('appointment_commission_enabled') !== '1') return null; if ($doctorRepId === null || $bookingRepId === null || $doctorRepId !== $bookingRepId) return null; return $this->resolveRep($doctorRepId); } /** * پورسانت ارتقاء اشتراک: درصد سراسری = upgrade_commission_percent. * گاردِ دامنه (مثل نوبت): مالک پزشک/کلینیک و نماینده‌ی دامنه‌ی خرید باید یکی باشند. */ public function processSubscription(Payment $payment, ?int $ownerRepId, ?int $bookingRepId, ?int $doctorId, ?int $clinicId): void { if ($this->configRepo->get('upgrade_commission_enabled') !== '1') return; if ($ownerRepId === null || $bookingRepId === null || $ownerRepId !== $bookingRepId) return; $rep = $this->resolveRep($ownerRepId); if ($rep === null) return; $this->settle( $payment, FinancialBreakdown::SOURCE_SUBSCRIPTION, (float) $this->configRepo->get('upgrade_commission_percent'), $rep, $doctorId, $clinicId, [], ); } private function resolveRep(?int $representationId): ?Representation { if ($representationId === null) return null; $rep = $this->representationRepo->find($representationId); return ($rep !== null && $rep->isActive()) ? $rep : null; } /** * @param list $secretaries */ private function settle( Payment $payment, string $source, float $commissionPercent, ?Representation $rep, ?int $doctorId, ?int $clinicId, array $secretaries, ): void { // پرداخت دوبار پردازش نشود. if ($this->breakdownRepo->existsForPayment($payment)) return; $gross = $payment->getAmountRials(); // مرحله ۱: کسر هزینه ثابت پنل پیامک. $smsFee = (int) $this->configRepo->get('sms_panel_fee_rials'); $afterSms = max(0, $gross - $smsFee); // مرحله ۲: مالیاتِ استخراجی از مبلغِ شامل مالیات: tax = amount × p/(100+p). $taxEnabled = $this->configRepo->get('tax_enabled') === '1'; $taxPercent = $taxEnabled ? (float) $this->configRepo->get('tax_percent') : 0.0; $taxRials = ($taxEnabled && $taxPercent > 0) ? (int) round($afterSms * $taxPercent / (100 + $taxPercent)) : 0; $netAfterTax = $afterSms - $taxRials; // مرحله ۳: سهم‌ها، همه از «خالصِ پس از مالیات» — نه از مبلغ کل و نه از // باقی‌ماندهٔ سهم دیگری، تا ترتیب اجرا روی مبالغ اثر نگذارد. [$commissionPercent, $secretaries] = $this->clipPercents($payment, $commissionPercent, $secretaries); $repShare = (int) round($netAfterTax * $commissionPercent / 100); $secretaryTotal = 0; $secretaryRows = []; foreach ($secretaries as $secretary) { $share = (int) round($netAfterTax * $secretary['percent'] / 100); if ($share <= 0) continue; $secretaryTotal += $share; $secretaryRows[] = $secretary + ['share' => $share]; } $systemShare = $gross - $smsFee - $taxRials - $repShare - $secretaryTotal; if ($rep !== null && $repShare > 0) { $this->credit($rep->getUser(), $repShare, $payment, sprintf('پورسانت %s %s', $source, $payment->getOrderId())); } $breakdown = new FinancialBreakdown( $payment, $source, $payment->getUser(), $gross, $smsFee, number_format($taxPercent, 2, '.', ''), $taxRials, $netAfterTax, number_format($commissionPercent, 2, '.', ''), $repShare, $systemShare, $rep?->getId(), $doctorId, $clinicId, ); $breakdown->setSecretaryShareRials($secretaryTotal); $this->breakdownRepo->save($breakdown, false); foreach ($secretaryRows as $row) { $this->credit($row['user'], $row['share'], $payment, sprintf('سهم نوبت آنلاین %s', $payment->getOrderId())); $this->earningRepo->save( new SecretaryEarning($breakdown, $row['user'], $row['relation_uuid'], $row['percent'], $row['share']), false, ); } $this->em->flush(); } private function credit(User $user, int $amountRials, Payment $payment, string $description): void { $balance = $this->settlementRepo->getWalletBalance($user); $tx = new WalletTransaction($user, $amountRials, WalletTransaction::TYPE_CREDIT, $balance + $amountRials); $tx->setPayment($payment); $tx->setDescription($description); $this->walletRepo->save($tx, false); } /** * سهم سیستم نباید منفی شود: اگر مجموع درصدها از ۱۰۰ بگذرد، به نسبت کلیپ می‌شود و * هشدار ثبت می‌گردد (سکوت نمی‌کنیم — تنظیمِ اشتباه باید دیده شود). * * @param list $secretaries * @return array{0: float, 1: list} */ private function clipPercents(Payment $payment, float $commissionPercent, array $secretaries): array { $total = $commissionPercent + array_sum(array_column($secretaries, 'percent')); if ($total <= 100.0 || $total <= 0.0) { return [$commissionPercent, $secretaries]; } $this->logger->warning('Commission + secretary shares exceed 100% — clipping proportionally', [ 'payment_uuid' => $payment->getUuid(), 'total_percent' => $total, ]); $factor = 100.0 / $total; $clipped = []; foreach ($secretaries as $secretary) { $secretary['percent'] *= $factor; $clipped[] = $secretary; } return [$commissionPercent * $factor, $clipped]; } }