<?php
declare(strict_types=1);
namespace VehicleIngestBundle\Controller;
use Pimcore\Controller\FrontendController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Annotation\Route;
use VehicleIngestBundle\Message\IngestCampaignMessage;
use VehicleIngestBundle\Service\ErrorResponseFactory;
/**
* Accept-and-enqueue only, mirroring LeasingController: this controller's only job is to accept
* automedia's campaign push and hand it off to Messenger; the actual persistence
* (campaignNames/campaignName + the `Kampagne` facet) happens asynchronously in
* IngestCampaignMessageHandler - kept off the request path for the same reason as
* leasing/images (a slow/delayed campaign push must never block the automedia exporter's ack).
*
* ## Path and namespace
*
* Confirmed against automedia's integration guide ("Delivering campaigns"):
*
* PUT /campaign-api/sellers/{sellerId}/ads/{adId}/campaigns
*
* Campaigns are not part of the mobile.de Seller API - its OpenAPI schema
* (Resources/openapi/mobile-seller-api.json) has no `campaigns` sub-resource at all - so they get
* a namespace of their own, deliberately separate from `/leasing-api` "so you can route on the
* prefix and implement one extension without the other". Everything under `/seller-api` is what
* mobile.de specifies and nothing else.
*
* The path was previously inferred (under /seller-api, mirroring the leasing sub-resource) from a
* single sample payload. It was wrong, and every campaign push would have 404ed.
*
* PUT is the only method: the body is the complete list, so every state including "no campaigns"
* is expressible, and there is nothing for a DELETE to do. The body is a JSON array of
* {id,name,validFrom,validUntil} objects.
*
* The same campaigns also arrive inside the ad body as the `campaignIds` and `campaign`
* attributes ("All three always carry the same content"), which AdFactory already accepts - this
* endpoint is the sub-resource variant, not the only channel.
*/
class CampaignController extends FrontendController
{
public function __construct(
private MessageBusInterface $bus,
private ErrorResponseFactory $errors,
) {
}
/**
* @Route("/campaign-api/sellers/{sellerId}/ads/{adId}/campaigns", name="campaign_api_ad_campaigns_put", methods={"PUT"}, requirements={"adId"="\d+"})
*/
public function putCampaignsAction(Request $request, string $adId): Response
{
$data = json_decode($request->getContent(), true);
// Must decode to a JSON array (a list), not an object/scalar - the real payload is
// `[{id,name,...}, ...]`, not `{...}`. array_is_list([]) is true, so an empty array still
// passes, which matters: it is the body that means "no campaigns, clear what you hold"
// (see IngestCampaignMessageHandler).
if (!is_array($data) || !array_is_list($data)) {
return $this->errors->keyed('body');
}
$this->bus->dispatch(new IngestCampaignMessage($adId, $data));
return new Response('', Response::HTTP_OK);
}
}