app/Plugin/NZEstimateSystem42/Controller/EstimateSimulatorController.php line 108

Open in your IDE?
  1. <?php
  2. namespace Plugin\NZEstimateSystem42\Controller;
  3. use Eccube\Controller\AbstractController;
  4. use Eccube\Entity\Product;
  5. use Eccube\Entity\ProductClass;
  6. use Eccube\Entity\ProductStock;
  7. use Eccube\Entity\ProductImage;
  8. use Eccube\Entity\Master\ProductStatus;
  9. use Eccube\Entity\Master\SaleType;
  10. use Eccube\Repository\ProductRepository;
  11. use Eccube\Repository\Master\ProductStatusRepository;
  12. use Eccube\Repository\Master\SaleTypeRepository;
  13. use Eccube\Service\CartService;
  14. use Eccube\Service\PurchaseFlow\PurchaseContext;
  15. use Plugin\NZEstimateSystem42\Repository\PartsCategoryRepository;
  16. use Plugin\NZEstimateSystem42\Repository\PartsRepository;
  17. use Plugin\NZEstimateSystem42\Repository\ConfigRepository;
  18. use Plugin\NZEstimateSystem42\Service\EstimateMailService;
  19. use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
  20. use Symfony\Component\HttpFoundation\Request;
  21. use Symfony\Component\HttpFoundation\Response;
  22. use Symfony\Component\Routing\Annotation\Route;
  23. use Doctrine\ORM\EntityManagerInterface;
  24. /**
  25.  * 見積もりシミュレーター(フロント)
  26.  */
  27. class EstimateSimulatorController extends AbstractController
  28. {
  29.     /**
  30.      * @var PartsCategoryRepository
  31.      */
  32.     protected $partsCategoryRepository;
  33.     /**
  34.      * @var PartsRepository
  35.      */
  36.     protected $partsRepository;
  37.     /**
  38.      * @var ConfigRepository
  39.      */
  40.     protected $configRepository;
  41.     /**
  42.      * @var EstimateMailService
  43.      */
  44.     protected $mailService;
  45.     /**
  46.      * @var EntityManagerInterface
  47.      */
  48.     protected $entityManager;
  49.     /**
  50.      * @var ProductRepository
  51.      */
  52.     protected $productRepository;
  53.     /**
  54.      * @var ProductStatusRepository
  55.      */
  56.     protected $productStatusRepository;
  57.     /**
  58.      * @var SaleTypeRepository
  59.      */
  60.     protected $saleTypeRepository;
  61.     /**
  62.      * @var CartService
  63.      */
  64.     protected $cartService;
  65.     /**
  66.      * EstimateSimulatorController constructor.
  67.      */
  68.     public function __construct(
  69.         PartsCategoryRepository $partsCategoryRepository,
  70.         PartsRepository $partsRepository,
  71.         ConfigRepository $configRepository,
  72.         EstimateMailService $mailService,
  73.         EntityManagerInterface $entityManager,
  74.         ProductRepository $productRepository,
  75.         ProductStatusRepository $productStatusRepository,
  76.         SaleTypeRepository $saleTypeRepository,
  77.         CartService $cartService
  78.     ) {
  79.         $this->partsCategoryRepository $partsCategoryRepository;
  80.         $this->partsRepository $partsRepository;
  81.         $this->configRepository $configRepository;
  82.         $this->mailService $mailService;
  83.         $this->entityManager $entityManager;
  84.         $this->productRepository $productRepository;
  85.         $this->productStatusRepository $productStatusRepository;
  86.         $this->saleTypeRepository $saleTypeRepository;
  87.         $this->cartService $cartService;
  88.     }
  89.     /**
  90.      * 見積もりシミュレーター
  91.      *
  92.      * @Route("/estimate/simulator", name="nz_estimate_simulator")
  93.      * @Template("@NZEstimateSystem42/default/simulator.twig")
  94.      */
  95.     public function index(Request $request)
  96.     {
  97.         // 全カテゴリを取得
  98.         $categories $this->partsCategoryRepository->findAllOrderBySortNo();
  99.         $config $this->configRepository->get();
  100.         // 子カテゴリとして使用されているカテゴリIDを取得
  101.         $childCategoryIds = [];
  102.         // シミュレーター表示対象のパーツのみ取得
  103.         $allParts $this->partsRepository->findBy(['is_active' => true'is_simulator' => true]);
  104.         foreach ($allParts as $parts) {
  105.             if ($parts->getChildCategoryId()) {
  106.                 // カンマ区切りの場合は分割して配列に追加
  107.                 $ids explode(','$parts->getChildCategoryId());
  108.                 foreach ($ids as $id) {
  109.                     $id trim($id);
  110.                     if (is_numeric($id)) {
  111.                         $childCategoryIds[] = (int)$id;
  112.                     }
  113.                 }
  114.             }
  115.         }
  116.         $childCategoryIds array_unique($childCategoryIds);
  117.         // 親カテゴリのみを表示(子カテゴリとして使用されていないカテゴリ)
  118.         $categoriesWithParts = [];
  119.         $allCategoriesWithParts = []; // 子カテゴリを含むすべてのカテゴリ
  120.         
  121.         foreach ($categories as $category) {
  122.             // シミュレーター表示対象のパーツのみ取得
  123.             $parts $this->partsRepository->findForSimulator($category);
  124.             
  125.             if (!empty($parts)) {
  126.                 // 子カテゴリを含むすべてのカテゴリデータ(JavaScript用)
  127.                 $allCategoriesWithParts[] = [
  128.                     'category' => $category,
  129.                     'parts' => $parts,
  130.                 ];
  131.                 
  132.                 // 子カテゴリとして使用されているカテゴリは初期表示しない
  133.                 if (!in_array($category->getId(), $childCategoryIds)) {
  134.                     $categoriesWithParts[] = [
  135.                         'category' => $category,
  136.                         'parts' => $parts,
  137.                     ];
  138.                 }
  139.             }
  140.         }
  141.         // デバイスに応じてレイアウトを取得
  142.         $Layout $this->getApplicableLayout($config$request);
  143.         return [
  144.             'categoriesWithParts' => $categoriesWithParts,
  145.             'allCategoriesWithParts' => $allCategoriesWithParts// JavaScript用に追加
  146.             'config' => $config,
  147.             'Layout' => $Layout,
  148.         ];
  149.     }
  150.     /**
  151.      * デバイスに応じて適用するレイアウトを取得
  152.      */
  153.     private function getApplicableLayout($configRequest $request)
  154.     {
  155.         // モバイルデバイスの判定
  156.         $userAgent $request->headers->get('User-Agent');
  157.         $isMobile $this->detectMobileDevice($userAgent);
  158.         
  159.         // デバイスに応じてレイアウトを取得
  160.         if ($isMobile && $config->getLayoutMobile()) {
  161.             return $config->getLayoutMobile();
  162.         }
  163.         
  164.         if ($config->getLayout()) {
  165.             return $config->getLayout();
  166.         }
  167.         
  168.         // デフォルトレイアウトを返す
  169.         return null;
  170.     }
  171.     /**
  172.      * User-Agentからモバイルデバイスかどうかを判定
  173.      */
  174.     private function detectMobileDevice($userAgent)
  175.     {
  176.         if (empty($userAgent)) {
  177.             return false;
  178.         }
  179.         // モバイルデバイスのパターン
  180.         $mobilePatterns = [
  181.             'iPhone',
  182.             'iPod',
  183.             'Android.*Mobile',
  184.             'Windows Phone',
  185.             'BlackBerry',
  186.             'webOS',
  187.             'Mobile',
  188.             'IEMobile',
  189.         ];
  190.         $pattern '/' implode('|'$mobilePatterns) . '/i';
  191.         return preg_match($pattern$userAgent) === 1;
  192.     }
  193.     /**
  194.      * カテゴリ別パーツ一覧取得(Ajax)
  195.      *
  196.      * @Route("/estimate/simulator/parts/{category_id}", name="nz_estimate_simulator_parts", methods={"GET"})
  197.      */
  198.     public function getPartsByCategory(Request $request$category_id)
  199.     {
  200.         if (!$request->isXmlHttpRequest()) {
  201.             throw $this->createNotFoundException();
  202.         }
  203.         $category $this->partsCategoryRepository->find($category_id);
  204.         
  205.         if (!$category) {
  206.             return $this->json(['error' => 'カテゴリが見つかりません。'], 404);
  207.         }
  208.         // シミュレーター表示対象のパーツのみ取得
  209.         $parts $this->partsRepository->findForSimulator($category);
  210.         $partsData = [];
  211.         foreach ($parts as $p) {
  212.             $partsData[] = [
  213.                 'id' => $p->getId(),
  214.                 'model_number' => $p->getModelNumber(),
  215.                 'name' => $p->getName(),
  216.                 'specifications' => $p->getSpecifications(),
  217.                 'price' => $p->getPrice(),
  218.                 'stock_status' => $p->getStockStatus(),
  219.             ];
  220.         }
  221.         return $this->json([
  222.             'success' => true,
  223.             'parts' => $partsData,
  224.         ]);
  225.     }
  226.     /**
  227.      * 見積もり確認・メール送信
  228.      *
  229.      * @Route("/estimate/simulator/send", name="nz_estimate_simulator_send", methods={"POST"})
  230.      */
  231.     public function send(Request $request)
  232.     {
  233.         if (!$request->isXmlHttpRequest()) {
  234.             throw $this->createNotFoundException();
  235.         }
  236.         $this->isTokenValid();
  237.         $name $request->request->get('name');
  238.         $email $request->request->get('email');
  239.         $tel $request->request->get('tel');
  240.         $company $request->request->get('company');
  241.         $message $request->request->get('message');
  242.         $items $request->request->get('items', []);
  243.         // デバッグログ
  244.         log_info('NZEstimateSystem42: 見積もり送信開始', [
  245.             'name' => $name,
  246.             'email' => $email,
  247.             'items_raw' => $items,
  248.         ]);
  249.         // バリデーション
  250.         if (empty($name) || empty($email) || empty($items)) {
  251.             log_error('NZEstimateSystem42: バリデーションエラー(必須項目)', [
  252.                 'name' => $name,
  253.                 'email' => $email,
  254.                 'items_count' => is_array($items) ? count($items) : 0,
  255.             ]);
  256.             return $this->json([
  257.                 'success' => false,
  258.                 'message' => '必須項目を入力してください。',
  259.             ], 400);
  260.         }
  261.         // メールアドレスバリデーション
  262.         if (!filter_var($emailFILTER_VALIDATE_EMAIL)) {
  263.             log_error('NZEstimateSystem42: メールアドレス形式エラー', ['email' => $email]);
  264.             return $this->json([
  265.                 'success' => false,
  266.                 'message' => '正しいメールアドレスを入力してください。',
  267.             ], 400);
  268.         }
  269.         // itemsがJSON文字列の場合はデコード
  270.         if (is_string($items)) {
  271.             $items json_decode($itemstrue);
  272.             if (json_last_error() !== JSON_ERROR_NONE) {
  273.                 log_error('NZEstimateSystem42: JSON デコードエラー', [
  274.                     'error' => json_last_error_msg(),
  275.                     'items' => $items,
  276.                 ]);
  277.                 return $this->json([
  278.                     'success' => false,
  279.                     'message' => 'データ形式が正しくありません。',
  280.                 ], 400);
  281.             }
  282.         }
  283.         
  284.         // パーツ情報を取得して見積もり内容を作成
  285.         $estimateItems = [];
  286.         $totalPrice 0;
  287.         foreach ($items as $item) {
  288.             $partsId = isset($item['parts_id']) ? $item['parts_id'] : null;
  289.             if (!$partsId) {
  290.                 log_warning('NZEstimateSystem42: parts_id が空', ['item' => $item]);
  291.                 continue;
  292.             }
  293.             
  294.             $parts $this->partsRepository->find($partsId);
  295.             if (!$parts || !$parts->getIsActive()) {
  296.                 log_warning('NZEstimateSystem42: パーツが見つからないか無効', ['parts_id' => $partsId]);
  297.                 continue;
  298.             }
  299.             // quantityは常に1とする(シミュレーターは数量選択なし)
  300.             $quantity 1;
  301.             $subtotal $parts->getPrice() * $quantity;
  302.             $estimateItems[] = [
  303.                 'model_number' => $parts->getModelNumber(),
  304.                 'name' => $parts->getProductName(),  // getName() → getProductName()
  305.                 'price' => number_format($parts->getPrice()),
  306.                 'quantity' => $quantity,
  307.                 'subtotal' => number_format($subtotal),
  308.             ];
  309.             $totalPrice += $subtotal;
  310.         }
  311.         if (empty($estimateItems)) {
  312.             log_error('NZEstimateSystem42: 有効なパーツが1つもない');
  313.             return $this->json([
  314.                 'success' => false,
  315.                 'message' => '選択されたパーツが無効です。',
  316.             ], 400);
  317.         }
  318.         log_info('NZEstimateSystem42: 見積もり内容作成完了', [
  319.             'items_count' => count($estimateItems),
  320.             'total_price' => $totalPrice,
  321.         ]);
  322.         // 設定から料金調整を取得
  323.         $config $this->configRepository->get();
  324.         $displayPrice $totalPrice// 顧客に表示する価格(パーツ合計)
  325.         $finalPrice $config->calculateFinalPrice($totalPrice); // 内部的な最終金額
  326.         log_info('NZEstimateSystem42: 料金調整適用', [
  327.             'display_price' => $displayPrice,
  328.             'additional_charge' => $config->getAdditionalCharge(),
  329.             'discount_rate' => $config->getDiscountRate(),
  330.             'final_price' => $finalPrice,
  331.         ]);
  332.         // メール送信
  333.         try {
  334.             $mailSent $this->mailService->sendEstimateRequest(
  335.                 $name,
  336.                 $email,
  337.                 $tel,
  338.                 $company,
  339.                 $message,
  340.                 $estimateItems,
  341.                 $displayPrice,  // 顧客向けメールには表示価格
  342.                 $finalPrice,    // 管理者向けメールには最終金額
  343.                 $config         // 設定情報を渡す
  344.             );
  345.             if (!$mailSent) {
  346.                 log_error('NZEstimateSystem42: メール送信失敗(戻り値がfalse)');
  347.                 return $this->json([
  348.                     'success' => false,
  349.                     'message' => 'メール送信に失敗しました。しばらくしてから再度お試しください。',
  350.                 ], 500);
  351.             }
  352.             log_info('NZEstimateSystem42: メール送信成功');
  353.             return $this->json([
  354.                 'success' => true,
  355.                 'message' => 'お見積もり依頼を受け付けました。担当者より折り返しご連絡いたします。',
  356.             ]);
  357.         } catch (\Exception $e) {
  358.             log_error('NZEstimateSystem42: メール送信で例外発生', [
  359.                 'exception' => $e->getMessage(),
  360.                 'trace' => $e->getTraceAsString(),
  361.             ]);
  362.             
  363.             return $this->json([
  364.                 'success' => false,
  365.                 'message' => 'メール送信中にエラーが発生しました: ' $e->getMessage(),
  366.             ], 500);
  367.         }
  368.     }
  369.     /**
  370.      * カートに追加
  371.      *
  372.      * @Route("/estimate_simulator/add_to_cart", name="nz_estimate_add_to_cart", methods={"POST"})
  373.      */
  374.     public function addToCart(Request $request)
  375.     {
  376.         // デバッグ:メソッドが呼ばれたことを確認
  377.         error_log('=== NZEstimateSystem42: addToCart method called ===');
  378.         log_info('NZEstimateSystem42: addToCart method called');
  379.         
  380.         if (!$request->isXmlHttpRequest()) {
  381.             error_log('NZEstimateSystem42: Not AJAX request');
  382.             return $this->json(['success' => false'message' => '不正なリクエストです'], 400);
  383.         }
  384.         $data json_decode($request->getContent(), true);
  385.         error_log('NZEstimateSystem42: Request data: ' json_encode($data));
  386.         
  387.         $selectedParts $data['selected_parts'] ?? [];
  388.         $totalPrice = (int)($data['total_price'] ?? 0);
  389.         if (empty($selectedParts) || $totalPrice <= 0) {
  390.             return $this->json(['success' => false'message' => 'パーツが選択されていません'], 400);
  391.         }
  392.         try {
  393.             log_info('NZEstimateSystem42: カート追加処理開始', [
  394.                 'total_price' => $totalPrice,
  395.                 'parts_count' => count($selectedParts)
  396.             ]);
  397.             // 商品名: オーダーメイドPC10/29 08:15
  398.             $now = new \DateTime();
  399.             $productName sprintf('オーダーメイドPC%s'$now->format('n/j H:i'));
  400.             // 14日後の有効期限を計算
  401.             $expirationDate = (new \DateTime())->modify('+14 days');
  402.             $expirationDateStr $expirationDate->format('n/j');
  403.             
  404.             // 商品説明(有効期限のみ、赤文字・中サイズ)
  405.             $description sprintf(
  406.                 '<p style="color: #e53e3e; font-size: 18px; font-weight: 600; margin: 15px 0;">この見積り商品ページは%sまで有効</p>',
  407.                 $expirationDateStr
  408.             );
  409.             // フリーエリア用のHTMLを自動生成
  410.             $freeAreaHtml "<h3>選択されたパーツ構成</h3>\n";
  411.             $freeAreaHtml .= "<div class=\"ec-descriptionRole__spec\">\n";
  412.             foreach ($selectedParts as $part) {
  413.                 $freeAreaHtml .= "    <dl>\n";
  414.                 $freeAreaHtml .= "        <dt>" htmlspecialchars($part['category_name']) . "</dt>\n";
  415.                 $freeAreaHtml .= "        <dd>" htmlspecialchars($part['name']) . "</dd>\n";
  416.                 $freeAreaHtml .= "    </dl>\n";
  417.             }
  418.             $freeAreaHtml .= "</div>\n";
  419.             $freeAreaHtml .= "<div style=\"margin-top: 20px; padding: 15px; background: #f7fafc; border-radius: 8px;\">\n";
  420.             $freeAreaHtml .= "    <p style=\"margin: 0; font-size: 14px; color: #718096;\">※ パーツのメーカー等も指定したい場合はこの商品ページの上部「この商品を問い合わせる」ボタンより各種質問をお送りください。</p>\n";
  421.             $freeAreaHtml .= "</div>\n";
  422.             // 販売中ステータスを取得
  423.             $ProductStatus $this->productStatusRepository->find(ProductStatus::DISPLAY_SHOW);
  424.             if (!$ProductStatus) {
  425.                 throw new \Exception('商品ステータスが取得できませんでした');
  426.             }
  427.             
  428.             // 販売種別を取得(販売種別クレカ対応 = 3)
  429.             $SaleType $this->saleTypeRepository->find(3);
  430.             if (!$SaleType) {
  431.                 throw new \Exception('販売種別が取得できませんでした');
  432.             }
  433.             // 商品を作成
  434.             $Product = new Product();
  435.             $Product->setName($productName);
  436.             $Product->setStatus($ProductStatus);
  437.             $Product->setDescriptionDetail($description);
  438.             $Product->setDescriptionList(''); // 一覧説明文
  439.             $Product->setSearchWord(''); // 検索ワード
  440.             $Product->setFreeArea($freeAreaHtml); // フリーエリアに見積もり内容を自動設定
  441.             $Product->setCreateDate(new \DateTime());
  442.             $Product->setUpdateDate(new \DateTime());
  443.             // ProductClassを作成
  444.             $ProductClass = new ProductClass();
  445.             $ProductClass->setProduct($Product);
  446.             $ProductClass->setSaleType($SaleType);
  447.             $ProductClass->setPrice01(null); // 通常価格は空白
  448.             $ProductClass->setPrice02($totalPrice); // 販売価格のみ
  449.             $ProductClass->setDeliveryFee(0); // 送料0円
  450.             $ProductClass->setStockUnlimited(true); // 在庫無制限
  451.             $ProductClass->setVisible(true);
  452.             $ProductClass->setCreateDate(new \DateTime());
  453.             $ProductClass->setUpdateDate(new \DateTime());
  454.             
  455.             log_info('NZEstimateSystem42: ProductClass作成', [
  456.                 'price01' => null,
  457.                 'price02' => $totalPrice,
  458.                 'delivery_fee' => 0,
  459.                 'stock_unlimited' => true
  460.             ]);
  461.             
  462.             // ProductStockを作成
  463.             $ProductStock = new ProductStock();
  464.             $ProductStock->setCreateDate(new \DateTime());
  465.             $ProductStock->setUpdateDate(new \DateTime());
  466.             
  467.             // 双方向の関連付け
  468.             $ProductStock->setProductClass($ProductClass);
  469.             $ProductClass->setProductStock($ProductStock);
  470.             
  471.             $Product->addProductClass($ProductClass);
  472.             // データベースに保存
  473.             $this->entityManager->persist($Product);
  474.             $this->entityManager->persist($ProductClass);
  475.             $this->entityManager->persist($ProductStock);
  476.             $this->entityManager->flush();
  477.             
  478.             // デフォルト画像を設定
  479.             $defaultImageFileName '1031020313_69039a51941e2.jpg';
  480.             $saveImagePath __DIR__ '/../../../../html/upload/save_image/' $defaultImageFileName;
  481.             
  482.             if (file_exists($saveImagePath)) {
  483.                 $ProductImage = new ProductImage();
  484.                 $ProductImage->setProduct($Product);
  485.                 $ProductImage->setFileName($defaultImageFileName);
  486.                 $ProductImage->setSortNo(1);
  487.                 $ProductImage->setCreateDate(new \DateTime());
  488.                 
  489.                 $Product->addProductImage($ProductImage);
  490.                 
  491.                 $this->entityManager->persist($ProductImage);
  492.                 $this->entityManager->flush();
  493.             }
  494.             
  495.             log_info('NZEstimateSystem42: データベース保存完了', [
  496.                 'product_id' => $Product->getId(),
  497.                 'product_class_id' => $ProductClass->getId()
  498.             ]);
  499.             // 商品詳細ページのURLを生成
  500.             $productUrl $this->generateUrl('product_detail', ['id' => $Product->getId()]);
  501.             log_info('NZEstimateSystem42: 商品作成成功、商品詳細ページへリダイレクト', [
  502.                 'product_id' => $Product->getId(),
  503.                 'product_url' => $productUrl
  504.             ]);
  505.             return $this->json([
  506.                 'success' => true,
  507.                 'message' => '商品を作成しました',
  508.                 'product_url' => $productUrl,
  509.             ]);
  510.         } catch (\Exception $e) {
  511.             log_error('NZEstimateSystem42: カート追加で例外発生', [
  512.                 'exception' => $e->getMessage(),
  513.                 'trace' => $e->getTraceAsString(),
  514.             ]);
  515.             return $this->json([
  516.                 'success' => false,
  517.                 'message' => 'カート追加中にエラーが発生しました: ' $e->getMessage(),
  518.             ], 500);
  519.         }
  520.     }
  521. }