feat: implement tax calculations for subscription and SMS wallet payments
- Updated SubscriptionPeriod interface to include tax-related fields: tax_percent, tax_rials, and payable_rials. - Modified payment API documentation to reflect changes in tax handling for subscriptions and SMS wallet charges. - Adjusted PaymentController to calculate payment amounts based on subscription period details instead of client input. - Enhanced PaymentManager to handle net amounts for SMS wallet charges, ensuring tax is not credited to the wallet. - Created PaymentTaxCalculator and SubscriptionTaxCalculator services to manage tax calculations consistently across payment types. - Added tests for tax calculations in both subscription and SMS wallet contexts, ensuring correct behavior with and without tax enabled. - Updated frontend components to display tax information appropriately during payment processes.
This commit is contained in:
@@ -15,6 +15,7 @@ use App\Subscription\Repository\SubscriptionPlanRepository;
|
||||
use App\Subscription\Repository\SubscriptionPeriodRepository;
|
||||
use App\Subscription\Repository\ClinicSubscriptionRepository;
|
||||
use App\Subscription\Service\SubscriptionService;
|
||||
use App\Subscription\Service\SubscriptionTaxCalculator;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@@ -36,6 +37,7 @@ class SubscriptionController extends BaseController
|
||||
private readonly UserActiveContextRepository $contextRepo,
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly \App\Secretary\Security\SecretaryAccessChecker $secretaryAccess,
|
||||
private readonly SubscriptionTaxCalculator $tax,
|
||||
) {}
|
||||
|
||||
// ── Public ──────────────────────────────────────────────────────────────
|
||||
@@ -45,10 +47,10 @@ class SubscriptionController extends BaseController
|
||||
{
|
||||
$plans = $this->planRepo->findAllActive();
|
||||
|
||||
return $this->success(array_map(
|
||||
return $this->success($this->tax->decoratePlans(array_map(
|
||||
fn(SubscriptionPlan $p) => $p->toArray(withPeriods: true),
|
||||
$plans
|
||||
));
|
||||
)));
|
||||
}
|
||||
|
||||
// ── Authenticated ────────────────────────────────────────────────────────
|
||||
@@ -123,7 +125,7 @@ class SubscriptionController extends BaseController
|
||||
$plans = $this->planRepo->findAllForAdmin();
|
||||
|
||||
return $this->paginated(
|
||||
array_map(fn(SubscriptionPlan $p) => $p->toArray(withPeriods: true), $plans),
|
||||
$this->tax->decoratePlans(array_map(fn(SubscriptionPlan $p) => $p->toArray(withPeriods: true), $plans)),
|
||||
count($plans),
|
||||
1,
|
||||
100
|
||||
@@ -247,7 +249,7 @@ class SubscriptionController extends BaseController
|
||||
|
||||
$this->periodRepo->save($period);
|
||||
|
||||
return $this->success($period->toArray(), 201);
|
||||
return $this->success($this->tax->decoratePeriod($period->toArray()), 201);
|
||||
}
|
||||
|
||||
#[Route('/api/v1/admin/subscription/period/{uuid}', methods: ['PATCH'])]
|
||||
@@ -269,7 +271,7 @@ class SubscriptionController extends BaseController
|
||||
|
||||
$this->periodRepo->save($period);
|
||||
|
||||
return $this->success($period->toArray());
|
||||
return $this->success($this->tax->decoratePeriod($period->toArray()));
|
||||
}
|
||||
|
||||
#[Route('/api/v1/admin/subscription/period/{uuid}', methods: ['DELETE'])]
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Subscription\Service;
|
||||
|
||||
use App\Payment\Service\PaymentTaxCalculator;
|
||||
use App\Subscription\Entity\SubscriptionPeriod;
|
||||
|
||||
/**
|
||||
* لایهٔ اشتراکیِ مالیات: حساب را به `PaymentTaxCalculator` میسپارد و فقط شکلِ
|
||||
* دوره/پلن را میشناسد.
|
||||
*
|
||||
* قیمت دوره (`price_rials`) خالص است و مالیات رویش اضافه میشود؛ پس مبلغ قابل
|
||||
* پرداخت از قیمت پلن بیشتر است.
|
||||
*/
|
||||
class SubscriptionTaxCalculator
|
||||
{
|
||||
public function __construct(private readonly PaymentTaxCalculator $tax) {}
|
||||
|
||||
public function percent(): float
|
||||
{
|
||||
return $this->tax->percent();
|
||||
}
|
||||
|
||||
public function taxOf(int $netRials): int
|
||||
{
|
||||
return $this->tax->taxOf($netRials);
|
||||
}
|
||||
|
||||
public function payableOf(int $netRials): int
|
||||
{
|
||||
return $this->tax->payableOf($netRials);
|
||||
}
|
||||
|
||||
public function payableForPeriod(SubscriptionPeriod $period): int
|
||||
{
|
||||
return $this->tax->payableOf($period->getPriceRials());
|
||||
}
|
||||
|
||||
/**
|
||||
* سه فیلد مالیاتی را کنار `price_rials` میگذارد. کلیدِ خودِ قیمت دست نمیخورد
|
||||
* تا کلاینتهای قدیمی نشکنند.
|
||||
*
|
||||
* @param array<string, mixed> $period خروجی `SubscriptionPeriod::toArray()`
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function decoratePeriod(array $period): array
|
||||
{
|
||||
$net = (int) ($period['price_rials'] ?? 0);
|
||||
|
||||
return $period + [
|
||||
'tax_percent' => $this->tax->percent(),
|
||||
'tax_rials' => $this->tax->taxOf($net),
|
||||
'payable_rials' => $this->tax->payableOf($net),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* همان کار را روی `periods` هر پلن انجام میدهد.
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $plans خروجی `SubscriptionPlan::toArray(withPeriods: true)`
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function decoratePlans(array $plans): array
|
||||
{
|
||||
return array_map(function (array $plan): array {
|
||||
if (isset($plan['periods']) && is_array($plan['periods'])) {
|
||||
$plan['periods'] = array_map($this->decoratePeriod(...), $plan['periods']);
|
||||
}
|
||||
|
||||
return $plan;
|
||||
}, $plans);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user