app/template/aneros2023/abtest/pdp_value_msg.twig line 1

Open in your IDE?
  1. {# === PDP Above-the-Fold Value Message (B-only; pill style; block after .price-box .point-area) === #}
  2. {# --- DeliveryDate42 holidays -> array of "YYYY-MM-DD" (bounded window for production use) --- #}
  3. {% set _from = ("now"|date_modify("-30 days"))|date("Y-m-d") %}
  4. {% set _to   = ("now"|date_modify("+400 days"))|date("Y-m-d") %}
  5. {% set _holidayRows = repository('Plugin\\DeliveryDate42\\Entity\\Holiday').findAll() %}
  6. {% set holiday_dates = [] %}
  7. {% for h in _holidayRows %}
  8.   {% if h.date is not null %}
  9.     {% set d = h.date|date('Y-m-d') %}
  10.     {% if d >= _from and d <= _to %}
  11.       {% set holiday_dates = holiday_dates|merge([d]) %}
  12.     {% endif %}
  13.   {% endif %}
  14. {% endfor %}
  15. {# --- Optional debug list (2026 full year) ONLY when ?debug=1&debug_holidays=1 --- #}
  16. {% set debug_holidays_2026 = [] %}
  17. {% if app.request.query.get('debug') == '1' and app.request.query.get('debug_holidays') == '1' %}
  18.   {% for h in _holidayRows %}
  19.     {% if h.date is not null %}
  20.       {% set d = h.date|date('Y-m-d') %}
  21.       {% if d >= '2026-01-01' and d <= '2026-12-31' %}
  22.         {% set debug_holidays_2026 = debug_holidays_2026|merge([d]) %}
  23.       {% endif %}
  24.     {% endif %}
  25.   {% endfor %}
  26. {% endif %}
  27. <meta id="pdp-msg-price"
  28.       data-price01="{{ Product.getPrice01IncTaxMin|default('') }}"
  29.       data-price02="{{ Product.getPrice02IncTaxMin|default('') }}">
  30. <style>
  31.   /* Block line placed AFTER .point-area inside .price-box (prevents ATC collisions) */
  32.   .price-box .pdp-msg-floater{
  33.     display: block;
  34.     font-size: 13px;
  35.     line-height: 1.2;
  36.     margin: 5px 0;
  37.     clear: both;
  38.   }
  39.   @media (min-width: 1000px){
  40.     #default-product-page .price-box .pdp-msg-floater{
  41.       text-align:left;
  42.     }
  43.   }
  44.   /* Pill styles */
  45.   .pdp-pill{
  46.     display: inline-block;
  47.     padding: 6px 10px;
  48.     border-radius: 9999px;
  49.     font-weight: 700;
  50.     line-height: 1.2;
  51.     max-width: 100%;
  52.     white-space: nowrap;   /* single line on wider screens */
  53.   }
  54.   .pdp-pill--free{     background:#ff0000; color:#fff; } /* 送料無料:赤 */
  55.   .pdp-pill--speed{    background:#1a73e8; color:#fff; } /* 即日出荷:青 */
  56.   .pdp-pill--fallback{ background:#2e7d32; color:#fff; } /* 翌営業日:緑 */
  57.   /* Mobile: allow wrapping so long text doesn't collide */
  58.   @media (max-width: 480px){
  59.     .pdp-pill{ white-space: normal; }
  60.   }
  61. </style>
  62. <script>
  63. (function(){
  64.   document.addEventListener('DOMContentLoaded', function(){
  65.     try{
  66.       /* ===== Constants ===== */
  67.       const EXP_NAME    = 'pdp_value_msg_v1';
  68.       const CAMPAIGN_ID = '2025-10-pdp-msg';
  69.       // B-only
  70.       const variant = 'B';
  71.       const FREE_SHIPPING_THRESHOLD = 5000;  // ¥5,000+
  72.       const CUTOFF_HOUR = 14;                // Same-day ship cutoff (平日)
  73.       /* ===== JST date helper ===== */
  74.       const now = new Date();
  75.       const todayJST = new Date(now.getTime() - now.getTimezoneOffset()*60000).toISOString().slice(0,10);
  76.       /* ===== Debug flags ===== */
  77.       const qs = new URLSearchParams(location.search);
  78.       const DEBUG = qs.get('debug') === '1';
  79.       const DEBUG_HOLIDAYS = DEBUG && qs.get('debug_holidays') === '1';
  80.       /* ===== Persist debug/metadata ===== */
  81.       localStorage.setItem('pdp_value_msg_experiment', EXP_NAME);
  82.       localStorage.setItem('pdp_value_msg_campaign',  CAMPAIGN_ID);
  83.       localStorage.setItem('ab_variant_pdp_msg_v1',   variant);
  84.       /* ===== Helpers ===== */
  85.                                                       
  86.       const safeInt = (x) => {
  87.         if (x == null) return null;
  88.         const v = parseInt(String(x).replace(/[^\d]/g,''), 10);
  89.         return Number.isNaN(v) ? null : v;
  90.       };
  91.       const getMetaPrice = (which) => {
  92.         const m = document.getElementById('pdp-msg-price');
  93.         return m ? safeInt(m.dataset[which]) : null;
  94.       };
  95.       function getGA4Price(){
  96.         try{
  97.           const arr = window.dataLayer || [];
  98.           for (let i = arr.length - 1; i >= 0; i--){
  99.             const e = arr[i];
  100.             if (e?.ecommerce?.items?.length){
  101.               const n = safeInt(e.ecommerce.items[0].price);
  102.               if (n != null) return n;
  103.             }
  104.             if (e?.ecommerce?.detail?.products?.length){
  105.               const n = safeInt(e.ecommerce.detail.products[0].price);
  106.               if (n != null) return n;
  107.             }
  108.           }
  109.         }catch(_){}
  110.         return null;
  111.       }
  112.       function getVisiblePrice(){
  113.         const el = document.querySelector('[itemprop="price"]') ||
  114.                    document.querySelector('.ec-productRole__price, .price02, .current-price');
  115.         return el ? safeInt(el.getAttribute('content') || el.textContent) : null;
  116.       }
  117.       const price02 = () => getMetaPrice('price02') ?? getVisiblePrice() ?? getGA4Price();
  118.       /* ===== Holidays from DeliveryDate42 (YYYY-MM-DD) ===== */
  119.                                                                                            
  120.                  
  121.                                      
  122.                                            
  123.         
  124.       const holidaySet = new Set({{ holiday_dates|json_encode|raw }});
  125.       function isHolidayOrWeekend(isoYmd){
  126.         // Force JST weekday calculation for consistency
  127.         const d = new Date(isoYmd + 'T00:00:00+09:00');
  128.         const wd = d.getUTCDay(); // weekday in JST
  129.         const weekend = (wd === 0 || wd === 6);
  130.         return weekend || holidaySet.has(isoYmd);
  131.       }
  132.       if (DEBUG_HOLIDAYS){
  133.         const rangeFrom = {{ _from|json_encode|raw }};
  134.         const rangeTo   = {{ _to|json_encode|raw }};
  135.         console.group('[pdp_value_msg] holiday debug');
  136.         console.log('Range used (server filtered):', rangeFrom, '→', rangeTo);
  137.         console.log('holidaySet size:', holidaySet.size);
  138.         console.log('todayJST:', todayJST, 'isHolidayOrWeekend(todayJST)=', isHolidayOrWeekend(todayJST));
  139.         // 2026-only list (only rendered when debug=1&debug_holidays=1)
  140.         const holidays2026 = {{ debug_holidays_2026|json_encode|raw }};
  141.         console.log('Holidays in 2026 (from plugin table): count=', holidays2026.length);
  142.         console.log(holidays2026);
  143.         console.groupEnd();
  144.       }
  145.       const htmlFor = (type) => ({
  146.         free:     '<span class="pdp-pill pdp-pill--free">送料無料対象</span>',
  147.         speed:    '<span class="pdp-pill pdp-pill--speed">本日14時までのご注文で即日出荷</span>',
  148.         fallback: '<span class="pdp-pill pdp-pill--fallback">今ご注文で翌営業日出荷</span>'
  149.       })[type];
  150.       function computeMessage(){
  151.         // QA forcing (only with debug=1 to avoid accidental use)
  152.                                                    
  153.         const forceType = DEBUG ? qs.get('pdp_msg_force') : null; // free | speed | fallback
  154.         if (forceType && ['free','speed','fallback'].includes(forceType)) {
  155.           return { type: forceType, html: htmlFor(forceType) };
  156.         }
  157.         const p2 = price02();
  158.         if (p2 != null && p2 >= FREE_SHIPPING_THRESHOLD) {
  159.           return { type: 'free', html: htmlFor('free') };
  160.         }
  161.         // weekday + before cutoff => speed
  162.         if (!isHolidayOrWeekend(todayJST) && (new Date()).getHours() < CUTOFF_HOUR) {
  163.           return { type: 'speed', html: htmlFor('speed') };
  164.         }
  165.         return { type: 'fallback', html: htmlFor('fallback') };
  166.       }
  167.       /* ===== Render (once) ===== */
  168.       if (document.querySelector('.pdp-msg-floater')) return; // avoid duplicates
  169.       const msg  = computeMessage();
  170.       const line = document.createElement('div');
  171.       line.className = 'pdp-msg-floater';
  172.       line.innerHTML = msg.html;
  173.       // Primary target: AFTER .price-box .point-area (inside .price-box)
  174.       const pointArea =
  175.         document.querySelector('.product-price-info .price-box .point-area') ||
  176.         document.querySelector('.price-box .point-area');
  177.       if (pointArea?.parentNode){
  178.         pointArea.insertAdjacentElement('afterend', line);
  179.       } else {
  180.         // Fallbacks: append in .price-box, else above ATC
  181.         const priceBox = document.querySelector('.product-price-info .price-box') ||
  182.                          document.querySelector('.price-box');
  183.         if (priceBox){
  184.           priceBox.appendChild(line);
  185.         } else {
  186.           const atc = document.querySelector('.ec-productRole__btn, .ec-productAction__btn, .product-cart');
  187.           if (atc?.parentNode) atc.parentNode.insertBefore(line, atc);
  188.           else return; // no anchor found
  189.         }
  190.       }
  191.       /* ===== Main tracking (kept) ===== */
  192.       window.dataLayer = window.dataLayer || [];
  193.       window.dataLayer.push({
  194.         event: 'pdp_msg_view',
  195.         experiment_name: EXP_NAME,
  196.         campaign_id: CAMPAIGN_ID,
  197.         variant: variant,
  198.         position: 'pdp',
  199.         pdp_msg_type: msg.type
  200.       });
  201.       localStorage.setItem('pdp_msg_type', msg.type);
  202.       /* ===== Click tracking (kept; only if link exists in pill) ===== */
  203.       line.addEventListener('click', function(e){
  204.         const a = e.target.closest('a');
  205.         if (!a) return;
  206.         window.dataLayer.push({
  207.           event: 'pdp_msg_click',
  208.           experiment_name: EXP_NAME,
  209.           campaign_id: CAMPAIGN_ID,
  210.           variant: variant,
  211.           position: 'pdp',
  212.           pdp_msg_type: msg.type,
  213.           href: a.href || null
  214.         });
  215.       });
  216.     }catch(e){
  217.       console && console.warn && console.warn('pdp_value_msg_v1 error', e);
  218.     }
  219.   });
  220. })();
  221. </script>