<?php
declare(strict_types=1);
namespace App\Tuer24\Security\Voter;
use App\Entity\Company;
use App\Tuer24\Config\Tuer24Constants;
use Roothirsch\CoreBundle\Behaviors\Attributable\MappedSuperclass\AbstractAttributeValue;
use Roothirsch\ShopBundle\Entity\Order;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* Voter for TÜR24 Order permissions
*
* Handles authorization for:
* - VIEW: Order owner OR assigned distributor
* - ACCEPT: Assigned distributor only (order must be in 'submitted' state)
* - REJECT: Assigned distributor only (order must be in 'submitted' state)
* - CONFIRM: Order owner who IS a distributor (order must be in 'placed' state)
* - SUBMIT: Order owner who is NOT a distributor + distributor assigned (order must be in 'placed' state)
*/
class Tuer24OrderVoter extends Voter
{
public const VIEW = 'TUER24_ORDER_VIEW';
public const EDIT = 'TUER24_ORDER_EDIT';
public const ACCEPT = 'TUER24_ORDER_ACCEPT';
public const REJECT = 'TUER24_ORDER_REJECT';
public const CONFIRM = 'TUER24_ORDER_CONFIRM';
public const SUBMIT = 'TUER24_ORDER_SUBMIT';
protected function supports(string $attribute, mixed $subject): bool
{
if (!in_array($attribute, [self::VIEW, self::ACCEPT, self::REJECT, self::CONFIRM, self::SUBMIT, self::EDIT])) {
return false;
}
if (!$subject instanceof Order) {
return false;
}
return $subject->getType() === Tuer24Constants::ORDER_TYPE;
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
$user = $token->getUser();
if (!$user instanceof UserInterface) {
return false;
}
/** @var Order $order */
$order = $subject;
return match ($attribute) {
self::VIEW => $this->canView($order, $user, $token),
self::EDIT => $this->canEdit($order, $user, $token),
self::ACCEPT => $this->canAccept($order, $user, $token),
self::REJECT => $this->canReject($order, $user, $token),
self::CONFIRM => $this->canConfirm($order, $user, $token),
self::SUBMIT => $this->canSubmit($order, $user, $token),
default => false,
};
}
private function canEdit(Order $order, UserInterface $user, TokenInterface $token): bool
{
return $this->isAssignedDistributor($order, $user, $token);
}
/**
* Check if user can view the order
* Allowed: Order owner OR assigned distributor
*/
private function canView(Order $order, UserInterface $user, TokenInterface $token): bool
{
// Order owner can always view
if ($this->isOrderOwner($order, $user)) {
return true;
}
// Distributor assigned to this order can view
if ($this->isAssignedDistributor($order, $user, $token)) {
return true;
}
return false;
}
/**
* Check if user can accept the order
* Allowed: Assigned distributor only, order must be in 'submitted' state
*/
private function canAccept(Order $order, UserInterface $user, TokenInterface $token): bool
{
// Only submitted orders can be accepted
if ($order->getState() !== 'submitted') {
return false;
}
// Only assigned distributors can accept
return $this->isAssignedDistributor($order, $user, $token);
}
/**
* Check if user can reject the order
* Allowed: Assigned distributor only, order must be in 'submitted' state
*/
private function canReject(Order $order, UserInterface $user, TokenInterface $token): bool
{
// Only submitted orders can be rejected
if ($order->getState() !== 'submitted') {
return false;
}
// Only assigned distributors can reject
return $this->isAssignedDistributor($order, $user, $token);
}
private function canConfirm(Order $order, UserInterface $user, TokenInterface $token): bool
{
// Only submitted orders can be rejected
if ($order->getState() !== 'placed') {
return false;
}
if(!$this->isOrderOwner($order, $user)) {
return false;
}
return $this->hasRole($token, Tuer24Constants::ROLE_DISTRIBUTOR);
}
/**
* Check if user can submit the order
* Allowed: Order owner who is NOT a distributor, with a distributor assigned
*/
private function canSubmit(Order $order, UserInterface $user, TokenInterface $token): bool
{
// Only placed orders can be submitted
if ($order->getState() !== 'placed') {
return false;
}
// Must have a distributor assigned
if ($this->getDistributorId($order) === null) {
return false;
}
// Must be order owner
if (!$this->isOrderOwner($order, $user)) {
return false;
}
// Only non-distributors submit (distributors use confirm instead)
return !$this->hasRole($token, Tuer24Constants::ROLE_DISTRIBUTOR);
}
/**
* Check if user is the order owner
*/
private function isOrderOwner(Order $order, UserInterface $user): bool
{
$orderOwner = $order->getOwner();
if ($orderOwner === null) {
return false;
}
if (!method_exists($user, 'getId') || !method_exists($orderOwner, 'getId')) {
return false;
}
return $user->getId() === $orderOwner->getId();
}
/**
* Check if user is an assigned distributor for this order
*/
private function isAssignedDistributor(Order $order, UserInterface $user, TokenInterface $token): bool
{
// Must have distributor role
if (!$this->hasRole($token, Tuer24Constants::ROLE_DISTRIBUTOR)) {
return false;
}
// Must have a company
if (!method_exists($user, 'getCompany')) {
return false;
}
$userCompany = $user->getCompany();
if ($userCompany === null) {
return false;
}
$distributorCompanyId = $this->getDistributorId($order);
if($distributorCompanyId === NULL) {
return false;
}
return (string) $userCompany->getId() === (string) $distributorCompanyId;
}
private function getDistributorId(Order $order): null|int
{
$distributorAttribute = $order->getAttribute('distributor');
if ($distributorAttribute === null) {
return null;
}
/** @var AbstractAttributeValue $attributeValue */
$attributeValue = $order->getAttributeValue($distributorAttribute);
if ($attributeValue === null) {
return null;
}
if($attributeValue->getEntity()) {
return $attributeValue->getEntity()->getId();
}
return $attributeValue->getEntityId();
}
/**
* Check if token has a specific role
*/
private function hasRole(TokenInterface $token, string $role): bool
{
foreach ($token->getRoleNames() as $roleName) {
if ($roleName === $role) {
return true;
}
}
return false;
}
}