{# === PDP Above-the-Fold Value Message (B-only; pill style; block after .price-box .point-area) === #}
{# --- DeliveryDate42 holidays -> array of "YYYY-MM-DD" (bounded window for production use) --- #}
{% set _from = ("now"|date_modify("-30 days"))|date("Y-m-d") %}
{% set _to = ("now"|date_modify("+400 days"))|date("Y-m-d") %}
{% set _holidayRows = repository('Plugin\\DeliveryDate42\\Entity\\Holiday').findAll() %}
{% set holiday_dates = [] %}
{% for h in _holidayRows %}
{% if h.date is not null %}
{% set d = h.date|date('Y-m-d') %}
{% if d >= _from and d <= _to %}
{% set holiday_dates = holiday_dates|merge([d]) %}
{% endif %}
{% endif %}
{% endfor %}
{# --- Optional debug list (2026 full year) ONLY when ?debug=1&debug_holidays=1 --- #}
{% set debug_holidays_2026 = [] %}
{% if app.request.query.get('debug') == '1' and app.request.query.get('debug_holidays') == '1' %}
{% for h in _holidayRows %}
{% if h.date is not null %}
{% set d = h.date|date('Y-m-d') %}
{% if d >= '2026-01-01' and d <= '2026-12-31' %}
{% set debug_holidays_2026 = debug_holidays_2026|merge([d]) %}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
<meta id="pdp-msg-price"
data-price01="{{ Product.getPrice01IncTaxMin|default('') }}"
data-price02="{{ Product.getPrice02IncTaxMin|default('') }}">
<style>
/* Block line placed AFTER .point-area inside .price-box (prevents ATC collisions) */
.price-box .pdp-msg-floater{
display: block;
font-size: 13px;
line-height: 1.2;
margin: 5px 0;
clear: both;
}
@media (min-width: 1000px){
#default-product-page .price-box .pdp-msg-floater{
text-align:left;
}
}
/* Pill styles */
.pdp-pill{
display: inline-block;
padding: 6px 10px;
border-radius: 9999px;
font-weight: 700;
line-height: 1.2;
max-width: 100%;
white-space: nowrap; /* single line on wider screens */
}
.pdp-pill--free{ background:#ff0000; color:#fff; } /* 送料無料:赤 */
.pdp-pill--speed{ background:#1a73e8; color:#fff; } /* 即日出荷:青 */
.pdp-pill--fallback{ background:#2e7d32; color:#fff; } /* 翌営業日:緑 */
/* Mobile: allow wrapping so long text doesn't collide */
@media (max-width: 480px){
.pdp-pill{ white-space: normal; }
}
</style>
<script>
(function(){
document.addEventListener('DOMContentLoaded', function(){
try{
/* ===== Constants ===== */
const EXP_NAME = 'pdp_value_msg_v1';
const CAMPAIGN_ID = '2025-10-pdp-msg';
// B-only
const variant = 'B';
const FREE_SHIPPING_THRESHOLD = 5000; // ¥5,000+
const CUTOFF_HOUR = 14; // Same-day ship cutoff (平日)
/* ===== JST date helper ===== */
const now = new Date();
const todayJST = new Date(now.getTime() - now.getTimezoneOffset()*60000).toISOString().slice(0,10);
/* ===== Debug flags ===== */
const qs = new URLSearchParams(location.search);
const DEBUG = qs.get('debug') === '1';
const DEBUG_HOLIDAYS = DEBUG && qs.get('debug_holidays') === '1';
/* ===== Persist debug/metadata ===== */
localStorage.setItem('pdp_value_msg_experiment', EXP_NAME);
localStorage.setItem('pdp_value_msg_campaign', CAMPAIGN_ID);
localStorage.setItem('ab_variant_pdp_msg_v1', variant);
/* ===== Helpers ===== */
const safeInt = (x) => {
if (x == null) return null;
const v = parseInt(String(x).replace(/[^\d]/g,''), 10);
return Number.isNaN(v) ? null : v;
};
const getMetaPrice = (which) => {
const m = document.getElementById('pdp-msg-price');
return m ? safeInt(m.dataset[which]) : null;
};
function getGA4Price(){
try{
const arr = window.dataLayer || [];
for (let i = arr.length - 1; i >= 0; i--){
const e = arr[i];
if (e?.ecommerce?.items?.length){
const n = safeInt(e.ecommerce.items[0].price);
if (n != null) return n;
}
if (e?.ecommerce?.detail?.products?.length){
const n = safeInt(e.ecommerce.detail.products[0].price);
if (n != null) return n;
}
}
}catch(_){}
return null;
}
function getVisiblePrice(){
const el = document.querySelector('[itemprop="price"]') ||
document.querySelector('.ec-productRole__price, .price02, .current-price');
return el ? safeInt(el.getAttribute('content') || el.textContent) : null;
}
const price02 = () => getMetaPrice('price02') ?? getVisiblePrice() ?? getGA4Price();
/* ===== Holidays from DeliveryDate42 (YYYY-MM-DD) ===== */
const holidaySet = new Set({{ holiday_dates|json_encode|raw }});
function isHolidayOrWeekend(isoYmd){
// Force JST weekday calculation for consistency
const d = new Date(isoYmd + 'T00:00:00+09:00');
const wd = d.getUTCDay(); // weekday in JST
const weekend = (wd === 0 || wd === 6);
return weekend || holidaySet.has(isoYmd);
}
if (DEBUG_HOLIDAYS){
const rangeFrom = {{ _from|json_encode|raw }};
const rangeTo = {{ _to|json_encode|raw }};
console.group('[pdp_value_msg] holiday debug');
console.log('Range used (server filtered):', rangeFrom, '→', rangeTo);
console.log('holidaySet size:', holidaySet.size);
console.log('todayJST:', todayJST, 'isHolidayOrWeekend(todayJST)=', isHolidayOrWeekend(todayJST));
// 2026-only list (only rendered when debug=1&debug_holidays=1)
const holidays2026 = {{ debug_holidays_2026|json_encode|raw }};
console.log('Holidays in 2026 (from plugin table): count=', holidays2026.length);
console.log(holidays2026);
console.groupEnd();
}
const htmlFor = (type) => ({
free: '<span class="pdp-pill pdp-pill--free">送料無料対象</span>',
speed: '<span class="pdp-pill pdp-pill--speed">本日14時までのご注文で即日出荷</span>',
fallback: '<span class="pdp-pill pdp-pill--fallback">今ご注文で翌営業日出荷</span>'
})[type];
function computeMessage(){
// QA forcing (only with debug=1 to avoid accidental use)
const forceType = DEBUG ? qs.get('pdp_msg_force') : null; // free | speed | fallback
if (forceType && ['free','speed','fallback'].includes(forceType)) {
return { type: forceType, html: htmlFor(forceType) };
}
const p2 = price02();
if (p2 != null && p2 >= FREE_SHIPPING_THRESHOLD) {
return { type: 'free', html: htmlFor('free') };
}
// weekday + before cutoff => speed
if (!isHolidayOrWeekend(todayJST) && (new Date()).getHours() < CUTOFF_HOUR) {
return { type: 'speed', html: htmlFor('speed') };
}
return { type: 'fallback', html: htmlFor('fallback') };
}
/* ===== Render (once) ===== */
if (document.querySelector('.pdp-msg-floater')) return; // avoid duplicates
const msg = computeMessage();
const line = document.createElement('div');
line.className = 'pdp-msg-floater';
line.innerHTML = msg.html;
// Primary target: AFTER .price-box .point-area (inside .price-box)
const pointArea =
document.querySelector('.product-price-info .price-box .point-area') ||
document.querySelector('.price-box .point-area');
if (pointArea?.parentNode){
pointArea.insertAdjacentElement('afterend', line);
} else {
// Fallbacks: append in .price-box, else above ATC
const priceBox = document.querySelector('.product-price-info .price-box') ||
document.querySelector('.price-box');
if (priceBox){
priceBox.appendChild(line);
} else {
const atc = document.querySelector('.ec-productRole__btn, .ec-productAction__btn, .product-cart');
if (atc?.parentNode) atc.parentNode.insertBefore(line, atc);
else return; // no anchor found
}
}
/* ===== Main tracking (kept) ===== */
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'pdp_msg_view',
experiment_name: EXP_NAME,
campaign_id: CAMPAIGN_ID,
variant: variant,
position: 'pdp',
pdp_msg_type: msg.type
});
localStorage.setItem('pdp_msg_type', msg.type);
/* ===== Click tracking (kept; only if link exists in pill) ===== */
line.addEventListener('click', function(e){
const a = e.target.closest('a');
if (!a) return;
window.dataLayer.push({
event: 'pdp_msg_click',
experiment_name: EXP_NAME,
campaign_id: CAMPAIGN_ID,
variant: variant,
position: 'pdp',
pdp_msg_type: msg.type,
href: a.href || null
});
});
}catch(e){
console && console.warn && console.warn('pdp_value_msg_v1 error', e);
}
});
})();
</script>