createUser(['ROLE_DOCTOR']); $doctor = new Doctor($owner, 'دکتر'); $this->em->persist($doctor); $this->em->flush(); $patient = $this->createUser(['ROLE_USER']); $record = new PatientRecord('doctor', $doctor->getId(), $patient, 'doctor', $doctor->getId()); $this->em->persist($record); $this->em->flush(); return [$owner, $record, $patient]; } private function sessionFor(PatientRecord $record, int $finalPriceRials): PatientSession { $session = new PatientSession($record); $session->setFinalPriceRials($finalPriceRials); $this->em->persist($session); $this->em->flush(); return $session; } // ── پرداخت جزئی ───────────────────────────────────────────────────────── public function testPartialPaymentReducesDebtButNotPaid(): void { [$owner, $record] = $this->recordFor(); $session = $this->sessionFor($record, 500_000); $res = $this->authJson('POST', '/api/v1/session/' . $session->getUuid() . '/payments', $owner, [ 'method' => 'cash', 'amount_rials' => 200_000, ]); self::assertSame(201, $this->responseCode()); self::assertFalse($res['data']['is_paid']); self::assertSame(200_000, $res['data']['paid_total_rials']); self::assertSame(300_000, $res['data']['patient_debt_rials']); self::assertCount(1, $res['data']['payments']); self::assertSame('cash', $res['data']['payments'][0]['method']); self::assertSame($owner->getMobileNumber(), $res['data']['payments'][0]['created_by_name']); } public function testFullSettlementViaMultiplePaymentsMarksPaid(): void { [$owner, $record] = $this->recordFor(); $session = $this->sessionFor($record, 500_000); $this->authJson('POST', '/api/v1/session/' . $session->getUuid() . '/payments', $owner, [ 'method' => 'cash', 'amount_rials' => 200_000, ]); $res = $this->authJson('POST', '/api/v1/session/' . $session->getUuid() . '/payments', $owner, [ 'method' => 'card', 'amount_rials' => 300_000, 'paid_at' => 1_800_000_000, ]); self::assertSame(201, $this->responseCode()); self::assertTrue($res['data']['is_paid']); self::assertSame('card', $res['data']['payment_method']); self::assertSame(1_800_000_000, $res['data']['paid_at']); self::assertSame(0, $res['data']['patient_debt_rials']); self::assertCount(2, $res['data']['payments']); } public function testWalletPartialPaymentDebitsWallet(): void { [$owner, $record, $patient] = $this->recordFor(); $this->em->persist(new WalletTransaction($patient, 1_000_000, 'credit', 1_000_000)); $this->em->flush(); $session = $this->sessionFor($record, 400_000); $res = $this->authJson('POST', '/api/v1/session/' . $session->getUuid() . '/payments', $owner, [ 'method' => 'wallet', 'amount_rials' => 150_000, ]); self::assertSame(201, $this->responseCode()); self::assertSame(250_000, $res['data']['patient_debt_rials']); $wallet = $this->authJson('GET', '/api/v1/patient/' . $record->getUuid() . '/wallet', $owner); self::assertSame(850_000, $wallet['data']['balance_rials']); $debit = $wallet['data']['recent_transactions'][0]; self::assertSame('debit', $debit['type']); self::assertSame('session:' . $session->getUuid(), $debit['reference']); } public function testWalletPaymentRejectedWhenInsufficient(): void { [$owner, $record, $patient] = $this->recordFor(); $this->em->persist(new WalletTransaction($patient, 100_000, 'credit', 100_000)); $this->em->flush(); $session = $this->sessionFor($record, 400_000); $res = $this->authJson('POST', '/api/v1/session/' . $session->getUuid() . '/payments', $owner, [ 'method' => 'wallet', 'amount_rials' => 200_000, ]); self::assertSame(422, $this->responseCode()); self::assertSame('ERR_WALLET_INSUFFICIENT', $res['errors'][0]['code']); // هیچ پرداختی ثبت نشد $sessions = $this->authJson('GET', '/api/v1/patient/' . $record->getUuid() . '/sessions', $owner); self::assertSame(400_000, $sessions['data'][0]['patient_debt_rials']); self::assertCount(0, $sessions['data'][0]['payments']); } public function testPaymentExceedingDebtRejected(): void { [$owner, $record] = $this->recordFor(); $session = $this->sessionFor($record, 300_000); $res = $this->authJson('POST', '/api/v1/session/' . $session->getUuid() . '/payments', $owner, [ 'method' => 'cash', 'amount_rials' => 400_000, ]); self::assertSame(422, $this->responseCode()); self::assertSame('ERR_SESSION_PAYMENT_EXCEEDS', $res['errors'][0]['code']); } public function testInvalidMethodRejected(): void { [$owner, $record] = $this->recordFor(); $session = $this->sessionFor($record, 300_000); $res = $this->authJson('POST', '/api/v1/session/' . $session->getUuid() . '/payments', $owner, [ 'method' => 'bitcoin', 'amount_rials' => 100_000, ]); self::assertSame(422, $this->responseCode()); self::assertSame('ERR_SESSION_PAYMENT_INVALID', $res['errors'][0]['code']); } public function testZeroAmountRejected(): void { [$owner, $record] = $this->recordFor(); $session = $this->sessionFor($record, 300_000); $res = $this->authJson('POST', '/api/v1/session/' . $session->getUuid() . '/payments', $owner, [ 'method' => 'cash', 'amount_rials' => 0, ]); self::assertSame(422, $this->responseCode()); self::assertSame('ERR_SESSION_PAYMENT_INVALID', $res['errors'][0]['code']); } public function testSessionNotFoundReturns404(): void { [$owner] = $this->recordFor(); $this->authJson('POST', '/api/v1/session/00000000-0000-0000-0000-000000000000/payments', $owner, [ 'method' => 'cash', 'amount_rials' => 100_000, ]); self::assertSame(404, $this->responseCode()); } // ── تخفیف تسویه ───────────────────────────────────────────────────────── public function testPercentDiscountReducesDebt(): void { [$owner, $record] = $this->recordFor(); $session = $this->sessionFor($record, 400_000); $res = $this->authJson('PATCH', '/api/v1/session/' . $session->getUuid(), $owner, [ 'discount_type' => 'percent', 'discount_value' => 25, ]); self::assertSame(200, $this->responseCode()); self::assertSame('percent', $res['data']['discount_type']); self::assertSame(100_000, $res['data']['discount_rials']); $sessions = $this->authJson('GET', '/api/v1/patient/' . $record->getUuid() . '/sessions', $owner); self::assertSame(300_000, $sessions['data'][0]['patient_debt_rials']); } public function testFixedDiscountThenRemove(): void { [$owner, $record] = $this->recordFor(); $session = $this->sessionFor($record, 400_000); $res = $this->authJson('PATCH', '/api/v1/session/' . $session->getUuid(), $owner, [ 'discount_type' => 'fixed', 'discount_value' => 150_000, ]); self::assertSame(150_000, $res['data']['discount_rials']); // حذف تخفیف $res = $this->authJson('PATCH', '/api/v1/session/' . $session->getUuid(), $owner, [ 'discount_type' => null, ]); self::assertSame(200, $this->responseCode()); self::assertNull($res['data']['discount_type']); self::assertSame(0, $res['data']['discount_rials']); } public function testDiscountOverLimitsRejected(): void { [$owner, $record] = $this->recordFor(); $session = $this->sessionFor($record, 400_000); $res = $this->authJson('PATCH', '/api/v1/session/' . $session->getUuid(), $owner, [ 'discount_type' => 'percent', 'discount_value' => 150, ]); self::assertSame(422, $this->responseCode()); self::assertSame('ERR_SESSION_DISCOUNT_INVALID', $res['errors'][0]['code']); $res = $this->authJson('PATCH', '/api/v1/session/' . $session->getUuid(), $owner, [ 'discount_type' => 'fixed', 'discount_value' => 500_000, ]); self::assertSame(422, $this->responseCode()); self::assertSame('ERR_SESSION_DISCOUNT_INVALID', $res['errors'][0]['code']); } public function testFullPercentDiscountMarksPaid(): void { [$owner, $record] = $this->recordFor(); $session = $this->sessionFor($record, 400_000); $res = $this->authJson('PATCH', '/api/v1/session/' . $session->getUuid(), $owner, [ 'discount_type' => 'percent', 'discount_value' => 100, ]); self::assertSame(200, $this->responseCode()); self::assertTrue($res['data']['is_paid']); self::assertNotNull($res['data']['paid_at']); } public function testDiscountPlusPaymentSettles(): void { [$owner, $record] = $this->recordFor(); $session = $this->sessionFor($record, 400_000); $this->authJson('PATCH', '/api/v1/session/' . $session->getUuid(), $owner, [ 'discount_type' => 'fixed', 'discount_value' => 100_000, ]); $res = $this->authJson('POST', '/api/v1/session/' . $session->getUuid() . '/payments', $owner, [ 'method' => 'pos', 'amount_rials' => 300_000, ]); self::assertSame(201, $this->responseCode()); self::assertTrue($res['data']['is_paid']); self::assertSame('pos', $res['data']['payment_method']); self::assertSame(0, $res['data']['patient_debt_rials']); } public function testPaymentOnSettledSessionRejected(): void { [$owner, $record] = $this->recordFor(); $session = $this->sessionFor($record, 200_000); $this->authJson('POST', '/api/v1/session/' . $session->getUuid() . '/payments', $owner, [ 'method' => 'cash', 'amount_rials' => 200_000, ]); self::assertSame(201, $this->responseCode()); // مراجعه تسویه شده — هر پرداخت بعدی از مانده (صفر) بیشتر است $res = $this->authJson('POST', '/api/v1/session/' . $session->getUuid() . '/payments', $owner, [ 'method' => 'cash', 'amount_rials' => 1, ]); self::assertSame(422, $this->responseCode()); self::assertSame('ERR_SESSION_PAYMENT_EXCEEDS', $res['errors'][0]['code']); } }