<?php
namespace App\Controller;
use App\AlphabeetException;
use App\DTO\ApplyEditDto;
use App\DTO\CropLikeListDto;
use App\DTO\ExtractSeedpackageInfoDto;
use App\Entity\Crop;
use App\Entity\Repository\CropRepository;
use App\Entity\Repository\FavoriteCropRepository;
use App\Entity\Repository\UserRepository;
use App\Repository\ClimatezoneRepository;
use App\Entity\User;
use App\Enum\ModerationNotificationType;
use App\Helper\AppversionHelper;
use App\Helper\SettingConstants;
use App\Queue\Message\TranslateCropMessage;
use App\Service\CroplistService;
use App\Service\CropService;
use App\Service\CropSuggestionService;
use App\Service\DynamicLinksService;
use App\Service\GardenService;
use App\Service\MauticService;
use App\Service\ModerationNotificationService;
use App\Service\NotificationService;
use App\Service\OpenAiService;
use App\Service\PatchService;
use App\Service\PushNotificationService;
use App\Structures\PushNotification;
use OpenApi\Annotations as OA;
use Psr\Log\LoggerInterface;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\RateLimiter\RateLimiterFactory;
use Symfony\Contracts\Cache\ItemInterface;
/**
* @OA\Tag(name="Crop")
*/
class CropController extends BaseController
{
public function __construct(
private readonly CropRepository $cropRepository,
private readonly FavoriteCropRepository $favoriteCropRepository,
private readonly ClimatezoneRepository $climatezoneRepository,
private readonly CropService $cropService,
private readonly CroplistService $croplistService,
private readonly PatchService $patchService,
private readonly PushNotificationService $pushNotificationService,
private readonly ModerationNotificationService $moderationNotificationService,
private readonly LoggerInterface $logger,
private readonly DynamicLinksService $dynamicLinksService,
private readonly GardenService $gardenService,
private readonly MauticService $mauticService,
private readonly CropSuggestionService $cropSuggestionService,
private readonly UserRepository $userRepository,
private readonly OpenAiService $openAiService,
private readonly MessageBusInterface $bus
) {
}
/**
* @OA\Get(
* path="/api/v3/crops",
* summary="Get a list of crops",
* description="Retrieves a list of crops based on the provided filters and search query.",
* @OA\Parameter(
* name="filters[]",
* in="query",
* description="Filters to be applied on crops data. Multiple filters can be applied by repeating the `filters[]` parameter with different values. Possible filters: cropcategories, light, water, nutrients, parentCrop.",
* required=false,
* @OA\Schema(
* type="array",
* @OA\Items(type="string")
* ),
* style="form"
* ),
* @OA\Parameter(
* name="order",
* in="query",
* description="The order in which the crops should be returned. Possible values: 'alpha', 'date', 'rating'.",
* required=false,
* @OA\Schema(
* type="string"
* ),
* style="form"
* ),
* @OA\Parameter(
* name="q",
* in="query",
* description="The search query to filter crops.",
* required=false,
* @OA\Schema(
* type="string"
* ),
* style="form"
* ),
* @OA\Parameter(
* name="fields",
* in="query",
* description="The fields to be returned for each crop. Possible values are all fields of the crop",
* required=false,
* @OA\Schema(
* type="string"
* ),
* style="form"
* ),
* @OA\Response(
* response=200,
* description="Successful operation",
* @OA\JsonContent(
* @OA\Property(
* property="crops",
* type="array",
* description="The list of crops",
* @OA\Items(
* type="object",
* @OA\Property(
* property="id",
* type="integer",
* description="The crop ID"
* ),
* )
* )
* )
* ),
* @OA\Response(
* response=403,
* description="Forbidden",
* @OA\JsonContent(
* @OA\Property(
* property="message",
* type="string",
* description="The error message"
* )
* )
* ),
* )
* @param Request $request
* @return JsonResponse
*/
public function listAction(Request $request): JsonResponse
{
$user = $this->getUser();
$fields = $request->get('fields');
$exclude = $request->get('exclude');
$filters = $request->get('filters');
$query = $request->get('q');
// $order = $request->get('order');
if (!empty($fields)) {
$fields = explode(',', $fields);
}
// todo: remove flag handling after a few versions
$appVersion = $request->headers->get('app-version');
if ($user === null || (!empty($appVersion) && AppversionHelper::hasRequiredVersion($appVersion,
'3.6.10') === true)) {
$this->cropService->setFlag(CropService::FLAG_CACHED_CROPLIST);
$cacheStr = '';
if ($query !== null) {
$cacheStr .= '#' . $query;
}
if ($filters !== null) {
$cacheStr .= '#' . implode(',', $filters);
}
if ($fields !== null) {
$cacheStr .= '#in#' . implode(',', $fields);
}
if($exclude !== null) {
$cacheStr .= '#ex#' . implode(',', $exclude);
}
$locale = !empty($user) ? $user->getLocale() : $request->headers->get('Locale');
$cacheKey = 'crops-list-' . $locale . '-' . md5($cacheStr);
if ($user !== null && $user->getSetting(SettingConstants::SOUTHERN_HEMISPHERE) === true) {
$cacheKey .= '-sh';
}
if ($user !== null && isset($user->getClimateSettings()['betaEnabled']) && $user->getClimateSettings()['betaEnabled'] === true
&& isset($user->getClimateSettings()['zoneId']) && !empty($user->getClimateSettings()['zoneId']))
{
$cacheKey .= '-zone' . $user->getClimateSettings()['zoneId'];
}
// cache result for 10 mins with filesystem adapter;
// apcu is too small for huge crop lists
$cacheAdapter = new FilesystemAdapter('crops');
$arrData = $cacheAdapter->get($cacheKey,
function (ItemInterface $item) use ($user, $query, $filters, $fields, $exclude) {
$item->expiresAfter(600);
return $this->cropService->fetchList($user, $query, $filters, $fields, $exclude);
});
} else {
$arrData = $this->cropService->fetchList($user, $query, $filters, $fields, $exclude);
}
return new JsonResponse(['crops' => $arrData]);
}
/**
* This call delivers user specific crop data, so we can cache the common crops call
* @return JsonResponse
*/
public function listUserDataAction(): JsonResponse
{
$user = $this->getUser();
if (empty($user)) {
return new JsonResponse('forbidden', 403);
}
$data = [
'ownShared' => $this->cropService->getSharedCropsForUser($user),
'ownPrivate' => $this->cropService->getPrivateCropsForUser($user),
'rated' => $this->cropService->getRatedCropsForUser($user),
'changes' => $this->cropService->getCropChangesForUser($user),
];
return new JsonResponse($data);
}
public function getUsageAction(Request $request)
{
/** @var User $user */
$user = $this->getUser();
if (empty($user) || !$user->isSuperModerator()) {
return new JsonResponse('not authorized', 403);
}
$cropId = $request->get('cropId');
$usersUsingCrop = $this->patchService->getCropUsage($cropId);
$response = new JsonResponse(['users' => $usersUsingCrop]);
return $response;
}
public function lockAction(Request $request)
{
/** @var User $user */
$user = $this->getUser();
if (empty($user) || !$user->isSuperModerator()) {
return new JsonResponse('not authorized', 403);
}
$cropId = $request->get('cropId');
$usersUsingCrop = $this->patchService->getCropUsage($cropId);
if ($usersUsingCrop > 0) {
// require alternative crop
$newCropId = $request->get('newCropId');
if (empty($newCropId)) {
return new JsonResponse('new crop id missing', 400);
}
$this->patchService->replaceCropInPatches($cropId, $newCropId);
$gardenIds1 = $this->patchService->replaceCropInPatchesWithPlants($cropId, $newCropId);
$gardenIds2 = $this->patchService->replaceVarietyInPatchesWithPlants($cropId, $newCropId);
$gardenIds = array_unique(array_merge($gardenIds1, $gardenIds2));
foreach ($gardenIds as $gardenId) {
$this->gardenService->markGardenAsUpdated($gardenId);
}
}
$crop = $this->cropService->lockCrop($user, $cropId);
$pn = $this->pushNotificationService->createPushNotification(
$crop->getUser(),
NotificationService::NOTIFICATION_TYPE_RELEASE_REVERTED,
['%variety%' => $crop->getName()]
);
$pn->setType(PushNotification::TYPE_VARIETY_EDIT);
$pn->setAdditionalData(['cropId' => $cropId]);
$this->pushNotificationService->sendPushNotification($crop->getUser(), $pn);
$response = new JsonResponse(['success' => true]);
return $response;
}
public function getAction(Request $request): JsonResponse
{
$user = $this->getUser();
$cropId = $request->get('cropId');
$arrData = $this->cropService->fetchOne($user, $cropId);
if (empty($arrData)) {
return new JsonResponse(null, 404);
}
return new JsonResponse(['crops' => $arrData]);
}
public function getVarietiesAction(Request $request): JsonResponse
{
$user = $this->getUser();
$cropId = $request->get('cropId');
$arrData = $this->cropService->fetchOneWithVarieties($user, $cropId);
if (empty($arrData)) {
return new JsonResponse(null, 404);
}
return new JsonResponse(['crops' => $arrData]);
}
public function getFavoritesAction(): JsonResponse
{
$user = $this->getUser();
if (empty($user)) {
return new JsonResponse('forbidden', 403);
}
$arrData = $this->cropService->fetchFavorites($user);
return new JsonResponse(['favorites' => $arrData]);
}
public function likeAction(Request $request): JsonResponse
{
$user = $this->getUser();
if (empty($user)) {
return new JsonResponse('not authorized', 403);
}
$cropId = $request->get('cropId');
$data = json_decode($request->getContent(), true);
$listId = $data['listId'] ?? null;
$success = $this->croplistService->addCropToList($user, $cropId, $listId);
if ($success === false) {
return new JsonResponse('bad request', 400);
}
return new JsonResponse(['success' => true]);
}
/**
* @OA\Post(
* description="Update the list of favorites of a user. All currently favorized
crop ids that are not sent will be deleted if delete param is set.",
* @OA\Response(
* response=200,
* description="List updated",
* ),
* @OA\Parameter(
* name="body",
* in="path",
* required=true,
* @OA\JsonContent(
* type="object",
* @OA\Property(property="cropIds", type="array", @OA\Items(type="number")),
* @OA\Property(property="deleteExisting", type="boolean")
* ),
* )
* )
* @param CropLikeListDto $dto
* @return JsonResponse
*/
public function likeListAction(CropLikeListDto $dto): JsonResponse
{
if (empty($dto->user)) {
return new JsonResponse('forbidden', 403);
}
if (empty($dto->cropIds) && !$dto->deleteExisting) {
return new JsonResponse('bad request', 400);
}
// delete existing favorites
if ($dto->deleteExisting) {
$this->favoriteCropRepository->deleteExceptCropIds($dto->user->getId(), $dto->cropIds);
}
foreach ($dto->cropIds as $cropId) {
$crop = $this->cropRepository->find($cropId);
if (!empty($crop)) {
$this->favoriteCropRepository->addFavorite($dto->user, $crop);
}
}
return new JsonResponse(['success' => true]);
}
public function dislikeAction(Request $request)
{
$user = $this->getUser();
if (empty($user)) {
return new JsonResponse('not authorized', 403);
}
$cropId = $request->get('cropId');
$crop = $this->cropRepository->find($cropId);
$fav = $this->favoriteCropRepository->findOneBy(['user' => $user, 'crop' => $crop]);
if (!empty($fav)) {
$this->favoriteCropRepository->delete($fav);
}
return new JsonResponse(['success' => true]);
}
public function saveAction(Request $request): JsonResponse
{
$user = $this->getUser();
if (empty($user)) {
return new JsonResponse('not authorized', 403);
}
$json = $request->getContent();
$data = json_decode($json, true);
$isNewVariety = null;
$isNewCrop = null;
/** @var ?Crop $newCrop */
$newCrop = null;
try {
$overrideLocale = $request->headers->get('Locale');
$saveResult = $this->cropService->saveCrop($data, $user, $overrideLocale);
$newCrop = $saveResult['variety'];
if (empty($newCrop->getDynamicLink())) {
$link = $this->dynamicLinksService->createCropLink($newCrop);
$newCrop->setDynamicLink($link);
$this->cropRepository->save($newCrop);
}
if(empty($newCrop->getClimateZone())) {
$parentCrop = $newCrop->getParentCrop();
if ($parentCrop !== null && !empty($parentCrop->getClimateZone())) {
$newCrop->setClimateZone($parentCrop->getClimateZone());
} else {
$defaultZone = $this->climatezoneRepository->findOneBy(['name' => '8a']);
$newCrop->setClimateZone($defaultZone);
$this->cropRepository->save($newCrop);
}
}
$updatedByModerator = $saveResult['updatedByModerator'];
$isNewVariety = $saveResult['isNewVariety'];
$isNewCrop = $saveResult['isNewCrop'];
} catch (AlphabeetException $e) {
if ($e->getCode()) {
$this->logger->error($e->getMessage(), ['data' => $data]);
return new JsonResponse(
[
'error' => [
'code' => $e->getCode(),
'description' => $e->getMessage()
]
], 400
);
}
}
if ($newCrop->getUser() !== null) {
if ($updatedByModerator && !$isNewCrop) {
$pn = $this->pushNotificationService->createPushNotification(
$newCrop->getUser(),
NotificationService::NOTIFICATION_TYPE_VARIETY_EDITED,
['%variety%' => $newCrop->getName()]
);
$pn->setType(PushNotification::TYPE_VARIETY_EDIT);
$pn->setAdditionalData(['cropId' => $newCrop->getId()]);
$this->pushNotificationService->sendPushNotification($newCrop->getUser(), $pn);
} // check if crop was edited -> if yes, create push notification
else {
if ($newCrop->getIsShared() && $user->getId() !== $newCrop->getUser()->getId()) {
$pn = $this->pushNotificationService->createPushNotification(
$newCrop->getUser(),
NotificationService::NOTIFICATION_TYPE_EDIT_SUGGESTED,
['%variety%' => $newCrop->getName()]
);
$pn->setType(PushNotification::TYPE_VARIETY_EDIT);
$pn->setAdditionalData(['cropId' => $newCrop->getId()]);
$this->pushNotificationService->sendPushNotification($newCrop->getUser(), $pn);
}
}
}
if ($isNewVariety) {
$this->moderationNotificationService->createNotification(
ModerationNotificationType::VarietyCreated->value,
['cropId' => $newCrop->getId(), 'userId' => $user->getId()]
);
} elseif ($isNewCrop) {
$notificationMetadata = [
'cropId' => $newCrop->getId(),
'userId' => $user->getId(),
];
// check if crop was on suggestion list and if so, create moderation notification +
// push notifications + emails for users that wished for that crop
if (isset($data['cropSuggestionId'])) {
$wishedByUserIds = $this->cropSuggestionService->deleteAndGetUserIds($data['cropSuggestionId']);
$notificationMetadata['wishedByUserIds'] = $wishedByUserIds;
foreach ($wishedByUserIds as $userId) {
$wishingUser = $this->userRepository->find($userId);
$pn = $this->pushNotificationService->createPushNotification(
$wishingUser,
NotificationService::NOTIFICATION_TYPE_CROP_CREATED,
[
'%crop_name%' => $newCrop->getName(),
'%moderator_name%' => $newCrop->getUser()?->getDisplayName()
]);
$this->pushNotificationService->sendPushNotification($wishingUser, $pn);
$this->mauticService->sendCropCreatedEmail($wishingUser, $newCrop);
}
}
$this->moderationNotificationService->createNotification(
ModerationNotificationType::CropCreated->value,
$notificationMetadata
);
}
if ($isNewCrop || $isNewVariety) {
// todo: add more locales if needed
if ($user->getLocale() === 'en') {
$this->bus->dispatch(new TranslateCropMessage($newCrop->getId(), 'de'));
} else {
$this->bus->dispatch(new TranslateCropMessage($newCrop->getId(), 'en'));
}
}
$crops = [$newCrop];
$arrData = $this->cropService->prepareData($crops, $user);
foreach ($arrData as &$crop) {
$parentCrop = $newCrop->getParentCrop();
if (!empty($parentCrop)) {
$crop['parentCrop'] = $this->cropService->prepareData([$parentCrop], $user)[0];
}
}
return new JsonResponse(['success' => true, 'crops' => $arrData]);
}
public function deleteAction(Request $request)
{
$user = $this->getUser();
if (empty($user)) {
return new JsonResponse('not authorized', 403);
}
$cropId = $request->get('cropId');
$success = $this->cropService->deleteCrop($user, $cropId);
if (!$success) {
return new JsonResponse(['success' => false], 400);
}
return new JsonResponse(['id' => $cropId]);
}
public function imageAction(Request $request)
{
$user = $this->getUser();
if (empty($user)) {
return new JsonResponse('not authorized', 403);
}
$cropId = $request->get('cropId');
$filePath = $this->cropService->getImagePath($user, $cropId);
if ($filePath === false) {
return new JsonResponse('not found', 404);
}
return new BinaryFileResponse($filePath, 200);;
}
public function rateAction(Request $request)
{
$user = $this->getUser();
if (empty($user)) {
return new JsonResponse('forbidden', 403);
}
$cropId = $request->get('cropId');
$data = json_decode($request->getContent(), true);
$comment = isset($data['comment']) ? $data['comment'] : '';
$rating = $data['rating'];
$result = $this->cropService->rateCrop($user, $cropId, $rating, $comment);
if ($result === false) {
return new JsonResponse('bad_request', 400);
}
return new JsonResponse(
[
'success' => true,
'cropId' => $cropId,
'rating' => $result['rating'],
'ratingCount' => $result['count']
]
);
}
public function applyEditAction(ApplyEditDto $dto): JsonResponse
{
$user = $this->getUser();
if (empty($user) || !$user->isModerator()) {
return new JsonResponse('forbidden', 403);
}
$result = $this->cropService->applyEdit($user, $dto->editId, $dto->changeset);
if ($result === false) {
return new JsonResponse('bad_request', 400);
}
/** @var Crop $crop */
$crop = $result['crop'];
/** @var User $editUser */
$editUser = $result['editUser'];
if ($result['isApplied'] === false) {
return new JsonResponse(["success" => true]);
}
if ($result['isApproval'] === true) {
$pn = $this->pushNotificationService->createPushNotification(
$crop->getUser(),
NotificationService::NOTIFICATION_TYPE_VARIETY_RELEASED,
['%variety%' => $crop->getName()]
);
$pn->setType(PushNotification::TYPE_VARIETY);
$pn->setAdditionalData(['cropId' => $crop->getId()]);
$this->pushNotificationService->sendPushNotification($crop->getUser(), $pn);
$this->moderationNotificationService->createNotification(
ModerationNotificationType::VarietyApproved->value,
['cropId' => $crop->getId(), 'userId' => $user->getId()]
);
} else {
// notify creator of edit that her edit was applied via push
$pn = $this->pushNotificationService->createPushNotification(
$editUser,
NotificationService::NOTIFICATION_TYPE_EDIT_APPROVED,
['%variety%' => $crop->getName()]
);
$pn->setType(PushNotification::TYPE_VARIETY);
$pn->setAdditionalData(['cropId' => $crop->getId()]);
$this->pushNotificationService->sendPushNotification($editUser, $pn);
}
// check if edit-user should be granted moderation status
if (!$editUser->isModerator()) {
$varieties = $this->cropRepository->getCreatedAndApprovedCountForUser($editUser);
$edits = $this->cropRepository->getApprovedEditsForUser($editUser);
if ($varieties + $edits >= User::MODERATION_TARGET) {
$alreadyInvited = $editUser->getSetting(SettingConstants::INVITED_TO_MODERATION);
if (!$alreadyInvited) {
$this->moderationNotificationService->createNotification(
ModerationNotificationType::ModerationInvite->value,
[],
$editUser
);
$pn = $this->pushNotificationService->createPushNotification(
$crop->getUser(),
NotificationService::NOTIFICATION_TYPE_MODERATION_INVITE
);
$pn->setType(PushNotification::TYPE_SCREEN);
$pn->setView(PushNotification::VIEW_PLANTS_STACK);
$pn->setViewParams(['screen' => PushNotification::SCREEN_MODERATION_NOTIFICATIONS]);
$pn->setAdditionalData(['cropId' => $crop->getId()]);
$this->pushNotificationService->sendPushNotification($editUser, $pn,
600); // pn is delayed 10 minutes
$this->mauticService->sendModerationInviteMail($editUser);
}
}
}
return new JsonResponse(["success" => true]);
}
public function declineEditAction(Request $request): JsonResponse
{
$user = $this->getUser();
if (empty($user)) {
return new JsonResponse('forbidden', 403);
}
$editId = $request->get('editId');
$data = json_decode($request->getContent(), true);
$feedback = isset($data['feedback']) ? $data['feedback'] : null;
$result = $this->cropService->declineEdit($user, $editId, $feedback);
if ($result === false) {
return new JsonResponse('bad_request', 400);
}
if ($result['isApplied'] === false) {
return new JsonResponse(['success' => true]);
}
/** @var Crop $crop */
$crop = $result['crop'];
/** @var User $editUser */
$editUser = $result['editUser'];
// if sharing of a crop was declined, send push notification
if ($result['isApproval'] === true) {
if ($crop->getParentCrop() === null) {
$this->moderationNotificationService->createNotification(
ModerationNotificationType::CropDeclined->value,
['cropId' => $crop->getId(), 'userId' => $user->getId()],
$crop->getUser()
);
} else {
$this->moderationNotificationService->createNotification(
ModerationNotificationType::VarietyDeclined->value,
['cropId' => $crop->getId(), 'userId' => $user->getId()],
$crop->getUser()
);
}
$pn = $this->pushNotificationService->createPushNotification(
$crop->getUser(),
NotificationService::NOTIFICATION_TYPE_VARIETY_DECLINED,
['%variety%' => $crop->getName()]
);
$pn->setType(PushNotification::TYPE_SCREEN);
$pn->setView(PushNotification::VIEW_PLANTS_STACK);
$pn->setViewParams(['screen' => PushNotification::SCREEN_MODERATION_NOTIFICATIONS]);
$pn->setAdditionalData(['cropId' => $crop->getId()]);
$this->pushNotificationService->sendPushNotification($crop->getUser(), $pn);
} else {
$this->moderationNotificationService->createNotification(
ModerationNotificationType::EditDeclined->value,
['cropId' => $crop->getId(), 'userId' => $user->getId()],
$editUser
);
// notify creator of edit that her edit was declined via push
$pn = $this->pushNotificationService->createPushNotification(
$editUser,
NotificationService::NOTIFICATION_TYPE_SUGGESTION_DECLINED,
['%variety%' => $crop->getName()]
);
$pn->setType(PushNotification::TYPE_SCREEN);
$pn->setView(PushNotification::VIEW_PLANTS_STACK);
$pn->setViewParams(['screen' => PushNotification::SCREEN_MODERATION_NOTIFICATIONS]);
$pn->setAdditionalData(['cropId' => $crop->getId()]);
$this->pushNotificationService->sendPushNotification($editUser, $pn);
}
return new JsonResponse(['success' => true]);
}
public function listEditsAction(Request $request): JsonResponse
{
/** @var User $user */
$user = $this->getUser();
if (empty($user)) {
return new JsonResponse('forbidden', 403);
}
$cropId = $request->get('cropId');
$applied = $request->get('applied');
if (!$applied) {
$edits = $this->cropService->fetchOpenEdits($user, $cropId);
} else {
$edits = $this->cropService->fetchAppliedEdits($user, $cropId);
}
return new JsonResponse(['cropedits' => $edits]);
}
public function listChangesetsAction(Request $request): JsonResponse
{
$user = $this->getUser();
if (empty($user)) {
return new JsonResponse('forbidden', 403);
}
if (!$user->isModerator()) {
return new JsonResponse(['changesets' => []]);
}
$applied = $request->get('applied');
if (!$applied) {
$changesets = $this->cropService->fetchOpenChangesets($user);
} else {
$changesets = $this->cropService->fetchAppliedChangesets($user);
}
return new JsonResponse(['changesets' => $changesets]);
}
public function listApprovalsAction(Request $request): JsonResponse
{
/** @var User $user */
$user = $this->getUser();
if (empty($user) || !$user->isModerator()) {
return new JsonResponse('forbidden', 403);
}
$page = $request->get('p') ?? 1;
$edits = $this->cropService->fetchOpenApprovals($user, $page);
return new JsonResponse(['approvals' => $edits]);
}
public function listBestRatedCropsAction(): JsonResponse
{
$user = $this->getUser();
if (empty($user)) {
return new JsonResponse('forbidden', 403);
}
$crops = $this->cropService->fetchBestRatedCrops($user);
return new JsonResponse(['crops' => $crops]);
}
public function listNewCropsAction(): JsonResponse
{
$user = $this->getUser();
if (empty($user)) {
return new JsonResponse('forbidden', 403);
}
$crops = $this->cropService->fetchNewCrops($user);
return new JsonResponse(['crops' => $crops]);
}
public function listIncompleteCropsAction(Request $request): JsonResponse
{
$user = $this->getUser();
if (empty($user)) {
return new JsonResponse('forbidden', 403);
}
$page = $request->get('p') ?? 1;
$crops = $this->cropService->fetchIncompleteCrops($user, $page);
return new JsonResponse(['crops' => $crops]);
}
public function getUpdateDateAction(): JsonResponse
{
$user = $this->getUser();
if (empty($user)) {
$lastUpdateDate = new \DateTime();
} else {
$lastUpdateDate = $this->cropService->getUpdateDateForUser($user);
}
return new JsonResponse(['last_update' => $lastUpdateDate->format(DATE_ATOM)]);
}
/**
* @OA\Get(
* summary="Get all crops that have been changed since a given date",
* @OA\Parameter(
* name="since",
* in="query",
* description="Date in ISO 8601 format",
* required=true,
* @OA\Schema(
* type="string",
* format="date-time"
* )
* ),
* )
* @param Request $request
* @return JsonResponse
*/
public function getChangedCropsSinceAction(Request $request): JsonResponse
{
$user = $this->getUser();
$lastUpdate = $request->get('since');
if (empty($lastUpdate)) {
return new JsonResponse('bad_request', 400);
}
// ensure that the + sign in the timezone is not lost due to url encoding issues
$lastUpdate = str_replace(' ', '+', $lastUpdate);
$datetime = new \DateTime($lastUpdate);
$datetime->setTimezone(new \DateTimeZone(date_default_timezone_get()));
$crops = $this->cropService->getChangedCropsSince($user, $datetime);
return new JsonResponse(['crops' => $crops]);
}
public function extractSeedpackageDataAction(
ExtractSeedpackageInfoDto $dto,
RateLimiterFactory $openaiLimiter
): JsonResponse {
if (empty($dto->user)) {
return new JsonResponse('forbidden', 403);
}
$limiter = $openaiLimiter->create('user-' . $dto->user->getId());
if (false === $limiter->consume()->isAccepted()) {
throw new TooManyRequestsHttpException();
}
$images = [
$dto->frontImageBase64,
$dto->backImageBase64
];
$result = $this->openAiService->extractDataFromSeedPackageImages(
$images,
$dto->user->getLocale() ?? User::DEFAULT_LOCALE
);
return new JsonResponse(['crop' => $result]);
}
}