Skip to content

Developer notes

app/addons/kv_vendor_commission_rules/
├── addon.xml scheme 3.0, MULTIVENDOR only, depends on vendor_plans, priority 2500
├── init.php / func.php hooks, helpers, simulator
├── controllers/backend/kv_commission_rules.php admin: manage, add/update, delete, statuses, simulator, report, export, recalculate
├── controllers/backend/kv_commission_rates.php vendor panel: "My commission rates" (admins: ?company_id=)
├── controllers/backend/orders.post.php commission breakdown for orders.details
├── controllers/backend/kv_vendor_commission_rules_license.php "Check now" / cron license check
├── schemas/menu/menu_multivendor.post.php, menu_vendor.post.php
├── schemas/permissions/admin.post.php, vendor_multivendor.post.php
└── Tygh/Addons/KvVendorCommissionRules/
├── Engine.php rule matching, specificity, commission base, per-rule amounts
├── Rules.php rule storage, validation, descriptions
├── Commission.php payout integration, log, recalculation, order breakdown, report, CSV
├── License/Client.php license check client (generated from the shared Kavio Labs template)
└── Upgrade/Synchronizer.php
design/backend/templates/addons/kv_vendor_commission_rules/
├── views/kv_commission_rules/{manage,update,simulator,report,recalculate}.tpl
├── views/kv_commission_rates/view.tpl
├── components/{navigation,groups,status_label}.tpl
├── license/info.tpl License tab (points to the card)
├── license/card.tpl License card (add-on page) / banner
├── license/status_bar.tpl status chip/banner for own admin pages
├── license/badge.tpl badge in Add-ons > Manage add-ons
└── hooks/{orders/totals.post.tpl, companies/payout_type_description.post.tpl, index/styles.post.tpl (also license.less),
addons/tabs_content.pre.tpl (card), addons/manage.post.tpl (badge)}
design/backend/css/addons/kv_vendor_commission_rules/license.less card, chip and badge styles (admin LESS variables)

The core commission is calculated by fn_calculate_commission_for_payout() of the bundled vendor_plans add-on (identical in 4.17.2 and 4.21.2). It is called for:

Payout type Caller
order_placed fn_vendor_plans_mve_place_order() (hook mve_place_order in fn_mve_place_order())
order_changed fn_vendor_plans_mve_update_order() (hook mve_update_order in fn_mve_update_order())
order_refunded fn_vendor_plans_rma_update_details_create_payout() (RMA)

The add-on handles vendor_plans_calculate_commission_for_payout_post($order_info, $company_data, &$payout_data). With priority 2500 it runs after vendor_plans (1000) and vendor_categories_fee (2000, which handles the same hook). It rewrites only marketplace_profit, commission_amount (keeping the core difference between them: vendor taxes collected), commission (effective percent), commission_type (P), and adds extra.kv_vcr. It never touches order_amount, so balances follow the core formulas (order_amount − commission_amount for vendors, marketplace_profit for the admin income).

  • Placed: Engine::calculate(); no matching rule → payout untouched.
  • Changed: only for orders with a rule-based log entry. The full commission of the new order is calculated by calling fn_calculate_commission_for_payout() again with a synthetic order_placed payout (order_id 0, so vendor_categories_fee does not subtract earlier payouts; a re-entry guard prevents loops). The payout gets full − SUM(marketplace_profit of the order's payouts).
  • Refunded: only for rule-based orders; order_amount × charged ÷ order total, full reversal when the refunded order subtotal is 0, never below 0 in total.

Payment add-ons that read the commission from the order_placed payout (Stripe Connect PayoutsManager::getOrderFee() uses commission_amount when commission_type = P) get the rule-based value. Direct payments: the withdrawal equals the order amount, so the vendor balance becomes −commission as in the core.

vendor_payouts_update_post writes the log entry (?:kv_vcr_log) after the payout is saved (the hook data carries a non-column key kv_vcr_log, which ?e/?u drop). delete_order removes the log of a deleted order (core deletes its payouts). delete_company removes a deleted vendor from rules (a rule left without vendors is disabled).

Type Hook Purpose
PHP vendor_plans_calculate_commission_for_payout_post Rule-based commission of a payout (see above)
PHP vendor_payouts_update_post Writes the log entry
PHP delete_order, delete_company Cleanup
PHP before_dispatch (admin area and console only; static handler License\Client::onBeforeDispatch) Runs the due license check and shows license notices (not on the add-on’s own pages, which show the status bar)
Template orders:totals (post), companies:payout_type_description (post), index:styles (post) Order breakdown, Accounting notes, styles (incl. license.less)
Template (admin) addons:tabs_content (pre) License card at the top of the add-on page
Template (admin) addons:manage (post) License badge, moved into the add-on’s row (4.17–4.21 have no per-row hook)

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

  • fn_kv_vendor_commission_rules_is_licensed() is the gate. It reads the stored state only (one storage read per request) and never contacts the server. Placing an order never triggers a license check. Gate points:
    • Commission::applyToPayout(): for an order_placed payout of a new order it returns early when the gate is closed, so the payout keeps the core commission (vendor_plans / vendor_categories_fee). The full recalculation of an existing rule-based order (computeFull(), used for order_changed) is not gated, and order_changed / order_refunded payouts are rule-based only for orders that already have a rule-based log entry. So the decision is made once per order, when it is placed: orders placed while licensed stay rule-based, orders placed while unlicensed stay core-based.
    • Commission::recalculate() with $apply = true and the recalculate POST of kv_commission_rules.php: applying is refused (error notice); the preview still works.
    • kv_commission_rates.view: engine_enabled is false, so vendors see the plan and category rates only.
    • The administrator pages of the add-on (kv_commission_rules.* and kv_commission_rates.view opened by an administrator) show the license status bar: a banner with the status and what is switched off (kv_vendor_commission_rules.license_off_effect). Vendors never see it.
  • In the admin panel and console, isActive() means “licensed for at least one storefront”; on the storefront it uses the current storefront’s host.
  • 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_vendor_commission_rules (?:storage_data), or var/kv_storage/kv_vendor_commission_rules/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_vendor_commission_rules_license.check. The endpoint accepts only CSRF-protected POST requests of root administrators, and console requests. Vendors cannot use it.
  • Admin UI: License\Client assigns the view data of the license card to $kv_lic_kv_vendor_commission_rules in its before_dispatch handler, only for administrators with the Manage settings privilege (update_settings); vendors never get it. The templates only read this variable, so they render nothing (and no error) while an older version of the PHP code is loaded, for example in the request that uploads a new zip over the installed add-on. PHP code can call fn_kv_vendor_commission_rules_license_ui($context). Every full page of the add-on’s own admin controllers (rules, rule editor, simulator, report, recalculation, and kv_commission_rates.view opened by an administrator) includes license/status_bar.tpl as the first line of its mainbox: a green chip when licensed, the card as a compact banner otherwise. The add-on’s license notice is not shown on these pages (the bar shows the status); it appears on the next other admin page.
  • 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.

?:kv_vcr_rules: rule_id, name, status, priority, company_ids, plan_ids, category_ids, include_subcategories, product_ids (comma lists), price_from/to, subtotal_from/to, sales_from/to (0 = no limit), date_from/to (timestamps, date_to = end of day), percent, fixed_amount, min_commission, max_commission, include_shipping, include_taxes (D vendor plans setting / Y / N), base_discounts (D add-on setting / A after / B before), created_at, updated_at.

?:kv_vcr_log: one row per payout calculated by rules: payout_id, order_id, company_id, payout_type, is_adjustment, created_at, rule_ids, order_amount, base_amount, core_commission (what the core calculated for that payout), commission (final marketplace_profit), data (JSON: groups per rule, lines per product).

use Tygh\Addons\KvVendorCommissionRules\Engine;
// $order_info: fn_get_order_info() result (or the cart-based order array); returns null if no rule matches
$result = Engine::calculate($order_info, $company_id, $plan_id, [
'force' => true, // also return a result when no rule matches (core rates for all lines)
'rules' => $rules, // optional: use these (normalized) rules instead of the active ones
]);
// $result: commission, base, effective_rate, rule_ids, groups[rule_id|core] {base, percent, fixed, min, max, amount, capped},
// lines[cart_id] {product_id, rule_id, candidates, rate, base, commission}

Commission::computeFull($order_info, $company_data) returns the payout data a new order would get now (core + rules). Commission::recalculate($params, $apply) is the recalculation tool.

  • Per-line taxes are read from order_info.taxes[*].applies (P_<cart_id> keys for the unit-price method; the P amount split by subtotal over items.P for the subtotal method). Shipping and the order discount are shared between lines by subtotal.
  • Products without a rule in a mixed order use the plan percent or the category fee (fn_vendor_categories_fee_get_category_fee() / parent fallback). This follows the core rates per product but uses the add-on’s base model, so a mixed order can differ by cents from a pure core calculation.
  • Monthly sales: SUM(total) of the vendor’s non-parent orders in payout statuses from the 1st of the order’s month up to the order time.
  • PHP 7.4-compatible syntax; Smarty templates compile on Smarty 4 (4.17/4.18) and 5 (4.21).
  • Kill switch for the self-healing upgrade: define('KV_UPGRADE_SYNC_DISABLED', true);.

Tygh/Addons/KvVendorCommissionRules/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.