Part of the TackQuote integrations family. All platforms are listed on the TackQuote GitHub organization page: github.com/tackquote (TackQuote integrations index).
A PrestaShop 1.7 / 8.x module that adds a "Request a Quote" button to product pages and connects the store to a TackQuote B2B quoting account (API base URL
- API key). It mirrors the pattern used by the TackQuote WooCommerce plugin.
This module is separate from apps/api/src/modules/integrations/prestashop/prestashop.service.ts.
That service is Tack acting as a client of your store's PrestaShop
Webservice API (pulling products/orders, pushing orders) using a PrestaShop
API key stored in Tack. This module is the reverse direction: your
PrestaShop store acting as a client of the Tack API, using a Tack API
key, the same way the WooCommerce plugin does.
Distribution authority: the public GitHub release asset is
tack-prestashop.zip,
built by scripts/package.sh. The link resolves to the newest release rather than a
pinned tag, so it never goes stale when the next version ships.
This repository is source only. No PrestaShop Addons listing is claimed.
-
Download
tack-prestashop.zipand follow the included README, or build it from this checkout. The module is the repository root, and the zip's top-level folder must betackquotes/whatever your checkout is called, so use the packager rather than zipping the checkout by hand:bash scripts/package.sh # writes dist/tack-prestashop.zip -
In your PrestaShop back office, go to Modules > Module Manager > Upload a module and upload
tack-prestashop.zip. -
Once installed, click Configure on the TackQuote module.
-
Enter your TackQuote API URL (defaults to
https://api.tackquote.com/v1) and paste your TackQuote API key (create one in TackQuote under Settings > Developer > API Keys — the same kind of key the WooCommerce plugin uses).The key must carry the
quotes:writescope. Test connection uses the unscopedpingroute, so a key without it passes the test and then fails every real quote submission with a 403. -
Click Save, then Test TackQuote connection to confirm connectivity.
-
Toggle "Show 'Request a Quote' button on product pages" and set the button label as desired.
- Module scaffold (
tackquotes.php,config.xml) — a standard installable PrestaShop module:install()/uninstall()register/remove config values and hooks; metadata (name, version, author "TackQuote") is set correctly for the Module Manager. - Settings page (
getContent()/views/templates/admin/configure.tpl) — a realHelperForm-based admin form that persists the API URL, API key, button-enabled flag, and button label viaConfiguration::updateValue(), plus a "Test connection" action. This is a genuine settings UI; PrestaShop had none before this module (there was no seller-facing PrestaShop settings page anywhere in the repo,apps/webincluded). - Storefront button (
hookDisplayProductActionsintackquotes.php+views/templates/hook/quote_button.tpl+views/js/tackquotes.js) — a real hook implementation.displayProductActionsis the current PrestaShop 1.7/8 hook for action buttons rendered next to "Add to cart" on the product page. The button opens a small modal (email, quantity, note) and POSTs to this module's own front controller. - Front controller (
controllers/front/quoterequest.php) — a realModuleFrontControllerthat validates the submitted email, loads theProductby ID, builds a line item (SKU, name, quantity, unit price, external product ID), and callsTackApiClient::createQuoteRequest(). - API client (
classes/TackApiClient.php) — a real cURL-based HTTP client authenticated the same way the WooCommerce plugin'sTack_Api_Clientis:Authorization: Bearer <key>+X-Api-Key: <key>headers, matching howApiKeyGuardon the Tack API accepts credentials (apps/api/src/common/guards/api-key.guard.ts).
PrestaShopPluginController
(apps/api/src/modules/integrations/prestashop/prestashop-plugin.controller.ts) serves all
of them, behind ApiKeyGuard — the tenant is resolved from the API key, not a seller JWT.
This section previously said "Neither route exists yet" and listed the controller as work still to do. That was stale: the controller shipped, and "Test connection" and the storefront button both work against a current Tack API.
-
GET /integrations/prestashop/ping→{ ok: true, tenantId }. Used by "Test connection", which falls back toGET /healthonly for a Tack deployment older than that route (a/healthsuccess proves the URL is reachable, not that the key is valid for a tenant). -
POST /integrations/prestashop/quote-requests→ upserts the referenced products, then registers the buyer and creates a draft quote through the canonical pipeline.Request:
{ buyerEmail, note, source: "prestashop", currency?, firstName?, lastName?, phone?, companyName?, lineItems: [{ sku, name, quantity, unitPrice, externalProductId }] }Response:
{ id, quoteNumber, portalUrl, company, awaitingApproval } -
POST /integrations/prestashop/order-sync→ exists, but this module does not push orders (no order hook is wired up, unlike the WooCommerce plugin'sTack_Order_Sync), so it is only relevant if order-push-from-storefront is wanted later.
firstName and lastName are collected by the product-page modal and sent as fields.
They did not exist before, and the endpoint — knowing only an email address — wrote its
local part into buyers.first_name. A shopper at ps-probe@example.com became a buyer
literally named "ps-probe", which a seller could not tell apart from a name somebody
typed. Nothing failed: the request answered 201. Confirmed against the live API, quote
TK-2026-001085.
- Both fields are optional and stay empty when blank. Every already-installed copy of
this module posts email/note/quantity/product_id and nothing else, and those submissions
still answer
201— the buyer is created with no name rather than an invented one. A blank input is omitted from the payload, never sent as"". - Validated with
Validate::isName(), notValidate::isCustomerName(): the latter is what PrestaShop uses forcustomers.firstname/lastnamebut only exists from 1.7.6, and this module declaresps_versions_compliancymin1.7.0.0. - Prefilled from the signed-in customer (
$this->context->customer, only onceisLogged()), the same source the OpenCart drawer uses. Guests get empty inputs; nothing is guessed. companyNameandphoneare accepted by the endpoint but not collected by this module. Adding a company input is a separate decision, because a company name resolves to a real company record and applies the seller's TackQuote registration policy — a tenant set tocompany_only, or requiring a business email or particular company details, would then refuse submissions this modal cannot collect the answers for.GET /v1/integrations/woocommerce/registration-configis the endpoint that exists to drive that rendering.
Configure > Quote-only (B2B catalog) mode. Off by default; installing the module never changes what a storefront is allowed to do.
When it is on, an in-scope shopper gets a storefront where "Add to cart" and checkout are refused by the server and the "Request a Quote" button takes their place.
TackQuotes::hookActionFrontControllerInitBefore() in tackquotes.php. It runs from
the first statement of FrontController::init()
(classes/controller/FrontController.php:264-269, PrestaShop 8.2), i.e. before any
controller does anything, and it does two things:
- Refuses the request. A cart-mutating request (
addorupdate, in$_GETor$_POST) is rejected outright — 403 + PrestaShop's own ajax error envelope for XHR, redirect back to the product otherwise.CartController::updateCart()never runs. This is what answers a crafted POST; no CSS or JS is involved anywhere in this module's enforcement. - Engages PrestaShop's own catalog mode for this request only, which inherits
core's whole enforcement surface:
Cart::updateQty()(classes/Cart.php:1563-1569),Cart::checkQuantities()(classes/Cart.php:4120), the cart page and its two ajax endpoints (controllers/front/CartController.php:99,:126,:166) and checkout (controllers/front/OrderController.php:246).
The persistent setting the merchant edits in Shop Parameters > Product Settings >
Catalog mode is never written by this module. Configuration::updateValue() is
never called for it, and uninstall() deliberately does not touch it.
What the module does call is Configuration::set(), whose own docblock reads "Set
TEMPORARY a single configuration value" (classes/Configuration.php:369-376) and
whose body writes only the in-memory caches — there is no Db call anywhere in
classes/Configuration.php:377-406. Nothing is persisted; the flag lasts exactly one
request.
Driving the real setting was rejected for three reasons:
- It is global.
PS_CATALOG_MODEhas no notion of a customer group, so "guests only, approved B2B buyers keep the cart" would be impossible. (Core does have a per-group catalog mode viaGroup::show_prices—Configuration::isCatalogMode()atclasses/Configuration.php:697-705— but it works by hiding prices for that group, which is a different feature.) - The merchant also controls it. Two writers on one setting means our uninstall either clobbers a value the merchant set by hand, or leaves the shop stuck in catalog mode after the module is gone. Both are worse than not writing it.
- It is not reversible per shopper. Employee preview and post-purchase pages both need the mode off within the same shop, in the same minute.
If the merchant turns the native setting on themselves, nothing here fights them:
Configuration::isCatalogMode() is already true and the module simply behaves as the
theme does.
In the classic theme, {hook h='displayProductActions'} sits at
themes/classic/templates/catalog/_partials/product-add-to-cart.tpl:64, and line 26
of that same file wraps everything from there down in {if !$configuration.is_catalog}.
Disabling the cart therefore stops the theme calling that hook at all — the quote
button would have vanished along with "Add to cart". This is the same shape of bug as
the WooCommerce plugin hanging its button off a hook that only fires inside the
add-to-cart form.
So the CTA is published on three hooks and exactly one renders per page:
| hook | used when | why it survives |
|---|---|---|
displayProductActions |
normal storefront | current placement, next to "Add to cart" |
displayProductAdditionalInfo |
quote-only mode | product-additional-info.tpl:26 is included by product.tpl:126-128 outside the {if !$configuration.is_catalog} guard |
displayFooter |
neither of the above rendered | last-resort net; deliberately conspicuous, because it means the theme needs a template tweak |
All three are template-emitted hooks, so a sufficiently unusual theme could still call none of them — which is what the footer net is for.
On top of that, quote-only refuses to engage at all unless the CTA can actually
render: it requires a saved API key and the storefront button switched on, because
renderQuoteWidget() returns '' without either. A store is never left unable to
both buy and ask. TackQuoteOnlyMode::applies() enforces this (ctaReady), the
settings form refuses the combination at save time, and the Modules page shows a
warning if it is ever reached.
| Setting | Behaviour |
|---|---|
| Everyone (default) | every visitor gets the quote-only storefront |
| Guests only | signed-in customers keep a working cart; everyone else must request a quote |
| Specific customer groups | a visitor in any ticked group gets quote-only; a group scope with nothing ticked does not engage |
Groups come from Customer::getGroupsStatic(), which answers with
PS_UNIDENTIFIED_GROUP for anonymous traffic (classes/Customer.php:1098-1100), so
there is no separate anonymous branch.
A back-office preview (?preview=1&adtoken=…&id_employee=…) sees the ordinary
storefront, cart included. The check reproduces core's own
(controllers/front/ProductController.php:136-142) rather than calling
ProductController::isPreview(), because that flag is set inside init() and this
guard runs before init(). Limit: the exemption covers page rendering. The
theme's add-to-cart form does not carry adtoken, so an employee clicking "Add to
cart" from a preview page is still refused unless the request itself carries the
preview credentials. Native catalog mode has no preview exemption at all, so this is
strictly better, not worse.
- Existing carts are not deleted. Toggling quote-only on leaves every cart row untouched, so toggling it off restores them intact. Destroying customer data on a settings change is not reversible and was not asked for.
- Removing products from an existing cart is still allowed.
deleteis deliberately excluded from the blocked operations (TackQuoteOnlyMode::CART_ADD_KEYS): a shopper who had a cart when the switch was flipped must be able to empty it rather than be stranded with items they can no longer check out. - Checkout is blocked by core, at
controllers/front/OrderController.php:246. - Post-purchase pages are explicitly left alone —
history,order-detail,order-follow,order-slip,order-return. Core's catalog mode redirects all five away (HistoryController.php:48,OrderDetailController.php:157,OrderFollowController.php:105,OrderSlipController.php:44,OrderReturnController.php:87), which would retroactively hide the invoices and order history of customers who bought while the shop was still selling. The request-scoped flag is simply not set on those pages (TackQuotes::POST_PURCHASE_PAGES). - Prices: PrestaShop hides prices in catalog mode unless "with prices" is also on
(
src/Core/Product/ProductPresentationSettings.php:46), so the module exposes a "Keep prices visible in quote-only mode" switch, default on.
tests/QuoteOnlyModeTest.php loads the real TackQuotes class against small
PrestaShop stubs and calls the real guard — it asserts behaviour, not source text.
docker run --rm -v "$PWD":/p -w /p php:8.3-cli \
php tests/QuoteOnlyModeTest.php
tests/ is excluded from the release zip by scripts/package.sh.
- Nothing here has been exercised in a running PrestaShop. The behaviour above is
derived from PrestaShop 8.2 source read out of the
prestashop/prestashop:8.2-apacheimage, and from the PrestaShop docs MCP for hook names; the admin form has not been rendered in a live back office. - UNVERIFIED: interaction with Smarty template caching (
PS_SMARTY_CACHE). Quote-only makes the product page differ per visitor.Module::getCacheId()does include the visitor's groups (classes/module/Module.php:2225-2228), which is the right shape, but this was not confirmed against a running shop with caching on. Treat quote-only + full-page caching as unverified. - Themes other than
classicwere not inspected.
- Order sync (PrestaShop order -> TackQuote). The WooCommerce plugin has
Tack_Order_Synchookingwoocommerce_order_status_changed; this module has no equivalent yet.PrestaShopService::syncOrdersInbound()already exists server-side (pull-based, via the seller-portal-authenticated connector), so this may not even be needed for parity — PrestaShop orders can be pulled instead of pushed. - A logo/icon asset for the Module Manager listing.
- Translation files (
translations/) — user-facing strings use$this->l()/{l}so they're translation-ready, but no.phptranslation catalogs are included. - A cart-level "Request a Quote" button (only the single-product page button is implemented, matching the task's storefront button requirement).
Reconciled against the TackQuote monorepo copy of this module before that copy was
retired. Nothing in tackquotes.php, classes/, controllers/ or views/ changed.
Carried across from the monorepo:
- The distribution link above pointed at release
v1.1.0. It now resolves to the newest release, which is whatscripts/package-all.shpublishestack-prestashop.zipto.
Corrected in this copy rather than taken from the monorepo:
- The packaging step said
cd integrations/prestashop/modulesand the intro pointed atintegrations/wordpress/tackquote/. Both carry the monorepo's directory prefix, which does not exist in this repository. (The monorepo copy additionally spelled the WooCommerce plugintack-quotes/; that directory does not exist in either repository, so that change was not taken.)
Newly pinned by tests/QuoteOnlyModeTest.php, because all three of these shipped once
and nothing could see them:
config.xml<version>must equal$this->version. They disagreed through two releases, and PrestaShop keys upgrades off that value — merchants were never offered an upgrade.config.xmlneed_instancemust be1, or the module's own "no API key is set" warning is never constructed and never shown.- The module description must not advertise order sync. It does not do order sync — see Not implemented above — and the monorepo copy still claims it does.