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:
@@ -14,6 +14,7 @@ use App\Rating\Entity\Comment;
|
||||
use App\Rating\Entity\Rate;
|
||||
use App\Representation\Entity\Representation;
|
||||
use App\Secretary\Entity\DoctorSecretary;
|
||||
use App\Settlement\Entity\FinancialBreakdown;
|
||||
use App\Settlement\Entity\Settlement;
|
||||
use App\Sms\Entity\SmsLog;
|
||||
use App\Sms\Entity\SmsTemplate;
|
||||
@@ -969,6 +970,113 @@ class AdminApiController extends BaseController
|
||||
return $this->paginated($items, (int) $total, $page, $limit);
|
||||
}
|
||||
|
||||
// ── Financial Breakdowns ──────────────────────────────────────────────────
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/v1/admin/financial-breakdowns',
|
||||
summary: 'لیست تفکیک مالی تراکنشها (paginated)',
|
||||
security: [['bearerAuth' => []]],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 1)),
|
||||
new OA\Parameter(name: 'limit', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15)),
|
||||
new OA\Parameter(name: 'representation_id', in: 'query', required: false, schema: new OA\Schema(type: 'integer')),
|
||||
new OA\Parameter(name: 'source', in: 'query', required: false, schema: new OA\Schema(type: 'string', enum: ['appointment', 'subscription'])),
|
||||
new OA\Parameter(name: 'from', in: 'query', required: false, schema: new OA\Schema(type: 'integer', description: 'Unix timestamp')),
|
||||
new OA\Parameter(name: 'to', in: 'query', required: false, schema: new OA\Schema(type: 'integer', description: 'Unix timestamp')),
|
||||
],
|
||||
responses: [new OA\Response(response: 200, description: 'لیست تفکیک مالی')]
|
||||
)]
|
||||
#[Route('/api/v1/admin/financial-breakdowns', methods: ['GET'])]
|
||||
public function financialBreakdowns(Request $request): JsonResponse
|
||||
{
|
||||
$page = max(1, (int) $request->query->get('page', 1));
|
||||
$limit = min(100, max(1, (int) $request->query->get('limit', 15)));
|
||||
$repId = $request->query->get('representation_id');
|
||||
$source = trim((string) $request->query->get('source', ''));
|
||||
$from = $request->query->get('from');
|
||||
$to = $request->query->get('to');
|
||||
|
||||
$qb = $this->em->createQueryBuilder()
|
||||
->select(
|
||||
'b.uuid, b.source, b.grossRials, b.smsFeeRials, b.taxPercent, b.taxRials,
|
||||
b.netAfterTaxRials, b.commissionPercent, b.representationShareRials, b.systemShareRials,
|
||||
b.representationId, b.doctorId, b.clinicId, b.createdAt,
|
||||
p.orderId as order_id, r.fullName as representation_name'
|
||||
)
|
||||
->from(FinancialBreakdown::class, 'b')
|
||||
->join('b.payment', 'p')
|
||||
->leftJoin(Representation::class, 'r', 'WITH', 'r.id = b.representationId')
|
||||
->orderBy('b.createdAt', 'DESC');
|
||||
|
||||
if ($repId !== null && $repId !== '') {
|
||||
$qb->andWhere('b.representationId = :repId')->setParameter('repId', (int) $repId);
|
||||
}
|
||||
if ($source !== '') {
|
||||
$qb->andWhere('b.source = :source')->setParameter('source', $source);
|
||||
}
|
||||
if ($from !== null && $from !== '') {
|
||||
$qb->andWhere('b.createdAt >= :from')->setParameter('from', (int) $from);
|
||||
}
|
||||
if ($to !== null && $to !== '') {
|
||||
$qb->andWhere('b.createdAt <= :to')->setParameter('to', (int) $to);
|
||||
}
|
||||
|
||||
$total = (clone $qb)->select('COUNT(b.uuid)')->getQuery()->getSingleScalarResult();
|
||||
|
||||
$rows = $qb->setFirstResult(($page - 1) * $limit)->setMaxResults($limit)
|
||||
->getQuery()->getArrayResult();
|
||||
|
||||
$items = array_map(fn(array $b) => [
|
||||
'uuid' => $b['uuid'],
|
||||
'order_id' => $b['order_id'],
|
||||
'source' => $b['source'],
|
||||
'gross_rials' => (int) $b['grossRials'],
|
||||
'sms_fee_rials' => (int) $b['smsFeeRials'],
|
||||
'tax_percent' => (float) $b['taxPercent'],
|
||||
'tax_rials' => (int) $b['taxRials'],
|
||||
'net_after_tax_rials' => (int) $b['netAfterTaxRials'],
|
||||
'commission_percent' => (float) $b['commissionPercent'],
|
||||
'representation_share_rials' => (int) $b['representationShareRials'],
|
||||
'system_share_rials' => (int) $b['systemShareRials'],
|
||||
'representation_id' => $b['representationId'],
|
||||
'representation_name' => $b['representation_name'] ?? null,
|
||||
'doctor_id' => $b['doctorId'],
|
||||
'clinic_id' => $b['clinicId'],
|
||||
'created_at' => date('c', (int) $b['createdAt']),
|
||||
], $rows);
|
||||
|
||||
return $this->paginated($items, (int) $total, $page, $limit);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/v1/admin/financial-summary',
|
||||
summary: 'جمعِ کلِ سهم نماینده، مالیات، هزینه پیامک و سهم سیستم',
|
||||
security: [['bearerAuth' => []]],
|
||||
responses: [new OA\Response(response: 200, description: 'جمعهای مالی')]
|
||||
)]
|
||||
#[Route('/api/v1/admin/financial-summary', methods: ['GET'])]
|
||||
public function financialSummary(): JsonResponse
|
||||
{
|
||||
$row = $this->em->createQueryBuilder()
|
||||
->select(
|
||||
'COALESCE(SUM(b.grossRials), 0) as gross,
|
||||
COALESCE(SUM(b.representationShareRials), 0) as rep_income,
|
||||
COALESCE(SUM(b.taxRials), 0) as tax,
|
||||
COALESCE(SUM(b.smsFeeRials), 0) as sms_fee,
|
||||
COALESCE(SUM(b.systemShareRials), 0) as system_share'
|
||||
)
|
||||
->from(FinancialBreakdown::class, 'b')
|
||||
->getQuery()->getSingleResult();
|
||||
|
||||
return $this->success([
|
||||
'total_gross' => (int) $row['gross'],
|
||||
'total_representation_income' => (int) $row['rep_income'],
|
||||
'total_tax_collected' => (int) $row['tax'],
|
||||
'total_sms_fee' => (int) $row['sms_fee'],
|
||||
'total_system_share' => (int) $row['system_share'],
|
||||
]);
|
||||
}
|
||||
|
||||
// ── Secretaries ───────────────────────────────────────────────────────────
|
||||
|
||||
#[OA\Get(
|
||||
|
||||
Reference in New Issue
Block a user