feat: Implement financial engine for commission and tax calculations
- Added new configuration keys for appointment and upgrade commissions, tax settings, and SMS panel fee in SiteConfigController and SiteConfigRepository. - Introduced CommissionService to handle commission calculations for appointments and subscriptions, including tax deductions and SMS fees. - Created FinancialBreakdown entity and repository to log financial transactions. - Updated PaymentController to process commissions upon successful payments for appointments and subscriptions. - Developed FinancialReportPage in the admin panel to display financial breakdowns and summaries. - Added database migration for the new financial_breakdowns table.
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace App\Settlement\Service;
|
||||
|
||||
use App\Config\Repository\SiteConfigRepository;
|
||||
use App\Payment\Entity\Payment;
|
||||
use App\Representation\Entity\Representation;
|
||||
use App\Representation\Repository\RepresentationRepository;
|
||||
use App\Settlement\Entity\FinancialBreakdown;
|
||||
use App\Settlement\Entity\WalletTransaction;
|
||||
use App\Settlement\Repository\FinancialBreakdownRepository;
|
||||
use App\Settlement\Repository\SettlementRepository;
|
||||
use App\Settlement\Repository\WalletTransactionRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
/**
|
||||
* موتور تقسیم مالی پس از پرداخت موفق.
|
||||
* ترتیب ثابت: ۱) کسر هزینه پنل پیامک ۲) کسر مالیات از باقیمانده ۳) پورسانت نماینده از خالصِ پس از مالیات.
|
||||
*/
|
||||
class CommissionService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SiteConfigRepository $configRepo,
|
||||
private readonly RepresentationRepository $representationRepo,
|
||||
private readonly SettlementRepository $settlementRepo,
|
||||
private readonly WalletTransactionRepository $walletRepo,
|
||||
private readonly FinancialBreakdownRepository $breakdownRepo,
|
||||
private readonly EntityManagerInterface $em,
|
||||
) {}
|
||||
|
||||
/** پورسانت نوبت: درصد = commission_percent همان نماینده. */
|
||||
public function processAppointment(Payment $payment, ?int $representationId, ?int $doctorId): void
|
||||
{
|
||||
if ($this->configRepo->get('appointment_commission_enabled') !== '1') return;
|
||||
|
||||
$rep = $this->resolveRep($representationId);
|
||||
if ($rep === null) return;
|
||||
|
||||
$this->settle(
|
||||
$payment,
|
||||
FinancialBreakdown::SOURCE_APPOINTMENT,
|
||||
(float) $rep->getCommissionPercent(),
|
||||
$rep,
|
||||
$doctorId,
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
/** پورسانت ارتقاء اشتراک: درصد سراسری = upgrade_commission_percent. */
|
||||
public function processSubscription(Payment $payment, ?int $representationId, ?int $doctorId, ?int $clinicId): void
|
||||
{
|
||||
if ($this->configRepo->get('upgrade_commission_enabled') !== '1') return;
|
||||
|
||||
$rep = $this->resolveRep($representationId);
|
||||
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;
|
||||
}
|
||||
|
||||
private function settle(
|
||||
Payment $payment,
|
||||
string $source,
|
||||
float $commissionPercent,
|
||||
Representation $rep,
|
||||
?int $doctorId,
|
||||
?int $clinicId,
|
||||
): 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;
|
||||
|
||||
// مرحله ۳: پورسانت نماینده از خالصِ پس از مالیات.
|
||||
$repShare = (int) round($netAfterTax * $commissionPercent / 100);
|
||||
$systemShare = $gross - $smsFee - $taxRials - $repShare;
|
||||
|
||||
$repUser = $rep->getUser();
|
||||
|
||||
if ($repShare > 0) {
|
||||
$balance = $this->settlementRepo->getWalletBalance($repUser);
|
||||
$tx = new WalletTransaction($repUser, $repShare, WalletTransaction::TYPE_CREDIT, $balance + $repShare);
|
||||
$tx->setPayment($payment);
|
||||
$tx->setDescription(sprintf('پورسانت %s %s', $source, $payment->getOrderId()));
|
||||
$this->walletRepo->save($tx, false);
|
||||
}
|
||||
|
||||
$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,
|
||||
);
|
||||
$this->breakdownRepo->save($breakdown, false);
|
||||
|
||||
$this->em->flush();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user