feat: implement staff management and subscription system
- Added StaffController for managing clinic staff, including listing, creating, updating, and toggling staff status. - Created ClinicStaff entity and repository for staff data handling. - Developed SubscriptionController to manage subscription plans and periods, including trial subscriptions. - Introduced SubscriptionPlan, SubscriptionPeriod, and ClinicSubscription entities for subscription management. - Implemented SubscriptionService for handling subscription logic, including trial activation and subscription creation from payments. - Added necessary repositories for subscription entities to facilitate data access and manipulation.
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
<?php
|
||||
|
||||
namespace App\Subscription\Controller;
|
||||
|
||||
use App\Auth\Entity\User;
|
||||
use App\Clinic\Repository\ClinicRepository;
|
||||
use App\Doctor\Repository\DoctorRepository;
|
||||
use App\Shared\Constant\ErrorCodes;
|
||||
use App\Shared\Controller\BaseController;
|
||||
use App\Shared\Exception\AppException;
|
||||
use App\Subscription\Entity\SubscriptionPeriod;
|
||||
use App\Subscription\Entity\SubscriptionPlan;
|
||||
use App\Subscription\Repository\SubscriptionPlanRepository;
|
||||
use App\Subscription\Repository\SubscriptionPeriodRepository;
|
||||
use App\Subscription\Repository\ClinicSubscriptionRepository;
|
||||
use App\Subscription\Service\SubscriptionService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
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;
|
||||
|
||||
class SubscriptionController extends BaseController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SubscriptionService $subscriptionService,
|
||||
private readonly SubscriptionPlanRepository $planRepo,
|
||||
private readonly SubscriptionPeriodRepository $periodRepo,
|
||||
private readonly ClinicSubscriptionRepository $subscriptionRepo,
|
||||
private readonly DoctorRepository $doctorRepo,
|
||||
private readonly ClinicRepository $clinicRepo,
|
||||
private readonly EntityManagerInterface $em,
|
||||
) {}
|
||||
|
||||
// ── Public ──────────────────────────────────────────────────────────────
|
||||
|
||||
#[Route('/api/v1/subscription/plans', methods: ['GET'])]
|
||||
public function plans(): JsonResponse
|
||||
{
|
||||
$plans = $this->planRepo->findAllActive();
|
||||
|
||||
return $this->success(array_map(
|
||||
fn(SubscriptionPlan $p) => $p->toArray(withPeriods: true),
|
||||
$plans
|
||||
));
|
||||
}
|
||||
|
||||
// ── Authenticated ────────────────────────────────────────────────────────
|
||||
|
||||
#[Route('/api/v1/subscription/my', methods: ['GET'])]
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
public function my(#[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
if ($entityId === null) {
|
||||
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
|
||||
}
|
||||
|
||||
$subscription = $this->subscriptionService->getActiveSubscription($entityType, $entityId);
|
||||
$usedTrial = $this->subscriptionService->hasUsedTrial($entityType, $entityId);
|
||||
|
||||
return $this->success([
|
||||
'subscription' => $subscription?->toArray(),
|
||||
'used_trial' => $usedTrial,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/api/v1/subscription/trial', methods: ['POST'])]
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
public function trial(#[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
if ($entityId === null) {
|
||||
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
|
||||
}
|
||||
|
||||
try {
|
||||
$subscription = $this->subscriptionService->activateTrial($entityType, $entityId);
|
||||
} catch (AppException $e) {
|
||||
return $this->error($e->getErrorCode(), $e->getMessage(), $e->getHttpStatus());
|
||||
}
|
||||
|
||||
return $this->success($subscription->toArray(), 201);
|
||||
}
|
||||
|
||||
// ── Admin ────────────────────────────────────────────────────────────────
|
||||
|
||||
#[Route('/api/v1/admin/subscription/plans', methods: ['GET'])]
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
public function adminPlans(): JsonResponse
|
||||
{
|
||||
$plans = $this->planRepo->findAllActive();
|
||||
|
||||
return $this->paginated(
|
||||
array_map(fn(SubscriptionPlan $p) => $p->toArray(withPeriods: true), $plans),
|
||||
count($plans),
|
||||
1,
|
||||
100
|
||||
);
|
||||
}
|
||||
|
||||
#[Route('/api/v1/admin/subscription/plan', methods: ['POST'])]
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
public function adminCreatePlan(Request $request): JsonResponse
|
||||
{
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
|
||||
$name = trim($data['name'] ?? '');
|
||||
if ($name === '' || !isset($data['level'])) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'name و level الزامی هستند', 422);
|
||||
}
|
||||
|
||||
$plan = new SubscriptionPlan(
|
||||
$name,
|
||||
(int) $data['level'],
|
||||
(int) ($data['max_secretaries'] ?? 1),
|
||||
$data['features'] ?? []
|
||||
);
|
||||
|
||||
$this->planRepo->save($plan);
|
||||
|
||||
return $this->success($plan->toArray(), 201);
|
||||
}
|
||||
|
||||
#[Route('/api/v1/admin/subscription/plan/{uuid}', methods: ['PATCH'])]
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
public function adminUpdatePlan(string $uuid, Request $request): JsonResponse
|
||||
{
|
||||
$plan = $this->planRepo->findByUuid($uuid);
|
||||
if ($plan === null) {
|
||||
return $this->error(ErrorCodes::ERR_SUBSCRIPTION_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_SUBSCRIPTION_NOT_FOUND), 404);
|
||||
}
|
||||
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
|
||||
if (isset($data['name'])) { $plan->setName($data['name']); }
|
||||
if (isset($data['level'])) { $plan->setLevel((int) $data['level']); }
|
||||
if (isset($data['max_secretaries'])) { $plan->setMaxSecretaries((int) $data['max_secretaries']); }
|
||||
if (isset($data['features'])) { $plan->setFeatures($data['features']); }
|
||||
if (isset($data['active'])) { $plan->setActive((bool) $data['active']); }
|
||||
|
||||
$this->planRepo->save($plan);
|
||||
|
||||
return $this->success($plan->toArray());
|
||||
}
|
||||
|
||||
#[Route('/api/v1/admin/subscription/period', methods: ['POST'])]
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
public function adminCreatePeriod(Request $request): JsonResponse
|
||||
{
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
|
||||
$planUuid = $data['plan_uuid'] ?? '';
|
||||
$plan = $this->planRepo->findByUuid($planUuid);
|
||||
if ($plan === null) {
|
||||
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'پنل یافت نشد', 404);
|
||||
}
|
||||
|
||||
if (empty($data['label']) || !isset($data['duration_months'])) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'label و duration_months الزامی هستند', 422);
|
||||
}
|
||||
|
||||
$period = new SubscriptionPeriod(
|
||||
$plan,
|
||||
$data['label'],
|
||||
(int) $data['duration_months'],
|
||||
(int) ($data['price_rials'] ?? 0),
|
||||
(bool) ($data['is_trial'] ?? false)
|
||||
);
|
||||
$period->setSortOrder((int) ($data['sort_order'] ?? 0));
|
||||
|
||||
$this->periodRepo->save($period);
|
||||
|
||||
return $this->success($period->toArray(), 201);
|
||||
}
|
||||
|
||||
#[Route('/api/v1/admin/subscription/period/{uuid}', methods: ['PATCH'])]
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
public function adminUpdatePeriod(string $uuid, Request $request): JsonResponse
|
||||
{
|
||||
$period = $this->periodRepo->findByUuid($uuid);
|
||||
if ($period === null) {
|
||||
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'دوره یافت نشد', 404);
|
||||
}
|
||||
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
|
||||
if (isset($data['label'])) { $period->setLabel($data['label']); }
|
||||
if (isset($data['duration_months'])) { $period->setDurationMonths((int) $data['duration_months']); }
|
||||
if (isset($data['price_rials'])) { $period->setPriceRials((int) $data['price_rials']); }
|
||||
if (isset($data['active'])) { $period->setActive((bool) $data['active']); }
|
||||
if (isset($data['sort_order'])) { $period->setSortOrder((int) $data['sort_order']); }
|
||||
|
||||
$this->periodRepo->save($period);
|
||||
|
||||
return $this->success($period->toArray());
|
||||
}
|
||||
|
||||
#[Route('/api/v1/admin/subscription/period/{uuid}', methods: ['DELETE'])]
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
public function adminDeletePeriod(string $uuid): JsonResponse
|
||||
{
|
||||
$period = $this->periodRepo->findByUuid($uuid);
|
||||
if ($period === null) {
|
||||
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'دوره یافت نشد', 404);
|
||||
}
|
||||
|
||||
$period->setActive(false);
|
||||
$this->periodRepo->save($period);
|
||||
|
||||
return $this->success(['message' => 'دوره غیرفعال شد']);
|
||||
}
|
||||
|
||||
#[Route('/api/v1/admin/subscription/report', methods: ['GET'])]
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
public function adminReport(Request $request): JsonResponse
|
||||
{
|
||||
$page = max(1, (int) $request->query->get('page', 1));
|
||||
$limit = min(100, max(10, (int) $request->query->get('limit', 20)));
|
||||
|
||||
$total = (int) $this->em->createQuery('SELECT COUNT(s.id) FROM App\Subscription\Entity\ClinicSubscription s')
|
||||
->getSingleScalarResult();
|
||||
|
||||
$subscriptions = $this->em->createQuery('
|
||||
SELECT s.uuid, s.entityType, s.entityId, s.isTrial, s.startsAt, s.expiresAt, s.createdAt,
|
||||
p.name AS plan_name, p.level AS plan_level
|
||||
FROM App\Subscription\Entity\ClinicSubscription s
|
||||
JOIN s.plan p
|
||||
ORDER BY s.id DESC
|
||||
')
|
||||
->setFirstResult(($page - 1) * $limit)
|
||||
->setMaxResults($limit)
|
||||
->getArrayResult();
|
||||
|
||||
return $this->paginated($subscriptions, $total, $page, $limit);
|
||||
}
|
||||
|
||||
// ── Helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
private function resolveEntity(User $user): array
|
||||
{
|
||||
if ($user->hasRole('ROLE_DOCTOR')) {
|
||||
$doctor = $this->doctorRepo->findByUser($user);
|
||||
return $doctor !== null ? ['doctor', $doctor->getId()] : ['doctor', null];
|
||||
}
|
||||
|
||||
if ($user->hasRole('ROLE_CLINIC')) {
|
||||
$clinic = $this->clinicRepo->findByUser($user);
|
||||
return $clinic !== null ? ['clinic', $clinic->getId()] : ['clinic', null];
|
||||
}
|
||||
|
||||
return ['unknown', null];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user