Jump to content

Bug: Non-applicable promotion code silently attached to domain on cart renewal, causing incorrect discounts on future renewal invoices


Recommended Posts

 Summary

  When a client renews a domain through the shopping cart while ANY valid promotion code is present in the cart session, WHMCS stores that promotion's ID in `tbldomains.promoid` — even when the promotion does NOT apply to the domain's TLD (the cart correctly shows "no items qualify" and no discount is given on the renewal order).

  One billing cycle later, the automated renewal invoice for that domain is generated with a `PromoDomain` discount line item for that promotion. This happens even though the promotion's "Applies To" configuration contains no TLDs, and even after the promotion's expiry date has passed — so the client receives a recurring discount from a promotion that was never valid for domains and is no longer active.

  Environment

  - WHMCS 8.13 (reproduced on demand; also observed on earlier 8.x)
  - Promotion: Percentage, Recurring enabled, "Applies To" contains hosting products only (no TLDs)

  Steps to Reproduce

  1. Create a recurring percentage promotion (e.g. 5%) whose "Applies To" contains only hosting products — no domain TLDs.
  2. As a client with an active domain, start a domain renewal in the cart.
  3. Apply the promotion code. The cart correctly shows: "The promotion code you entered has been applied to your cart but no items qualify for the discount yet" and the discount line shows 0.
  4. Complete checkout. The renewal invoice is generated at full price with no discount line — correct so far.
  5. Inspect the database: `SELECT promoid FROM tbldomains WHERE id = <domain>`
     → the promotion's ID has been stored, despite the promotion not applying.
  6. Wait for (or trigger) the next automated renewal invoice generation for this domain.

  Expected Result

  The promotion is never associated with the domain (it does not apply to the TLD), and future renewal invoices are generated at full price.

 Actual Result

  The next automated renewal invoice contains a `PromoDomain` line item discounting the renewal (e.g. "Promotion Code: XXXX - 5% Recurring Discount"). This repeats every renewal cycle. The discount is applied even after the promotion's expiry date has passed, and even though the promotion's "Applies To" has never included any TLD — so neither appears to be validated when the renewal invoice is generated.

  The issue is also effectively invisible to auditing: because the promotion was not applied at checkout, `tblorders.promocode` remains empty, and no activity log entry records the change to `tbldomains.promoid`.

  Real-World Occurrence (our production data)

  - 2024-09: Domain registered normally, no promotion (order promocode empty, promoid = 0).
  - 2025-08: Client renewed the domain via cart with a hosting-only recurring promo code in the cart session. Renewal invoice was full price, order promocode empty — but `tbldomains.promoid` was set to that promotion.
  - 2026-07: Cron-generated renewal invoice included a PromoDomain discount line for that promotion — which had also expired in 2025-08.

  We then reproduced this on demand with a second domain and a different non-applicable promo code: the cart showed "no items qualify", the checkout invoice had no discount, yet `tbldomains.promoid` was written, and a forced next-cycle invoice generation produced the erroneous PromoDomain discount line.

 Suggested Fix

  1. During cart domain renewal checkout, only associate the promotion with the domain when the promotion actually applied to that domain's TLD/period — i.e. the same situations in which the order itself records the promotion code and a discount is given.
  2. During automated invoice generation, validate the stored promotion against its "Applies To" configuration and expiration date before adding a PromoDomain line item.

  Related history for context: Per the published changelogs, CORE-7379 (7.1) introduced applying recurring promotion codes to renewal invoices; CORE-14101 (8.1) fixed the one-time promo path for domain renewals. The recurring path described above remains affected.

Link to comment
Share on other sites

  • 2 months later...

Thanks for writing this up properly, the repro steps are clearer than most tickets I've seen. Nobody's replied in a while, so a few things that might help anyone else who lands here while it's open.

 

On the expiry part: I don't think that piece is a separate bug. Per the Promotions docs, the expiry date only stops the code being used on new orders. Anything that already has a recurring promo attached keeps it on future invoices until the "Set Recur For" count runs out, and if that's 0 it keeps it forever. So once promoid gets written to the domain, the expiry won't save you. The real bug is the first half, the promo getting associated with a TLD it never applied to.

 

Until it's fixed, what I'd do on a live install:

 

1. Audit it. Take a DB backup, then pull every row in tbldomains where promoid isn't 0 and join it to tblpromotions on the promotion id so you can see the code and its Applies To list next to each domain. Any domain linked to a promo whose Applies To has no TLDs (or not that TLD) is affected. On most installs that list is short enough to check by hand.

2. Clean it. Set promoid back to 0 on those domains only. Then check for unpaid renewal invoices that already went out with a PromoDomain line, because clearing the field won't touch invoices that already exist. Edit those or cancel and regenerate them.

3. Limit the damage going forward. For hosting-only recurring promos, give "Set Recur For" a real number instead of leaving it unlimited, so even if one leaks onto a domain it stops on its own. If you run promos for marketing, it's also worth telling staff not to apply them in the cart when a client is only renewing domains.

4. Recheck after each cron run for a while. A saved query you rerun weekly is enough to catch new ones until a fix ships.

 

Have you opened a ticket with this repro? If support gives you a case ID, please post it here so others can track it in the changelogs. Did you also test this on 9.x, or only 8.13?

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...

Important Information

By using this site, you agree to our Terms of Use & Guidelines and understand your posts will initially be pre-moderated