<?php
/*
* This file is part of EC-CUBE
*
* Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved.
*
* http://www.ec-cube.co.jp/
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Customize\Controller\Mypage;
use Customize\Service\MailService;
use Customize\Service\TaxRuleServiceCustom as TaxRuleService;
use Eccube\Controller\AbstractController;
use Eccube\Entity\BaseInfo;
use Eccube\Entity\Customer;
use Eccube\Entity\Master\OrderStatus;
use Eccube\Entity\Order;
use Eccube\Entity\Product;
use Eccube\Entity\ProductClass;
use Eccube\Event\EccubeEvents;
use Eccube\Event\EventArgs;
use Eccube\Exception\CartException;
use Eccube\Form\Type\Front\CustomerLoginType;
use Eccube\Repository\BaseInfoRepository;
use Eccube\Repository\CustomerFavoriteProductRepository;
use Eccube\Repository\Master\OrderStatusRepository;
use Eccube\Repository\OrderRepository;
use Eccube\Repository\PaymentRepository;
use Eccube\Repository\ProductRepository;
use Customize\Service\CartService;
use Eccube\Service\Payment\PaymentDispatcher;
use Eccube\Service\Payment\PaymentMethodInterface;
use Eccube\Service\PurchaseFlow\PurchaseContext;
use Eccube\Service\PurchaseFlow\PurchaseFlow;
use Knp\Component\Pager\PaginatorInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
class MypageController extends AbstractController
{
/**
* @var ProductRepository
*/
protected $productRepository;
/**
* @var CustomerFavoriteProductRepository
*/
protected $customerFavoriteProductRepository;
/**
* @var BaseInfo
*/
protected $BaseInfo;
/**
* @var CartService
*/
protected $cartService;
/**
* @var OrderRepository
*/
protected $orderRepository;
/**
* @var PurchaseFlow
*/
protected $purchaseFlow;
protected $serviceContainer;
protected $taxRuleService;
protected $orderStatusRepository;
protected $paymentRepository;
protected $mailService;
/**
* MypageController constructor.
*
* @param OrderRepository $orderRepository
* @param CustomerFavoriteProductRepository $customerFavoriteProductRepository
* @param CartService $cartService
* @param BaseInfoRepository $baseInfoRepository
* @param PurchaseFlow $purchaseFlow
*/
public function __construct(
OrderRepository $orderRepository,
CustomerFavoriteProductRepository $customerFavoriteProductRepository,
CartService $cartService,
BaseInfoRepository $baseInfoRepository,
PurchaseFlow $purchaseFlow,
ContainerInterface $serviceContainer,
TaxRuleService $taxRuleService,
OrderStatusRepository $orderStatusRepository,
PaymentRepository $paymentRepository,
MailService $mailService
) {
$this->orderRepository = $orderRepository;
$this->customerFavoriteProductRepository = $customerFavoriteProductRepository;
$this->BaseInfo = $baseInfoRepository->get();
$this->cartService = $cartService;
$this->purchaseFlow = $purchaseFlow;
$this->serviceContainer = $serviceContainer;
$this->taxRuleService = $taxRuleService;
$this->orderStatusRepository = $orderStatusRepository;
$this->paymentRepository = $paymentRepository;
$this->mailService = $mailService;
}
/**
* ログイン画面.
*
* @Route("/mypage/login", name="mypage_login", methods={"GET", "POST"})
* @Template("Mypage/login.twig")
*/
public function login(Request $request, AuthenticationUtils $utils)
{
if ($this->isGranted('IS_AUTHENTICATED_FULLY')) {
log_info('認証済のためログイン処理をスキップ');
return $this->redirectToRoute('mypage');
}
/* @var $form \Symfony\Component\Form\FormInterface */
$builder = $this->formFactory
->createNamedBuilder('', CustomerLoginType::class);
$builder->get('login_memory')->setData((bool) $request->getSession()->get('_security.login_memory'));
if ($this->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
$Customer = $this->getUser();
if ($Customer instanceof Customer) {
$builder->get('login_email')
->setData($Customer->getEmail());
}
}
$event = new EventArgs(
[
'builder' => $builder,
],
$request
);
$this->eventDispatcher->dispatch($event, EccubeEvents::FRONT_MYPAGE_MYPAGE_LOGIN_INITIALIZE);
$form = $builder->getForm();
return [
'error' => $utils->getLastAuthenticationError(),
'form' => $form->createView(),
];
}
/**
* マイページ.
*
* @Route("/mypage/", name="mypage", methods={"GET"})
* @Template("Mypage/index.twig")
*/
public function index(Request $request, PaginatorInterface $paginator)
{
$Customer = $this->getUser();
// 購入処理中/決済処理中ステータスの受注を非表示にする.
$this->entityManager
->getFilters()
->enable('incomplete_order_status_hidden');
// paginator
$qb = $this->orderRepository->getQueryBuilderByCustomer($Customer);
$event = new EventArgs(
[
'qb' => $qb,
'Customer' => $Customer,
],
$request
);
$this->eventDispatcher->dispatch($event, EccubeEvents::FRONT_MYPAGE_MYPAGE_INDEX_SEARCH);
$pagination = $paginator->paginate(
$qb,
$request->get('pageno', 1),
$this->eccubeConfig['eccube_search_pmax']
);
return [
'pagination' => $pagination,
];
}
/**
* 購入履歴詳細を表示する.
*
* @Route("/mypage/history/{order_no}", name="mypage_history", methods={"GET"})
* @Template("Mypage/history.twig")
*/
public function history(Request $request, $order_no)
{
$this->entityManager->getFilters()
->enable('incomplete_order_status_hidden');
$Order = $this->orderRepository->findOneBy(
[
'order_no' => $order_no,
'Customer' => $this->getUser(),
]
);
$event = new EventArgs(
[
'Order' => $Order,
],
$request
);
$this->eventDispatcher->dispatch($event, EccubeEvents::FRONT_MYPAGE_MYPAGE_HISTORY_INITIALIZE);
/** @var Order $Order */
$Order = $event->getArgument('Order');
if (!$Order) {
throw new NotFoundHttpException();
}
$stockOrder = true;
foreach ($Order->getOrderItems() as $orderItem) {
if ($orderItem->isProduct() && $orderItem->getQuantity() < 0) {
$stockOrder = false;
break;
}
}
$taxRate = $this->taxRuleService->getTaxRate();
$userRate = 0;
if($this->getUser()) {
$userRate = $this->BaseInfo->getOptionUserRate();
}
// Check if can repay and detect payment type
$canRepay = $this->canRepay($Order);
$paymentType = $this->detectPaymentType($Order);
return [
'Order' => $Order,
'stockOrder' => $stockOrder,
'taxRate' => $taxRate,
'userRate' => $userRate,
'canRepay' => $canRepay,
'paymentType' => $paymentType,
];
}
/**
* 再購入を行う.
*
* @Route("/mypage/order/{order_no}", name="mypage_order", methods={"PUT"})
*/
public function order(Request $request, $order_no)
{
$this->isTokenValid();
log_info('再注文開始', [$order_no]);
$Customer = $this->getUser();
/* @var $Order \Eccube\Entity\Order */
$Order = $this->orderRepository->findOneBy(
[
'order_no' => $order_no,
'Customer' => $Customer,
]
);
if($Order->getOrderStatus()->getId() == 10) {
$this->mailService->sendOrderMail($Order);
$this->entityManager->flush();
$OrderStatus = $this->orderStatusRepository->find(OrderStatus::NEW);
$Order->setOrderStatus($OrderStatus);
$this->entityManager->persist($Order);
$this->entityManager->flush();
//set flash message
$this->addFlash('reorder_success', '注文を確定しました');
// return $this->redirect($this->generateUrl('mypage_history', ['order_no' => $Order->getOrderNo()]));
return $this->redirectToRoute('mypage');
}
$event = new EventArgs(
[
'Order' => $Order,
'Customer' => $Customer,
],
$request
);
$this->eventDispatcher->dispatch($event, EccubeEvents::FRONT_MYPAGE_MYPAGE_ORDER_INITIALIZE);
if (!$Order) {
log_info('対象の注文が見つかりません', [$order_no]);
throw new NotFoundHttpException();
}
$remessageProduct = false;
// エラーメッセージの配列
$errorMessages = [];
try {
foreach ($Order->getOrderItems() as $OrderItem) {
if ($OrderItem->getProduct() && $OrderItem->getProductClass()) {
$ProductClass = $this->entityManager
->getRepository(ProductClass::class)
->find($OrderItem->getProductClass()->getId());
if($OrderItem->getProduct()->isEnable() == false || ($OrderItem->getIsPrint() != NULL && $OrderItem->getIsPrint() != 1 && $OrderItem->getProduct()->getProductPrintType() != 1) || ($OrderItem->getIsPrint() != NULL && $OrderItem->getIsPrint() == 1 && $OrderItem->getProduct()->getProductPrintType() == 1) || strpos($OrderItem->getProduct()->getProductPrintColor(), $OrderItem->getProductPrintColor()) === false || (strpos($OrderItem->getProduct()->getProductPrintColor(), $OrderItem->getProductPrintColor()) !== false && $ProductClass->isVisible() == false)) {
$remessageProduct = true;
continue;
}
$this->cartService->addProduct($OrderItem->getProductClass(), $OrderItem->getQuantity(), [
'product_print_type' => $OrderItem->getProductPrintType(),
'product_print_fee_type' => $OrderItem->getProductPrintFeeType(),
'product_print_fee_price' => $OrderItem->getProductPrintFeePrice(),
'product_plate_type_print' => $OrderItem->getProductPlateTypePrint(),
'product_print_color' => $OrderItem->getProductPrintColor(),
'is_print' => ($OrderItem->getProduct()->getProductPrintType() == 1) ? 0 : $OrderItem->getIsPrint(),
'is_repurchase' => $OrderItem->getIsRepurchase(),
'product_code_by_color' => $OrderItem->getProductCodeByColor()
]);
// 明細の正規化
$Carts = $this->cartService->getCarts();
foreach ($Carts as $Cart) {
$result = $this->purchaseFlow->validate($Cart, new PurchaseContext($Cart, $this->getUser()));
// 復旧不可のエラーが発生した場合は追加した明細を削除.
if ($result->hasError()) {
$this->cartService->removeProduct($OrderItem->getProductClass());
foreach ($result->getErrors() as $error) {
$errorMessages[] = $error->getMessage();
}
}
foreach ($result->getWarning() as $warning) {
$errorMessages[] = $warning->getMessage();
}
}
}
}
$this->cartService->save();
} catch (CartException $e) {
log_info($e->getMessage(), [$order_no]);
$this->addRequestError($e->getMessage());
}
foreach ($errorMessages as $errorMessage) {
$this->addRequestError($errorMessage);
}
$event = new EventArgs(
[
'Order' => $Order,
'Customer' => $Customer,
],
$request
);
$this->eventDispatcher->dispatch($event, EccubeEvents::FRONT_MYPAGE_MYPAGE_ORDER_COMPLETE);
if ($event->getResponse() !== null) {
return $event->getResponse();
}
log_info('再注文完了', [$order_no]);
if($remessageProduct == true) {
return $this->redirect($this->generateUrl('cart', ['no_product' => $remessageProduct]));
} else {
return $this->redirect($this->generateUrl('cart'));
}
}
/**
* 再度のお支払い(未決済の受注).
*
* @Route("/mypage/history/{order_no}/repayment", name="mypage_history_repayment", methods={"POST"})
*/
public function repayment(Request $request, $order_no)
{
$this->isTokenValid();
log_info('再支払い開始', [$order_no]);
$Customer = $this->getUser();
/* @var $Order \Eccube\Entity\Order */
$Order = $this->orderRepository->findOneBy(
[
'order_no' => $order_no,
'Customer' => $Customer,
]
);
if (!$Order) {
throw new NotFoundHttpException();
}
// Check if payment is allowed
if (!$this->canRepay($Order)) {
$this->addError('このご注文は再度のお支払いができません。');
return $this->redirectToRoute('mypage_history', ['order_no' => $order_no]);
}
$methodClass = $this->getPaymentMethodClass($Order);
if (!$methodClass) {
$this->addError('お支払い方法情報が見つかりません。');
return $this->redirectToRoute('mypage_history', ['order_no' => $order_no]);
}
$methodClass = ltrim($methodClass, '\\');
if (!$this->serviceContainer->has($methodClass)) {
log_info('再支払い対象の決済サービスが見つかりません', [$order_no, $methodClass]);
$this->addError('このお支払い方法は再決済に対応していません。');
return $this->redirectToRoute('mypage_history', ['order_no' => $order_no]);
}
$PaymentMethod = $this->serviceContainer->get($methodClass);
if (!$PaymentMethod instanceof PaymentMethodInterface) {
log_info('決済サービスがPaymentMethodInterfaceではありません', [$order_no, $methodClass]);
$this->addError('このお支払い方法は再決済に対応していません。');
return $this->redirectToRoute('mypage_history', ['order_no' => $order_no]);
}
$PaymentMethod->setOrder($Order);
// Restore the session context that payment plugins use to look up the Order.
// apply() was already called when the order was originally created, so we do NOT
// call it again. We just restore the necessary session state and redirect directly
// to the payment entry page.
if ($Order->getPreOrderId()) {
$this->cartService->setPreOrderId($Order->getPreOrderId())->save();
}
$request->getSession()->set('eccube.front.shopping.order.id', $Order->getId());
// ShoppingController と同じ順序: apply() -> checkout()
if ($response = $this->executeRepaymentApply($PaymentMethod)) {
log_info('再支払い: PaymentMethod::apply で遷移', [$order_no, $methodClass]);
return $response;
}
// Migrateでmethod_classがCash固定になっているケースの救済.
// まずは現在のmethod_classでShopping同等の処理を試し、
// 反応がない場合のみ同名/同種別の候補クラスでapply()を再試行する。
if ($methodClass === 'Eccube\\Service\\Payment\\Method\\Cash') {
foreach ($this->resolveRepaymentFallbackMethodClasses($Order) as $fallbackClass) {
if (!$this->serviceContainer->has($fallbackClass)) {
continue;
}
$FallbackPaymentMethod = $this->serviceContainer->get($fallbackClass);
if (!$FallbackPaymentMethod instanceof PaymentMethodInterface) {
continue;
}
$FallbackPaymentMethod->setOrder($Order);
if ($response = $this->executeRepaymentApply($FallbackPaymentMethod)) {
log_info('再支払い: fallback apply で遷移', [$order_no, $fallbackClass]);
return $response;
}
}
}
if ($response = $this->executeRepaymentCheckout($PaymentMethod, $order_no)) {
log_info('再支払い: PaymentMethod::checkout で遷移', [$order_no, $methodClass]);
return $response;
}
$this->entityManager->flush();
$this->addError('このお支払い方法は再決済に対応していません。');
return $this->redirectToRoute('mypage_history', ['order_no' => $order_no]);
}
/**
* Resolve fallback payment method classes when migrated data keeps Cash class.
*/
private function resolveRepaymentFallbackMethodClasses(Order $Order): array
{
$paymentMethodName = (string) $Order->getPaymentMethod();
$paymentType = $this->detectPaymentType($Order);
$candidates = [];
foreach ($this->paymentRepository->findBy(['visible' => true]) as $Payment) {
$candidateClass = (string) $Payment->getMethodClass();
$candidateName = (string) $Payment->getMethod();
if (!$candidateClass || $candidateClass === 'Eccube\\Service\\Payment\\Method\\Cash') {
continue;
}
if ($candidateName === $paymentMethodName) {
$candidates[] = ltrim($candidateClass, '\\');
continue;
}
if ($paymentType === 'Credit Card' && (strpos($candidateClass, 'Credit') !== false || strpos($candidateClass, 'Card') !== false)) {
$candidates[] = ltrim($candidateClass, '\\');
}
}
return array_values(array_unique($candidates));
}
/**
* Check if order can be repaid.
*
* @param Order $Order
* @return bool
*/
private function canRepay(Order $Order): bool
{
// Only allow repayment if:
// 1. Order status is NEW, PENDING, or PROCESSING
// 2. Payment date is null (not yet paid)
// 3. Payment is not already completed
// Disallowed statuses: PAID, DELIVERED, CANCEL, RETURNED
$disallowedStatuses = [
OrderStatus::PAID, // 6
OrderStatus::DELIVERED, // 8
OrderStatus::CANCEL, // 1
OrderStatus::RETURNED, // 9
];
if (in_array($Order->getOrderStatus()->getId(), $disallowedStatuses)) {
return false;
}
// If payment_date is not null, already paid
if ($Order->getPaymentDate() !== null) {
return false;
}
return true;
}
/**
* Get payment method class for detecting payment type.
*
* @param Order $Order
* @return string|null
*/
private function getPaymentMethodClass(Order $Order): ?string
{
if ($Order->getPayment() && $Order->getPayment()->getMethodClass()) {
return $Order->getPayment()->getMethodClass();
}
return null;
}
/**
* PaymentMethod::apply を実行する. (ShoppingController 相当)
*/
private function executeRepaymentApply(PaymentMethodInterface $paymentMethod): ?Response
{
$dispatcher = $paymentMethod->apply();
if (!$dispatcher instanceof PaymentDispatcher) {
return null;
}
return $this->buildRepaymentResponseFromDispatcher($dispatcher);
}
/**
* PaymentMethod::checkout を実行する. (ShoppingController 相当)
*/
private function executeRepaymentCheckout(PaymentMethodInterface $paymentMethod, string $orderNo): ?Response
{
$PaymentResult = $paymentMethod->checkout();
$response = $PaymentResult->getResponse();
if ($response instanceof Response && ($response->isRedirection() || $response->isSuccessful())) {
$this->entityManager->flush();
return $response;
}
if (!$PaymentResult->isSuccess()) {
foreach ($PaymentResult->getErrors() as $error) {
$this->addError($error);
}
return $this->redirectToRoute('mypage_history', ['order_no' => $orderNo]);
}
$this->addFlash('success', 'お支払い処理が完了しました。');
return $this->redirectToRoute('mypage_history', ['order_no' => $orderNo]);
}
/**
* PaymentDispatcher を Response に変換する. (ShoppingController 相当)
*/
private function buildRepaymentResponseFromDispatcher(PaymentDispatcher $dispatcher): ?Response
{
$response = $dispatcher->getResponse();
// Plugin が相対パスを返す場合、mypage 配下では壊れるため route 判定して正規化する.
if ($response instanceof RedirectResponse) {
$targetUrl = $response->getTargetUrl();
try {
$this->generateUrl($targetUrl);
return $this->redirectToRoute($targetUrl);
} catch (RouteNotFoundException $e) {
// keep original redirect response when target is already URL
}
}
if ($response instanceof Response && ($response->isRedirection() || $response->isSuccessful())) {
return $response;
}
if ($dispatcher->isForward()) {
return $this->forwardToRoute(
$dispatcher->getRoute(),
$dispatcher->getPathParameters(),
$dispatcher->getQueryParameters()
);
}
return $this->redirectToRoute(
$dispatcher->getRoute(),
array_merge($dispatcher->getPathParameters(), $dispatcher->getQueryParameters())
);
}
/**
* Detect payment method type from Payment entity.
*
* @param Order $Order
* @return string Credit Card, Bank Transfer, Other
*/
public function detectPaymentType(Order $Order): string
{
$methodClass = $this->getPaymentMethodClass($Order);
$paymentMethodName = (string) $Order->getPaymentMethod();
if (!$methodClass) {
if (
mb_stripos($paymentMethodName, 'クレジット') !== false
|| stripos($paymentMethodName, 'credit') !== false
|| mb_stripos($paymentMethodName, 'thẻ') !== false
) {
return 'Credit Card';
}
if (
mb_stripos($paymentMethodName, 'QR') !== false
|| stripos($paymentMethodName, 'paypay') !== false
|| stripos($paymentMethodName, 'paydesign') !== false
) {
return 'QR Code';
}
if (
mb_stripos($paymentMethodName, '銀行振込') !== false
|| mb_stripos($paymentMethodName, 'chuyển khoản') !== false
|| stripos($paymentMethodName, 'bank transfer') !== false
) {
return 'Bank Transfer';
}
return 'Other';
}
$normalizedMethodClass = ltrim($methodClass, '\\');
// Map payment method classes to types
$paymentMethods = [
'Plugin\SlnPayment42\Service\Method\CreditCard' => 'Credit Card',
'Plugin\SlnPayment42\Service\Method\RegisteredCreditCard' => 'Credit Card',
'Plugin\paypay4\Service\Method\PayPay' => 'QR Code',
'Plugin\paydesign4\Service\Method\PayDesign' => 'QR Code',
'Plugin\adnet4\Service\Method\PayDesign' => 'QR Code',
];
foreach ($paymentMethods as $class => $type) {
if ($normalizedMethodClass === $class || strpos($normalizedMethodClass, $class) !== false) {
return $type;
}
}
// Default detection by class name
if (strpos($methodClass, 'Credit') !== false || strpos($methodClass, 'Card') !== false) {
return 'Credit Card';
}
if (strpos($methodClass, 'PayPay') !== false || strpos($methodClass, 'QR') !== false) {
return 'QR Code';
}
if (
mb_stripos($paymentMethodName, '銀行振込') !== false
|| mb_stripos($paymentMethodName, 'chuyển khoản') !== false
|| stripos($paymentMethodName, 'bank transfer') !== false
) {
return 'Bank Transfer';
}
return 'Other';
}
/**
* お気に入り商品を表示する.
*
* @Route("/mypage/favorite", name="mypage_favorite", methods={"GET"})
* @Template("Mypage/favorite.twig")
*/
public function favorite(Request $request, PaginatorInterface $paginator)
{
if (!$this->BaseInfo->isOptionFavoriteProduct()) {
throw new NotFoundHttpException();
}
$Customer = $this->getUser();
// paginator
$qb = $this->customerFavoriteProductRepository->getQueryBuilderByCustomer($Customer);
$event = new EventArgs(
[
'qb' => $qb,
'Customer' => $Customer,
],
$request
);
$this->eventDispatcher->dispatch($event, EccubeEvents::FRONT_MYPAGE_MYPAGE_FAVORITE_SEARCH);
$pagination = $paginator->paginate(
$qb,
$request->get('pageno', 1),
$this->eccubeConfig['eccube_search_pmax'],
['wrap-queries' => true]
);
return [
'pagination' => $pagination,
];
}
/**
* お気に入り商品を削除する.
*
* @Route("/mypage/favorite/{id}/delete", name="mypage_favorite_delete", methods={"DELETE"}, requirements={"id" = "\d+"})
*/
public function delete(Request $request, Product $Product)
{
$this->isTokenValid();
$Customer = $this->getUser();
log_info('お気に入り商品削除開始', [$Customer->getId(), $Product->getId()]);
$CustomerFavoriteProduct = $this->customerFavoriteProductRepository->findOneBy(['Customer' => $Customer, 'Product' => $Product]);
if ($CustomerFavoriteProduct) {
$this->customerFavoriteProductRepository->delete($CustomerFavoriteProduct);
} else {
throw new BadRequestHttpException();
}
$event = new EventArgs(
[
'Customer' => $Customer,
'CustomerFavoriteProduct' => $CustomerFavoriteProduct,
], $request
);
$this->eventDispatcher->dispatch($event, EccubeEvents::FRONT_MYPAGE_MYPAGE_DELETE_COMPLETE);
log_info('お気に入り商品削除完了', [$Customer->getId(), $CustomerFavoriteProduct->getId()]);
return $this->redirect($this->generateUrl('mypage_favorite'));
}
}