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()); } }