Files
clinicpro/tests/Payment/PaymentTenantTest.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

179 lines
7.0 KiB
PHP

<?php
namespace App\Tests\Payment;
use App\Clinic\Entity\Clinic;
use App\Doctor\Entity\Doctor;
use App\Payment\Entity\Payment;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Tenant\TenantFilter;
use App\Tests\ApiTestCase;
use Doctrine\ORM\EntityManagerInterface;
/**
* پرداخت به محیطِ **گیرنده** تعلق دارد، نه به پرداخت‌کننده: نوبت → محیط همان نوبت،
* اشتراک → محیطی که خریدار صاحبش است، شارژ پیامک → محیطِ همان کیف پول.
*
* بیمار در هیچ محیطی نیست، پس TenantFilter برایش خاموش می‌ماند و پرداخت خودش را
* می‌بیند — همان دلیلی که فاز ۴ فیلتر را فقط روی «محیط انتخاب‌شده» روشن کرد.
*/
class PaymentTenantTest extends ApiTestCase
{
private function em(): EntityManagerInterface
{
return static::getContainer()->get(EntityManagerInterface::class);
}
protected function tearDown(): void
{
$filters = $this->em()->getFilters();
if ($filters->isEnabled(TenantFilter::NAME)) {
$filters->disable(TenantFilter::NAME);
}
parent::tearDown();
}
private function makePricedPeriod(int $priceRials): \App\Subscription\Entity\SubscriptionPeriod
{
$plan = new \App\Subscription\Entity\SubscriptionPlan('plan-' . bin2hex(random_bytes(4)), 5, 1, []);
$this->em->persist($plan);
$period = new \App\Subscription\Entity\SubscriptionPeriod($plan, 'یک ماهه', 1, $priceRials);
$this->em->persist($period);
$this->em->flush();
return $period;
}
private function makeDoctor(): Doctor
{
$doctor = new Doctor($this->createUser(['ROLE_DOCTOR']), 'دکتر پرداخت');
$this->em->persist($doctor);
$this->em->flush();
return $doctor;
}
private function makeClinic(): Clinic
{
$clinic = new Clinic($this->createUser(['ROLE_CLINIC']));
$clinic->setName('کلینیک پرداخت');
$this->em->persist($clinic);
$this->em->flush();
return $clinic;
}
/** پرداخت را از دیتابیس می‌خواند، نه از پاسخ — پاسخ محیط را برنمی‌گرداند. */
private function storedPayment(string $uuid): Payment
{
$this->em->clear();
$payment = $this->em->getRepository(Payment::class)->findOneBy(['uuid' => $uuid]);
self::assertNotNull($payment, 'پرداخت باید ذخیره شده باشد');
return $payment;
}
private function payForAppointment(?Clinic $clinic): Payment
{
$doctor = $this->makeDoctor();
$patient = $this->createUser(['ROLE_USER']);
$start = strtotime('+10 days') + random_int(0, 500_000) * 7;
$appointment = $this->newAppointment($doctor, $patient, $start, $start + 900, $clinic);
$this->em->persist($appointment);
$this->em->flush();
$res = $this->authJson('POST', '/api/v1/payment/appointment', $patient, [
'appointment_uuid' => $appointment->getUuid(),
'gateway' => 'mellat',
]);
self::assertSame(200, $this->responseCode(), 'شروع پرداخت نوبت باید موفق باشد');
return $this->storedPayment($res['data']['payment_uuid']);
}
/** ✅ پرداخت نوبتِ یک کلینیک به همان کلینیک می‌نشیند. */
public function testAppointmentPaymentBelongsToTheClinicOfTheAppointment(): void
{
$clinic = $this->makeClinic();
$payment = $this->payForAppointment($clinic);
self::assertSame('clinic', $payment->getEntityType());
self::assertSame($clinic->getId(), $payment->getEntityId());
}
/** ✅ نوبتِ مطب شخصی به خودِ پزشک. */
public function testAppointmentPaymentOfAPersonalPracticeBelongsToTheDoctor(): void
{
$payment = $this->payForAppointment(null);
self::assertSame('doctor', $payment->getEntityType());
}
/** ✅ اشتراک به محیطی که خریدار صاحبش است. */
public function testSubscriptionPaymentBelongsToTheEnvironmentTheBuyerOwns(): void
{
$doctor = $this->makeDoctor();
// مبلغ از خودِ دوره می‌آید (قیمت خالص + مالیات)، نه از بدنهٔ درخواست.
$res = $this->authJson('POST', '/api/v1/subscription-payment', $doctor->getUser(), [
'gateway' => 'mellat',
'period_uuid' => $this->makePricedPeriod(1_000_000)->getUuid(),
]);
self::assertSame(200, $this->responseCode());
$payment = $this->storedPayment($res['data']['payment_uuid']);
self::assertSame('doctor', $payment->getEntityType());
self::assertSame($doctor->getId(), $payment->getEntityId());
}
/**
* ❌ کاربری که نه پزشک است نه کلینیک، اشتراک برای هیچ محیطی نمی‌خرد. بدون این
* گارد، ردیفی با محیطِ نامعتبر ساخته می‌شد یا flush بی‌پیام می‌شکست.
*/
public function testSubscriptionPaymentWithoutAnOwnedEnvironmentIsRejected(): void
{
$res = $this->authJson('POST', '/api/v1/subscription-payment', $this->createUser(['ROLE_USER']), [
'gateway' => 'mellat',
'period_uuid' => $this->makePricedPeriod(1_000_000)->getUuid(),
]);
self::assertSame(422, $this->responseCode());
self::assertSame(ErrorCodes::ERR_PAYMENT_004, $res['errors'][0]['code']);
}
/** ⚠️ مرزی: بیمار محیطی ندارد، پس فیلتر خاموش است و پرداخت خودش را می‌بیند. */
public function testThePayingPatientStillSeesTheirOwnPayment(): void
{
$clinic = $this->makeClinic();
$payment = $this->payForAppointment($clinic);
$patient = $payment->getUser();
$res = $this->authJson('GET', '/api/v1/payment/' . $payment->getUuid(), $patient);
self::assertSame(200, $this->responseCode(), 'بیمار باید پرداخت خودش را ببیند');
self::assertSame($payment->getUuid(), $res['data']['uuid']);
}
/** ⚠️ محیط دیگر همان پرداخت را اصلاً نمی‌بیند — تور ایمنیِ TenantFilter. */
public function testAnotherEnvironmentDoesNotSeeThePaymentAtAll(): void
{
$clinic = $this->makeClinic();
$payment = $this->payForAppointment($clinic);
$uuid = $payment->getUuid();
$this->em->clear();
$this->em()->getFilters()
->enable(TenantFilter::NAME)
->setParameter(TenantFilter::PARAM_TYPE, 'clinic', 'string')
->setParameter(TenantFilter::PARAM_ID, $clinic->getId() + 1000, 'integer');
self::assertNull(
$this->em->getRepository(Payment::class)->findOneBy(['uuid' => $uuid]),
'پرداخت محیط دیگر نباید دیده شود',
);
}
}