Акция 1+1=3 для магазинов на Тильде

Сделал удобный скрипт для этой акции в интернет-магазинах на Тильде. Пользуйтесь!

Подготовил модификацию в двух вариантах:
1) акция для всех товаров в каталоге;
2) акция только для выбранных товаров
  • Применяется к товару с наименьшей стомостью!

  • В обеих модификациях предусмотрен симпатичный баннер с уведомлением о акции:
1) Скрипт для всех товаров в каталоге
Он хорошо подходит для распродажи, короткой сезонной акции или магазина с небольшим каталогом, где нет необходимости исключать отдельные позиции.

В начале каждого скрипта есть блок настроек. В нём можно изменить:
1. Цену третьего товара после применения акции. Вместо 1 можно поставить, например, 100, если подарок должен быть не бесплатным, а продаваться за символическую сумму.
2. Текст баннера в корзине
Копировать код
<!--
  Скрипт от Арсена: акция 1+1=3 для ВСЕХ товаров каталога.
  Вставьте код целиком в Footer или в блок T123 перед </body>.
-->
<style>
  .promo113-notice {
    box-sizing: border-box;
    width: 100%;
    margin: 15px 0;
    padding: 14px 16px;
    color: #1f5132;
    background: #eef8f1;
    border: 1px solid #b9ddc4;
    border-radius: 6px;
    font-size: 14px;
    line-height: 1.45;
  }
  .promo113-notice__title {
    display: block;
    margin-bottom: 4px;
    font-size: 15px;
    font-weight: 700;
  }
  .promo113-notice__line {
    display: block;
  }
</style>

<script>
(function () {
  'use strict';

  if (window.__promo113V2Loaded) {
    console.warn('[PROMO113] Скрипт уже подключён. Удалите его дубликат.');
    return;
  }
  window.__promo113V2Loaded = true;

  /* ==================== НАСТРОЙКИ ==================== */

  // В ЭТОЙ ВЕРСИИ УЧАСТВУЮТ ВСЕ ТОВАРЫ В КОРЗИНЕ.

  // 1. ЦЕНА третьего товара после применения акции.
  const PROMO_PRODUCT_PRICE = 1;
  const CURRENCY_LABEL = '₽';
  const MONEY_LOCALE = 'ru-RU';

  // 2. ТЕКСТ БАННЕРА В КОРЗИНЕ.
  //    {product}, {price}, {discount} и {currency} скрипт подставит сам.
  const BANNER_TITLE = 'Акция 1+1=3 применена';
  const BANNER_LINE = 'Третий товар: {product} за {price} {currency}';
  const BANNER_DISCOUNT_LINE = 'Ваша скидка: −{discount} {currency}';
  const BANNER_FOOTER = 'Акция действует один раз на заказ. Промокоды не суммируются.';

  // 3. true - скрывать и сбрасывать промокод, пока акция активна.
  const BLOCK_PROMOCODES = true;

  /* ==================================================== */

  const SCRIPT_VERSION = '2.0.0';
  const CART_ROOT_SELECTOR = '.t706';
  const CART_CONTROL_SELECTOR = [
    '.t706__product-plus',
    '.t706__product-minus',
    '.t706__product-del',
    '.t706__product-deleted__timer__return',
    '.t706__product-quantity-inp'
  ].join(',');

  let promoActive = false;
  let promoDiscount = 0;
  let promoItem = null;
  let running = false;
  let saveHookInstalled = false;
  let originalSaveCart = null;
  let cartObserver = null;
  let observerFrame = 0;
  let bootAttempt = 0;

  function parseNumber(value) {
    if (typeof value === 'number') return Number.isFinite(value) ? value : 0;

    let normalized = String(value == null ? '' : value)
      .replace(/[\s\u00a0]/g, '')
      .replace(/[^\d,.-]/g, '');

    const comma = normalized.lastIndexOf(',');
    const dot = normalized.lastIndexOf('.');

    if (comma !== -1 && dot !== -1) {
      const decimalSeparator = comma > dot ? ',' : '.';
      const thousandsSeparator = decimalSeparator === ',' ? /\./g : /,/g;
      normalized = normalized.replace(thousandsSeparator, '');
      if (decimalSeparator === ',') normalized = normalized.replace(',', '.');
    } else if (comma !== -1) {
      normalized = /,\d{1,2}$/.test(normalized)
        ? normalized.replace(',', '.')
        : normalized.replace(/,/g, '');
    } else if (dot !== -1 && !/\.\d{1,2}$/.test(normalized)) {
      normalized = normalized.replace(/\./g, '');
    }

    const number = Number(normalized);
    return Number.isFinite(number) ? number : 0;
  }

  function roundMoney(value) {
    return Math.round((parseNumber(value) + Number.EPSILON) * 100) / 100;
  }

  function getQuantity(product) {
    return Math.max(0, parseNumber(product.quantity || 0));
  }

  function getUnitPrice(product) {
    return Math.max(0, parseNumber(product.price || 0));
  }

  function isActiveProduct(product) {
    return product && product.deleted !== 'yes' && getQuantity(product) > 0;
  }

  function getProducts() {
    return window.tcart && Array.isArray(window.tcart.products)
      ? window.tcart.products
      : [];
  }

  function migrateOldVersion(product) {
    const oldOriginalPrice = parseNumber(product._promo113OriginalPrice);

    if (oldOriginalPrice > 0) {
      product.price = oldOriginalPrice;
    }

    delete product._promo113OriginalPrice;
    delete product._promo113OriginalAmount;
  }

  function resetProductAmounts(products) {
    products.forEach(function (product) {
      if (!product) return;
      migrateOldVersion(product);
      if (!isActiveProduct(product)) return;
      product.amount = roundMoney(getUnitPrice(product) * getQuantity(product));
    });
  }

  function collectPromoItems(products) {
    const items = [];

    products.forEach(function (product, productIndex) {
      if (!isActiveProduct(product)) return;

      const quantity = Math.floor(getQuantity(product));
      const price = getUnitPrice(product);
      if (price <= PROMO_PRODUCT_PRICE) return;

      for (let i = 0; i < quantity; i++) {
        items.push({
          productIndex: productIndex,
          name: String(product.name || 'Товар'),
          price: price
        });
      }
    });

    return items;
  }

  function getPromoItem(items) {
    if (items.length < 3) return null;

    // Акция срабатывает только ОДИН раз: выбираем самый дешёвый товар.
    return items.slice().sort(function (a, b) {
      return a.price - b.price;
    })[0];
  }

  function applyDiscountToAmount(products, item) {
    if (!item || !products[item.productIndex]) return 0;

    const product = products[item.productIndex];
    const discount = roundMoney(Math.max(0, item.price - PROMO_PRODUCT_PRICE));
    const baseAmount = roundMoney(getUnitPrice(product) * getQuantity(product));

    // ВАЖНО: меняется только сумма строки. Цена единицы всегда остаётся исходной.
    product.amount = roundMoney(Math.max(PROMO_PRODUCT_PRICE, baseAmount - discount));
    return discount;
  }

  function getPromocodeInputs() {
    return Array.from(document.querySelectorAll([
      'input[name="promocode"]',
      'input[name="promo"]',
      'input[name*="promocode" i]',
      '.t-inputpromocode',
      '.t706__cartwin-promocode-input'
    ].join(',')));
  }

  function clearAppliedPromocode() {
    if (!window.tcart) return;

    delete window.tcart.promocode;
    delete window.tcart.prodamount_discountsum;
    delete window.tcart.prodamount_withdiscount;

    if (window.cartCalculator && 'appliedPromocode' in window.cartCalculator) {
      window.cartCalculator.appliedPromocode = null;
    }
  }

  function setPromocodeState(blocked) {
    if (!BLOCK_PROMOCODES) return;
    if (blocked) clearAppliedPromocode();

    getPromocodeInputs().forEach(function (input) {
      const wrapper = input.closest([
        '.t-inputpromocode__wrapper',
        '.t-input-group',
        '.t-form__promocode',
        '.t706__cartwin-promocode',
        '.t706__cartwin-promocode-wrapper'
      ].join(','));

      if (blocked && input.value) input.value = '';
      if (input.disabled !== blocked) input.disabled = blocked;

      if (wrapper) {
        const display = blocked ? 'none' : '';
        if (wrapper.style.display !== display) wrapper.style.display = display;
      }
    });
  }

  function formatMoney(value) {
    return new Intl.NumberFormat(MONEY_LOCALE, {
      maximumFractionDigits: 2
    }).format(roundMoney(value));
  }

  function interpolate(template) {
    return String(template)
      .replace(/\{product\}/g, promoItem ? promoItem.name : '')
      .replace(/\{price\}/g, formatMoney(PROMO_PRODUCT_PRICE))
      .replace(/\{discount\}/g, formatMoney(promoDiscount))
      .replace(/\{currency\}/g, CURRENCY_LABEL);
  }

  function appendNoticeLine(notice, text, className) {
    const line = document.createElement('span');
    line.className = className || 'promo113-notice__line';
    line.textContent = text;
    notice.appendChild(line);
  }

  function renderNotice() {
    const cartRoot = document.querySelector(CART_ROOT_SELECTOR);
    if (!cartRoot) return;

    let notice = cartRoot.querySelector('.promo113-notice');

    if (!promoActive || !promoItem || promoDiscount <= 0) {
      if (notice) notice.remove();
      return;
    }

    const anchor = cartRoot.querySelector([
      '.t706__cartwin-prodamount-wrap',
      '.t706__cartwin-totalamount-wrap',
      '.t706__orderform'
    ].join(','));
    const parent = anchor && anchor.parentNode
      ? anchor.parentNode
      : cartRoot.querySelector('.t706__cartwin-bottom, .t706__cartwin-content, .t706__cartwin');

    if (!parent) return;

    if (!notice) {
      notice = document.createElement('div');
      notice.className = 'promo113-notice';
      notice.setAttribute('role', 'status');
      notice.setAttribute('aria-live', 'polite');
    }

    notice.textContent = '';
    appendNoticeLine(notice, BANNER_TITLE, 'promo113-notice__title');
    appendNoticeLine(notice, interpolate(BANNER_LINE));
    appendNoticeLine(notice, interpolate(BANNER_DISCOUNT_LINE));
    appendNoticeLine(notice, interpolate(BANNER_FOOTER));

    if (!notice.parentNode) {
      if (anchor && anchor.parentNode) anchor.parentNode.insertBefore(notice, anchor);
      else parent.appendChild(notice);
    }
  }

  function callTilda(name) {
    if (typeof window[name] === 'function') {
      window[name]();
      return true;
    }
    return false;
  }

  function saveCart() {
    if (typeof originalSaveCart === 'function') {
      originalSaveCart.call(window);
      return;
    }
    callTilda('tcart__saveLocalObj');
  }

  function refreshCart(options) {
    const settings = options || {};

    callTilda('tcart__updateTotalProductsinCartObj');
    if (settings.redrawProducts !== false) callTilda('tcart__reDrawProducts');
    callTilda('tcart__reDrawTotal');
    if (settings.save !== false) saveCart();
  }

  function applyPromo(options) {
    if (running) return;

    const settings = options || {};
    const products = getProducts();

    running = true;

    try {
      resetProductAmounts(products);

      const item = getPromoItem(collectPromoItems(products));
      promoItem = item;
      promoDiscount = item ? applyDiscountToAmount(products, item) : 0;
      promoActive = Boolean(item && promoDiscount > 0);

      setPromocodeState(promoActive);
      refreshCart(settings);
    } finally {
      running = false;
    }

    if (settings.render !== false) renderNotice();
  }

  function installSaveHook() {
    const currentSave = window.tcart__saveLocalObj;

    if (typeof currentSave !== 'function') return false;
    if (currentSave.__promo113Wrapped) {
      saveHookInstalled = true;
      originalSaveCart = currentSave.__promo113Original;
      return true;
    }

    originalSaveCart = currentSave;

    function wrappedSaveCart() {
      if (!running) {
        applyPromo({
          redrawProducts: true,
          save: false,
          render: true
        });
      }
      return originalSaveCart.apply(this, arguments);
    }

    wrappedSaveCart.__promo113Wrapped = true;
    wrappedSaveCart.__promo113Original = originalSaveCart;
    window.tcart__saveLocalObj = wrappedSaveCart;
    saveHookInstalled = true;
    return true;
  }

  function installCartObserver() {
    if (cartObserver) return true;

    const cartRoot = document.querySelector(CART_ROOT_SELECTOR);
    if (!cartRoot) return false;

    cartObserver = new MutationObserver(function () {
      if (observerFrame) return;

      observerFrame = window.requestAnimationFrame(function () {
        observerFrame = 0;
        setPromocodeState(promoActive);

        const noticeExists = Boolean(cartRoot.querySelector('.promo113-notice'));
        if ((promoActive && !noticeExists) || (!promoActive && noticeExists)) {
          renderNotice();
        }
      });
    });

    cartObserver.observe(cartRoot, { childList: true, subtree: true });
    return true;
  }

  function handleCartControl(event) {
    if (!event.target || !event.target.closest(CART_CONTROL_SELECTOR)) return;

    // Основной путь - перехват сохранения Тильды. Это резервный синхронный путь.
    if (!saveHookInstalled) {
      applyPromo({
        redrawProducts: true,
        save: true,
        render: true
      });
    }
  }

  function handleSubmit(event) {
    const form = event.target;
    if (!form || !form.closest || !form.closest(CART_ROOT_SELECTOR)) return;

    // Последняя синхронная проверка непосредственно перед отправкой заказа.
    applyPromo({
      redrawProducts: false,
      save: true,
      render: false
    });
  }

  function handleCartOpened() {
    applyPromo({
      redrawProducts: true,
      save: true,
      render: true
    });
  }

  function boot() {
    bootAttempt += 1;

    const hookReady = installSaveHook();
    const observerReady = installCartObserver();
    const cartReady = Boolean(window.tcart && Array.isArray(window.tcart.products));

    if (cartReady) {
      applyPromo({
        redrawProducts: true,
        save: true,
        render: true
      });
    }

    if ((!hookReady || !observerReady || !cartReady) && bootAttempt < 40) {
      window.setTimeout(boot, 250);
      return;
    }

    if (!hookReady) {
      console.error('[PROMO113] Не найдена tcart__saveLocalObj. Работает резервный обработчик элементов корзины.');
    }
    if (!cartReady) {
      console.error('[PROMO113] Корзина Тильды не инициализирована.');
    }
  }

  document.addEventListener('click', handleCartControl);
  document.addEventListener('focusout', handleCartControl);
  document.addEventListener('change', handleCartControl);
  document.addEventListener('submit', handleSubmit, true);
  document.addEventListener('fullscreenCartOpened', handleCartOpened);

  document.addEventListener('DOMContentLoaded', boot);
  if (document.readyState !== 'loading') boot();

  console.log('[PROMO113] Версия ' + SCRIPT_VERSION + ' загружена.');
})();
</script>
2) Скрипт только для выбранных товаров
Этот вариант подходит, если в акции участвуют не все товары магазина, а конкретные позиции или отдельная категория.

В начале кода нужно заменить тестовые значения на реальные ID товаров из Tilda. Так можно ограничить акцию, например, только средствами одного бренда, конкретными наборами или сезонной подборкой.
Копировать код
<!--
  Скрипт от Арсена: акция 1+1=3 по ID товаров.
  Вставьте код целиком в Footer или в блок T123 перед </body>.
-->
<style>
  .promo113-notice {
    box-sizing: border-box;
    width: 100%;
    margin: 15px 0;
    padding: 14px 16px;
    color: #1f5132;
    background: #eef8f1;
    border: 1px solid #b9ddc4;
    border-radius: 6px;
    font-size: 14px;
    line-height: 1.45;
  }
  .promo113-notice__title {
    display: block;
    margin-bottom: 4px;
    font-size: 15px;
    font-weight: 700;
  }
  .promo113-notice__line {
    display: block;
  }
</style>

<script>
(function () {
  'use strict';

  if (window.__promo113V2Loaded) {
    console.warn('[PROMO113] Скрипт уже подключён. Удалите его дубликат.');
    return;
  }
  window.__promo113V2Loaded = true;

  /* ==================== НАСТРОЙКИ ==================== */

  // 1. ВПИШИТЕ ID ТОВАРОВ, УЧАСТВУЮЩИХ В АКЦИИ.
  //    ID берётся из window.tcart.products: uid, lid или gen_uid.
  const PROMO_PRODUCT_IDS = [
    'ID-ПЕРВОГО-ТОВАРА',
    'ID-ВТОРОГО-ТОВАРА',
    'ID-ТРЕТЬЕГО-ТОВАРА'
  ];

  // 2. ЦЕНА третьего товара после применения акции.
  const PROMO_PRODUCT_PRICE = 1;
  const CURRENCY_LABEL = '₽';
  const MONEY_LOCALE = 'ru-RU';

  // 3. ТЕКСТ БАННЕРА В КОРЗИНЕ.
  //    {product}, {price}, {discount} и {currency} скрипт подставит сам.
  const BANNER_TITLE = 'Акция 1+1=3 применена';
  const BANNER_LINE = 'Третий товар: {product} за {price} {currency}';
  const BANNER_DISCOUNT_LINE = 'Ваша скидка: −{discount} {currency}';
  const BANNER_FOOTER = 'Акция действует один раз на заказ. Промокоды не суммируются.';

  // 4. true - скрывать и сбрасывать промокод, пока акция активна.
  const BLOCK_PROMOCODES = true;

  /* ==================================================== */

  const SCRIPT_VERSION = '2.0.0';
  const promoProductIds = new Set(PROMO_PRODUCT_IDS.map(String));
  const CART_ROOT_SELECTOR = '.t706';
  const CART_CONTROL_SELECTOR = [
    '.t706__product-plus',
    '.t706__product-minus',
    '.t706__product-del',
    '.t706__product-deleted__timer__return',
    '.t706__product-quantity-inp'
  ].join(',');

  let promoActive = false;
  let promoDiscount = 0;
  let promoItem = null;
  let running = false;
  let saveHookInstalled = false;
  let originalSaveCart = null;
  let cartObserver = null;
  let observerFrame = 0;
  let bootAttempt = 0;

  function parseNumber(value) {
    if (typeof value === 'number') return Number.isFinite(value) ? value : 0;

    let normalized = String(value == null ? '' : value)
      .replace(/[\s\u00a0]/g, '')
      .replace(/[^\d,.-]/g, '');

    const comma = normalized.lastIndexOf(',');
    const dot = normalized.lastIndexOf('.');

    if (comma !== -1 && dot !== -1) {
      const decimalSeparator = comma > dot ? ',' : '.';
      const thousandsSeparator = decimalSeparator === ',' ? /\./g : /,/g;
      normalized = normalized.replace(thousandsSeparator, '');
      if (decimalSeparator === ',') normalized = normalized.replace(',', '.');
    } else if (comma !== -1) {
      normalized = /,\d{1,2}$/.test(normalized)
        ? normalized.replace(',', '.')
        : normalized.replace(/,/g, '');
    } else if (dot !== -1 && !/\.\d{1,2}$/.test(normalized)) {
      normalized = normalized.replace(/\./g, '');
    }

    const number = Number(normalized);
    return Number.isFinite(number) ? number : 0;
  }

  function roundMoney(value) {
    return Math.round((parseNumber(value) + Number.EPSILON) * 100) / 100;
  }

  function getQuantity(product) {
    return Math.max(0, parseNumber(product.quantity || 0));
  }

  function getUnitPrice(product) {
    return Math.max(0, parseNumber(product.price || 0));
  }

  function isActiveProduct(product) {
    return product && product.deleted !== 'yes' && getQuantity(product) > 0;
  }

  function getProducts() {
    return window.tcart && Array.isArray(window.tcart.products)
      ? window.tcart.products
      : [];
  }

  function getProductIds(product) {
    return [
      product && product.uid,
      product && product.lid,
      product && product.gen_uid
    ].filter(Boolean).map(String);
  }

  function isPromoProduct(product) {
    return getProductIds(product).some(function (id) {
      return promoProductIds.has(id);
    });
  }

  function migrateOldVersion(product) {
    const oldOriginalPrice = parseNumber(product._promo113OriginalPrice);

    if (oldOriginalPrice > 0) {
      product.price = oldOriginalPrice;
    }

    delete product._promo113OriginalPrice;
    delete product._promo113OriginalAmount;
  }

  function resetProductAmounts(products) {
    products.forEach(function (product) {
      if (!product) return;
      migrateOldVersion(product);
      if (!isActiveProduct(product)) return;
      product.amount = roundMoney(getUnitPrice(product) * getQuantity(product));
    });
  }

  function collectPromoItems(products) {
    const items = [];

    products.forEach(function (product, productIndex) {
      if (!isActiveProduct(product) || !isPromoProduct(product)) return;

      const quantity = Math.floor(getQuantity(product));
      const price = getUnitPrice(product);
      if (price <= PROMO_PRODUCT_PRICE) return;

      for (let i = 0; i < quantity; i++) {
        items.push({
          productIndex: productIndex,
          name: String(product.name || 'Товар'),
          price: price
        });
      }
    });

    return items;
  }

  function getPromoItem(items) {
    if (items.length < 3) return null;

    // Акция срабатывает только ОДИН раз: выбираем самый дешёвый товар.
    return items.slice().sort(function (a, b) {
      return a.price - b.price;
    })[0];
  }

  function applyDiscountToAmount(products, item) {
    if (!item || !products[item.productIndex]) return 0;

    const product = products[item.productIndex];
    const discount = roundMoney(Math.max(0, item.price - PROMO_PRODUCT_PRICE));
    const baseAmount = roundMoney(getUnitPrice(product) * getQuantity(product));

    // ВАЖНО: меняется только сумма строки. Цена единицы всегда остаётся исходной.
    product.amount = roundMoney(Math.max(PROMO_PRODUCT_PRICE, baseAmount - discount));
    return discount;
  }

  function getPromocodeInputs() {
    return Array.from(document.querySelectorAll([
      'input[name="promocode"]',
      'input[name="promo"]',
      'input[name*="promocode" i]',
      '.t-inputpromocode',
      '.t706__cartwin-promocode-input'
    ].join(',')));
  }

  function clearAppliedPromocode() {
    if (!window.tcart) return;

    delete window.tcart.promocode;
    delete window.tcart.prodamount_discountsum;
    delete window.tcart.prodamount_withdiscount;

    if (window.cartCalculator && 'appliedPromocode' in window.cartCalculator) {
      window.cartCalculator.appliedPromocode = null;
    }
  }

  function setPromocodeState(blocked) {
    if (!BLOCK_PROMOCODES) return;
    if (blocked) clearAppliedPromocode();

    getPromocodeInputs().forEach(function (input) {
      const wrapper = input.closest([
        '.t-inputpromocode__wrapper',
        '.t-input-group',
        '.t-form__promocode',
        '.t706__cartwin-promocode',
        '.t706__cartwin-promocode-wrapper'
      ].join(','));

      if (blocked && input.value) input.value = '';
      if (input.disabled !== blocked) input.disabled = blocked;

      if (wrapper) {
        const display = blocked ? 'none' : '';
        if (wrapper.style.display !== display) wrapper.style.display = display;
      }
    });
  }

  function formatMoney(value) {
    return new Intl.NumberFormat(MONEY_LOCALE, {
      maximumFractionDigits: 2
    }).format(roundMoney(value));
  }

  function interpolate(template) {
    return String(template)
      .replace(/\{product\}/g, promoItem ? promoItem.name : '')
      .replace(/\{price\}/g, formatMoney(PROMO_PRODUCT_PRICE))
      .replace(/\{discount\}/g, formatMoney(promoDiscount))
      .replace(/\{currency\}/g, CURRENCY_LABEL);
  }

  function appendNoticeLine(notice, text, className) {
    const line = document.createElement('span');
    line.className = className || 'promo113-notice__line';
    line.textContent = text;
    notice.appendChild(line);
  }

  function renderNotice() {
    const cartRoot = document.querySelector(CART_ROOT_SELECTOR);
    if (!cartRoot) return;

    let notice = cartRoot.querySelector('.promo113-notice');

    if (!promoActive || !promoItem || promoDiscount <= 0) {
      if (notice) notice.remove();
      return;
    }

    const anchor = cartRoot.querySelector([
      '.t706__cartwin-prodamount-wrap',
      '.t706__cartwin-totalamount-wrap',
      '.t706__orderform'
    ].join(','));
    const parent = anchor && anchor.parentNode
      ? anchor.parentNode
      : cartRoot.querySelector('.t706__cartwin-bottom, .t706__cartwin-content, .t706__cartwin');

    if (!parent) return;

    if (!notice) {
      notice = document.createElement('div');
      notice.className = 'promo113-notice';
      notice.setAttribute('role', 'status');
      notice.setAttribute('aria-live', 'polite');
    }

    notice.textContent = '';
    appendNoticeLine(notice, BANNER_TITLE, 'promo113-notice__title');
    appendNoticeLine(notice, interpolate(BANNER_LINE));
    appendNoticeLine(notice, interpolate(BANNER_DISCOUNT_LINE));
    appendNoticeLine(notice, interpolate(BANNER_FOOTER));

    if (!notice.parentNode) {
      if (anchor && anchor.parentNode) anchor.parentNode.insertBefore(notice, anchor);
      else parent.appendChild(notice);
    }
  }

  function callTilda(name) {
    if (typeof window[name] === 'function') {
      window[name]();
      return true;
    }
    return false;
  }

  function saveCart() {
    if (typeof originalSaveCart === 'function') {
      originalSaveCart.call(window);
      return;
    }
    callTilda('tcart__saveLocalObj');
  }

  function refreshCart(options) {
    const settings = options || {};

    callTilda('tcart__updateTotalProductsinCartObj');
    if (settings.redrawProducts !== false) callTilda('tcart__reDrawProducts');
    callTilda('tcart__reDrawTotal');
    if (settings.save !== false) saveCart();
  }

  function applyPromo(options) {
    if (running) return;

    const settings = options || {};
    const products = getProducts();

    running = true;

    try {
      resetProductAmounts(products);

      const item = getPromoItem(collectPromoItems(products));
      promoItem = item;
      promoDiscount = item ? applyDiscountToAmount(products, item) : 0;
      promoActive = Boolean(item && promoDiscount > 0);

      setPromocodeState(promoActive);
      refreshCart(settings);
    } finally {
      running = false;
    }

    if (settings.render !== false) renderNotice();
  }

  function installSaveHook() {
    const currentSave = window.tcart__saveLocalObj;

    if (typeof currentSave !== 'function') return false;
    if (currentSave.__promo113Wrapped) {
      saveHookInstalled = true;
      originalSaveCart = currentSave.__promo113Original;
      return true;
    }

    originalSaveCart = currentSave;

    function wrappedSaveCart() {
      if (!running) {
        applyPromo({
          redrawProducts: true,
          save: false,
          render: true
        });
      }
      return originalSaveCart.apply(this, arguments);
    }

    wrappedSaveCart.__promo113Wrapped = true;
    wrappedSaveCart.__promo113Original = originalSaveCart;
    window.tcart__saveLocalObj = wrappedSaveCart;
    saveHookInstalled = true;
    return true;
  }

  function installCartObserver() {
    if (cartObserver) return true;

    const cartRoot = document.querySelector(CART_ROOT_SELECTOR);
    if (!cartRoot) return false;

    cartObserver = new MutationObserver(function () {
      if (observerFrame) return;

      observerFrame = window.requestAnimationFrame(function () {
        observerFrame = 0;
        setPromocodeState(promoActive);

        const noticeExists = Boolean(cartRoot.querySelector('.promo113-notice'));
        if ((promoActive && !noticeExists) || (!promoActive && noticeExists)) {
          renderNotice();
        }
      });
    });

    cartObserver.observe(cartRoot, { childList: true, subtree: true });
    return true;
  }

  function handleCartControl(event) {
    if (!event.target || !event.target.closest(CART_CONTROL_SELECTOR)) return;

    // Основной путь - перехват сохранения Тильды. Это резервный синхронный путь.
    if (!saveHookInstalled) {
      applyPromo({
        redrawProducts: true,
        save: true,
        render: true
      });
    }
  }

  function handleSubmit(event) {
    const form = event.target;
    if (!form || !form.closest || !form.closest(CART_ROOT_SELECTOR)) return;

    // Последняя синхронная проверка непосредственно перед отправкой заказа.
    applyPromo({
      redrawProducts: false,
      save: true,
      render: false
    });
  }

  function handleCartOpened() {
    applyPromo({
      redrawProducts: true,
      save: true,
      render: true
    });
  }

  function boot() {
    bootAttempt += 1;

    const hookReady = installSaveHook();
    const observerReady = installCartObserver();
    const cartReady = Boolean(window.tcart && Array.isArray(window.tcart.products));

    if (cartReady) {
      applyPromo({
        redrawProducts: true,
        save: true,
        render: true
      });
    }

    if ((!hookReady || !observerReady || !cartReady) && bootAttempt < 40) {
      window.setTimeout(boot, 250);
      return;
    }

    if (!hookReady) {
      console.error('[PROMO113] Не найдена tcart__saveLocalObj. Работает резервный обработчик элементов корзины.');
    }
    if (!cartReady) {
      console.error('[PROMO113] Корзина Тильды не инициализирована.');
    }
  }

  document.addEventListener('click', handleCartControl);
  document.addEventListener('focusout', handleCartControl);
  document.addEventListener('change', handleCartControl);
  document.addEventListener('submit', handleSubmit, true);
  document.addEventListener('fullscreenCartOpened', handleCartOpened);

  document.addEventListener('DOMContentLoaded', boot);
  if (document.readyState !== 'loading') boot();

  console.log('[PROMO113] Версия ' + SCRIPT_VERSION + ' загружена.');
})();
</script>
Внимание!!!

Я, конечно, протестировал, и вроде все работает хорошо. Но, если у вас возникнет какая-то проблема с этим кодом – напишите мне, попробую помочь решить