- 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.
179 lines
7.0 KiB
PHP
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]),
|
|
'پرداخت محیط دیگر نباید دیده شود',
|
|
);
|
|
}
|
|
}
|