Files
clinicpro/tests/Subscription/SubscriptionTaxTest.php
T
hamed 7716b40f6a 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.
2026-08-09 16:51:22 +03:30

151 lines
5.5 KiB
PHP

<?php
namespace App\Tests\Subscription;
use App\Config\Entity\SiteConfig;
use App\Payment\Entity\Payment;
use App\Subscription\Entity\SubscriptionPeriod;
use App\Subscription\Entity\SubscriptionPlan;
use App\Subscription\Service\SubscriptionTaxCalculator;
use App\Tests\ApiTestCase;
/**
* قیمت دورهٔ اشتراک خالص است و مالیات رویش اضافه می‌شود — برخلاف نوبت که مبلغش
* شامل مالیات است. پس مبلغِ پرداختی همیشه ≥ قیمت پلن است، و مبلغ از سمت سرور
* محاسبه می‌شود نه از بدنهٔ درخواست.
*/
class SubscriptionTaxTest extends ApiTestCase
{
private function setConfig(string $key, string $value): void
{
$cfg = $this->em->getRepository(SiteConfig::class)->findOneBy(['configKey' => $key]);
if ($cfg === null) {
$this->em->persist(new SiteConfig($key, $value));
} else {
$cfg->setValue($value);
}
$this->em->flush();
}
private function enableTax(string $percent = '10'): void
{
$this->setConfig('tax_enabled', '1');
$this->setConfig('tax_percent', $percent);
}
private function makePeriod(int $priceRials): SubscriptionPeriod
{
$plan = new SubscriptionPlan('plan-' . bin2hex(random_bytes(4)), 5, 1, []);
$this->em->persist($plan);
$period = new SubscriptionPeriod($plan, 'یک ماهه', 1, $priceRials);
$this->em->persist($period);
$this->em->flush();
return $period;
}
private function calculator(): SubscriptionTaxCalculator
{
return static::getContainer()->get(SubscriptionTaxCalculator::class);
}
public function testTaxIsAddedOnTopOfTheNetPrice(): void
{
$this->enableTax('10');
$calc = $this->calculator();
self::assertSame(29_000, $calc->taxOf(290_000));
self::assertSame(319_000, $calc->payableOf(290_000), 'مالیات باید اضافه شود، نه استخراج');
}
public function testDisabledTaxLeavesThePriceUntouched(): void
{
$this->setConfig('tax_enabled', '0');
$this->setConfig('tax_percent', '10');
$calc = $this->calculator();
self::assertSame(0.0, $calc->percent());
self::assertSame(0, $calc->taxOf(290_000));
self::assertSame(290_000, $calc->payableOf(290_000));
}
public function testFreePeriodStaysFree(): void
{
$this->enableTax('10');
self::assertSame(0, $this->calculator()->payableOf(0));
}
public function testPublicPlanListExposesTaxPerPeriod(): void
{
$this->enableTax('10');
$this->makePeriod(290_000);
// این روت پشت firewall نشسته (بر خلاف چیزی که docs می‌گوید)، پس با توکن زده می‌شود.
$this->authJson('GET', '/api/v1/subscription/plans', $this->createUser());
self::assertSame(200, $this->responseCode());
$periods = [];
foreach (json_decode((string) $this->client->getResponse()->getContent(), true)['data'] as $plan) {
foreach ($plan['periods'] ?? [] as $period) {
$periods[] = $period;
}
}
self::assertNotEmpty($periods, 'حداقل یک دوره باید برگردد');
foreach ($periods as $period) {
self::assertArrayHasKey('tax_percent', $period);
self::assertArrayHasKey('tax_rials', $period);
self::assertArrayHasKey('payable_rials', $period);
self::assertSame(
$period['price_rials'] + $period['tax_rials'],
$period['payable_rials'],
);
}
}
public function testPaymentChargesPricePlusTaxAndIgnoresTheClientAmount(): void
{
$this->enableTax('10');
$period = $this->makePeriod(290_000);
$user = $this->createUser(['ROLE_DOCTOR']);
$doctor = new \App\Doctor\Entity\Doctor($user, 'دکتر مالیات');
$this->em->persist($doctor);
$this->em->flush();
$this->authJson('POST', '/api/v1/subscription-payment', $user, [
'gateway' => 'mellat',
'period_uuid' => $period->getUuid(),
// مبلغِ دستکاری‌شده باید نادیده گرفته شود.
'amount_rials' => 1_000,
]);
self::assertSame(200, $this->responseCode());
$body = json_decode((string) $this->client->getResponse()->getContent(), true)['data'];
self::assertSame(290_000, $body['price_rials']);
self::assertSame(29_000, $body['tax_rials']);
self::assertSame(319_000, $body['payable_rials']);
$payment = $this->em->getRepository(Payment::class)->findOneBy(['uuid' => $body['payment_uuid']]);
self::assertSame(319_000, $payment->getAmountRials(), 'مبلغ ذخیره‌شده باید شامل مالیات باشد');
}
public function testPaymentRejectsAnUnknownPeriod(): void
{
$this->enableTax('10');
$user = $this->createUser(['ROLE_DOCTOR']);
$doctor = new \App\Doctor\Entity\Doctor($user, 'دکتر بدون دوره');
$this->em->persist($doctor);
$this->em->flush();
$this->authJson('POST', '/api/v1/subscription-payment', $user, [
'gateway' => 'mellat',
'period_uuid' => 'does-not-exist',
'amount_rials' => 290_000,
]);
self::assertSame(422, $this->responseCode());
}
}