Dahu SAV
Solution de SAV tickets alliant SendCloud & Hubspot
by Hugo Vial-Jaime · github.com/epilouptique/dahu-sav · website
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/epilouptique/dahu-sav/archive/refs/heads/main.zipHubSpot-driven after-sales service (SAV) orchestration for WooCommerce.
The support team works entirely in a dedicated HubSpot ticket pipeline; this plugin listens to HubSpot private-app webhooks and executes every operational step on the WooCommerce side: warranty qualification, Sendcloud return labels, zero-total repair orders, and a read-only "My repairs" tab in the customer account.
Description
HubSpot decides, the plugin executes. Each ticket stage change fires a webhook to the plugin's REST endpoint. The plugin runs the matching action and writes the result back to the ticket (properties + note). WooCommerce remains the single source of truth for orders and stock — HubSpot never writes into WooCommerce directly.
Features
- Secure webhook endpoints — HubSpot v3 signature verification
(HMAC-SHA256 over method + URI + body + timestamp, 5-minute tolerance) and
Sendcloud HMAC signature. Immediate
200response, asynchronous processing through Action Scheduler (groupdahu-sav), deduplication by event id. - Automatic warranty qualification — configurable warranty duration (default 24 months) starting from the PDF invoice date, with traced fallbacks to the payment date then the creation date. Repair orders carry the original order reference, so warranty is always evaluated against the original purchase (recursive lookup with loop guard).
- Sendcloud return labels — shipping-option discovery through
POST /api/v3/shipping-optionswithfunctionalities.returns=true, weight-tier selection, strict idempotency viaexternal_reference_id = sav-{ticket_id}(re-triggering a stage never creates a second label). The PDF is stored in a protected upload folder and served only through a signed download URL. A customizable WooCommerce email delivers the label to the customer, then the ticket advances automatically. - Zero-total repair orders — real product references with line subtotals
and totals explicitly set to 0 (no tax recalculation), created
on-hold; moving the order toprocessingmanually triggers the standard WooCommerce stock decrement and shipping export. Optional 0-cost shipping line for carrier routing rules. - "My repairs" account tab — read-only, fed by a local mirror table kept in sync by webhooks. No HubSpot API call is ever made on page load.
- Staging mode — forces a return method billed on first scan only, so test labels that are never dropped off are never invoiced.
- Logging — WC_Logger source
dahu-savplus a 500-event table displayed on the settings page. No silent failures: every automation error is written to the ticket (sav_erreurproperty + note) and logged. - Secrets can be stored encrypted (AES-256-CBC) by defining a
DAHU_SAV_KEYconstant inwp-config.php.
Installation
- Upload the
dahu-savfolder to/wp-content/plugins/(or install the zip from Plugins → Add New → Upload). - Activate the plugin. WooCommerce 7.0+ is required (HPOS compatible).
- Optionally add
define( 'DAHU_SAV_KEY', 'a-long-random-string' );towp-config.phpto encrypt stored API keys.
Configuration
Open WooCommerce → SAV HubSpot:
- HubSpot — paste the private-app token and client secret, set the SAV pipeline id, then use Load pipelines to review the stage ids and pre-fill the stage mapping. Every stage key must be mapped to a HubSpot stage id; events from any other pipeline are ignored.
- Sendcloud — API public/secret keys, webhook secret, preferred return shipping option code, default parcel weight and the workshop return address.
- Warranty & emails — warranty duration, packaging instructions, return deadline.
- Use Test connections to verify both APIs, then register the two
webhook URLs shown at the top of the page in the HubSpot private app
(
object.propertyChangeonhs_pipeline_stageandsav_numero_commande) and in the Sendcloud panel.
Changelog
1.8.0
- Fix return carrier being ignored: the idempotency paths reused an existing Sendcloud return without any carrier check. Carrier is now verified on every path (reuse, remote lookup, conflict, creation) with a tracking-format heuristic; strict priority order: staging mode always forces Colissimo, otherwise the ticket carrier property, otherwise a traced block — the default-method setting and every silent fallback are removed.
- New customer "shipped" email at the Shipped stage (case reference, diagnosed failure, workshop notes, tracking number), sent once.
- Warranty cases are automatically closed after shipping (quote cases stay in Shipped for manual closing).
- Optional internal notification email on every plugin-created order.
- "SAV intervention" block (reference, failure, workshop notes) on PDF invoices and packing slips (wpo_wcpdf hook, data copied to order metas — no API call during PDF generation).
- Qualification note reminds the team to use the quote thread for partially-warranted cases (warranted lines at 0, others at real price).
1.7.0
- Per-ticket return carrier: a dedicated dropdown ticket property (Chronopost / Colissimo) selects the Sendcloud option family for the return label, with the weight tier from the weight property. Empty or unrecognized value falls back to the default method setting (traced). Staging mode keeps absolute priority and forces Colissimo regardless of the ticket choice. The label ticket note always states the retained carrier and why (team choice / fallback / staging forced).
1.6.1
- Fix admin AJAX responses being corrupted by an invisible UTF-8 BOM at the top of two plugin PHP files (introduced by a tooling encoding issue): the three stray bytes were prepended to every admin-ajax JSON response, which broke the strict JSON parsing of WooCommerce enhanced selects — most visibly the customer search on the new-order screen, stuck on "Searching…" with a seemingly valid response and a clean console.
1.6.0
- Fix staging mode not being honoured by Sendcloud return creation: the requested shipping option code is now sent in both known payload shapes, the created return's carrier is verified against the requested one (loud failure with cancellation instructions on mismatch, no customer email), and the retained option plus active staging mode are always logged.
- Richer return-label email: three configurable sections (pro-partner warning, "How to proceed?", packaging instructions) with sensible defaults, editable in the settings.
- Qualification now runs as soon as a SAV-pipeline ticket has an order number (property change or "new ticket" stage), with idempotency: no re-qualification for the same order number, re-qualification when it changes. The "Qualification" stage remains a manual re-trigger.
1.5.0
- Return parcel weight now comes from a dedicated ticket property (kg) entered by the team, replacing the product-based resolution of the returned-items text (that field is now free human text only used in the customer email). Falls back to the default weight setting when empty or invalid, with the retained weight and its source stated in the label ticket note. The product resolver code is removed.
1.4.0
- Short sequential case reference SAV-{year}-{4-digit counter} generated at qualification (atomic counter stored as a WordPress option), written to the dedicated ticket property and used to rewrite the ticket subject as "{reference} — Order {no} — {Customer}". The reference is cited in every customer email, order note and ticket note, and becomes the main identifier in the "My repairs" tab (case list with reference, click through to a detail view).
1.3.0
- Amendment 2.2 — parts are now entered in WooCommerce, never as free text: repair and quote orders are created without product lines; the workshop adds parts through the native order item selector. Warranty orders force every added line to 0 while on hold; quote orders keep natural catalog prices. The stock-exclusion mechanism and the replaced-parts property parsing are removed — all lines follow standard stock behaviour.
- Quotes are no longer emailed on creation: a new "Send SAV quote to the customer" order action sends (or re-sends, traced) the quote email, and the unpaid-quote reminder now counts from that send date.
- New workshop-notes ticket property surfaced in the customer "My repairs" tab, in the ticket note at repair completion, and stored in the mirror table (schema upgraded automatically).
1.2.1
- Fix stock being reduced on customer-return lines at the status change to
processing: the automatic reduction path (
wc_reduce_stock_levels) never goes throughwoocommerce_prevent_adjust_line_item_product_stock(that filter only covers manual admin adjustments and refund restocks). Flagged lines now also return a quantity of 0 throughwoocommerce_order_item_quantity, keeping stock intact on both paths.
1.2.0
- Phase 2.1 amendment: free-text failure detail property picked up in
qualification notes; repair/quote orders now build two line categories —
customer parts being returned (always 0, excluded from stock adjustments
via a line meta and the
woocommerce_prevent_adjust_line_item_product_stockfilter) and workshop-replaced new parts (standard stock behaviour, 0 under warranty, catalog price otherwise). Order creation is blocked when the replaced-parts property is empty (conventional value "aucune" accepted). - Stage mapping is now fully generic: new pipeline stages only require a settings entry, no code change. New "refusal — call customer" stage supported out of the box; the quote-refusal note reminds the team to call the customer before closing.
1.1.0
- Phase 2 — out-of-warranty thread: "ship at your expense" customer email
(items list, return address, deadline; no return label on this thread),
quote order creation (
pending, real prices, configurable labor product, native payment link emailed to the customer), automatic ticket advance to "in repair" on payment, daily unpaid-quote check (ticket note + error property, no automatic cancellation) and quote-refusal handling (pending order cancelled, optional customer email). - New settings: labor product (ID or SKU), unpaid-quote reminder delay, extra text for the out-of-warranty email, refusal email toggle.
1.0.4
- Fix fatal error on the signed label download route: the
ticket_idargument used nativeis_numericas REST validate callback, which receives three arguments (ArgumentCountError). Replaced with a closure plusabsintsanitization. Other routes audited: no other occurrence.
1.0.3
- Fix webhook event routing: HubSpot sends
ticket.propertyChange(notobject.propertyChange) as subscription type, so stage changes never triggered any automation. Routing now matches any*.propertyChange. - Richer debug logging before the "no action" decision (subscription type, property, pipeline read vs configured, stage id vs mapping) and explicit error when a received stage id is missing from the configured mapping.
1.0.2
- Sendcloud webhook: tolerant mode when no webhook secret is configured (signature check skipped with a logged warning); strict verification is unchanged when a secret is set. HubSpot verification remains strict.
1.0.1
- Fix fatal error on activation (email class was declared inside a method).
1.0.0
- Initial release: guaranteed-repair loop end to end — webhook endpoints, warranty qualification, Sendcloud return labels, zero-total repair orders, "My repairs" account tab, settings page, logging.
License
GPL v2 or later — see https://www.gnu.org/licenses/gpl-2.0.html.