Discounts And Coupons

Does obfuscation break discount and coupon rules?

A coupon engine is a pile of small flags on data objects, and each one is the only thing standing between a promotion and a cart that collects nothing. This pass protected a file that configures one, renamed its property names a group at a time, and read the order total off the far side. The result is not the one the series usually produces: the two arms that looked harmless were harmless, and the arm that combined them cost seventy-four percent of the cart.

The cart, and the two things protecting it

The sample is an ordinary storefront configuration. Seven coupons are defined. Most are combinable. One, FLASH60, is sixty percent off and carries an exclusive flag, so the engine refuses to apply it alongside anything else. Above all of them sits a maxDiscountPercent of thirty, a ceiling on how much of any cart a promotion may consume. The gift-card line is on an exclusion list so it can never be discounted, and an eligibility callback keeps the staff coupon in the hands of staff.

The order that matters is the one a shopper assembles after finding every code on a deal forum: WELCOME10, SUMMER20, FLASH60 and BULK100 presented together against a 500 dollar cart. Unprotected, the engine applies three of them, refuses FLASH60 with exclusive-cannot-combine, hits the thirty percent ceiling, and collects 350 dollars. Two independent controls each did part of that work, and either one alone would have kept the damage small.

That is a deliberate design. Defence in depth on a discount engine looks exactly like this: a rule about which coupons may combine, and a separate rule about how far any combination may go. They are written by different people at different times for different reasons, which is precisely why nobody thinks of them as one control.

Each half on its own is the finding a reviewer closes

Renaming exclusive alone changes the visible outcome by nothing at all. FLASH60 now combines, so the engine applies four coupons instead of three and the refusal line goes from FLASH60:exclusive-cannot-combine to refused=(none). The order total is 350 dollars, exactly as before. The reported terms move from FLASH60=60%/exclusive to FLASH60=60%/combinable. Nothing else. The ceiling absorbed the entire loss.

Renaming maxDiscountPercent alone is visible but survivable. The cap reads (none - a cart can go to zero), the exclusivity rule still refuses FLASH60, and the total drops from 350 to 310. Forty dollars on a five-hundred-dollar cart. A merchandising team would find that eventually in a margin report, and might well attribute it to a promotion that ran longer than planned.

Renaming both in one pattern took the same cart to 130 dollars. Four coupons applied, no refusals, no ceiling: a discount of 370 dollars where the configuration intended 150. Nothing threw, nothing logged an error, and the order record is a perfectly ordinary one. The two clean-looking halves were not evidence that the pair was clean.

Why the halves lie, and what that means for reading any matrix

This is the shape a previous pass in this series named after a data-retention engine: two options on one object whose library defaults point in opposite directions, so a wider pattern can be safer than a narrower one. Here the mechanism is different but the reading lesson is identical. The ceiling is consulted last, and while it is intact it truncates whatever the stacking rules let through, so any damage upstream of it is invisible in the total.

The practical consequence is about how a test is designed, not about obfuscation. If this configuration had been checked with one broad, realistic pattern covering the whole options object, the 130 dollar cart would have appeared and been fixed. It shows up in the narrow, careful, single-name patterns too, but only in the one that happens to take both names. Test the halves, and test them in combination; the dangerous cell is rarely at the wide end of the matrix.

It also says something about triage. An arm that moves only a reported reason string -- exclusive-cannot-combine becoming (none) -- reads as cosmetic and gets closed. In this measurement that exact signature was the leading indicator of the worst result in the file.

The rest of the coupon surface, in one list

Renaming expiresAt on a coupon object made a promotion that ended on 30 April work in August: the engine parses the date, gets NaN, and every comparison against NaN is false, so the coupon is never expired. The spring sale ran silently forever, worth 75 dollars on the sample cart. Renaming maxRedemptions retired the limit on a fifty-percent code that had already been used its ten times. Renaming minSpend let a 100-dollar-off code apply to a 120-dollar cart.

Two arms failed the other way. Renaming percentOff made every percentage coupon worth nothing -- the terms line reads WELCOME10=?/combinable and the discount goes to zero, which customers report within the hour. And renaming eligible, the field the site's own eligibility callback returns its answer in, refused every coupon including the legitimate ones.

That last refusal is worth quoting because of how it reads in a log: refused=WELCOME10:not-eligible(ok). The callback ran, decided the customer was eligible, and put the word ok in the reason field it also returns. The engine could no longer find the decision, defaulted to refusing, and printed the guard's own reason for allowing it. A support ticket containing a line that says both things at once is the fastest diagnosis available here, and it is worth recognising on sight.

What protection alone did

Nothing. The file was protected on five profiles -- the ES5 default, the modern target, both emit-gate configurations and the string-encoding profile -- and produced byte-identical output on all five. Every coupon applied, every refusal fired, and the totals matched to the cent. That is the expected result and it is the one worth stating plainly: renaming identifiers and encoding strings does not disturb a promotion engine.

Every result above needed member renaming, which is off by default and requires a regular expression naming the members it may rewrite. The failures are not a property of obfuscation; they are a property of pointing a renaming pattern at names that a library you did not build is reading.

What to do about it

Keep MemberRegexp anchored to names your own code owns on both sides. A discount configuration is the opposite of that: your file writes the names and an installed engine reads them, so every key on that object is a contract. The same applies to the flags on the coupon rows themselves, which are easy to overlook because they look like data rather than configuration.

Where a discount rule matters to the accounts, do not let the client be the only place it is enforced. A cart total assembled in the browser and accepted by the server is a problem long before anyone obfuscates it; the measurement here simply shows one more way the client-side copy can drift from the intent without anybody noticing. Recompute the discount server-side and compare.

And add one assertion to the checkout tests that no rename can satisfy accidentally: take a cart with a known answer, run the full coupon set against it, and assert the exact total. Every result in this article would have failed that assertion on the first build.

Frequently asked questions

Does obfuscating a storefront change what a coupon is worth?

Not by itself. The sample was protected on five profiles including the ES5 default, the modern target and string encoding, and produced identical order totals on all five. Every result in this article required member renaming, which is off unless you enable it and give it a pattern.

Why did renaming the no-stacking flag look harmless?

Because a separate cart-wide discount ceiling truncated the extra discount before it reached the total. The order total was unchanged; only the reported refusal reason moved. That is why a half-arm that moves only a label should be treated as a warning rather than a cosmetic finding.

What happened when both the flag and the ceiling were renamed?

A 500 dollar cart collected 130 dollars. Four coupons applied instead of three, no refusal fired, and no ceiling truncated the result: a 370 dollar discount where the configuration intended 150.

Can a renamed expiry date make an old coupon work again?

Yes. The engine parses the date and compares it to now. A date it cannot read yields NaN, every comparison against NaN is false, and the coupon is never expired. A promotion that ended in April applied in August in the measurement.

How would this show up in a support queue rather than a test?

As a refusal whose reason contradicts itself. When the field carrying an eligibility decision is renamed, the engine cannot read the verdict, refuses by default, and still prints the reason the check supplied for allowing it -- a log line reading not-eligible(ok). If you see one, look at renaming before looking at the rule.

Which names should a renaming pattern never touch here?

Any key on the options object the promotion engine reads, and any flag on the coupon rows themselves. Both are read by installed code that is not rebuilt with your bundle, so your side of the name moves and theirs does not.

Is a server-side recomputation enough on its own?

It is the control that matters most, because it does not depend on the client build being correct. Recompute the discount from the coupon definitions server-side and compare it to what the client applied; a mismatch is then a refused order rather than a silent margin loss.

Related reading