Skip to content

Developer guide

app/addons/kv_price_stock_alerts/
addon.xml settings, tables, e-mail templates
init.php / func.php hook registration and handlers, install/uninstall, settings info handlers
controllers/frontend/kv_price_stock_alerts.php widget, subscribe, cancel, confirm, unsubscribe, manage, cron URL
controllers/frontend/products.pre.php routes the core "Notify me" request into the add-on (replace mode)
controllers/backend/kv_price_stock_alerts.php report, subscriptions, queue, process_queue, import_core, console cron
controllers/backend/kv_price_stock_alerts_license.php "Check now" / cron license check
schemas/block_manager/block_cache_properties.post.php license state in the block cache key
schemas/menu/menu.post.php, menu_vendor.post.php
schemas/permissions/admin.post.php, vendor_multivendor.post.php
schemas/gdpr/user_data.post.php GDPR export/anonymization (loaded only with the GDPR add-on)
Tygh/Addons/KvPriceStockAlerts/
Config.php settings accessor and constants
ProductState.php public price (after catalog promotions) and stock state
Repository.php subscriptions and queue storage, purge
SubscriptionService.php subscribe flow, double opt-in, core list import
Evaluator.php fires alerts for changed products, rolling scan
QueueProcessor.php throttled, locked queue run
MessageBuilder.php message data for all channels
Report.php demand report, summary, admin lists
Logger.php last 20 internal errors (storage_data)
Channels/ ChannelInterface, ChannelRegistry, EmailChannel, TelegramChannel, TelegramBot
License/Client.php license check client (generated from the shared Kavio Labs template)
Upgrade/Synchronizer.php self-healing in-place upgrades
design/backend/templates/addons/kv_price_stock_alerts/license/info.tpl License tab

cscart_kv_psa_subscriptions: one row per product, alert type and e-mail (UNIQUE product_id, alert_type, email).

Column Notes
alert_type P price drop, S back in stock, L low stock
status P pending confirmation, A active, N notified (finished), U unsubscribed
baseline_price reference price in the primary currency; moves down after an alert in “keep watching” mode
target_price 0 = no target
company_id / storefront_id Ultimate storefront company / storefront of the subscription; 0 in Multi-Vendor
lang_code, currency_code language and currency of the messages
token 32 hex chars for the confirm, unsubscribe and manage links and the Telegram deep link
telegram_chat_id linked chat, shared by all subscriptions of the e-mail address

cscart_kv_psa_queue: one row per fired alert and channel. status: Q queued, S sent, F failed after 3 attempts, C cancelled. Both tables are InnoDB.

Runtime state lives in cscart_storage_data under kv_psa_* keys (last run, last result, scan cursor, Telegram offset, error log, confirmation throttle) and is removed on uninstall.

Hook Why
update_product_post product saved (admin, vendor panel, API, import that uses fn_update_product)
update_product_amount_post stock changed by orders, returns, stock edits
global_update_products bulk price/stock update
send_product_notifications_before_fetch_subscriptions core detected “back in stock” (also the warehouses add-on and imports)
tools_change_status product status toggled in a list
delete_product_post, delete_user remove the alerts of deleted products and customers
before_dispatch admin-activity fallback run at shutdown; storefront: assigns $kv_psa_licensed (license state) for the product page hooks
before_dispatch (admin area and console only; static handler License\Client::onBeforeDispatch) runs the due license check and shows license notices

The product hooks only mark the product as changed. The check runs once in a shutdown function, after every price and amount of the request is saved, and never throws.

Hook File
products:product_detail_bottom (post) empty widget holder, filled by kv_price_stock_alerts.widget through AJAX
products:out_of_stock_block (override) hides the core form on the product page in Replace mode; outputs nothing otherwise, so the core block is untouched
profiles:my_account_menu (post) “My stock & price alerts” link
index:styles / index:scripts (post) styles and js/addons/kv_price_stock_alerts/func.js

The holder keeps id="..." as the last attribute: the CS-Cart AJAX layer looks for the exact string id="<result_id>"> when it extracts result_ids from a response.

Tygh\Addons\KvPriceStockAlerts\License\Client verifies the license against the Kavio Labs license server.

  • fn_kv_price_stock_alerts_is_licensed() is the gate. On the storefront it checks the current storefront; in the admin panel, console and cron it means “licensed for at least one storefront”. It reads the stored state only (one storage read per request) and never contacts the server.
  • When it returns false: the products:product_detail_bottom holder and the products:out_of_stock_block override output nothing (the core form is shown as usual), products.pre.php no longer routes the core “Notify me” request, kv_price_stock_alerts.widget renders an empty holder (pages cached before the change) and .subscribe ignores the request. Evaluator::flushDirty() queues no alerts and QueueProcessor::run() returns skipped = license without scanning or sending (cron URL, console cron, admin fallback, Process queue now, which shows a notice).
  • Kept: .confirm, .unsubscribe, .manage and .cancel (existing subscriptions), the account menu link, the admin report, subscriptions and queue pages, the GDPR export and anonymization.
  • The widget holder and the override are printed inside the product page’s main block, which CS-Cart may cache. The license state is part of every block cache key (block_cache_properties.post.php, callable_handlers), so they switch at once.
  • Client::boot() (init.php) registers a before_dispatch handler for the admin area and the console only. On admin page views and console requests it runs the check when it is due (key changed, or 24 hours after the last check; 1 hour after a failure) and shows the admin notice once per session.
  • Admin license UI (generated): the License card at the top of the add-on page (license/card.tpl through the addons:tabs_content pre hook), the badge in Add-ons > Manage (license/badge.tpl, addons:manage post hook), the status bar (license/status_bar.tpl, first line inside the mainbox capture of views/kv_price_stock_alerts/{report,subscriptions,queue}.tpl; nothing for vendors) and css/addons/kv_price_stock_alerts/license.less (index:styles post hook). The off-effect sentence is the language variable kv_price_stock_alerts.license_off_effect.
  • Request: POST https://cs-cart.kaviolabs.com/index.php?dispatch=kv_licenses.check with JSON {"v":1, "license", "addon", "version", "hosts":[…], "platform":{"version", "edition"}, "nonce"}, sent through Tygh\Http (3 s connect, 5 s total; kept out of the CS-Cart request log). The answer is accepted only when its Ed25519 signature matches a public key embedded in the client and it echoes the nonce.
  • State: storage key kv_license_kv_price_stock_alerts (?:storage_data), or var/kv_storage/kv_price_stock_alerts/license.json while the full page cache is active, protected by an HMAC with the store’s crypt key.
  • Cron: php admin.php --dispatch=kv_price_stock_alerts_license.check.
  • Development stores only (http_host and https_host are development hosts): the constants KV_LICENSE_SERVER_URL, KV_LICENSE_TEST_HOST (act as if the development storefronts ran on that host) and KV_LICENSE_TEST_TIME_OFFSET (seconds). They are ignored on production hosts.

Implement Tygh\Addons\KvPriceStockAlerts\Channels\ChannelInterface and register it from your add-on:

init.php
fn_register_hooks('kv_price_stock_alerts_register_channels');
// func.php
function fn_my_sms_kv_price_stock_alerts_register_channels(&$channels)
{
$channels['sms'] = new \Tygh\Addons\MySms\SmsChannel();
}
class SmsChannel implements \Tygh\Addons\KvPriceStockAlerts\Channels\ChannelInterface
{
public function getId() { return 'sms'; } // max 16 chars
public function isEnabled() { return true; } // configured?
public function canDeliver(array $subscription) { ... } // e.g. look up a phone by user_id
public function send(array $subscription, array $message)
{
// $message: title, text, product_name, product_url, old_price, new_price, amount, lang_code,
// unsubscribe_url, manage_url, ... (see MessageBuilder::build())
return true; // or an error string; the queue retries 3 times
}
}

The queue creates one row per channel that is enabled and can deliver at the moment the alert fires.

use Tygh\Addons\KvPriceStockAlerts\QueueProcessor;
use Tygh\Addons\KvPriceStockAlerts\Evaluator;
Evaluator::evaluate([$product_id]); // queue alerts for products you changed with direct SQL
$summary = QueueProcessor::run('cron'); // throttled; QueueProcessor::run('manual', true) ignores the interval

Runs use a MySQL named lock (GET_LOCK), so overlapping cron calls and admin fallbacks never send twice.

ProductState reads MIN(price) from cscart_product_prices for lower_limit = 1 and usergroup_id = 0 (percentage discounts applied), then applies active catalog promotions for a guest with fn_promotion_apply('catalog', ...) when Include catalog promotions is on. Option modifiers, taxes and user-group prices are not part of the comparison.

  • Storefront: kv_price_stock_alerts.widget (AJAX only), .subscribe, .cancel (POST), .confirm, .unsubscribe, .manage (token links), .cron (key required).
  • Admin: kv_price_stock_alerts.report|subscriptions|queue (privilege view_catalog), POST modes need manage_catalog.
  • Vendor panel (MV): only kv_price_stock_alerts.report, filtered by the vendor’s company_id. The controller also denies every other mode for vendors.
  • The license check endpoint (kv_price_stock_alerts_license.check) accepts only CSRF-protected POST requests of administrators, and console requests. Vendors cannot use it.

Tygh/Addons/KvPriceStockAlerts/License/Ed25519.php (checks the signature of the license server’s answers on servers whose PHP has no sodium extension) is derived from paragonie/sodium_compat, ISC License, Copyright (c) 2016-2023 Paragon Initiative Enterprises and Copyright (c) 2013-2019 Frank Denis. The full license notice is in the file header.