Skip to content

Developer notes

app/addons/kv_vendor_sponsored_products/
addon.xml settings, 9 tables, 2 privileges
init.php / func.php hooks, block content function, cache key, visitor data, licence helpers
controllers/backend/kv_sponsored.php campaigns, moderation, statistics, CSV, posted charges, vendors, cron
controllers/backend/kv_vendor_sponsored_products_license.php "Check now" and licence cron
controllers/frontend/kv_sponsored.php tracking endpoint (event)
controllers/frontend/categories.post.php category strip data
controllers/frontend/products.post.php search strip and product page row data
schemas/block_manager/blocks.post.php block type + cache keys of the cached main blocks
schemas/menu/*.post.php Marketing > Sponsored products (admin, vendor panel)
schemas/permissions/*.post.php privileges, vendor modes, cron as trusted controller
schemas/security/csrf_validation.post.php tracking endpoint without form token
Tygh/Addons/KvVendorSponsoredProducts/
Config.php settings (amounts in cents), placements, days, rotation bucket
Campaigns.php storage, validation, approval rules, vendor flags
Selector.php slot selection (cacheable), ranking, visibility through fn_get_products
Token.php signed slot tokens, hashed visitor data, signed click cookie
Tracker.php impressions, clicks, click-fraud guard
Billing.php charges under a vendor lock, budgets, available balance, funded flag
FreshPayouts.php core VendorPayouts without the per-request balance cache
Settlement.php posting to core vendor accounting, crash repair
Stats.php daily statistics, order attribution, reports, CSV
Maintenance.php scheduled tasks (cron and pseudo-cron)
Money.php cents
StorageData.php run marks (files instead of storage_data while the full page cache is on)
License/, Upgrade/ licence client, self-healing upgrades
design/backend/templates/addons/kv_vendor_sponsored_products/ pages, components, settings info, licence UI
var/themes_repository/responsive/templates/addons/kv_vendor_sponsored_products/
components/strip.tpl the sponsored slot markup
hooks/categories/view_description.post.tpl, hooks/wrapper/mainbox_general_title_wrapper.post.tpl,
hooks/products/product_tabs.post.tpl, blocks/sponsored/{grid,row}.tpl
var/themes_repository/responsive/css/addons/kv_vendor_sponsored_products/styles.less
js/addons/kv_vendor_sponsored_products/func.js (storefront tracking), backend.js (campaign form)
var/langs/en/addons/kv_vendor_sponsored_products.po
  • A charge is a row in kv_spads_charges (click or fixed day). It is recorded under a MySQL named lock per vendor (GET_LOCK), after checking the campaign (status, approval, dates, vendor active and not blocked), the daily budget (kv_spads_spend), the total budget and the vendor’s available amount: core balance − unposted charges − minimum balance. All comparisons use integer cents. A charge never exceeds a budget or the available amount, also with concurrent clicks.
  • The core balance is read through Tygh\VendorPayouts::getBalance() (subclass FreshPayouts only skips the per-request Registry cache of the sums), so Vendor plans commission and every add-on that hooks vendor_payouts_get_income/_get_balance are applied.
  • Posting (Settlement) groups unposted charges per vendor and day (or hour) into one core ?:vendor_payouts row: payout_type = payout, positive payout_amount, approval_status = C, comment with the reference SP-<settlement id>. Core counts such a row against the vendor income and as admin income (the Vendor plans plan fee uses the same row type). Vendor debt payout only completes pending payout rows when a debt is paid, so posted ad charges are never touched by it.
  • Posting order: settlement row (status N) → charges bound to it → core row → settlement status P. A run finds settlements left in status N (crash) and finishes them when the core row with the reference exists, otherwise unbinds the charges and deletes the row. One posting run at a time (GET_LOCK), and each vendor is posted under the vendor lock.
  • Direct payments: such vendors receive order money directly and usually have no positive balance, so their campaigns pause for not enough balance until they top up. Ads never charge into a balance below the minimum you keep.
  • Uninstall keeps every posted core row; unposted charges are dropped with the tables.
Table Purpose
kv_spads_campaigns Campaign: vendor, name, pricing (C cost per click, F fixed), bid, daily and total budget, placements, dates, status (A, H paused, E ended), approval (P, Y, R, S), reason, exhausted day + reason
kv_spads_campaign_products Products of a campaign
kv_spads_spend Spend and clicks per campaign and day; fixed-price day charged flag
kv_spads_stats Daily statistics per campaign, product and placement: impressions, clicks, invalid clicks, spend
kv_spads_clicks Click log (charged and filtered with a reason); IP, user agent and visitor id are stored only as keyed hashes
kv_spads_charges Money ledger of recorded charges, settlement id once posted
kv_spads_settlements Postings: vendor, period, amount, number of charges, core payout id, status
kv_spads_attributions Order lines that followed a click: order, product, campaign, amount
kv_spads_vendors Per vendor: blocked, automatic approval, funded flag

Identical arguments in Multi-Vendor 4.17.2 and 4.21.2.

Hook Use
place_order Attribute order lines to clicks (storefront orders only)
delete_order Remove attribution rows
vendor_payouts_update_post A vendor balance changed (order, refund, withdrawal, top-up): re-check whether the vendor can pay for its campaigns; resumes campaigns paused for balance
delete_company Remove the vendor’s campaigns and ad data
delete_product_post Remove a deleted product from campaigns
before_dispatch Pseudo-cron in the admin and vendor panel (every 15 minutes, GET pages only)
init_templater_post Self-healing upgrade (only while pending)

Template hooks: categories:view_description (post), wrapper:mainbox_general_title_wrapper (post, main content block of the search page only), products:product_tabs (post), index:styles, index:scripts.

  • The slot selection depends only on the context (category, search query, product), the live campaigns and a 15-minute rotation bucket, never on the visitor. Pages stay cacheable for all guests.
  • The live campaign list is read through Registry::registerCache tagged with kv_spads_campaigns, kv_spads_campaign_products, kv_spads_vendors and companies: approving, pausing or a budget running out purges the cached pages.
  • The cached main blocks of categories.view and products.view get these tables as update handlers and a callable handler (licence state, rotation bucket, settings).
  • No storefront GET writes anything (no storage_data, no session). Impressions and clicks go to index.php?dispatch=kv_sponsored.event by navigator.sendBeacon (POST, never cached, no session write).

kv_sponsored.event accepts POST only and answers 204. Tokens printed in the slots are <campaign>-<product>-<placement>-<bucket>-<HMAC> (key derived from the store’s crypt key) and are accepted for 48 hours. Clicks: bot user agents → invalid; administrators and the campaign vendor’s users → ignored; same visitor (cookie kv_spads_vid, else hash of IP + user agent), campaign and product within the dedupe window → invalid duplicate; per-IP limits per campaign and day and per hour → invalid. Behind a local reverse proxy the first public X-Forwarded-For address is used. Charged clicks set the signed cookie kv_spads_clk (last 10 clicks, attribution window).

Cookies: kv_spads_vid (random visitor id for click deduplication, 1 year) and kv_spads_clk (signed list of recent sponsored clicks for order attribution; not set when the window is 0).

fn_kv_vendor_sponsored_products_is_licensed() (License\Client::isActive()) gates the storefront slots and the block, the tracking endpoint (impressions, clicks, charges, attribution) and posting. Campaign editing, moderation, statistics and CSV are never gated. The licence check itself runs only in the admin panel or from cron (php admin.php --dispatch=kv_vendor_sponsored_products_license.check).

  • Change the markup: copy components/strip.tpl into your theme (design/themes/<theme>/templates/addons/kv_vendor_sponsored_products/components/strip.tpl).
  • Other positions: place the Sponsored products block, or include components/strip.tpl with a slot array from \Tygh\Addons\KvVendorSponsoredProducts\Selector::forCategory(), forSearch(), forProduct() or forBlock().
  • Read statistics: \Tygh\Addons\KvVendorSponsoredProducts\Stats::totals($filter, 'campaign_id'|'company_id'|'day').