feat: port payment management tab from tauri to admin dashboard
Add per-clinic payment methods (bank accounts + POS/card-reader devices)
under the "مدیریت پرداخت" settings tab at /admin/my-financial, ported from
clinic-pro-tauri's mock-only PaymentManagement tab into a real persisted
feature. These records are referenceable (by uuid) from patient invoices to
record which method a service payment was made with.
Backend (new src/PaymentMethod domain):
- BankAccount + Pos entities, repositories, PaymentMethodService (validation,
ownership scoping, create/update/toggle logic).
- Thin PaymentMethodController exposing /api/v1/my/payment-methods/{bank-accounts,pos}
(GET/POST/PUT + PATCH .../status), guarded to clinic/doctor/secretary/admin.
- Migration for bank_accounts + pos_devices tables.
- Functional tests (success + validation/404/403 + empty boundaries).
- docs/api/payment-method.md.
Frontend:
- Replace MyFinancialPage content with the payment-management UI (two tabs,
tables, add/edit modals, status toggle) using the admin design system.
- usePaymentMethods hook (TanStack Query) + presentational components.
- Update page test to cover tabs, data, empty state and the add modal.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace App\PaymentMethod\Controller;
|
||||
|
||||
use App\Auth\Entity\User;
|
||||
use App\PaymentMethod\Service\PaymentMethodService;
|
||||
use App\Shared\Constant\ErrorCodes;
|
||||
use App\Shared\Controller\BaseController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\CurrentUser;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
/**
|
||||
* Per-clinic payment methods: bank accounts and POS (card reader) devices.
|
||||
* Scoped to the acting user; only clinic/doctor/secretary roles may manage them.
|
||||
*/
|
||||
#[OA\Tag(name: 'Payment Methods')]
|
||||
#[Route('/api/v1/my/payment-methods')]
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
class PaymentMethodController extends BaseController
|
||||
{
|
||||
private const ALLOWED_ROLES = ['ROLE_CLINIC', 'ROLE_DOCTOR', 'ROLE_SECRETARY', 'ROLE_ADMIN'];
|
||||
|
||||
public function __construct(
|
||||
private readonly PaymentMethodService $service,
|
||||
) {}
|
||||
|
||||
// ---- Bank accounts -----------------------------------------------------
|
||||
|
||||
#[Route('/bank-accounts', methods: ['GET'])]
|
||||
public function listBankAccounts(#[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->assertRole($user);
|
||||
|
||||
return $this->success($this->service->listBankAccounts($user));
|
||||
}
|
||||
|
||||
#[Route('/bank-accounts', methods: ['POST'])]
|
||||
public function createBankAccount(Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->assertRole($user);
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
|
||||
return $this->success($this->service->createBankAccount($user, $data), 201);
|
||||
}
|
||||
|
||||
#[Route('/bank-accounts/{uuid}', methods: ['PUT'])]
|
||||
public function updateBankAccount(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->assertRole($user);
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
|
||||
return $this->success($this->service->updateBankAccount($user, $uuid, $data));
|
||||
}
|
||||
|
||||
#[Route('/bank-accounts/{uuid}/status', methods: ['PATCH'])]
|
||||
public function toggleBankAccountStatus(string $uuid, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->assertRole($user);
|
||||
|
||||
return $this->success($this->service->toggleBankAccountStatus($user, $uuid));
|
||||
}
|
||||
|
||||
// ---- POS devices -------------------------------------------------------
|
||||
|
||||
#[Route('/pos', methods: ['GET'])]
|
||||
public function listPos(#[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->assertRole($user);
|
||||
|
||||
return $this->success($this->service->listPos($user));
|
||||
}
|
||||
|
||||
#[Route('/pos', methods: ['POST'])]
|
||||
public function createPos(Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->assertRole($user);
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
|
||||
return $this->success($this->service->createPos($user, $data), 201);
|
||||
}
|
||||
|
||||
#[Route('/pos/{uuid}', methods: ['PUT'])]
|
||||
public function updatePos(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->assertRole($user);
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
|
||||
return $this->success($this->service->updatePos($user, $uuid, $data));
|
||||
}
|
||||
|
||||
#[Route('/pos/{uuid}/status', methods: ['PATCH'])]
|
||||
public function togglePosStatus(string $uuid, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->assertRole($user);
|
||||
|
||||
return $this->success($this->service->togglePosStatus($user, $uuid));
|
||||
}
|
||||
|
||||
private function assertRole(User $user): void
|
||||
{
|
||||
if (!array_intersect(self::ALLOWED_ROLES, $user->getRoles())) {
|
||||
throw new \App\Shared\Exception\AppException(ErrorCodes::ERR_FORBIDDEN_001, 'دسترسی ندارید', 403);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user