feat(discount): admin CRUD + session discount-suggestions endpoints
DiscountController exposes owner-scoped CRUD for discount rules
(/api/v1/admin/discount-rules) and GET /api/v1/session/{uuid}/discount-
suggestions which runs the engine for a session. Owner resolved from the
doctor/clinic user. Verified end-to-end (create, list, suggestions). Adds
docs/api/discount.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
|
||||
namespace App\Discount\Controller;
|
||||
|
||||
use App\Auth\Entity\User;
|
||||
use App\Clinic\Repository\ClinicRepository;
|
||||
use App\Discount\Entity\DiscountRule;
|
||||
use App\Discount\Repository\DiscountRuleRepository;
|
||||
use App\Discount\Service\DiscountEngine;
|
||||
use App\Doctor\Repository\DoctorRepository;
|
||||
use App\Patient\Repository\PatientSessionRepository;
|
||||
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;
|
||||
|
||||
class DiscountController extends BaseController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly DiscountRuleRepository $ruleRepo,
|
||||
private readonly DiscountEngine $engine,
|
||||
private readonly DoctorRepository $doctorRepo,
|
||||
private readonly ClinicRepository $clinicRepo,
|
||||
private readonly PatientSessionRepository $sessionRepo,
|
||||
) {}
|
||||
|
||||
/** @return array{0: string, 1: ?int} */
|
||||
private function resolveOwner(User $user): array
|
||||
{
|
||||
if ($user->hasRole('ROLE_DOCTOR')) {
|
||||
return ['doctor', $this->doctorRepo->findByUser($user)?->getId()];
|
||||
}
|
||||
if ($user->hasRole('ROLE_CLINIC')) {
|
||||
return ['clinic', $this->clinicRepo->findByUser($user)?->getId()];
|
||||
}
|
||||
return ['unknown', null];
|
||||
}
|
||||
|
||||
// ── Admin CRUD ────────────────────────────────────────────────────────────
|
||||
|
||||
#[Route('/api/v1/admin/discount-rules', methods: ['GET'])]
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
public function list(#[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
[$ownerType, $ownerId] = $this->resolveOwner($user);
|
||||
if ($ownerId === null) {
|
||||
return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی ممنوع', 403);
|
||||
}
|
||||
return $this->success(['data' => $this->ruleRepo->listForOwner($ownerType, $ownerId)]);
|
||||
}
|
||||
|
||||
#[Route('/api/v1/admin/discount-rules', methods: ['POST'])]
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
public function create(Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
[$ownerType, $ownerId] = $this->resolveOwner($user);
|
||||
if ($ownerId === null) {
|
||||
return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی ممنوع', 403);
|
||||
}
|
||||
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
$name = trim((string) ($data['name'] ?? ''));
|
||||
$type = (string) ($data['type'] ?? '');
|
||||
|
||||
if ($name === '') {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'نام قانون الزامی است', 422, 'name');
|
||||
}
|
||||
if (!in_array($type, DiscountRule::TYPES, true)) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'نوع قانون نامعتبر است', 422, 'type');
|
||||
}
|
||||
|
||||
$rule = new DiscountRule($ownerType, $ownerId, $name, $type);
|
||||
$this->applyPayload($rule, $data);
|
||||
$this->ruleRepo->save($rule);
|
||||
|
||||
return $this->success(['data' => $rule->toArray()], 201);
|
||||
}
|
||||
|
||||
#[Route('/api/v1/admin/discount-rules/{uuid}', methods: ['PATCH'])]
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
public function update(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
[$ownerType, $ownerId] = $this->resolveOwner($user);
|
||||
if ($ownerId === null) {
|
||||
return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی ممنوع', 403);
|
||||
}
|
||||
$rule = $this->ruleRepo->findByUuidForOwner($uuid, $ownerType, $ownerId);
|
||||
if ($rule === null) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'قانون یافت نشد', 404);
|
||||
}
|
||||
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
if (array_key_exists('name', $data)) {
|
||||
$name = trim((string) $data['name']);
|
||||
if ($name === '') {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'نام قانون الزامی است', 422, 'name');
|
||||
}
|
||||
$rule->setName($name);
|
||||
}
|
||||
if (array_key_exists('type', $data)) {
|
||||
if (!in_array((string) $data['type'], DiscountRule::TYPES, true)) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'نوع قانون نامعتبر است', 422, 'type');
|
||||
}
|
||||
$rule->setType((string) $data['type']);
|
||||
}
|
||||
$this->applyPayload($rule, $data);
|
||||
$this->ruleRepo->save($rule);
|
||||
|
||||
return $this->success(['data' => $rule->toArray()]);
|
||||
}
|
||||
|
||||
#[Route('/api/v1/admin/discount-rules/{uuid}', methods: ['DELETE'])]
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
public function delete(string $uuid, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
[$ownerType, $ownerId] = $this->resolveOwner($user);
|
||||
if ($ownerId === null) {
|
||||
return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی ممنوع', 403);
|
||||
}
|
||||
$rule = $this->ruleRepo->findByUuidForOwner($uuid, $ownerType, $ownerId);
|
||||
if ($rule === null) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'قانون یافت نشد', 404);
|
||||
}
|
||||
$this->ruleRepo->remove($rule);
|
||||
return $this->success(['data' => ['deleted' => true]]);
|
||||
}
|
||||
|
||||
// ── Suggestions for a session ─────────────────────────────────────────────
|
||||
|
||||
#[Route('/api/v1/session/{uuid}/discount-suggestions', methods: ['GET'])]
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
public function suggestions(string $uuid, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
[$ownerType, $ownerId] = $this->resolveOwner($user);
|
||||
if ($ownerId === null) {
|
||||
return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی ممنوع', 403);
|
||||
}
|
||||
$session = $this->sessionRepo->findByUuid($uuid);
|
||||
if ($session === null) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'پرونده یافت نشد', 404);
|
||||
}
|
||||
$record = $session->getRecord();
|
||||
if ($record->getEntityType() !== $ownerType || $record->getEntityId() !== $ownerId) {
|
||||
return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی ممنوع', 403);
|
||||
}
|
||||
return $this->success(['data' => $this->engine->evaluate($session)]);
|
||||
}
|
||||
|
||||
private function applyPayload(DiscountRule $rule, array $data): void
|
||||
{
|
||||
if (array_key_exists('discount_type', $data)) {
|
||||
$dt = (string) $data['discount_type'];
|
||||
$rule->setDiscountType($dt === DiscountRule::DISCOUNT_FIXED ? DiscountRule::DISCOUNT_FIXED : DiscountRule::DISCOUNT_PERCENT);
|
||||
}
|
||||
if (array_key_exists('value', $data)) { $rule->setValue((int) $data['value']); }
|
||||
if (array_key_exists('priority', $data)) { $rule->setPriority((int) $data['priority']); }
|
||||
if (array_key_exists('combinable', $data)) { $rule->setCombinable((bool) $data['combinable']); }
|
||||
if (array_key_exists('active', $data)) { $rule->setActive((bool) $data['active']); }
|
||||
if (array_key_exists('valid_from', $data)) { $rule->setValidFrom($data['valid_from'] !== null ? (int) $data['valid_from'] : null); }
|
||||
if (array_key_exists('valid_to', $data)) { $rule->setValidTo($data['valid_to'] !== null ? (int) $data['valid_to'] : null); }
|
||||
if (array_key_exists('target_tag_id', $data)) { $rule->setTargetTagId($data['target_tag_id'] !== null ? (int) $data['target_tag_id'] : null); }
|
||||
if (array_key_exists('target_record_id', $data)) { $rule->setTargetRecordId($data['target_record_id'] !== null ? (int) $data['target_record_id'] : null); }
|
||||
if (array_key_exists('target_service_item_id', $data)) { $rule->setTargetServiceItemId($data['target_service_item_id'] !== null ? (int) $data['target_service_item_id'] : null); }
|
||||
if (array_key_exists('min_amount_rials', $data)) { $rule->setMinAmountRials($data['min_amount_rials'] !== null ? (int) $data['min_amount_rials'] : null); }
|
||||
if (array_key_exists('min_visit_count', $data)) { $rule->setMinVisitCount($data['min_visit_count'] !== null ? (int) $data['min_visit_count'] : null); }
|
||||
if (array_key_exists('occasion_kind', $data)) { $rule->setOccasionKind($data['occasion_kind'] !== null ? (string) $data['occasion_kind'] : null); }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user