<?php
namespace Bidcoz\Bundle\FrontendBundle\Controller\Auction;
use Bidcoz\Bundle\CoreBundle\Controller\CoreController;
use Bidcoz\Bundle\CoreBundle\Entity\Auction\Auction;
use Bidcoz\Bundle\CoreBundle\Entity\Auction\FundANeed;
use Bidcoz\Bundle\CoreBundle\Entity\Auction\Item;
use Bidcoz\Bundle\CoreBundle\Entity\Auction\Shop;
use Bidcoz\Bundle\CoreBundle\Entity\Campaign;
use Bidcoz\Bundle\CoreBundle\Entity\Organization;
use Bidcoz\Bundle\CoreBundle\Entity\Proxy\ItemPurchaseProxy;
use Bidcoz\Bundle\FrontendBundle\Form\Type\ItemBidType;
use Bidcoz\Bundle\FrontendBundle\Form\Type\ItemBuyType;
use Bidcoz\Bundle\FrontendBundle\Form\Type\ItemDonationType;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Entity;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/{orgSlug}/{campaignSlug}")
*
* @IsGranted("VIEW", subject="organization")
* @IsGranted("FRONT_END", subject="campaign")
*/
class ItemController extends CoreController
{
/**
* @Route("/auction/item/{itemId}", name="auction_item", methods={"GET"})
*
* @Cache(maxage="0", smaxage="0", expires="now", public="false")
*
* @Entity("item", class="Bidcoz\Bundle\CoreBundle\Entity\Auction\Item", options={"id" = "itemId"}, expr="repository.findHydrated(itemId)")
*
* @IsGranted("VIEW", subject="item")
*/
public function viewAuctionItemAction(Organization $organization, Campaign $campaign, Auction $auction, Item $item)
{
return $this->viewItem($organization, $campaign, $auction, $item);
}
/**
* @Route("/shop/item/{itemId}", name="shop_item", methods={"GET"})
*
* @Cache(maxage="0", smaxage="0", expires="now", public="false")
*
* @Entity("item", class="Bidcoz\Bundle\CoreBundle\Entity\Auction\Item", options={"id" = "itemId"}, expr="repository.findHydrated(itemId)")
*
* @IsGranted("VIEW", subject="item")
*/
public function viewShopItemAction(Organization $organization, Campaign $campaign, Shop $shop, Item $item)
{
return $this->viewItem($organization, $campaign, $shop, $item);
}
/**
* @Route("/fund-a-need/item/{itemId}", name="fund_a_need_item", methods={"GET"})
*
* @Cache(maxage="0", smaxage="0", expires="now", public="false")
*
* @Entity("item", class="Bidcoz\Bundle\CoreBundle\Entity\Auction\Item", options={"id" = "itemId"}, expr="repository.findHydrated(itemId)")
*
* @IsGranted("VIEW", subject="item")
*/
public function viewFundANeedItemAction(Organization $organization, Campaign $campaign, ?FundANeed $fundANeed, Item $item)
{
if (!$fundANeed) {
throw $this->createNotFoundException('Fund-a-Need not found');
}
return $this->viewItem($organization, $campaign, $fundANeed, $item);
}
/**
* Lightweight JSON bid status for the item detail page's live poll
* (auction-item-poll.js). Returns the re-rendered read-only bid-status
* fragment plus flags so the client can detect a bid change and adapt.
*
* @Route("/auction/item/{itemId}/status", name="auction_item_status", methods={"GET"})
*
* @Entity("item", class="Bidcoz\Bundle\CoreBundle\Entity\Auction\Item", options={"id" = "itemId"}, expr="repository.findHydrated(itemId)")
*
* @IsGranted("VIEW", subject="item")
*/
public function itemStatusAction(Request $request, Organization $organization, Campaign $campaign, Item $item)
{
$user = $this->getUser();
$biddable = $this->isItemBiddable($item);
$winningBid = $item->getWinningBid();
$isWinning = $user && $winningBid && $winningBid->getUser() === $user;
$isOutbid = $user && !$isWinning && $item->hasUserBid($user);
$bidCount = $item->getActiveBids()->count();
$html = $this->renderView('@BidcozFrontend/Auction/_item_bid_status.html.twig', [
'item' => $item,
'biddable' => $biddable,
]);
// Signature of the state the heavy fragments depend on: the winning bid and
// the bid count. The client echoes back the signature it last rendered
// (?signature=); when it matches, skip re-rendering _bid_activity and
// _outbid_note. The client only consumes those fragments on a real change
// (see auction-item-poll.js), so near auction close — polling every 1–3s
// per viewer while the DB is under bid-write load — a hot item no longer
// does hundreds of full activity-table renders/min that are discarded.
$signature = ($winningBid ? $winningBid->getId() : 0) . ':' . $bidCount;
$activityHtml = null;
$outbidNoteHtml = null;
if ($request->query->get('signature') !== $signature) {
// Re-rendered bidding activity so the poll can refresh the table live.
$activityHtml = $this->renderView('@BidcozFrontend/Auction/_bid_activity.html.twig', [
'item' => $item,
'campaign' => $campaign,
]);
// Re-rendered outbid notice so the poll can update its visibility AND wording
// (regular vs auto-bidder) + next-increment amount live, without a page reload.
$outbidNoteHtml = $this->renderView('@BidcozFrontend/Auction/_outbid_note.html.twig', [
'item' => $item,
]);
}
return $this->json([
'html' => $html,
'activityHtml' => $activityHtml,
'outbidNoteHtml' => $outbidNoteHtml,
'signature' => $signature,
'currentBid' => $winningBid ? $winningBid->getAmount() : $item->getMinBid(),
'bidCount' => $bidCount,
'biddable' => $biddable,
'isWinning' => (bool) $isWinning,
'isOutbid' => (bool) $isOutbid,
// True when the user is outbid AND the current winner is an auto-bid proxy raise.
'outbidByAuto' => (bool) ($isOutbid && $winningBid && $winningBid->isAutoBid()),
]);
}
private function viewItem(Organization $organization, Campaign $campaign, Auction $auction, Item $item)
{
[$proxy, $bidForm, $buyForm, $donationForm, $showBidForm, $showBuyForm, $showDonationForm] = $this->getItemVars($item);
$internalUsers = null;
if ($auction instanceof Shop) {
$type = 'shop';
$internalUsers = $this->getRepository('InternalUser')->findByOrganization($organization);
} elseif ($auction instanceof FundANeed) {
$type = 'fund-a-need';
} else {
$type = 'auction';
}
$auctionItems = $this->getPagination($this->getRepository('Auction\Item')->getAuctionQueryBuilder($auction, true, false));
$params = [
'organization' => $organization,
'campaign' => $campaign,
'auction' => $auction,
'item' => $item,
'bidForm' => $bidForm->createView(),
'buyForm' => $buyForm->createView(),
'donationForm' => $donationForm->createView(),
'showBidForm' => $showBidForm,
'showBuyForm' => $showBuyForm,
'showDonationForm' => $showDonationForm,
'requireCC' => !$this->isGranted('WITH_CC', $campaign),
'type' => $type,
'auctionItems' => $auctionItems,
'internalUsers' => $internalUsers,
];
return $this->render('@BidcozFrontend/Auction/item.html.twig', $params);
}
/**
* @Route("/auction/item/{itemId}/bid", name="item_bid")
*
* @Entity("item", class="Bidcoz\Bundle\CoreBundle\Entity\Auction\Item", options={"id" = "itemId"}, expr="repository.findHydrated(itemId)")
*
* @IsGranted("VIEW", subject="item")
*/
public function bidItemAction(Request $request, Organization $organization, Campaign $campaign, Auction $auction, Item $item)
{
$user = $this->getUser();
if (!$user) {
$this->messageAccessDeniedException('You must login or create an account to place a bid');
}
if ('POST' !== $request->getMethod() || !$this->isItemBiddable($item)) {
return $this->redirectToRoute('auction_item', [
'orgSlug' => $organization->getSlug(),
'campaignSlug' => $campaign->getSlug(),
'itemId' => $item->getId(),
]);
}
[$proxy, $bidForm, $buyForm, $donationForm, $showBidForm, $showBuyForm, $showDonationForm] = $this->getItemVars($item);
if (!$this->isGranted('WITH_CC', $campaign)) {
$params = [
'organization' => $organization,
'campaign' => $campaign,
'auction' => $auction,
'item' => $item,
'bidForm' => $bidForm->createView(),
'buyForm' => $buyForm->createView(),
'donationForm' => $donationForm->createView(),
'showBidForm' => $showBidForm,
'showBuyForm' => $showBuyForm,
'showDonationForm' => $showDonationForm,
'ccRequiredWarning' => true,
'requireCC' => true,
'type' => 'auction',
];
return $this->render('@BidcozFrontend/Auction/item.html.twig', $params);
}
$bidForm->handleRequest($request);
if ($bidForm->isValid()) {
try {
$this->getBidManager()->createBid($user, $proxy);
$this->getEntityManager()->flush();
} catch (\Exception $e) {
$this->addFlash('danger', $e->getMessage());
}
return $this->redirectToRoute('auction_item', [
'orgSlug' => $organization->getSlug(),
'campaignSlug' => $campaign->getSlug(),
'itemId' => $item->getId(),
]);
} else {
$this->addFlash('warning', 'There was an error placing your bid, please try again');
}
$params = [
'organization' => $organization,
'campaign' => $campaign,
'auction' => $auction,
'item' => $item,
'bidForm' => $bidForm->createView(),
'buyForm' => $buyForm->createView(),
'donationForm' => $donationForm->createView(),
'showBidForm' => $showBidForm,
'showBuyForm' => $showBuyForm,
'showDonationForm' => $showDonationForm,
'requireCC' => false,
'type' => 'auction',
];
return $this->render('@BidcozFrontend/Auction/item.html.twig', $params);
}
/**
* @Route("/auction/item/{itemId}/buy", name="auction_item_buy")
*
* @Entity("item", class="Bidcoz\Bundle\CoreBundle\Entity\Auction\Item", options={"id" = "itemId"}, expr="repository.findHydrated(itemId)")
*
* @IsGranted("VIEW", subject="item")
*/
public function buyAuctionItemAction(Request $request, Organization $organization, Campaign $campaign, Auction $auction, Item $item)
{
return $this->buyItem($request, $organization, $campaign, $auction, $item);
}
/**
* @Route("/shop/item/{itemId}/buy", name="shop_item_buy")
* @Route("/shop/item/{itemId}/donation", name="shop_item_donation")
*
* @Entity("item", class="Bidcoz\Bundle\CoreBundle\Entity\Auction\Item", options={"id" = "itemId"}, expr="repository.findHydrated(itemId)")
*
* @IsGranted("VIEW", subject="item")
*/
public function buyShopItemAction(Request $request, Organization $organization, Campaign $campaign, Shop $shop, Item $item)
{
return $this->buyItem($request, $organization, $campaign, $shop, $item);
}
/**
* @Route("/fund-a-need/item/{itemId}/buy", name="fund_a_need_item_buy")
* @Route("/fund-a-need/item/{itemId}/donation", name="fund_a_need_item_donation")
*
* @Entity("item", class="Bidcoz\Bundle\CoreBundle\Entity\Auction\Item", options={"id" = "itemId"}, expr="repository.findHydrated(itemId)")
*
* @IsGranted("VIEW", subject="item")
*/
public function buyFundANeedItemAction(Request $request, Organization $organization, Campaign $campaign, ?FundANeed $fundANeed, Item $item)
{
if (!$fundANeed) {
throw $this->createNotFoundException('Fund-a-Need not found');
}
return $this->buyItem($request, $organization, $campaign, $fundANeed, $item);
}
private function buyItem(Request $request, Organization $organization, Campaign $campaign, Auction $auction, Item $item)
{
$user = $this->getUser();
if (!$user) {
$this->messageAccessDeniedException('You must login or create an account to purchase an item');
}
if ('POST' !== $request->getMethod() || !($this->isItemBuyitNow($item) || $this->isItemDonation($item))) {
return $this->redirectToRoute('auction_item', [
'orgSlug' => $organization->getSlug(),
'campaignSlug' => $campaign->getSlug(),
'itemId' => $item->getId(),
]);
}
[$proxy, $bidForm, $buyForm, $donationForm, $showBidForm, $showBuyForm, $showDonationForm] = $this->getItemVars($item);
if ($auction instanceof Shop) {
$type = 'shop';
} elseif ($auction instanceof FundANeed) {
$type = 'fund-a-need';
} else {
$type = 'auction';
}
if (!$this->isGranted('WITH_CC', $campaign)) {
$params = [
'organization' => $organization,
'campaign' => $campaign,
'auction' => $auction,
'item' => $item,
'bidForm' => $bidForm->createView(),
'buyForm' => $buyForm->createView(),
'donationForm' => $donationForm->createView(),
'showBidForm' => $showBidForm,
'showBuyForm' => $showBuyForm,
'showDonationForm' => $showDonationForm,
'ccRequiredWarning' => true,
'requireCC' => true,
'type' => $type,
];
return $this->render('@BidcozFrontend/Auction/item.html.twig', $params);
}
if ($showBuyForm) {
$buyForm->handleRequest($request);
if ($buyForm->isValid()) {
try {
$holdInCart = $campaign->getHoldItemsInCart();
$expirationDate = $holdInCart
? null
: new \DateTime('+15 minutes');
$this->getPurchaseManager()->createBuyItNowItemPurchase($user, $proxy, $campaign->getHoldItemsInCart(), $expirationDate);
if ('auction' == $type) {
$this->getWatchListManager()->createWatchListItem($user, $proxy->getItem());
}
$this->getEntityManager()->flush();
} catch (\Exception $e) {
$this->addFlash('warning', $e->getMessage());
}
return $this->redirectToRoute('account_campaign_purchases', [
'orgSlug' => $organization->getSlug(),
'campaignSlug' => $campaign->getSlug(),
]);
} else {
$this->addFlash('warning', 'There was an error purchasing this item, please try again');
}
} elseif ($showDonationForm) {
$donationForm->handleRequest($request);
if ($donationForm->isValid()) {
try {
$this->getPurchaseManager()->createDonationItemPurchase($user, $proxy);
$this->getEntityManager()->flush();
} catch (\Exception $e) {
$this->addFlash('warning', $e->getMessage());
}
return $this->redirectToRoute('account_campaign_purchases', [
'orgSlug' => $organization->getSlug(),
'campaignSlug' => $campaign->getSlug(),
]);
} else {
$this->addFlash('warning', 'There was an error making this purchase, please try again');
}
}
$params = [
'organization' => $organization,
'campaign' => $campaign,
'auction' => $auction,
'item' => $item,
'bidForm' => $bidForm->createView(),
'buyForm' => $buyForm->createView(),
'donationForm' => $donationForm->createView(),
'showBidForm' => $showBidForm,
'showBuyForm' => $showBuyForm,
'showDonationForm' => $showDonationForm,
'requireCC' => false,
'type' => $auction instanceof Shop ? 'shop' : 'auction',
];
return $this->render('@BidcozFrontend/Auction/item.html.twig', $params);
}
/**
* @Route("/auction/item/{itemId}/max-bid", name="item_max_bid")
*
* @Entity("item", class="Bidcoz\Bundle\CoreBundle\Entity\Auction\Item", options={"id" = "itemId"}, expr="repository.findHydrated(itemId)")
*
* @IsGranted("VIEW", subject="item")
*/
public function updateMaxBidAction(Request $request, Organization $organization, Campaign $campaign, Auction $auction, Item $item)
{
$user = $this->getUser();
if (!$user) {
$this->messageAccessDeniedException('You must login or create an account to place a bid');
}
if ('POST' !== $request->getMethod() || !$this->isItemBiddable($item)) {
return $this->redirectToRoute('auction_item', [
'orgSlug' => $organization->getSlug(),
'campaignSlug' => $campaign->getSlug(),
'itemId' => $item->getId(),
]);
}
if (!$this->isGranted('WITH_CC', $campaign)) {
[$proxy, $bidForm, $buyForm, $donationForm, $showBidForm, $showBuyForm, $showDonationForm] = $this->getItemVars($item);
$params = [
'organization' => $organization,
'campaign' => $campaign,
'auction' => $auction,
'item' => $item,
'bidForm' => $bidForm->createView(),
'buyForm' => $buyForm->createView(),
'donationForm' => $donationForm->createView(),
'showBidForm' => $showBidForm,
'showBuyForm' => $showBuyForm,
'showDonationForm' => $showDonationForm,
'ccRequiredWarning' => true,
'requireCC' => true,
'type' => 'auction',
];
return $this->render('@BidcozFrontend/Auction/item.html.twig', $params);
}
try {
$amount = $request->request->get('amount', 0);
$cancelBid = (bool) $request->request->get('_cancel', false);
$this->getBidManager()->setMaxBid($user, $item, $amount, $cancelBid);
$this->getEntityManager()->flush();
} catch (\Exception $e) {
$this->addFlash('warning', $e->getMessage());
}
return $this->redirectToRoute('auction_item', [
'orgSlug' => $organization->getSlug(),
'campaignSlug' => $campaign->getSlug(),
'itemId' => $item->getId(),
]);
}
protected function isItemBiddable(Item $item)
{
return $this->getItemManager()->isItemBiddable($item);
}
protected function isItemBuyitNow(Item $item)
{
return $this->getItemManager()->isItemBuyitNow($item);
}
protected function isItemDonation(Item $item)
{
return $this->getItemManager()->isItemDonation($item);
}
protected function createItemPurchaseProxy(Item $item)
{
return new ItemPurchaseProxy($item);
}
protected function getBidForm(ItemPurchaseProxy $proxy)
{
$item = $proxy->getItem();
if ($item->isAuctionType()) {
$itemMinimumBid = $this->getBidManager()->getItemMinimumBid($item);
$proxy->setAmount($itemMinimumBid);
}
$form = $this->createForm(ItemBidType::class, $proxy);
return $form;
}
protected function getBuyForm(ItemPurchaseProxy $proxy)
{
$item = $proxy->getItem();
$form = $this->createForm(ItemBuyType::class, $proxy, [
'item' => $item,
]);
if ($item->isFixedPrice()) {
$form->get('quantity')->setData(1);
}
return $form;
}
protected function getDonationForm(ItemPurchaseProxy $proxy)
{
$item = $proxy->getItem();
return $this->createForm(ItemDonationType::class, $proxy, [
'item' => $item,
]);
}
protected function getItemVars(Item $item)
{
$proxy = $this->createItemPurchaseProxy($item);
$bidForm = $this->getBidForm($proxy);
$buyForm = $this->getBuyForm($proxy);
$donationForm = $this->getDonationForm($proxy);
$showBidForm = $this->getItemManager()->isItemBiddable($item);
$showBuyForm = $this->getItemManager()->isItemBuyitNow($item);
$showDonationForm = $this->getItemManager()->isItemDonation($item);
return [$proxy, $bidForm, $buyForm, $donationForm, $showBidForm, $showBuyForm, $showDonationForm];
}
}