feat: port tauri create-service payment flow to admin session settlement
Port the tauri /files/create-service page (payment mode) to the admin SPA
and back it with real multi-part session settlement:
Backend:
- New SessionPayment entity (session_payments table): partial payments
per session with method (wallet/pos/cash/card), amount, paid_at, actor
- PatientSession: settlement discount (percent/fixed), discount_rials,
paid_at, payments relation; remaining debt derived from
final - discount - paid total
- POST /api/v1/session/{uuid}/payments: register a partial payment;
wallet method debits the patient wallet; zero remaining marks paid
- PATCH /api/v1/session/{uuid}: accepts discount_type/discount_value
(null removes) and paid_at, backward compatible
- New error codes: ERR_SESSION_PAYMENT_INVALID/_EXCEEDS,
ERR_SESSION_DISCOUNT_INVALID
- Migration + 14 functional tests (partial/full/wallet/exceed/discount)
Frontend (admin):
- SessionPaymentPage: two-step stepper (پرداخت ← جزییات) ported from
tauri AddService payment mode — service cost, settlement discount
input, Jalali payment date, wallet balance, 4-method payment accordion,
paid-list box, details summary
- SessionStepper + stepper/payment icons ported verbatim from tauri SVGs
- «تکمیل پرداخت» on SessionServiceCard now navigates to the payment page
(replaces the small settle modal on PatientDetailPage)
- Routes for patients/ and my-patients/ variants; vitest coverage
- docs/api/patient.md updated
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -979,10 +979,10 @@ class PatientController extends BaseController
|
||||
|
||||
if ($data['is_paid']) {
|
||||
$data['patient_debt_rials'] = 0;
|
||||
} elseif ($invoice !== null) {
|
||||
$data['patient_debt_rials'] = $invoice->getPatientRials();
|
||||
} else {
|
||||
$data['patient_debt_rials'] = $session->getFinalPriceRials();
|
||||
// سهم بیمار (از فاکتور در صورت وجود) منهای تخفیف تسویه و پرداختهای جزئی
|
||||
$share = $invoice !== null ? $invoice->getPatientRials() : $session->getFinalPriceRials();
|
||||
$data['patient_debt_rials'] = max(0, $share - $session->getDiscountRials() - $session->getPaidTotalRials());
|
||||
}
|
||||
|
||||
return $data;
|
||||
@@ -1046,6 +1046,14 @@ class PatientController extends BaseController
|
||||
|
||||
if (isset($data['notes'])) { $session->setNotes($data['notes']); }
|
||||
|
||||
// تخفیف تسویه: discount_type = percent|fixed|null (null = حذف تخفیف)
|
||||
if (array_key_exists('discount_type', $data)) {
|
||||
$type = $data['discount_type'] !== null ? (string) $data['discount_type'] : null;
|
||||
$this->patientService->applyDiscount($session, $type, (int) ($data['discount_value'] ?? 0));
|
||||
}
|
||||
|
||||
if (isset($data['paid_at'])) { $session->setPaidAt((int) $data['paid_at']); }
|
||||
|
||||
if (isset($data['payment_method'])) {
|
||||
$method = (string) $data['payment_method'];
|
||||
// پرداخت از کیف پول: سهمِ بیمار را از موجودی کسر کن (فقط یکبار، اگر
|
||||
@@ -1064,6 +1072,36 @@ class PatientController extends BaseController
|
||||
return $this->success($session->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* ثبت پرداخت جزئی روی مراجعه (تسویه چندتکه).
|
||||
* body: { method: wallet|pos|cash|card, amount_rials: int, paid_at?: int }
|
||||
* روش wallet همان مبلغ را از کیف پول بیمار کسر میکند. وقتی مانده صفر شود
|
||||
* مراجعه تسویهشده (is_paid) میشود.
|
||||
*/
|
||||
#[Route('/api/v1/session/{uuid}/payments', methods: ['POST'])]
|
||||
public function addSessionPayment(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
$this->assertPatientGate($entityType, $entityId);
|
||||
|
||||
$session = $this->sessionRepo->findByUuid($uuid);
|
||||
if ($session === null || !$this->ownsRecord($session->getRecord(), $entityType, $entityId)) {
|
||||
return $this->error(ErrorCodes::ERR_SESSION_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_SESSION_NOT_FOUND), 404);
|
||||
}
|
||||
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
|
||||
$this->patientService->addSessionPayment(
|
||||
$session,
|
||||
(string) ($data['method'] ?? ''),
|
||||
(int) ($data['amount_rials'] ?? 0),
|
||||
isset($data['paid_at']) ? (int) $data['paid_at'] : null,
|
||||
$user,
|
||||
);
|
||||
|
||||
return $this->success($this->sessionWithBilling($session), 201);
|
||||
}
|
||||
|
||||
private function resolveEntity(User $user): array
|
||||
{
|
||||
if ($user->hasRole('ROLE_DOCTOR')) {
|
||||
|
||||
Reference in New Issue
Block a user