<?php
declare(strict_types=1);
namespace App\Controller\Rest;
use App\Entity\Appointment;
use App\Entity\Customer;
use App\Repository\AppointmentRepository;
use App\Repository\CustomerRepository;
use Doctrine\ORM\EntityManagerInterface;
use Ksante\CoreBundle\Entity\Channel;
use Ksante\CoreBundle\Repository\ChannelRepository;
use Psr\Log\LoggerInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Validator\Validator\ValidatorInterface;
/**
* @Route("/api", name="api_")
*/
class CustomerController extends AbstractController
{
private CustomerRepository $customerRepository;
private AppointmentRepository $appointmentRepository;
private ChannelRepository $channelsRepository;
private EntityManagerInterface $entityManager;
private ValidatorInterface $validator;
private LoggerInterface $logger;
public function __construct(
CustomerRepository $customerRepository,
AppointmentRepository $appointmentRepository,
ChannelRepository $channelsRepository,
EntityManagerInterface $entityManager,
ValidatorInterface $validator,
LoggerInterface $logger,
) {
$this->customerRepository = $customerRepository;
$this->appointmentRepository = $appointmentRepository;
$this->channelsRepository = $channelsRepository;
$this->entityManager = $entityManager;
$this->validator = $validator;
$this->logger = $logger;
}
/**
* @Route("/customer", name="get_customer_details", methods={"GET"})
*/
public function getCurrentAppointment(Request $request): JsonResponse
{
$data = json_decode($request->getContent(), true);
if ([] === $data) {
return $this->json(['errors' => ['form' => 'Invalid data']], Response::HTTP_BAD_REQUEST, [], [AbstractNormalizer::GROUPS => ['read']]);
}
$channel = $this->channelsRepository->findOneBy(['code' => $data['channel_code']]);
if (!$channel instanceof Channel) {
throw new NotFoundHttpException('Channel not found.');
}
$customer = $this->customerRepository->findOneByChannelAndExternalId($channel, $data['customer_external_id']);
$appointment = $this->appointmentRepository->findNextOneByCustomer($customer, $channel);
$canTake = $customer->getAppointmentCredit() > 0 || null != $appointment ? true : false;
$lastAppointment = $this->appointmentRepository->findLastOneByCustomer($customer, $channel);
return $this->json(
[
'is_first_appointment' => $this->appointmentRepository->isFirstAppointment($customer),
'has_current_appointment' => null === $appointment ? false : true,
'current_appointment_date' => $appointment?->getDate(),
'current_appointment' => [
'id' => $appointment?->getId(),
'date' => $appointment?->getDate()->format('Y-m-d H:i:s'),
],
'last_appointment' => null !== $lastAppointment ? $this->formatAppointmentDate($lastAppointment) : '',
'has_diet' => $customer->hasDiet(),
'diet' => $customer->getDiet(),
'appointment_credit' => $customer->getAppointmentCredit(),
'cancelled_appointment_count' => $customer?->getCancelledAppointments(),
'can_take' => $canTake,
],
Response::HTTP_OK,
[],
[AbstractNormalizer::GROUPS => ['customer:read']]
);
}
/**
* Returns the customer diet details filtered by channel Id
*
* @Route("/customer/{customerId}/diet-details", name="get_customer_diet_details", methods={"GET"})
*
* @IsGranted("ROLE_API")
*/
public function getDietDetailsByCustomerId(int $customerId): JsonResponse
{
$customer = $this->customerRepository->findOneBy(['customerId' => $customerId]);
if (!$customer instanceof Customer || $customer->getAppointments()->isEmpty()) {
return new JsonResponse(null, Response::HTTP_NOT_FOUND);
}
$dietDetails = [];
foreach ($customer->getAppointments() as $appointment) {
$diet = $appointment->getDiet();
$channel = $appointment->getChannel();
$lastAppointment = $this->appointmentRepository->findLastOneByCustomer($customer, $channel);
$nextAppointment = $this->appointmentRepository->findNextOneByCustomer($customer, $channel);
$appointmentCredit = $this->appointmentRepository->countByCustomer($customer, $channel);
$dietDetails[$channel->getCode()] = [
'diet' => null !== $diet ? $diet->getFullName() : '',
'last_appointment' => null !== $lastAppointment ? $this->formatAppointmentDate($lastAppointment) : '',
'next_appointment' => null !== $nextAppointment ? $this->formatAppointmentDate($nextAppointment) : '',
'appointment_credit' => $appointmentCredit,
];
}
return $this->json($dietDetails, Response::HTTP_OK);
}
/**
* Cancel point for a customer on a specific channel
*
* @Route("/customer/cancel-point", name="cancel_customer_point", methods={"POST"})
*
* @IsGranted("ROLE_API")
*/
public function cancelCustomerPoint(Request $request): JsonResponse
{
$data = json_decode($request->getContent(), true);
$point = (int) $data['points'];
$channelCode = $data['channelCode'];
$channel = $this->channelsRepository->findOneBy(['code' => $channelCode]);
if (!$channel instanceof Channel) {
throw new NotFoundHttpException('Channel not found.');
}
if (0 === (int) $data['customerExternalId'] || 0 >= $point) {
return new JsonResponse('Invalid data provided', Response::HTTP_BAD_REQUEST);
}
$customer = $this->customerRepository->findOneByChannelAndExternalId($channel, $data['customerExternalId']);
if (!$customer instanceof Customer) {
return new JsonResponse('Customer not found.', Response::HTTP_NOT_FOUND);
}
if (0 === $customer->getAppointmentCredit() || $customer->getAppointmentCredit() < $point) {
return new JsonResponse('Points can\'t be a negative number.', Response::HTTP_UNAUTHORIZED);
}
$customer->setAppointmentCredit($customer->getAppointmentCredit() - $point);
$this->entityManager->persist($customer);
$this->entityManager->flush();
return $this->json(['customer' => $customer], Response::HTTP_OK, [], [AbstractNormalizer::GROUPS => ['read']]);
}
/**
* @Route("/customers/{externalId}/add-credit", name="customer_add_appointments_credit", methods={"PATCH"})
*
* @IsGranted("ROLE_API")
*/
public function addAppointmentCredit(Request $request, string $externalId): JsonResponse
{
$payload = json_decode($request->getContent(), true);
if (null === $payload || 0 === \count($payload) || !\array_key_exists('channel_code', $payload) || null === $payload['channel_code'] || '' === trim($payload['channel_code'])) {
return new JsonResponse([
'code' => Response::HTTP_BAD_REQUEST,
'message' => 'The channel_code is required',
], Response::HTTP_BAD_REQUEST);
}
$channel = $this->channelsRepository->findOneBy(['code' => $payload['channel_code']]);
if (null === $channel) {
return new JsonResponse([
'code' => Response::HTTP_NOT_FOUND,
'message' => 'Channel not found',
], Response::HTTP_NOT_FOUND);
}
$customer = $this->customerRepository->findOneByChannelAndExternalId($channel, $externalId);
if (null === $customer) {
$customer = new Customer();
$customer->setExternalId((string) $externalId);
$customer->setChannel($channel);
$this->entityManager->persist($customer);
}
$customer->setFirstName($payload['first_name'] ?? $customer->getFirstName());
$customer->setLastName($payload['last_name'] ?? $customer->getLastName());
$customer->setEmail($payload['email'] ?? $customer->getEmail());
$customer->setPhone($payload['phone_number'] ?? $customer->getPhone());
$violations = $this->validator->validate($customer);
if (0 !== \count($violations)) {
$errors = [];
foreach ($violations as $violation) {
$input = mb_strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $violation->getPropertyPath()));
$errors[$input] = $violation->getMessage();
}
$this->logger->error('Couldn t save customer data', $errors);
return new JsonResponse([
'code' => Response::HTTP_BAD_REQUEST,
'message' => $errors,
], Response::HTTP_BAD_REQUEST);
}
$points = \array_key_exists('points', $payload) ? (int) $payload['points'] : 1;
$customer->setAppointmentCredit($customer->getAppointmentCredit() + $points);
$this->entityManager->flush();
return new JsonResponse('Credit has been successfully added');
}
/**
* Delete customer data ( GDPR )
*
* @Route("/customer/{customerId}/data", name="delete_customer_data", methods={"DELETE"})
*
* @IsGranted("ROLE_API")
*
* @param int $customerId
*
* @return JsonResponse
*/
public function deleteCustomerData(int $customerId): JsonResponse
{
$customers = $this->customerRepository->findBy(['customerId' => $customerId]);
foreach ($customers as $customer) {
$customerAppointments = $this->appointmentRepository->findBy(['customer' => $customer->getId()]);
foreach ($customerAppointments as $customerAppointment) {
$this->entityManager->remove($customerAppointment);
}
$this->entityManager->remove($customer);
}
$this->entityManager->flush();
return new JsonResponse('Customer data deleted successfully', Response::HTTP_OK);
}
/**
* Delete the assigned diet and cancel any upcoming appointment
*
* @Route("/customers/{externalId}/delete-diet", name="customer_delete_assigned_diet", methods={"PATCH"})
*
* @IsGranted("ROLE_API")
*/
public function deleteAssignedDiet(Request $request, string $externalId): JsonResponse
{
$payload = json_decode($request->getContent(), true);
if (null === $payload || 0 === \count($payload) || !\array_key_exists('channel_code', $payload) || null === $payload['channel_code'] || '' === trim($payload['channel_code'])) {
return new JsonResponse([
'code' => Response::HTTP_BAD_REQUEST,
'message' => 'The channel_code is required',
], Response::HTTP_BAD_REQUEST);
}
$channel = $this->channelsRepository->findOneBy(['code' => $payload['channel_code']]);
if (null === $channel) {
return new JsonResponse([
'code' => Response::HTTP_NOT_FOUND,
'message' => 'Channel not found',
], Response::HTTP_NOT_FOUND);
}
$customer = $this->customerRepository->findOneByChannelAndExternalId($channel, $externalId);
if (null === $customer) {
return new JsonResponse([
'code' => Response::HTTP_NOT_FOUND,
'message' => 'Customer not found',
], Response::HTTP_NOT_FOUND);
}
if (null === $customer->getDiet()) {
return new JsonResponse([
'code' => Response::HTTP_BAD_REQUEST,
'message' => 'Invalid request',
], Response::HTTP_BAD_REQUEST);
}
$appointments = $this->appointmentRepository->findUpcomingByDietCustomerAndChannel($customer->getDiet(), $customer, $channel);
if ([] !== $appointments) {
foreach ($appointments as $appointment) {
$appointment->setStatus(Appointment::STATUS_CANCELED);
$appointment->setReminderStatus(Appointment::EMAIL_REMINDER_CANCELED);
$customer->incrementAppointmentCredit();
}
}
$customer->setDiet(null);
$this->entityManager->flush();
return new JsonResponse('Diet successfully unassigned');
}
/**
* Concat and format appointment date and time
*
* @param Appointment $appointment
*
* @return string
*/
private function formatAppointmentDate(Appointment $appointment): string
{
return date(\DATE_ISO8601, strtotime($appointment->getDate()->format('Y-m-d H:i:s')));
}
}