Skip to content

Developer notes

Item Value
Add-on id kv_b2b_quote_request (scheme 3.0, priority 4700)
Namespace Tygh\Addons\KvB2bQuoteRequest in app/addons/kv_b2b_quote_request/Tygh/Addons/KvB2bQuoteRequest/
Tables ?:kv_rfq_quotes, ?:kv_rfq_quote_items, ?:kv_rfq_messages, ?:kv_rfq_attachments, ?:kv_rfq_rules (product/category display rules), ?:kv_rfq_drafts (saved quote lists of signed-in customers)
Privileges view_kv_b2b_quote_request, manage_kv_b2b_quote_request (group kv_b2b_quote_request, section Orders)
Controllers kv_b2b_quote_request (front end and back end)
Files var/custom_files/kv_b2b_quote_request/<quote_id>/ (custom_files storage)
License Tygh/Addons/KvB2bQuoteRequest/License/Client.php (license check client, generated from the shared Kavio Labs template), controllers/backend/kv_b2b_quote_request_license.php (“Check now” / cron license check), design/backend/templates/addons/kv_b2b_quote_request/license/info.tpl (License tab (points to the card)), design/backend/templates/addons/kv_b2b_quote_request/license/card.tpl (License card (add-on page) / banner), design/backend/templates/addons/kv_b2b_quote_request/license/status_bar.tpl (status chip/banner for own admin pages), design/backend/templates/addons/kv_b2b_quote_request/license/badge.tpl (badge in Add-ons > Manage add-ons), design/backend/templates/addons/kv_b2b_quote_request/hooks/addons/ (tabs_content.pre.tpl (card), manage.post.tpl (badge)), design/backend/templates/addons/kv_b2b_quote_request/hooks/index/styles.post.tpl (loads license.less), design/backend/css/addons/kv_b2b_quote_request/license.less (card, chip and badge styles (admin LESS variables))

Prices in quotes are stored in the primary currency, like product prices. Status codes: N New, R In review, O Offered, A Accepted, D Rejected, E Expired, C Ordered. Rule modes: B next to Add to cart, O quote only, D do not show (no row = inherit).

Accepting an offer adds each item with extra.kv_rfq = {quote_id, item_id}; prices and quantities never come from the request.

Hook What happens
pre_add_to_cart Removes kv_rfq markers sent by the browser; on cart updates restores the marker of existing quote lines from the server-side cart; refuses products that are quote only for the visitor.
generate_cart_id Quote lines get their own cart id, so they never merge with regular lines.
exclude_products_from_calculation Runs before core reads product prices. Each quote line is checked against the database: the visitor owns the quote (same account, or a guest session that opened the signed link), the quote is Accepted and not expired, item, product and options match. Valid lines get stored_price = Y, the quoted unit price (quantity break by amount) and the allowed quantity; anything else is removed from the cart.
get_cart_product_data_post Undoes catalog promotions on quoted lines unless “Apply catalog promotions to quoted prices” is on.
pre_place_order The complete quote must be in the cart with allowed quantities; then one atomic UPDATE … SET status = 'C' WHERE status = 'A' reserves it. A second session loses the race and the order is refused.
place_order Stores the order id (the vendor’s child order in Multi-Vendor).

Quoted prices apply on the storefront only; the back-end order editor keeps the prices stored in the order (stored_price).

gather_additional_product_data_post (storefront) adds $product.kv_rfq = {mode: B|O, hide_price: bool} and, for hidden prices, zeroes every price field (price, list_price, discounts, quantity discounts, zero_price_action) so core templates print no price. The SEO add-on’s structured data gets no offer price (seo_get_schema_org_markup_items_post). The button is rendered in the buttons:add_to_cart hook: .override for quote only, .post for “next to”.

The result depends only on the product and the visitor’s user groups. Product blocks and the cached category page get the user group signature in their block cache key (callable handler fn_kv_b2b_quote_request_block_cache_key, only when the settings make the output group-dependent) and kv_rfq_rules as an update handler.

  • Product pages: the button depends on user groups only; guests all get the same output.
  • Per-visitor output comes from the account menu (my_account is rendered per visitor) and the Quote list block (session_dependent, cache => false); after an AJAX add the counters are updated by func.js from the response.
  • The add-on’s own pages are not cacheable dispatches. No ?:storage_data writes: offer expiry is a cheap indexed UPDATE when quote pages load.

Events (schemas/notifications/events.post.php, vendor receivers in events_multivendor.post.php): kv_b2b_quote_request.created, .offer_sent, .staff_message, .customer_update. Data provider: QuoteDataProvider (variables: quote_number, customer_name, customer_label, company_name, items[] with product, options_text, requested_qty, quantity, price, subtotal, tiers[], total, currency_code, valid_until, shipping_note, message, author_name, status_name, update_title, quote_url, is_guest). E-mail templates are Twig and valid in Twig 2 and 3; prices use the core |price(currency_code) filter.

Type Hook / schema Purpose
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)
Schema block_manager/blocks (products cache; main cache overrides of categories.view, companies.products, products.view) The existing callable handler fn_kv_b2b_quote_request_block_cache_key() returns off without a valid license, so cached product blocks and pages switch between the quote button / hidden prices and normal prices at once
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)
Template (admin) index:styles (post) license.less

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

  • fn_kv_b2b_quote_request_is_licensed() is the gate. It reads the stored state only (one storage read per request) and never contacts the server. When it returns false:
    • gather_additional_products_data_pre / gather_additional_product_data_post add no kv_rfq data and hide no prices: no “Request a quote” button, normal prices;
    • pre_add_to_cart (CartBridge::filterAddToCart()) no longer refuses quote-only products;
    • the storefront controller redirects add, update_cart, submit and cart to My quotes (guests: the home page) without a message, refuses negotiate, reject and message, and allows accept only for quotes that are already Accepted (puts them into the cart again); the quote page is read-only (kv_rfq_readonly);
    • fn_kv_b2b_quote_request_cart_count() and fn_kv_b2b_quote_request_offers_count() return 0 (account menu, Quote list block);
    • the REST API refuses customer PUT (messages, rejections); reading and staff access are unchanged;
    • the admin quote list and quote page show the license status bar as a banner with what is switched off (root administrators only, never vendors).
  • Not gated, so accepted quotes keep working: CartBridge::validateCart() (quoted price on every cart calculation), keepQuotedPrice(), reserveForOrder(), linkOrder(), the back end, notifications, expiry and the REST API for staff.
  • 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_b2b_quote_request (?:storage_data), or var/kv_storage/kv_b2b_quote_request/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_b2b_quote_request_license.check.
  • Admin UI: License\Client assigns the view data of the license card to $kv_lic_kv_b2b_quote_request 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_b2b_quote_request_license_ui($context). Every full page of the add-on’s own admin controllers (quote list, quote page) 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.
  • The license check endpoint (kv_b2b_quote_request_license.check) accepts only CSRF-protected POST requests of administrators, and console requests. Vendors cannot use it.
Hook Arguments When
kv_b2b_quote_request_submit_pre &$quote, $group_items, $form, $auth Before a quote row is saved (once per vendor quote).
kv_b2b_quote_request_get_quotes $params, &$conditions Quote list query (add SQL conditions).
kv_b2b_quote_request_notify_pre $event, &$data, &$rules Before an event is dispatched.

/api/kv_b2b_quotes (administrators; vendors with “vendor quotes” on; customers with a token see their own quotes).

Method Path Description
GET /api/kv_b2b_quotes List. Filters: status, email, company_id, group_id, order_id, q, page, items_per_page.
GET /api/kv_b2b_quotes/:id Quote with items and messages (staff also get internal notes).
PUT /api/kv_b2b_quotes/:id Staff: items ({item_id: {quantity, price, discount, tiers: [{qty, price}], delete}}), shipping_note, valid_until (timestamp), send_offer (bool, with message), status + notify_customer, message + is_internal. Customer: message, reject.
DELETE /api/kv_b2b_quotes/:id Delete (administrators only).

Quotes are created on the storefront; accepting needs the customer’s cart, so it is a storefront action. Example:

Terminal window
curl -u [email protected]:API_KEY -H "Content-Type: application/json" -X PUT \
-d '{"items":{"12":{"price":24.5,"quantity":100,"tiers":[{"qty":500,"price":22}]}},"send_offer":true,"message":"Thank you"}' \
https://shop.example.com/api/kv_b2b_quotes/41

The add-on ships Upgrade\Synchronizer: after files are replaced in place, the first administrator request adds missing tables, columns, settings, language variables and e-mail templates, and updates the add-on version. Disable with define('KV_UPGRADE_SYNC_DISABLED', true);.

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