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