Developer guide
Code layout
Section titled “Code layout”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 upgradesdesign/backend/templates/addons/kv_price_stock_alerts/license/info.tpl License tabDatabase
Section titled “Database”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.
PHP hooks used
Section titled “PHP hooks used”| 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.
Template hooks
Section titled “Template hooks”| 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.
License check
Section titled “License check”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: theproducts:product_detail_bottomholder and theproducts:out_of_stock_blockoverride output nothing (the core form is shown as usual),products.pre.phpno longer routes the core “Notify me” request,kv_price_stock_alerts.widgetrenders an empty holder (pages cached before the change) and.subscribeignores the request.Evaluator::flushDirty()queues no alerts andQueueProcessor::run()returnsskipped = licensewithout scanning or sending (cron URL, console cron, admin fallback, Process queue now, which shows a notice). - Kept:
.confirm,.unsubscribe,.manageand.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 abefore_dispatchhandler 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.tplthrough theaddons:tabs_contentpre hook), the badge in Add-ons > Manage (license/badge.tpl,addons:managepost hook), the status bar (license/status_bar.tpl, first line inside the mainbox capture ofviews/kv_price_stock_alerts/{report,subscriptions,queue}.tpl; nothing for vendors) andcss/addons/kv_price_stock_alerts/license.less(index:stylespost hook). The off-effect sentence is the language variablekv_price_stock_alerts.license_off_effect. - Request:
POST https://cs-cart.kaviolabs.com/index.php?dispatch=kv_licenses.checkwith JSON{"v":1, "license", "addon", "version", "hosts":[…], "platform":{"version", "edition"}, "nonce"}, sent throughTygh\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), orvar/kv_storage/kv_price_stock_alerts/license.jsonwhile 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_hostandhttps_hostare development hosts): the constantsKV_LICENSE_SERVER_URL,KV_LICENSE_TEST_HOST(act as if the development storefronts ran on that host) andKV_LICENSE_TEST_TIME_OFFSET(seconds). They are ignored on production hosts.
Adding a delivery channel
Section titled “Adding a delivery channel”Implement Tygh\Addons\KvPriceStockAlerts\Channels\ChannelInterface and register it from your add-on:
fn_register_hooks('kv_price_stock_alerts_register_channels');
// func.phpfunction 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.
Running the queue from code
Section titled “Running the queue from code”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 intervalRuns use a MySQL named lock (GET_LOCK), so overlapping cron calls and admin fallbacks never send twice.
Price used for the comparison
Section titled “Price used for the comparison”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.
Controllers and permissions
Section titled “Controllers and permissions”- Storefront:
kv_price_stock_alerts.widget(AJAX only),.subscribe,.cancel(POST),.confirm,.unsubscribe,.manage(token links),.cron(keyrequired). - Admin:
kv_price_stock_alerts.report|subscriptions|queue(privilegeview_catalog), POST modes needmanage_catalog. - Vendor panel (MV): only
kv_price_stock_alerts.report, filtered by the vendor’scompany_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.
Third-party code
Section titled “Third-party code”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.