From 37baff71e2cef3039950b4da78454d7bb38fde1c Mon Sep 17 00:00:00 2001
From: ispdomnet <95652995+ispdomnet@users.noreply.github.com>
Date: Tue, 22 Sep 2026 09:43:16 +0300
Subject: [PATCH 01/11] Add Ukrainian translation
---
translations/uk-ua.yaml | 692 ++++++++++++++++++++++++++++++++++++++++
1 file changed, 692 insertions(+)
create mode 100644 translations/uk-ua.yaml
diff --git a/translations/uk-ua.yaml b/translations/uk-ua.yaml
new file mode 100644
index 00000000..38fe4995
--- /dev/null
+++ b/translations/uk-ua.yaml
@@ -0,0 +1,692 @@
+storefront:
+ extension-name: Storefront
+ common:
+ address: Адреса
+ alerts: Сповіщення
+ amount: Сума
+ api: API
+ back: Назад
+ backdrop: Банер
+ building: Будинок
+ bulk-action: Масова дія
+ cash: Готівка
+ city: Місто
+ config: Конфігурація
+ coordinates: Координати
+ country: Країна
+ created: Створено
+ currency: Валюта
+ customer: Клієнт
+ customers: Клієнти
+ category: Категорія
+ delivery-tip: Чайові кур'єру
+ delivery: Доставка
+ details: Деталі
+ description: Опис
+ driver: Водій
+ dropoff: Місце вивантаження
+ earning: Доходи
+ email: Електронна пошта
+ export: Експорт
+ id: ID
+ internal-id: Внутрішній ID
+ import: Імпорт
+ invalid: Недійсний
+ loading: Завантаження...
+ logo: Логотип
+ name: Назва
+ neighborhood: Район
+ online: В мережі
+ orders: Замовлення
+ phone: Телефон
+ photos: Фотографії
+ pickup: Місце забору
+ product: Товари
+ remove: Видалити
+ state: Область/Штат
+ status: Статус
+ store: Магазин
+ street-1: Вулиця 1
+ storefront: Вітрина
+ tag: Теги
+ tip: Чайові
+ title: Заголовок
+ total: Загалом
+ type: Тип
+ upload-new: Завантажити нове
+ uploading: Завантаження...
+ view: Перегляд
+ website: Веб-сайт
+ edit: Редагувати
+ delete: Видалити
+ network: Мережі
+ file: файли
+ general: Загальні
+ location: Локація
+ notification: Сповіщення
+ gateways: Шлюзи
+ settings: Налаштування
+ new: Новий
+ facebook: Facebook
+ instagram: Instagram
+ twitter: Twitter
+ food-trucks: Фуд-траки
+ catalog: Каталог
+ catalogs: Каталоги
+ sidebar:
+ storefront: Вітрина
+ dashboard: Панель керування
+ products: Товари
+ catalogs: Каталоги
+ food-trucks: Фуд-траки
+ customers: Клієнти
+ orders: Замовлення
+ networks: Мережі
+ promotions: Акції
+ settings: Налаштування
+ launch-app: Запустити додаток
+ component:
+ storefront-order-summary:
+ order-summary-title: Деталізація замовлення вітрини
+ modals:
+ add-store-hours:
+ start-time: Час початку
+ operation-start: Час початку доступності/роботи
+ end-time: Час завершення
+ operation-end: Час завершення доступності/роботи
+ assign-driver:
+ message-driver: Оберіть водія для призначення на це замовлення.
+ name-driver: Обрати водія
+ placeholder-driver: Оберіть водія для призначення
+ create-first-store:
+ title: Ласкаво просимо до Storefront!
+ message: Storefront дозволяє швидко налаштувати автономний та динамічний магазин на вимогу на базі Fleetbase.
+ submit-message: Заповніть форму нижче, щоб створити свій перший магазин.
+ storefront-name: Назва вітрини
+ enter-name: Введіть назву вашого магазину
+ description: Опис вітрини
+ brief-description: Надайте короткий опис товарів, які продаються у вашому магазині
+ currency: Валюта вітрини
+ select-currency: Оберіть основну валюту для вашої вітрини
+ create-getaway:
+ getaway: Тип шлюзу
+ getaway-text: Оберіть тип шлюзу
+ getaway-name: Назва шлюзу
+ name-text: Назвіть платіжний шлюз (використовується лише внутрішньо)
+ getaway-code: Код шлюзу
+ code-text: Ідентифікаційний код, який використовується для визначення шлюзу в SDK
+ callback-url: Callback URL
+ callback-text: URL зворотного виклику (якщо застосовно)
+ return-url: Return URL
+ return-text: URL повернення (якщо застосовно)
+ sandbox-getaway: Це тестовий шлюз (песочниця)
+ create-network-category:
+ parent-category: Батьківська категорія
+ select-parent: Оберіть батьківську категорію (якщо є)
+ category-name: Назва категорії
+ name-text: Введіть назву категорії
+ description: Опис категорії
+ description-text: Надайте короткий опис категорії мережі
+ category-icon: Тип іконки категорії
+ create-network:
+ title: Створити мережу
+ message-line1: Storefront дозволяє швидко створювати маркетплейси, об'єднуючи кілька магазинів під одним брендом.
+ message-line2: Під час створення мережі ви визначаєте власні правила та можете розширювати її за допомогою запрошень.
+ message-line3: Розпочніть зі створення мережі нижче.
+ name: Назва мережі
+ name-text: Введіть назву вашого магазину
+ description: Опис мережі
+ description-text: Надайте короткий опис товарів, які продаються
+ currency: Валюта мережі
+ currency-text: Оберіть основну валюту для вашої вітрини
+ create-new-variant:
+ name: Назва варіанта
+ name-text: Вкажіть назву варіанта товару
+ description: Опис варіанта
+ description-text: Короткий опис варіанта
+ checkbox-1: Дозволити користувачеві обирати кілька варіантів одночасно
+ checkbox-2: Вибір цього варіанта товару є обов'язковим
+ create-notification-channel:
+ title: Канал
+ title-text: Оберіть канал сповіщень
+ name: Назва каналу
+ name-text: Вкажіть назву каналу сповіщень (використовується лише внутрішньо)
+ key: Ключ каналу
+ key-text: Це ключ вашого каналу сповіщень, використовуйте його в налаштуваннях SDK, щоб вказати, які додатки мають його використовувати.
+ create-product-category:
+ name: Назва категорії
+ name-text: Введіть назву категорії
+ description: Опис категорії
+ description-text: Надайте короткий опис категорії товарів
+ icon: Іконка категорії
+ uploading-new: Завантажити нову іконку категорії
+ create-store:
+ name: Назва вітрини
+ name-text: Введіть назву вашого магазину
+ description: Опис вітрини
+ description-text: Надайте короткий опис товарів, які продаються у вашому магазині
+ currency: Валюта вітрини
+ currency-text: Оберіть основну валюту для вашої вітрини
+ import-products:
+ uploading-message: Обробка імпорту...
+ drop-upload: Перетягніть для завантаження
+ ready-upload: готово до завантаження.
+ upload-spreadsheets: Завантажити електронні таблиці
+ drag-drop: Перетягніть файли таблиць у цю область
+ select-spreadsheets: або оберіть таблиці для завантаження.
+ upload-queue: Черга завантаження
+ optionally-select-category: За бажанням оберіть категорію для імпорту товарів
+ select-category: Обрати категорію
+ incoming-order:
+ assigned: Водія призначено
+ not-assigned: Водія не призначено
+ no-phone: Немає телефону
+ change-driver: Змінити водія
+ assign-driver: Призначити водія
+ pickup-order: Клієнт повинен забрати замовлення з
+ address: Адреса клієнта
+ tracking: Відстеження
+ summary: Деталізація замовлення
+ subtotal: Проміжний підсумок
+ fee: Вартість доставки
+ tip: Чайові
+ delivery-tip: Чайові кур'єру
+ manage-addons:
+ title: Нова категорія
+ delete: Видалити категорію
+ new-addon: Нова надбудова
+ addon-name: Назва надбудови
+ description: Опис надбудови
+ delete-this-addon-category-assosiated-will-lost: Ви впевнені, що хочете видалити цю категорію надбудов? Усі пов'язані надбудови буде втрачено!
+ untitled-addon-category: Категорія без назви
+ order-ready-assign-driver:
+ driver-dispatch: Ви збираєтеся позначити замовлення як готове та відправити його. Скористайтеся випадаючим списком нижче, щоб обрати водія.
Або ви можете відправити замовлення у режимі adhoc, що надішле сповіщення усім найближчим водіям.
+ adhoc-dispatch-nearby-drivers: Adhoc-відправка найближчим водіям
+ select-driver: Обрати водія
+ select-driver-to-assign: Оберіть водія для призначення
+ share-network:
+ invite-member: Запросити учасників до вашої мережі
+ email-below: Скористайтеся посиланням для запрошення або надішліть індивідуальні запрошення на електронну пошту нижче.
+ invite-network: Введіть електронні адреси людей або організацій, яким ви хочете надіслати запрошення.
+ add-email: Додати email
+ get-link: Отримати посилання
+ enable-shareable-link: Увімкнути спільне посилання
+ anyone-with-this-link-able-to-join-your-network: Будь-хто з цим посиланням зможе приєднатися до вашої мережі
+ store-form:
+ storefront-name: Назва вітрини
+ the-name-of-your-store: Назва вашого магазину.
+ storefront-description-placeholder: Опис вітрини
+ give-your-store-brief-description: Надайте короткий опис магазину, щоб користувачі знали, що ви продаєте.
+ add-tags-placeholder: Додати теги
+ store-location-form:
+ street-2: Вулиця 2
+ security-access-code: Код доступу / Пароль
+ postal-code: Поштовий індекс
+ widget:
+ key-metrics:
+ title: Метрики вітрини
+ customers:
+ widget-title: Останні клієнти
+ phone: Телефон
+ email: Електронна пошта
+ orders:
+ widget-title: Останні замовлення
+ id-column: ID замовлення
+ cancel-order: Скасувати замовлення
+ cancel-orders: Скасувати замовлення
+ cancel-order-modal-title: Скасувати це замовлення?
+ cancel-order-modal-body: Це скасує замовлення у Storefront і зупинить усі подальші дії з його виконання.
+ cancel-order-success: "Замовлення {orderId} скасовано."
+ accept-order: Прийняти замовлення!
+ mark-as-ready: Позначити як готове!
+ dispatch: Відправити
+ mark-as-preparing: Позначити як готується
+ mark-as-completed: Позначити як завершено
+ more-details: Більше деталей
+ customer: Клієнт
+ driver: Водій
+ subtotal: Проміжний підсумок
+ delivery-fee: Вартість доставки
+ delivery-tip: Чайові кур'єру
+ tip: Чайові
+ total: Загалом
+ change-driver: Змінити водія
+ assign-driver: Призначити водія
+ customer-pickup: Самовивіз клієнтом
+ pickup-order: Забрати замовлення
+ mark-as-ready-modal-pickup-title: Позначити замовлення готовим до самовивозу?
+ mark-as-ready-modal-pickup-body: Позначення замовлення як готового сповістить клієнта про те, що його замовлення можна забирати!
+ mark-as-ready-modal-pickup-accept-button-text: Готово до самовивозу!
+ mark-as-ready-modal-not-adhoc-title: Призначити водія та відправити замовлення
+ mark-as-ready-modal-not-adhoc-accept-button-text: Призначити та відправити!
+ mark-as-ready-modal-title: Ви впевнені, що хочете позначити замовлення як готове?
+ mark-as-ready-modal-body: Позначення замовлення як готового відправить його найближчим водіям. Позначайте замовлення готовим лише тоді, коли його дійсно можна забрати.
+ mark-as-ready-modal-accept-button-text: Відправити!
+ mark-as-completed-modal-title: Ви впевнені, що хочете позначити замовлення як завершене?
+ mark-as-completed-modal-body: Позначення замовлення як завершеного є підтвердженням того, що клієнт отримав/забрав замовлення і воно виконане.
+ mark-as-completed-accept-button-text: Замовлення завершено!
+ mark-as-preparing-modal-title: Ви впевнені, що хочете позначити замовлення як таке, що готується?
+ mark-as-preparing-modal-body: Позначення замовлення як таке, що готується, є підтвердженням того, що замовлення обробляється.
+ mark-as-preparing-accept-button-text: Замовлення готується
+ assign-driver-modal-title: Призначити водія
+ assign-driver-modal-accept-button-text: Призначити водія
+ storefront-metrics:
+ last-day: Останні 30 днів
+ file-record:
+ delete-this-file: Ви впевнені, що хочете видалити цей файл?
+ delete-this-file-you-will-unable-recover: Після видалення цього файлу ви не зможете його відновити.
+ network-category-picker:
+ no-subcategory: Немає підкатегорій для
+ no-categories: Немає категорій для цієї мережі
+ create-category: Створити нову категорію
+ schedule-manager:
+ add-hours: Додати години
+ add-new-hours-for-day: Додати новий робочий час для {day}
+ new-hours-added-for-day: Додано новий робочий час для {day}
+ you-wish-to-remove-these-hour: Ви впевнені, що хочете видалити цей робочий час?
+ by-removing-these-operation: Видалення цього часу роботи/доступності може зробити ваш магазин або товар неактивним чи недоступним відповідно до оновленого розкладу...
+ store-selector:
+ no-stores: Немає магазинів для вибору
+ create-storefront: Створити нову
+ home:
+ title: Панель керування Storefront
+ customers:
+ index:
+ internal-id: Внутрішній ID
+ create-at: Створено
+ update-at: Оновлено
+ vendor-action: Дії продавця
+ view-customer-details: Переглянути деталі клієнта
+ edit-customer: Редагувати клієнта
+ success-message: Вашу нову мережу вітрин успішно створено!
+ delete-customer: Видалити клієнта
+ customer-panel:
+ details:
+ web-url: URL-адреса веб-сайту
+ edit-button: Редагувати клієнта
+ cancel-edit-button: Скасувати редагування клієнта
+ cancel-new-button: Скасувати створення клієнта
+ networks:
+ index:
+ network-option: Параметри мережі
+ manage-network: Керувати мережею
+ send-invite: Надіслати запрошення
+ delete-network: Видалити мережу
+ create-new-network: Створити нову мережу
+ success-message: Вашу нову мережу вітрин успішно створено!
+ delete-this-network-all-assigned-stores-customer-order: Ви впевнені, що хочете видалити цю мережу ({networkName})? Усі призначені магазини, клієнти та замовлення більше не будуть доступні.
+ network:
+ manage-network: Керувати мережею
+ settings: Налаштування
+ customers: Клієнти
+ orders: Замовлення
+ Network-changes-not-save: Зміни мережі не збережено!
+ going-back-will-rollback-all-unsaved-changes: Повернення назад скасує всі незбережені зміни, ви впевнені, що хочете продовжити?
+ index:
+ change-network-saved: Зміни в мережі збережено.
+ create-new-payment-gateway: Створити новий платіжний шлюз
+ save-gateway: Зберегти шлюз
+ new-gateway-add-network: Новий шлюз додано до мережі
+ payment-gateway-changes-success: Зміни платіжного шлюзу збережено!
+ create-new-notification-channel: Створити новий канал сповіщень
+ create-notification-channel: Створити канал сповіщень
+ notification-channel-add-network: Новий канал сповіщень додано до мережі!
+ notification-channel-changes-save: Зміни каналу сповіщень збережено!
+ general-network-settings-title: Загальні налаштування мережі
+ general-network-settings-subtitle: Вкажіть загальні налаштування для вашої мережі або маркетплейсу.
+ general-network-settings-form:
+ save-changes-button-text: Зберегти зміни
+ name: Назва
+ name-placeholder: Назва вітрини
+ name-help-text: Назва вашої мережі або маркетплейсу.
+ description: Опис
+ description-placeholder: Опис вітрини
+ description-help-text: Надайте короткий опис, щоб користувачі знали, чому присвячена ваша мережа.
+ currency: Валюта
+ contact-social-panel:
+ panel-title: Контакти та соцмережі
+ phone: Телефон
+ email: Електронна пошта
+ email-help-text: Робочий email.
+ website: Веб-сайт
+ website-help-text: URL-адреса вашого веб-сайту.
+ facebook: Facebook
+ facebook-help-text: ID сторінки вашого бізнесу у Facebook.
+ instagram: Instagram
+ instagram-help-text: Ім’я користувача вашого бізнесу в Instagram.
+ twitter: Twitter
+ twitter-help-text: Ім’я користувача вашого бізнесу в Twitter.
+ logo-backdrop-panel:
+ panel-title: Логотип та банер
+ logo-label: Логотип
+ logo-help-text: Логотип вашого магазину.
+ upload-logo-button-text: Завантажити новий логотип
+ backdrop-label: Банер
+ backdrop-help-text: Додатковий баннер або фонове зображення для вашого магазину.
+ upload-backdrop-button-text: Завантажити новий банер
+ alert-panel:
+ panel-title: Сповіщення
+ panel-description: Налаштуйте, хто повинен отримувати сповіщення про певні події
+ new-order-alert-label: Сповіщення про нове замовлення
+ new-order-alert-placeholder: Оберіть користувачів для сповіщення про нове замовлення
+ config-switches:
+ online: В мережі
+ enable-tax: Увімкнути податок
+ tax-percentage-input-label: Відсоток податку
+ tax-percentage-input-help-text: Відсоток податку з продажів, який застосовується до замовлень.
+ enable-minimum-order-amount: Увімкнути мінімальну суму замовлення
+ minimum-order-amount-input-label: Мінімальна сума замовлення
+ minimum-order-amount-input-help-text: Мінімальна сума замовлення, необхідна для оформлення.
+ auto-accept-incoming-orders: Автоматично приймати вхідні замовлення
+ auto-dispatch-orders: Автоматично відправляти замовлення
+ require-proof-of-delivery: Вимагати підтвердження доставки
+ proof-of-delivery-method-label: Метод підтвердження доставки
+ enable-cash-on-delivery: Увімкнути оплату при отриманні
+ enable-order-pickup: Увімкнути самовивіз
+ enable-tips: Увімкнути чайові
+ enable-delivery-tips: Увімкнути чайові кур'єру
+ enable-multi-cart-checkout: Увімкнути оформлення з кількох кошиків
+ enable-user-reviews: Увімкнути відгуки користувачів
+ api-panel:
+ panel-title: API
+ panel-header: Налаштування API
+ panel-description: Ключ доступу для Storefront API та інтеграцій
+ network-key-label: Ключ мережі
+ network-key-help-text: Це ключ вашої мережі, використовуйте його для підключення додатків та інтеграцій.
+ payment-gateways-panel:
+ panel-title: Налаштування шлюзів
+ panel-description: Додавайте або керуйте налаштуваннями платіжних шлюзів тут.
+ new-gateway-button-text: Створити новий шлюз
+ gateway-form:
+ name: Назва шлюзу
+ name-help-text: Назвіть платіжний шлюз (використовується лише внутрішньо)
+ code: Код шлюзу
+ code-help-text: Ідентифікаційний код, який використовується для визначення шлюзу в SDK
+ callback-url: Callback URL
+ callback-url-help-text: URL зворотного виклику (якщо застосовно)
+ return-url: Return URL
+ return-url-help-text: URL повернення (якщо застосовно)
+ sandbox-toggle-label: Це тестовий шлюз (песочниця)
+ config-header-label: Конфігурація
+ delete-gateway-button-text: Видалити платіжний шлюз
+ notification-channels-panel:
+ panel-title: Канали сповіщень
+ panel-header: Налаштування сповіщень
+ panel-description: Налаштуйте канали сповіщень для вашої вітрини. Кожен канал додатково може мати власні правила та параметри.
+ new-channel-button-text: Створити новий канал
+ stores:
+ store: Магазини
+ add-stores-button-text: Додати магазини
+ invite-stores-button-text: Запросити магазини
+ created-at: Створено
+ view-store-details: Переглянути деталі магазину
+ edit-store: Редагувати магазин
+ assign-category: Призначити категорію
+ remove-category: Видалити категорію
+ remove-store-from-network: Видалити магазин з мережі
+ delete-network-category: Видалити категорію мережі?
+ deleting-category-move-all-stores-inside-on-top-level: Видалення цієї категорії перемістить усі магазини всередині неї на верхній рівень, ви впевнені, що хочете видалити цю категорію?
+ add-store-to-category: Додати магазин до категорії
+ save-change: Зберегти зміни
+ add-new-network-category: Додати нову категорію мережі
+ create-new-category: Створити нову категорію
+ new-category-created: Нову категорію створено.
+ network-category-create: Створено нову категорію мережі.
+ edit-category: Редагувати категорію ({categoryName})
+ add-stores-to-network: Додати магазини до мережі
+ network-stores-update: Магазини мережі оновлено.
+ remove-this-store: Видалити цей магазин ({storeName}) з цієї мережі ({networkName})?
+ done: Готово
+ longer-findable-by-this-network: Ви впевнені, що хочете видалити цей магазин з мережі? Його більше не можна буде знайти через цю мережу.
+ viewing-storefront: Перегляд вітрини {storeName}
+ editing-storefront: Редагування вітрини {storeName}
+ send-invitations: Надіслати запрошення
+ invitation-sent-recipients: Запрошення надіслано отримувачам!
+ invalid-emails-provided-error: Вказано недійсні електронні адреси!
+ remove-store-category: Видалити категорію магазину
+ remove-store-category-body: Ви збираєтеся видалити {storeName} з категорії {categoryName}, ви бажаєте продовжити?
+ orders:
+ index:
+ internal-id: Внутрішній ID
+ customer: Клієнт
+ select-order-customer: Оберіть клієнта замовлення
+ select-order-pickup-location: Оберіть місце забору замовлення
+ select-order-dropoff-location: Оберіть місце доставки замовлення
+ scheduled-at: Заплановано на
+ transaction-total: Сума транзакції
+ total: Загалом
+ tracking-number: Номер відстеження
+ driver-assigned: Водія призначено
+ select-driver-for-order: Оберіть водія для замовлення
+ created-at: Створено
+ updated-at: Оновлено
+ created-by: Створив
+ updated-by: Оновив
+ select-user: Оберіть користувача
+ view-order: Переглянути замовлення
+ cancel-order: Скасувати замовлення
+ delete-order: Видалити замовлення
+ view:
+ route-panel-title: Маршрут
+ comments-title: Коментарі
+ notes-title: Примітки
+ dispatch: Відправити
+ unassign-driver: Скасувати призначення водія
+ assign-driver: Призначити водія
+ cancel-order: Скасувати замовлення
+ delete-order: Видалити замовлення
+ activity: Активність
+ unable-load-order-activity: Не вдалося завантажити активність замовлення
+ ad-hoc: Ad-Hoc
+ customer: Клієнт
+ facilitator: Посередник
+ driver-assigned: Водія призначено
+ change-driver: Змінити водія
+ tracking-number: Номер відстеження
+ date-scheduled: Запланована дата
+ date-dispatched: Дата відправки
+ date-started: Дата початку
+ order-details-title: Деталі замовлення
+ route: Маршрут
+ pickup: Забір
+ dropoff: Доставка
+ return: Повернення
+ order-summary: Деталізація замовлення
+ subtotal: Проміжний підсумок
+ delivery-fee: Вартість доставки
+ tip: Чайові
+ delivery-tip: Чайові кур'єру
+ total: Загалом
+ payload: Вміст / Вантаж
+ items-drop: Товари вивантажуються в
+ tracking: Відстеження
+ view-activity: Переглянути активність
+ order-metadata: Метадані замовлення
+ unassign-title: Ви впевнені, що хочете зняти водія ({driverName}) з цього замовлення?
+ unassign-body: Після зняття призначення водій більше не матиме доступу до деталей цього замовлення.
+ unassign-success: Водія знято з цього замовлення.
+ route-error: Оптимізація маршруту не вдалася, перевірте точки маршруту та спробуйте знову.
+ invalid-warning: Введено недійсний користувацький статус.
+ change-order: Змінити водія замовлення
+ assign-order: Призначити водія на замовлення
+ assign-success: Водія для {orderId} успішно оновлено.
+ order-title: Мітка замовлення
+ waypoint-title: Мітка путівної точки
+ proof-of-delivery: Підтвердження доставки
+ notes-placeholder: Введіть примітки до замовлення тут...
+ save-order-note: Зберегти примітку до замовлення
+ order-notes-updated: Примітки до замовлення оновлено.
+ products:
+ index:
+ manage-addons: Керування надбудовами
+ manage-addons-dialog:
+ manage-addons-title: Керування категоріями та опціями надбудов товарів
+ manage-addons-accept-button: Зберегти зміни
+ done: Готово
+ create-new-product-category: Створити нову категорію товарів
+ product-category-created-success: Нову категорію товарів створено.
+ import-products-via-spreadsheets: Импортувати товари через електронні таблиці
+ start-upload: Розпочати завантаження
+ warning-no-file-upload: Немає файлів у черзі для завантаження!
+ products-success-message: Успішно імпортовано {resultsLength} товарів...
+ delete-product: Ви впевнені, що хочете видалити цей товар?
+ body-warning: Попередження! Після видалення цього товару він більше не буде доступний.
+ aside-scroller:
+ title: Категорії товарів
+ custom-top-item-text: Усі товари
+ import-products-success-message: Успішно імпортовано {resultsLength} товарів...
+ edit:
+ title: Товар не збережено!
+ body: Ви втратите всі незбережені зміни, ви впевнені, що хочете скасувати?
+ changes-saved: Зміни збережено!
+ new:
+ new-product-created-success: Новий товар успішно створено!
+ warning-only-select-an-image-file-to-be-primary: Ви можете обрати лише файл зображення як основний!
+ made-the-primary-success-image: >-
+ {fileName} зроблено основним зображенням.
+ title: Товар не збережено!
+ body: Повернення назад скасує створення цього товару. Якщо ви продовжите, усі введені поля будуть очищені!
+ select-addon-categories: Обрати категорії надбудов
+ add-new-product-variant: Додати новий варіант товару
+ edit-product-variant: Редагувати варіант товару
+ untitled-field: Поле без назви
+ category:
+ category: категорія
+ title: Товар не збережено!
+ body: Попередження! Видалення цієї категорії також вилучить усі товари, розміщені в ній.
+ edit-category: Редагувати категорію
+ save-changes: Зберегти зміни
+ category-changes-saved-success: Зміни категорії збережено.
+ delete-product: Ви впевнені, що хочете видалити цей товар?
+ body-warning: Попередження! Після видалення цього товару він більше не буде доступний.
+ settings:
+ api:
+ api-settings: Налаштування API
+ access-key-for-storefront-api-integrations: Ключ доступу для Storefront API та інтеграцій.
+ store-key: Ключ магазину
+ store-key-help-text: Це ключ вашого магазину, використовуйте його для підключення додатків та інтеграцій для вашої вітрини.
+ gateways:
+ geteway-settings: Налаштування шлюзів
+ add-or-manage-your-paymeny-gateway-settings-here: Додавайте або керуйте налаштуваннями платіжних шлюзів тут.
+ create-new-gateway: Створити новий шлюз
+ gateway-name: Назва шлюзу
+ gateway-help-text: Назвіть платіжний шлюз (використовується лише внутрішньо)
+ gateway-code: Код шлюзу
+ gateway-code-help-text: Ідентифікаційний код, який використовується для визначення шлюзу в SDK
+ callback-url: Callback URL
+ callback-url-help-text: URL зворотного виклику (якщо застосовно)
+ return-url: Return URL
+ return-url-help-text: URL повернення (якщо застосовно)
+ this-is-a-sandbox-gateway: Це тестовий шлюз (песочниця)
+ delete-payment-gateway: Видалити платіжний шлюз
+ create-new-payment-gateway: Створити новий платіжний шлюз
+ save-gateway: Зберегти шлюз
+ edit-payment-gateway: Редагувати платіжний шлюз
+ save-changes: Зберегти зміни
+ title-remove: Ви впевнені, що хочете видалити цей шлюз?
+ application-website-utilizing-gateway: Робота всіх додатків та веб-сайтів, що використовують цей шлюз у конфігурації, може бути порушена, якщо шлюз не замінити заздалегідь
+ index:
+ general-settings-header: Загальні налаштування
+ general-settings-message: Вкажіть загальні налаштування для вашої вітрини.
+ name-placeholder: Назва вітрини
+ name-help-text: Назва вашого магазину.
+ description-placeholder: Опис вітрини
+ description-help-text: Надайте короткий опис магазину, щоб користувачі знали, що ви продаєте.
+ tags-placeholder: Додати теги
+ contact-social: Контакти та соцмережі
+ website-help-text: URL-адреса вашого веб-сайту.
+ facebook-placeholder: Facebook ID
+ facebook-help-text: ID сторінки вашого бізнесу у Facebook.
+ instagram-placeholder: Ім'я користувача Instagram
+ instagram-help-text: Ім’я користувача вашого бізнесу в Instagram.
+ twitter-placeholder: Ім'я користувача Twitter
+ twitter-help-text: Ім’я користувача вашого бізнесу в Twitter.
+ logo-backdrop: Логотип та банер
+ logo-help-text: Логотип вашого магазину.
+ upload-new-logo: Завантажити новий логотип
+ backdrop-help-text: Додатковий баннер або фонове зображення для вашого магазину.
+ upload-new-backdrop: Завантажити новий банер
+ images-videos: Зображення та відео
+ drop-to-upload: Перетягніть для завантаження
+ ready-to-upload: готово до завантаження.
+ upload-image-video: Завантажити зображення та відео
+ drag-and-drop-image: Перетягніть файли зображень та відео в цю область
+ upload-queue: Черга завантаження
+ alerted-for-certain-event: Налаштуйте, хто повинен отримувати сповіщення про певні події.
+ new-order-alert: Сповіщення про нове замовлення
+ alert-placeholder: Оберіть користувачів для сповіщення про нове замовлення
+ enable-tax: Увімкнути податок
+ tax-percentage: Відсоток податку
+ tax-percentage-help-text: Відсоток податку з продажів, який застосовується до замовлень.
+ enable-minimum-order-amount: Увімкнути мінімальну суму замовлення
+ minimum-order-amount: Мінімальна сума замовлення
+ minimum-order-amount-help-text: Мінімальна сума замовлення, необхідна для оформлення.
+ auto-accept-incoming-order: Автоматично приймати вхідні замовлення
+ auto-dispatch-order: Автоматично відправляти замовлення
+ require-proof-of-delivery: Вимагати підтвердження доставки
+ proof-of-delivery-method: Метод підтвердження доставки
+ enable-cash-on-delivery: Увімкнути оплату при отриманні
+ enable-order-pickup: Увімкнути самовивіз
+ enable-tips: Увімкнути чайові
+ enable-delivery-tips: Увімкнути чайові кур'єру
+ enable-integrated-vendors: Увімкнути інтегрованих продавців
+ save-changes: Зберегти зміни
+ locations:
+ new-store-location: Нова локація магазину
+ add-new-location: Додати нову локацію
+ edit-store-location: Редагувати локацію магазину
+ save-changes: Зберегти зміни
+ remove-this-store-location: Ви впевнені, що хочете видалити цю локацію магазину?
+ solely-accosciated-location: Усі товари, пов'язані виключно з цією локацією, більше не будуть доступні.
+ new-store-hour: Додати новий робочий час магазину для {day}
+ add-hours: Додати години
+ store-hour-added: Додано новий робочий час магазину для {day}
+ remove-these-hour: Ви впевнені, що хочете видалити цей робочий час?
+ update-schedule-location: Видалення цього часу роботи може зробити ваш магазин неактивним відповідно до оновленого розкладу для цієї локації..
+ location-setting: Налаштування локації
+ manage-store: Керуйте робочими локаціями та годинами роботи ваших магазинів
+ text-location: Створити нову локацію
+ store-hour: Керувати годинами роботи магазину
+ notification:
+ create-new-notification-channel: Створити новий канал сповіщень
+ create-notification-channel: Створити канал сповіщень
+ edit-notification-channel: Редагувати канал сповіщень
+ save-changes: Зберегти зміни
+ new-notification-channel-added: Додано новий канал сповіщень
+ remove-this-notification-channel: Ви впевнені, що хочете видалити цей канал сповіщень?
+ application-websites-utillizing-channel: Робота всіх додатків та веб-сайтів, що використовують цей канал у конфігурації, може бути порушена, якщо канал не замінити заздалегідь
+ notification-settings: Налаштування сповіщень
+ notification-storefront: Налаштуйте канали сповіщень для вашої вітрини. Кожен канал додатково може мати власні правила та параметри.
+ new-channel: Новий канал
+ service:
+ storefront:
+ new-incoming-order: У вас нове вхідне замовлення!
+ accept-order: Прийняти замовлення
+ decline-order: Відхилити замовлення
+ create-first-storefront: Створіть свою першу вітрину
+ storefront-has-been-create-success: Вашу нову вітрину успішно створено!
+ create-new-storefront: Створити нову вітрину
+ storefront-create-success: Вашу нову вітрину успішно створено!
+ application:
+ launch-app: Запустити додаток
+ promotions:
+ title: Акції
+ push-notifications:
+ tab-title: Push-сповіщення
+ header: Надіслати Push-сповіщення
+ description: Насилайте рекламні push-сповіщення вашим клієнтам
+ title-label: Заголовок сповіщення
+ title-placeholder: Введіть заголовок сповіщення
+ title-help-text: Заголовок, який відображатиметься в push-сповіщенні
+ body-label: Текст сповіщення
+ body-placeholder: Введіть повідомлення сповіщення
+ body-help-text: Вміст повідомлення, яке буде надіслано клієнтам
+ select-all-customers: Обрати всіх клієнтів
+ select-all-customers-help-text: Надіслати сповіщення усім клієнтам цього магазину
+ customers-label: Обрати клієнтів
+ customers-placeholder: Оберіть клієнтів для надсилання сповіщення
+ send-button: Надіслати сповіщення
+ validation-title-body-required: Будь ласка, введіть заголовок і текст для сповіщення
+ validation-customers-required: Будь ласка, оберіть принаймні одного клієнта для надсилання сповіщення
+ notification-sent-success: Push-сповіщення успішно надіслано обраним клієнтам
+ notification-sent-error: Не вдалося надіслати push-сповіщення. Будь ласка, спробуйте знову.
\ No newline at end of file
From ea48962eb2be4f61def4075160fdb1e570d4bebf Mon Sep 17 00:00:00 2001
From: "Ronald A. Richardson"
Date: Sat, 26 Sep 2026 16:00:05 +0800
Subject: [PATCH 02/11] fix: rebuild storefront push notifications on a single
push channel
Android pushes never worked with store-level FCM credentials and some iOS
devices were rejected, because of several stacked defects:
- configureFcm put the service account JSON into credentials.private_key,
so no valid Firebase client was ever built from a store channel
- FcmChannel required the platform-wide Firebase project to be configured
- credentials were always resolved from the store, never the network app
- one APNs environment per channel rejected sandbox/dev build tokens
- registerDevice never reassigned a token to the latest customer and did
not normalize platform casing
- mail/database errors ran before push and stopped it; failures and dead
tokens were never handled or logged
Introduce Push\StorefrontPushChannel with isolated Firebase/APNs clients,
network-first credential resolution with wrong-app and wrong-environment
retries, dead token pruning, and high priority payloads. Order
notifications share a StorefrontOrderNotification base class. Add a
customers/unregister-device endpoint and an admin test push action.
---
.../modals/create-notification-channel.hbs | 14 +
.../modals/test-notification-channel.hbs | 33 ++
.../networks/index/network/index.js | 10 +
addon/controllers/settings/notifications.js | 39 ++
.../networks/index/network/index.hbs | 1 +
addon/templates/settings/notifications.hbs | 1 +
addon/utils/get-notification-schemas.js | 5 +-
.../modals/test-notification-channel.js | 1 +
server/config/storefront.php | 13 +
.../NotificationChannelController.php | 62 +++
.../Controllers/v1/CustomerController.php | 98 +++-
server/src/Listeners/HandleOrderCompleted.php | 5 +-
.../Listeners/HandleOrderDriverAssigned.php | 5 +-
server/src/Listeners/HandleOrderStarted.php | 5 +-
.../PromotionalPushNotification.php | 131 +----
.../Notifications/StorefrontOrderAccepted.php | 144 +----
.../Notifications/StorefrontOrderCanceled.php | 137 +----
.../StorefrontOrderCompleted.php | 144 +----
.../StorefrontOrderDriverAssigned.php | 146 +----
.../Notifications/StorefrontOrderEnroute.php | 144 +----
.../Notifications/StorefrontOrderNearby.php | 154 +----
.../StorefrontOrderNotification.php | 151 +++++
.../StorefrontOrderPreparing.php | 144 +----
.../StorefrontOrderReadyForPickup.php | 144 +----
server/src/Push/ApnClientFactory.php | 87 +++
.../Push/Contracts/SendsPushNotification.php | 28 +
server/src/Push/FirebaseMessagingFactory.php | 87 +++
.../src/Push/PushConfigurationException.php | 7 +
server/src/Push/PushCredentialResolver.php | 82 +++
server/src/Push/PushMessage.php | 193 +++++++
server/src/Push/PushOutcome.php | 58 ++
server/src/Push/StorefrontPushChannel.php | 236 ++++++++
server/src/Push/Transports/ApnTransport.php | 68 +++
server/src/Push/Transports/FcmTransport.php | 76 +++
server/src/Support/PushNotification.php | 152 -----
server/src/Support/Storefront.php | 3 +-
server/src/routes.php | 8 +-
.../CustomerDeviceRegistrationTest.php | 175 ++++++
.../NotificationChannelTestPushTest.php | 121 ++++
.../NotificationContractsTest.php | 509 ++---------------
server/tests/Unit/Push/PushLayerTest.php | 526 ++++++++++++++++++
translations/en-us.yaml | 13 +
42 files changed, 2299 insertions(+), 1861 deletions(-)
create mode 100644 addon/components/modals/test-notification-channel.hbs
create mode 100644 app/components/modals/test-notification-channel.js
create mode 100644 server/src/Notifications/StorefrontOrderNotification.php
create mode 100644 server/src/Push/ApnClientFactory.php
create mode 100644 server/src/Push/Contracts/SendsPushNotification.php
create mode 100644 server/src/Push/FirebaseMessagingFactory.php
create mode 100644 server/src/Push/PushConfigurationException.php
create mode 100644 server/src/Push/PushCredentialResolver.php
create mode 100644 server/src/Push/PushMessage.php
create mode 100644 server/src/Push/PushOutcome.php
create mode 100644 server/src/Push/StorefrontPushChannel.php
create mode 100644 server/src/Push/Transports/ApnTransport.php
create mode 100644 server/src/Push/Transports/FcmTransport.php
delete mode 100644 server/src/Support/PushNotification.php
create mode 100644 server/tests/Unit/Http/Controllers/CustomerDeviceRegistrationTest.php
create mode 100644 server/tests/Unit/Http/Controllers/NotificationChannelTestPushTest.php
create mode 100644 server/tests/Unit/Push/PushLayerTest.php
diff --git a/addon/components/modals/create-notification-channel.hbs b/addon/components/modals/create-notification-channel.hbs
index be48cf72..59e63251 100644
--- a/addon/components/modals/create-notification-channel.hbs
+++ b/addon/components/modals/create-notification-channel.hbs
@@ -34,6 +34,20 @@
+ {{else if (eq key "environment")}}
+
+
+
+ {{else if (eq key "android_channel_id")}}
+
+
+
{{else if (or (eq key "private_key_content") (eq key "firebase_credentials_json"))}}
diff --git a/addon/components/modals/test-notification-channel.hbs b/addon/components/modals/test-notification-channel.hbs
new file mode 100644
index 00000000..bf2ae53f
--- /dev/null
+++ b/addon/components/modals/test-notification-channel.hbs
@@ -0,0 +1,33 @@
+
+
+ {{/if}}
+
diff --git a/app/adapters/campaign.js b/app/adapters/campaign.js
new file mode 100644
index 00000000..bbba6e09
--- /dev/null
+++ b/app/adapters/campaign.js
@@ -0,0 +1 @@
+export { default } from '@fleetbase/storefront-engine/adapters/campaign';
diff --git a/app/adapters/customer-segment.js b/app/adapters/customer-segment.js
new file mode 100644
index 00000000..53423d97
--- /dev/null
+++ b/app/adapters/customer-segment.js
@@ -0,0 +1 @@
+export { default } from '@fleetbase/storefront-engine/adapters/customer-segment';
diff --git a/app/adapters/promotion-code.js b/app/adapters/promotion-code.js
new file mode 100644
index 00000000..2f00dba3
--- /dev/null
+++ b/app/adapters/promotion-code.js
@@ -0,0 +1 @@
+export { default } from '@fleetbase/storefront-engine/adapters/promotion-code';
diff --git a/app/adapters/promotion.js b/app/adapters/promotion.js
new file mode 100644
index 00000000..f0fa631b
--- /dev/null
+++ b/app/adapters/promotion.js
@@ -0,0 +1 @@
+export { default } from '@fleetbase/storefront-engine/adapters/promotion';
diff --git a/app/components/modals/campaign-form.js b/app/components/modals/campaign-form.js
new file mode 100644
index 00000000..775cf40f
--- /dev/null
+++ b/app/components/modals/campaign-form.js
@@ -0,0 +1 @@
+export { default } from '@fleetbase/storefront-engine/components/modals/campaign-form';
diff --git a/app/components/modals/customer-segment-form.js b/app/components/modals/customer-segment-form.js
new file mode 100644
index 00000000..46e1cf7b
--- /dev/null
+++ b/app/components/modals/customer-segment-form.js
@@ -0,0 +1 @@
+export { default } from '@fleetbase/storefront-engine/components/modals/customer-segment-form';
diff --git a/app/components/modals/promotion-announce.js b/app/components/modals/promotion-announce.js
new file mode 100644
index 00000000..a7c96876
--- /dev/null
+++ b/app/components/modals/promotion-announce.js
@@ -0,0 +1 @@
+export { default } from '@fleetbase/storefront-engine/components/modals/promotion-announce';
diff --git a/app/components/modals/promotion-codes.js b/app/components/modals/promotion-codes.js
new file mode 100644
index 00000000..65f69766
--- /dev/null
+++ b/app/components/modals/promotion-codes.js
@@ -0,0 +1 @@
+export { default } from '@fleetbase/storefront-engine/components/modals/promotion-codes';
diff --git a/app/components/modals/promotion-form.js b/app/components/modals/promotion-form.js
new file mode 100644
index 00000000..9da05a09
--- /dev/null
+++ b/app/components/modals/promotion-form.js
@@ -0,0 +1 @@
+export { default } from '@fleetbase/storefront-engine/components/modals/promotion-form';
diff --git a/app/components/storefront/promotion/runs-cell.js b/app/components/storefront/promotion/runs-cell.js
new file mode 100644
index 00000000..ca25d189
--- /dev/null
+++ b/app/components/storefront/promotion/runs-cell.js
@@ -0,0 +1 @@
+export { default } from '@fleetbase/storefront-engine/components/storefront/promotion/runs-cell';
diff --git a/app/components/storefront/promotion/trigger-cell.js b/app/components/storefront/promotion/trigger-cell.js
new file mode 100644
index 00000000..b3addc19
--- /dev/null
+++ b/app/components/storefront/promotion/trigger-cell.js
@@ -0,0 +1 @@
+export { default } from '@fleetbase/storefront-engine/components/storefront/promotion/trigger-cell';
diff --git a/app/components/storefront/promotion/type-cell.js b/app/components/storefront/promotion/type-cell.js
new file mode 100644
index 00000000..02432544
--- /dev/null
+++ b/app/components/storefront/promotion/type-cell.js
@@ -0,0 +1 @@
+export { default } from '@fleetbase/storefront-engine/components/storefront/promotion/type-cell';
diff --git a/app/controllers/promotions/campaigns.js b/app/controllers/promotions/campaigns.js
new file mode 100644
index 00000000..4ce23ac4
--- /dev/null
+++ b/app/controllers/promotions/campaigns.js
@@ -0,0 +1 @@
+export { default } from '@fleetbase/storefront-engine/controllers/promotions/campaigns';
diff --git a/app/controllers/promotions/index.js b/app/controllers/promotions/index.js
new file mode 100644
index 00000000..c6732540
--- /dev/null
+++ b/app/controllers/promotions/index.js
@@ -0,0 +1 @@
+export { default } from '@fleetbase/storefront-engine/controllers/promotions/index';
diff --git a/app/controllers/promotions/segments.js b/app/controllers/promotions/segments.js
new file mode 100644
index 00000000..066207ba
--- /dev/null
+++ b/app/controllers/promotions/segments.js
@@ -0,0 +1 @@
+export { default } from '@fleetbase/storefront-engine/controllers/promotions/segments';
diff --git a/app/models/campaign.js b/app/models/campaign.js
new file mode 100644
index 00000000..6b519835
--- /dev/null
+++ b/app/models/campaign.js
@@ -0,0 +1 @@
+export { default } from '@fleetbase/storefront-engine/models/campaign';
diff --git a/app/models/customer-segment.js b/app/models/customer-segment.js
new file mode 100644
index 00000000..397c5860
--- /dev/null
+++ b/app/models/customer-segment.js
@@ -0,0 +1 @@
+export { default } from '@fleetbase/storefront-engine/models/customer-segment';
diff --git a/app/models/promotion-code.js b/app/models/promotion-code.js
new file mode 100644
index 00000000..323dc077
--- /dev/null
+++ b/app/models/promotion-code.js
@@ -0,0 +1 @@
+export { default } from '@fleetbase/storefront-engine/models/promotion-code';
diff --git a/app/models/promotion.js b/app/models/promotion.js
new file mode 100644
index 00000000..9b181e55
--- /dev/null
+++ b/app/models/promotion.js
@@ -0,0 +1 @@
+export { default } from '@fleetbase/storefront-engine/models/promotion';
diff --git a/app/routes/promotions/campaigns.js b/app/routes/promotions/campaigns.js
new file mode 100644
index 00000000..f5afc1ce
--- /dev/null
+++ b/app/routes/promotions/campaigns.js
@@ -0,0 +1 @@
+export { default } from '@fleetbase/storefront-engine/routes/promotions/campaigns';
diff --git a/app/routes/promotions/index.js b/app/routes/promotions/index.js
new file mode 100644
index 00000000..de18bb76
--- /dev/null
+++ b/app/routes/promotions/index.js
@@ -0,0 +1 @@
+export { default } from '@fleetbase/storefront-engine/routes/promotions/index';
diff --git a/app/routes/promotions/segments.js b/app/routes/promotions/segments.js
new file mode 100644
index 00000000..59fc6234
--- /dev/null
+++ b/app/routes/promotions/segments.js
@@ -0,0 +1 @@
+export { default } from '@fleetbase/storefront-engine/routes/promotions/segments';
diff --git a/translations/en-us.yaml b/translations/en-us.yaml
index 353e68b9..587a7836 100644
--- a/translations/en-us.yaml
+++ b/translations/en-us.yaml
@@ -703,3 +703,165 @@ storefront:
validation-customers-required: Please select at least one customer to send the notification to
notification-sent-success: Push notification sent successfully to selected customers
notification-sent-error: Failed to send push notification. Please try again.
+ common:
+ new: New
+ edit: Edit
+ delete: Delete
+ save: Save
+ cancel: Cancel
+ name: Name
+ description: Description
+ status: Status
+ created: Created
+ updated: Updated
+ none: None
+ all-customers: All customers
+ done: Done
+ statuses:
+ draft: Draft
+ active: Active
+ paused: Paused
+ ended: Ended
+ scheduled: Scheduled
+ sending: Sending
+ sent: Sent
+ canceled: Canceled
+ list:
+ tab-title: Promotions
+ header: Promotions
+ new-promotion: New promotion
+ edit-promotion: Edit promotion
+ type: Type
+ trigger: Trigger
+ runs: Runs
+ redemptions: Redemptions
+ discount-given: Discount given
+ actions: Promotion actions
+ manage-codes: Manage codes
+ announce: Announce to customers
+ activate: Activate
+ pause: Pause
+ end: End promotion
+ created-success: Promotion created.
+ saved-success: Promotion saved.
+ status-updated: Promotion status updated.
+ empty: No promotions yet. Create one to offer discounts at checkout.
+ types:
+ percentage: Percentage off
+ fixed_amount: Amount off
+ free_delivery: Free delivery
+ bogo: Buy X get Y
+ triggers:
+ automatic: Automatic
+ code: Promo code
+ form:
+ basics: Basics
+ name-help: Shown to customers at checkout and in the app.
+ description-help: Optional details shown with the deal in the app.
+ type-help: How the discount is calculated.
+ trigger-help: Automatic promotions apply to every eligible cart; code promotions need a promo code.
+ percent-off: Percent off
+ amount-off: Amount off
+ buy-quantity: Buy quantity
+ get-quantity: Get quantity
+ get-discount-percent: Discount on the free items (%)
+ bogo-help: For every group of "buy" + "get" eligible items, the cheapest "get" items are discounted.
+ schedule: Schedule
+ starts-at: Starts at
+ ends-at: Ends at
+ schedule-help: Leave empty to run until paused.
+ conditions: Conditions
+ min-subtotal: Minimum spend
+ min-items: Minimum items
+ first-order-only: First order only
+ first-order-only-help: Only customers who have never ordered can use it.
+ targeting: Targeting
+ targeting-help: Limit the discount to these products or categories. Leave empty for the whole cart.
+ products: Products
+ categories: Categories
+ exclude-products: Excluded products
+ limits: Limits
+ usage-limit: Total uses
+ usage-limit-per-customer: Uses per customer
+ budget-amount: Total budget
+ max-discount-amount: Maximum discount per order
+ limits-help: Leave empty for no limit.
+ stacking: Stacking and visibility
+ stackable: Can combine with other promotions
+ stackable-help: Otherwise it applies alone, when it gives the customer the bigger discount.
+ priority: Priority
+ priority-help: Higher priority promotions apply first.
+ is-public: Show in the app's deals
+ codes:
+ title: Promo codes for {name}
+ generate: Generate codes
+ count: How many
+ prefix: Prefix
+ length: Code length
+ usage-limit: Uses per code
+ usage-limit-help: 1 for single-use codes. Leave empty for unlimited.
+ specific-code: Or create a specific code
+ specific-code-placeholder: e.g. SUMMER25
+ code: Code
+ used: Used
+ expires: Expires
+ disable: Disable
+ enable: Enable
+ generated-success: Codes generated.
+ empty: No codes yet.
+ announce:
+ title: Announce {name}
+ help: Send a notification about this promotion to your customers. It is sent when the promotion starts, or right away if it already has.
+ segment: Audience
+ segment-placeholder: All customers
+ message-title: Notification title
+ message-body: Notification message
+ send-at: Send at (optional)
+ success: Announcement scheduled.
+ campaigns:
+ tab-title: Campaigns
+ header: Campaigns
+ new-campaign: New campaign
+ edit-campaign: Edit campaign
+ audience: Audience
+ channels: Channels
+ channel-push: Push notification
+ channel-inbox: In-app inbox
+ send-at: Send at
+ sent-at: Sent
+ targeted: Customers reached
+ send-now: Send now
+ send-now-confirm: Send this campaign to its audience now?
+ cancel-campaign: Cancel campaign
+ cancel-confirm: Cancel this campaign? It will not be sent.
+ audience-count: This campaign will reach {count} customers.
+ title-label: Notification title
+ body-label: Notification message
+ promotion: Linked promotion
+ promotion-help: Tapping the notification opens this promotion.
+ link-url: Or link to a URL
+ when: When to send
+ when-draft: Save as draft
+ when-now: Send now
+ when-schedule: Schedule
+ saved-success: Campaign saved.
+ sent-success: Campaign sent.
+ canceled-success: Campaign canceled.
+ empty: No campaigns yet.
+ segments:
+ tab-title: Segments
+ header: Customer segments
+ new-segment: New segment
+ edit-segment: Edit segment
+ rules: Rules
+ rules-help: Customers must match every rule you fill in.
+ min-orders: At least this many orders
+ max-orders: At most this many orders
+ ordered-within-days: Ordered in the last (days)
+ not-ordered-within-days: Has not ordered in the last (days)
+ joined-within-days: Joined in the last (days)
+ min-spent: Spent at least
+ has-push-device: Can receive push notifications
+ matching: '{count} matching customers'
+ saved-success: Segment saved.
+ empty: No segments yet. Segments let you target campaigns at groups of customers.
From b6738c043ad47420a3a91c96949bfff8a5f98a08 Mon Sep 17 00:00:00 2001
From: "Ronald A. Richardson"
Date: Sat, 26 Sep 2026 18:29:59 +0800
Subject: [PATCH 09/11] fix: allow a promotion end date without a start date
after:starts_at failed validation whenever starts_at was empty.
---
server/src/Http/Requests/PromotionRequest.php | 7 ++++++-
.../tests/Unit/Promotions/PromotionEndpointsTest.php | 12 ++++++++++++
2 files changed, 18 insertions(+), 1 deletion(-)
diff --git a/server/src/Http/Requests/PromotionRequest.php b/server/src/Http/Requests/PromotionRequest.php
index 93db0c97..040017a0 100644
--- a/server/src/Http/Requests/PromotionRequest.php
+++ b/server/src/Http/Requests/PromotionRequest.php
@@ -47,7 +47,12 @@ public function rules()
'priority' => ['sometimes', 'integer'],
'is_public' => ['sometimes', 'boolean'],
'starts_at' => ['nullable', 'date'],
- 'ends_at' => ['nullable', 'date', 'after:starts_at'],
+ 'ends_at' => ['nullable', 'date', function ($attribute, $value, $fail) {
+ $startsAt = $this->input('promotion.starts_at', $this->input('starts_at'));
+ if ($value && $startsAt && strtotime($value) <= strtotime($startsAt)) {
+ $fail('The end date must be after the start date.');
+ }
+ }],
'schedule' => ['nullable', 'array'],
'schedule.*.days' => ['sometimes', 'array'],
'schedule.*.days.*' => ['integer', 'between:1,7'],
diff --git a/server/tests/Unit/Promotions/PromotionEndpointsTest.php b/server/tests/Unit/Promotions/PromotionEndpointsTest.php
index 5ea1db72..57556924 100644
--- a/server/tests/Unit/Promotions/PromotionEndpointsTest.php
+++ b/server/tests/Unit/Promotions/PromotionEndpointsTest.php
@@ -199,6 +199,18 @@ protected function generateCode(int $length, string $prefix): string
$failures[] = $message;
});
+ $endsAt = PromotionRequest::createFrom(promotionRequest(['promotion' => ['starts_at' => '2026-10-01 00:00:00']], 'POST'))->rules()['ends_at'][2];
+ $dateFailures = [];
+ $collect = function ($message) use (&$dateFailures) {
+ $dateFailures[] = $message;
+ };
+ $endsAt('ends_at', '2026-11-01 00:00:00', $collect);
+ $endsAt('ends_at', '2026-09-01 00:00:00', $collect);
+ $endsAt('ends_at', null, $collect);
+ PromotionRequest::createFrom(promotionRequest([], 'POST'))->rules()['ends_at'][2]('ends_at', '2026-09-01 00:00:00', $collect);
+
+ expect($dateFailures)->toBe(['The end date must be after the start date.']);
+
expect($rules['name'][0])->toBe('required')
->and($rules['type'][1])->toBe('in:percentage,fixed_amount,free_delivery,bogo')
->and($rules['value'][3])->toBe('required_if:type,percentage,fixed_amount')
From c6d4f7a7c0e490bf188e445ebad8cfdcf9878355 Mon Sep 17 00:00:00 2001
From: "Ronald A. Richardson"
Date: Mon, 28 Sep 2026 11:21:16 +0800
Subject: [PATCH 10/11] chore(release): v0.4.22
Cuts the release carrying the push notification rebuild (#102), the
customer notifications inbox (#103), the promotions engine (#104),
customer segments and campaigns (#105), their console screens (#106)
and the Ukrainian translation (#101).
---
RELEASE.md | 76 ++++++++++++++++++++++++++++++++++++--------------
composer.json | 2 +-
extension.json | 2 +-
package.json | 2 +-
4 files changed, 58 insertions(+), 24 deletions(-)
diff --git a/RELEASE.md b/RELEASE.md
index 82adc8cc..ec3b5f40 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -1,42 +1,76 @@
-> v0.4.21 ~ "QPay checkout restored and marketplace network repairs"
+> v0.4.22 ~ "Reliable push notifications, a customer inbox, and promotions"
---
## Highlights
-QPay checkout works again. v0.4.20 cached the QPay access token using `expires_in`, which QPay returns as an absolute timestamp rather than a lifetime in seconds — so the token was held far past its real life, and once QPay expired it every storefront request kept presenting a dead token and was answered with `NO_CREDENTIALS` even though the merchant credentials were valid. A token is minted per authentication again.
+Push notifications reach Android and iOS devices again. Storefront's push code had several separate faults:
+- A store's Firebase credentials were stored in a config slot the Firebase client never reads, so Android pushes could not be sent with them.
+- iOS development and sandbox tokens were rejected.
+- Marketplace orders were signed with the store's credentials instead of the network app's.
+- A reinstalled or shared device kept notifying the previous customer.
-Marketplace networks also pick up three repairs: saving a network with options, listing the categories its stores are grouped under, and creating those categories all failed in ways that only surfaced against a fully seeded marketplace.
+Push delivery has been rebuilt on a single channel that picks the right credentials per device, retries tokens Apple or Google say belong to another app or environment, prunes dead tokens, and logs every failure.
----
-## Fixes
+Customers now have a **notifications inbox**. Order updates, promotions and campaigns are kept for the app to list, badge and mark as read, and new ones arrive in realtime.
-- QPay authenticates fresh on every call, restoring checkout invoice creation, the `capture-qpay` callback, checkout status polling, and eBarimt receipts. Caching the token is only safe once the expiry timestamp is read as a timestamp, so it is gone rather than patched.
-- Saving a `Network` with options no longer fails: the options mutator bypassed the model's JSON cast and stored a raw array.
-- `Network::categories()` matched `for = 'network_category'`, a value nothing writes, so the relation was always empty. It now matches the `storefront_network` categories the console and the v1 API already use.
-- `Network::createCategory()` derived its owner type from `network:storefront`, which resolves to no class. It now resolves to the `Network` model, so created categories are owned correctly.
+Stores and networks get **promotions**:
+- percentage off, amount off, free delivery, and buy X get Y discounts
+- automatic or unlocked by promo codes
+- targeting, schedules, usage limits, budgets and stacking rules
+- priced into cash, Stripe and QPay checkouts alike
----
-## Testing Fixtures
+**Customer segments** and **campaigns** send targeted notifications now or on a schedule, and a promotion can be announced to customers when it starts. All of it is managed from new console screens under Promotions.
-The storefront testing seeders are split into two complete, idempotent fixtures over a shared concern, both run by `TestingSeeder`:
+---
+## Features
+
+- **Notifications inbox** (`storefront/v1/notifications`): list with `unread`/`type`/`limit`/`offset`, unread count, retrieve, mark one or all as read, delete. Notifications are scoped to the customer and to the storefront app they use. Realtime delivery goes over `contact.{uuid}`.
+- **Notification preferences** (`notifications/preferences`): `order_updates` turns off order status pushes, and the inbox copy is kept. `promotions` turns off promotional notifications and campaigns entirely.
+- **Promotions:** automatic or promo-code discounts owned by a store or network, with:
+ - product, category and store targeting
+ - minimum spend or items, and first order only
+ - date ranges and weekly time windows
+ - total, per-customer and per-code usage limits
+ - budgets and maximum discounts
+ - stacking and priorities
+- **Checkout integration:** checkouts price promotions when they're created and reserve each use under a row lock, so limits and budgets can't be overspent and the amount charged always matches the discount shown. Captured orders record the discount on the transaction and order, and marketplace orders split it per store.
+- **Public promotion endpoints:** `promotions` lists the storefront's live deals. Carts can apply, remove and preview promo codes (`carts/{id}/promo-code`, `carts/{id}/promotions`), and checkouts accept `promo_codes`.
+- **Customer segments:** rule-based audiences by order count, recent or lapsed ordering, join date, spend and push reachability, with live audience previews.
+- **Campaigns:** push and/or inbox messages with an image and deep link, sent to a segment, chosen customers or everyone, now or on a schedule, in queued batches. A promotion can be announced with a campaign that goes out when it starts.
+- **Console:** Promotions, Campaigns and Segments screens, including promo-code generation and management.
+- **Notification channel test:** a "Send test" action in the console shows Apple's or Google's exact answer for a pasted device token.
+- **Ukrainian translation.**
-- `StoreSeeder` seeds one standalone store end to end — order config, a store location with weekly hours, a sandbox Stripe gateway, categories, products covering variants, addons and sale, unavailable and draft cases, a published catalog, customers, an open cart, a pending checkout, orders spread across the last month, and reviews.
-- `NetworkSeeder` seeds a marketplace: a network with its own sandbox gateway, network categories, member stores built the same way, and network-tagged orders across them.
+---
+## Fixes
-This closes gaps that made local testing misleading: no gateway, location or hours were seeded, so Stripe checkout and delivery quoting could not be exercised at all; core product categories were never purged and duplicated on every run; addon categories had no owner, so the console never listed them; every order collapsed to `created` because the status is reset when the tracking number is generated; and all records shared a single timestamp. Each seeder now tags and purges only its own fixtures.
+- **Firebase credentials:** FCM clients are built from the store's own Firebase service account. Previously the account JSON was written into `credentials.private_key` of the platform project and never worked. A store channel no longer needs the platform to have its own Firebase project either.
+- **Marketplace pushes:** network orders use the network app's push credentials first, then the store's.
+- **iOS environments:** tokens Apple rejects with `BadDeviceToken` are retried in the other APNs environment. APNs channels gain an `environment` setting (auto, production or sandbox).
+- **Device registration:** registering a device moves the token to the customer who registered it most recently, normalizes the platform (`iOS` used to match nothing), and validates input. A new `customers/unregister-device` endpoint removes a token on sign out.
+- **Push ordering and errors:** push is sent before mail and database delivery, so an email failure no longer stops it. A notification failure when auto-accepting an order is now logged instead of swallowed.
+- **Payloads:** Android pushes are sent with high priority and an optional channel id, and iOS pushes play a sound.
+- `Storefront::about()` returns `null` for an unknown storefront key instead of throwing.
---
## Upgrade Steps
-No database migration is required.
+1. Run the migrations. They add `promotions`, `promotion_codes`, `promotion_redemptions`, `customer_segments` and `campaigns`:
-Nothing needs clearing after deploying: the fixed code never reads the cached QPay token, so any entry left by v0.4.20 is inert. Installations still on v0.4.20 that need QPay working before they can deploy can drop that entry to buy one token lifetime, but the upgrade is the fix:
+ ```bash
+ php artisan migrate
+ ```
-```bash
-php artisan cache:forget storefront:qpay:token:|">
-```
+2. **Scheduler:** make sure it runs. `storefront:dispatch-campaigns` runs every minute and `storefront:release-promotion-reservations` every 15 minutes.
+3. **Queue worker:** make sure one runs. Campaigns are delivered in queued batches, and several order notifications were already queued.
+4. **Optional:** fleetbase/core-api adds `user_devices.app_identifier`, `environment` and `last_seen_at`. With it, devices remember which app and APNs environment issued their token. Without it everything works and the server infers both.
+5. **Optional:** set `STOREFRONT_PUSH_ANDROID_CHANNEL_ID`, or the per-channel `android_channel_id`, to post Android pushes to a specific notification channel.
-Releases are now cut from `release/v0.0.0` branches instead of `dev-v0.0.0`. The tag workflow accepts both while open branches move over, so no in-flight release branch is stranded.
+**Behavior changes to note:**
+- **Order notification data:** order notifications stored in the database now keep the readable body in `message`; most types used to store the status code there. `type`, `title` and `body` are added, and every earlier key is kept.
+- **Register device:** `POST customers/register-device` now requires `token` and a `platform` of `ios` or `android`.
+- **Promotional push action:** the console's promotional push notification action now sends a campaign. It's recorded, respects customers' promotion preferences, also appears in their inbox, and is delivered by the queue.
+- **Discount transaction item:** the discount is stored as a positive amount with `code: discount` and `meta.direction: credit`, because the Money cast drops signs. Anything that sums transaction items should subtract it.
---
## Need help?
diff --git a/composer.json b/composer.json
index 4ed6382c..31554d37 100644
--- a/composer.json
+++ b/composer.json
@@ -1,6 +1,6 @@
{
"name": "fleetbase/storefront-api",
- "version": "0.4.21",
+ "version": "0.4.22",
"description": "Headless Commerce & Marketplace Extension for Fleetbase",
"keywords": [
"fleetbase-extension",
diff --git a/extension.json b/extension.json
index 03ca6d02..a55a465c 100644
--- a/extension.json
+++ b/extension.json
@@ -1,6 +1,6 @@
{
"name": "Storefront",
- "version": "0.4.21",
+ "version": "0.4.22",
"description": "Headless Commerce & Marketplace Extension for Fleetbase",
"repository": "https://github.com/fleetbase/storefront",
"license": "AGPL-3.0-or-later",
diff --git a/package.json b/package.json
index f9cd6b8c..fdb3f2b1 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@fleetbase/storefront-engine",
- "version": "0.4.21",
+ "version": "0.4.22",
"description": "Headless Commerce & Marketplace Extension for Fleetbase",
"fleetbase": {
"route": "storefront",
From 6f8659de2dfd1413dcc95b2c580ac2ebb6dbea91 Mon Sep 17 00:00:00 2001
From: "Ronald A. Richardson"
Date: Mon, 28 Sep 2026 16:05:15 +0800
Subject: [PATCH 11/11] fix: correct category selection and polish promotion
workflows
---
addon/components/modals/campaign-form.hbs | 189 +++++++++++-------
addon/components/modals/campaign-form.js | 32 ++-
.../modals/customer-segment-form.hbs | 63 ++++--
.../modals/customer-segment-form.js | 42 +++-
.../components/modals/promotion-announce.hbs | 19 +-
addon/components/modals/promotion-codes.hbs | 81 +++++---
addon/components/modals/promotion-codes.js | 28 ++-
addon/components/modals/promotion-form.hbs | 102 +++++-----
addon/controllers/application.js | 9 +-
addon/controllers/promotions/campaigns.js | 33 ++-
addon/controllers/promotions/index.js | 10 +
addon/controllers/promotions/segments.js | 10 +
addon/templates/promotions/campaigns.hbs | 31 ++-
addon/templates/promotions/index.hbs | 31 ++-
addon/templates/promotions/segments.hbs | 31 ++-
docs/promotions.md | 49 +++++
tests/unit/controllers/application-test.js | 28 ++-
translations/en-us.yaml | 57 +++++-
18 files changed, 629 insertions(+), 216 deletions(-)
create mode 100644 docs/promotions.md
diff --git a/addon/components/modals/campaign-form.hbs b/addon/components/modals/campaign-form.hbs
index a803a755..df168849 100644
--- a/addon/components/modals/campaign-form.hbs
+++ b/addon/components/modals/campaign-form.hbs
@@ -1,80 +1,131 @@
-