The moderator opened by pressure-testing a proposed fix for a promotion stacking bug that risked $2.3M/week in revenue. Feedback from the panel steered discussion toward enforcing deterministic sort orders and replacing global flags with a two-phase evaluation engine before closing on strict checkout invariants and fail-loud configuration policies.
The session landed on a GO decision for a two-phase evaluator that enforces global dominance and authoritative checkout repricing, provided you treat multi-global winner rules as provisional defaults pending product review.
Threads
Two-phase evaluation guarantees global dominance over group combinations.
The panel moved from debating priority-based sorting to adopting a two-phase mechanism where global promotions are evaluated first and win exclusively. This shift resolved the risk of high-value staff discounts being skipped by group logic, though it silently decides that globals beat any group sum—a policy choice marked as provisional.
Authoritative checkout repricing replaces stale-price grace periods.
From the start, the panel rejected honoring cached cart totals to prevent fraud and accounting errors. The final invariant mandates that payment authorization matches only the exact amount and quote version most recently confirmed by the customer after a fresh engine evaluation.
Fail-loud configuration blocks runtime guessing on unknown group policies.
An early proposal to default unknown group behaviors to a safe singleton mode was challenged as a silent margin leak. The session converged on failing closed at activation, rejecting any promotion where the group policy is undefined to ensure no runtime improvisation occurs.
Pressure-test a proposed fix to our promotion stacking engine - CORE checkout path $2.3M/week. Attack correctness. ## System: cart-pricer Go `promotions(code, type, priority INT, exclusive_group TEXT, exclusivity_scope ENUM('group','global','none'), stackable BOOL)` `cart_discounts`. Old code: `has_non_stackable bool` global flag. ## Bug SUMMER20 (20% off, exclusive_group='seasonal', exclusivity_scope='group', stackable=false, priority 10) and WELCOME10 ($10 off, group='lifecycle', scope='group', stackable=true, priority 20) - user expects both (different groups). Engine set global `has_non_stackable=true` after SUMMER20 and skipped WELCOME10. 114 tickets. Second trap: EMP50 (50% staff, group='employee', stackable=false, scope='global') - if we naively fix to group-scoped only, EMP50 would stack with seasonal, which must NOT happen. This is the missing concept. ## Proposed fix Replace global bool with: ``` nonStackableByGroup map[group]bool globalExclusiveApplied bool ``` Evaluation: 1. If `globalExclusiveApplied` -> skip any further promo (global blocks all). 2. If current `exclusivity_scope='global'` and any `applied>0` -> skip current (global cannot be added to existing cart). 3. If `nonStackableByGroup[current.group]` -> skip (group already consumed by non-stackable). 4. If current.stackable==false && len(appliedInGroup[current.group])>0 -> skip (non-stackable cannot join occupied group). 5. Else apply, update trackers. Migration: Audit existing promos where `stackable=false` meant global - we found EMP50 and PARTNER100 should be scope='global', rest 'group'. Backfill script ready. ## What I want Attack: - Where does group+global still misapply? Two non-stackable in same group different priority - which wins? One global + two groups? - Interaction with `exclusive_group` - are we now double-enforcing same concept? Can two stackable=true in same group both apply - is that desired? - Back-compat: carts in Redis with old `applied_discounts` computed under old logic. On deploy, do we reprice open carts (price jump at checkout) or version the cart? - Black Friday prep is 20 weeks out, but we have "Summer Sale" in 3 days - $2.3M/week at risk. Be concrete highest-risk.
The proposed fix is a solid start, but without a deterministic sort order and a safer migration plan, you risk trading one set of bugs for unpredictable pricing errors during your $2.3M sale.
Stop letting loop order dictate your margin; enforce a deterministic sort.
Relying on the order in which items appear makes the outcome random, especially when priorities and map-based trackers are involved. You need a stable total sort—like priority plus a unique ID—and golden tests for conflict scenarios to ensure the engine isn't just picking winners based on iteration.
Force a two-phase evaluation to keep global exclusives from being dropped.
Current logic allows global exclusives to be skipped if they’re processed after a group promo, which effectively kills high-value discounts like employee offers. Check for global promotions in a separate first phase so they don't lose just because they were evaluated late.
Do not force-reprice open carts; use versioning to handle the transition safely.
Blindly overwriting Redis carts risks mass support tickets for price changes or checkout errors. Use a versioning system and perform final repricing at the exact moment of checkout, logging the delta for transparency rather than forcing an update on every read.
Lock down edge cases like scope='none' and group-stacking rules.
Leaving 'none' scope unhandled will accidentally block valid promotions, and failing to define if multiple stackables can coexist in a group will create margin leaks. Hardcode these constraints now so the engine doesn't improvise behavior at runtime.
The highest-risk defect is not the map; it is that the proposal remains order-dependent. With the five rules as written, [SUMMER20, EMP50] produces SUMMER20 while [EMP50, SUMMER20] produces EMP50. Replacing a boolean with two trackers does not fix correctness unless candidate ordering and conflict resolution are part of the contract.
Define winner selection before shipping
Sort all eligible candidates by an explicit precedence—presumably lower priority wins, though that must be confirmed—then use a stable secondary key such as promotion ID. Do not rely on database, map, coupon-entry, or automatic-promotion order. Without a tie-breaker, two non-stackable promotions with equal priority can produce nondeterministic discounts across executions.
Under priority-greedy semantics:
- Two non-stackable promotions in the same group: the higher-priority one wins.
- A higher-priority stackable promotion in a group can cause a lower-priority non-stackable promotion in that group to lose.
- A global promotion wins alone if encountered first.
- If any higher-priority group promotion is accepted first, the global promotion loses, and promotions from other groups may subsequently apply.
That last case needs explicit product approval. “Global exclusive” can mean either “must be alone if selected” or “always beats every non-global promotion.” Those are different policies. Your current algorithm implements the first policy only when candidates are deterministically priority-sorted; it does not implement “global always wins”.
If priority means campaign importance, priority-greedy is defensible. If the intended winner is the largest customer discount, smallest merchant cost, or best valid combination, greedy evaluation is wrong; you need to compare valid sets. Do not accidentally let loop order decide this business question.
The schema currently has contradictory states
I would define exclusive_group as a combination bucket, not itself as an exclusivity instruction. Under that interpretation, two stackable=true promotions in the same group should both apply. stackable=false makes the promotion conflict with every other applied promotion in its declared scope. There is no double-enforcement—but the names make it look like there is.
The valid states should be constrained:
scope='group'requires a nonempty group.scope='global'should requirestackable=false.scope='none'should requirestackable=true.- Group values need normalization or, preferably, foreign keys/IDs rather than free text.
- The same promotion must be deduplicated before evaluation.
Without those constraints, values such as scope='global', stackable=true and scope='none', stackable=false have ambiguous behavior that different code paths can interpret differently. Empty or null groups are also dangerous: collapsing all missing groups into "" can make unrelated promotions conflict, while treating every missing group as distinct can allow unintended stacking.
Longer-term, I would replace the two partly overlapping fields with one policy enum:
STACKABLE
EXCLUSIVE_WITHIN_GROUP
EXCLUSIVE_GLOBAL
Keep combination_group only for the middle state. Do not do that redesign three days before Summer Sale, but make the current state constraints equivalent.
Tighten the evaluator
“Applied” must mean a promotion produced a positive, committed discount—not merely that it was eligible or appended to a result slice. A zero-value promotion consuming a group or blocking a later global promotion can suppress a real discount. Also decide whether “positive” is before or after rounding; a calculated fraction that rounds to zero should normally not consume exclusivity.
I would express the core conflict symmetrically and then apply it to priority-sorted candidates:
func conflicts(candidate Promo, accepted []AppliedPromo) bool {
for _, prior := range accepted {
if candidate.Scope == Global || prior.Scope == Global {
return true
}
if candidate.Group == prior.Group &&
(candidate.Scope == Group || prior.Scope == Group) &&
(!candidate.Stackable || !prior.Stackable) {
return true
}
}
return false
}
The exact condition can be optimized into trackers after the semantics are proven. Using a direct pairwise reference implementation in tests or shadow mode is safer than treating the optimized maps as the specification.
Also return structured rejection reasons such as CONFLICT_GLOBAL, GROUP_CONSUMED, and LOWER_PRIORITY. That will make both support cases and rollout monitoring substantially easier.
Minimum correctness matrix
Before enabling this, I would require golden tests covering:
SUMMER20 + WELCOME10→ both, independent of input order after sorting.- Two non-stackable seasonal promotions → deterministic priority winner.
- One stackable and one non-stackable in the same group, in both priority orders.
- Two stackable promotions in the same group → both, assuming the bucket interpretation is approved.
EMP50 + SUMMER20 + WELCOME10, with EMP50 highest priority → EMP50 only.- The same three with SUMMER20 higher priority than EMP50 → EMP50 rejected, then SUMMER20 and WELCOME10 apply, if that is the approved global policy.
- Equal priorities → stable ID tie-break.
- Global or group promotion calculating zero after caps/rounding → does not consume exclusivity.
- Missing/blank group → configuration rejection, not runtime improvisation.
- Duplicate submission of the same code → one application.
- Fixed and percentage discounts in different priority orders, with exact allocation and rounding snapshots.
- Ineligible, expired, exhausted, and no-effect promotions do not alter trackers.
The fifth and sixth tests are the executive decision hiding inside this engineering fix.
Existing Redis carts: reprice, don’t freeze the bug
Version the quote, not the customer’s cart forever. Store at least:
pricing_engine_version
promotion_config_revision
priced_at
input_hash
discount_breakdown
On cart read, mutation, and checkout, reprice if the engine version, promotion revision, or pricing inputs differ. Checkout must recompute authoritatively and must not trust a cached payable total. Persisting an old applied_discounts list while evaluating new promotions around it can create combinations that neither the old nor new engine would have selected.
I would not eagerly rewrite every Redis cart at deploy. Lazy repricing avoids a bulk cache storm and ensures only active carts incur work. For an increased total, show “price updated” and require confirmation before payment; decreases can generally be accepted automatically while still returning the new breakdown. Preserve completed orders and already-finalized payment attempts as immutable historical snapshots.
For the reported SUMMER20/WELCOME10 defect, correct repricing should lower affected totals rather than create a price jump. However, unrelated expiry, eligibility, or migration changes can increase totals, so the checkout path still needs explicit changed-price handling.
Migration risk
Auditing only rows with stackable=false is too narrow. Audit every active and scheduled promotion for invalid field combinations, blank groups, duplicate codes, and assumptions embedded outside the main table. A scheduled promotion omitted from the audit can reactivate the same failure after the immediate launch appears healthy.
Use expand/migrate/enable sequencing:
- Deploy readers that understand the new scope while retaining the old behavior behind a flag.
- Backfill and validate all active and scheduled records.
- Add database constraints preventing contradictory states.
- Shadow-evaluate old and new engines on real cart inputs.
- Enable new semantics by percentage or promotion allowlist.
- Remove fallback only after the launch window.
EMP50 and PARTNER100 should be explicit migration fixtures, not merely rows found by a one-off query.
Summer Sale decision
Ship a narrow, flagged correction—not a broad promotion-engine redesign. Today, make deterministic sorting, pairwise reference tests, configuration constraints, quote versioning, and rejection telemetry release blockers. Shadow old versus new results immediately, then canary SUMMER20/WELCOME10 before wider rollout.
Monitor disagreements by promotion pair, rejection reason, average discount per order, unusually deep effective discounts, checkout total changes, and payment failures. Keep a kill switch that restores old evaluation without rolling back the database migration.
The most likely catastrophic failure is not SUMMER20 continuing to miss WELCOME10; it is an unordered or misclassified global promotion either suppressing a major sale across carts or stacking EMP50/PARTNER100 with public discounts. Resolve the global-versus-priority policy and prove input-order independence before this reaches the CORE path.
The proposed fix correctly eliminates the original global-flag over-reach that suppressed WELCOME10, and the dual trackers plus the five evaluation rules are a reasonable sketch for distinguishing group-local versus cart-global exclusion. However, several concrete correctness holes remain; the highest-risk ones will surface under real cart traffic within days, not weeks.
Start with evaluation order, because every subsequent rule is order-dependent and the proposal never states it. Promotions must be sorted by priority before the walk; without an explicit stable sort (and a defined direction—lower number first or higher number first) the same set of eligible codes can produce different applied sets on different checkouts. In the SUMMER20 (priority 10) + WELCOME10 (priority 20) case the outcome is benign either way, but consider two non-stackable promotions that share exclusive_group='seasonal': SUMMER20 (20 %, priority 10, stackable=false, scope=group) and FLASH30 (30 %, priority 5, stackable=false, scope=group). Whichever the sort places first wins; the second is rejected by rule 3 or 4. That is deterministic only if the sort is total and stable. Priority ties are undefined and will be non-deterministic under map iteration or concurrent Redis loads. The engine therefore needs an explicit secondary key (creation timestamp or code) and a unit-test matrix that asserts the exact winner for every pair of same-group non-stackables.
The same ordering problem appears with a global. EMP50 (scope=global, stackable=false) versus SUMMER20 (scope=group) never stack—good, that was the missing concept—but which one the customer actually receives is purely a function of which code is examined first. If EMP50 has the worse priority it is simply skipped once any other discount has been applied (rule 2); the staff member walks away with 20 % instead of 50 %. There is no “replace earlier discounts if the new exclusive is better” step, nor a final “choose the maximum-value exclusive set” pass. Greedy left-to-right application therefore silently selects the wrong exclusive under realistic priority configurations, and the margin or customer-experience impact is unbounded. The same pattern occurs with one global plus two different groups: the global is admitted only when it sorts first; otherwise the two group discounts both apply and the global is dropped. If the business intent for EMP50 or PARTNER100 is “this code alone, always,” the current rules are insufficient; you need either (a) forced highest priority plus a clear-all-previous action when a global is accepted, or (b) a post-pass that re-evaluates the exclusive candidates by discount value.
Next, the interaction of exclusive_group with the new trackers is under-specified and risks double-enforcement or under-enforcement. The proposal treats exclusive_group solely as the map key for nonStackableByGroup and appliedInGroup. That works for the SUMMER20/WELCOME10 case, yet leaves open whether two promotions that both carry stackable=true and the same exclusive_group are allowed to co-exist. Under the five rules they are; nothing sets nonStackableByGroup and rule 4 only fires for stackable=false. If the column name exclusive_group is meant to convey mutual exclusion inside the group regardless of the stackable bit, then the rules are too permissive and you will recreate the original stacking complaints in the opposite direction. Conversely, if exclusive_group is merely a bucket that exclusivity_scope consults, then the stackable flag is now partially redundant with scope and the migration audit must also decide what stackable=true + scope=group actually means. Either interpretation requires an explicit product decision and a corresponding test; the current write-up papers over it. Scope='none' is likewise unhandled: a stackable=false promotion with scope=none will still populate nonStackableByGroup for its (possibly null) group and can therefore block unrelated codes, which is almost certainly unintended.
The tracker set itself is incomplete as written. The bullet list mentions nonStackableByGroup and globalExclusiveApplied, yet rules 2 and 4 also require a total applied count and a per-group applied collection. Those structures must be updated atomically with every successful apply, including the case in which a global is accepted (does it also mark its own exclusive_group? the proposal is silent). Any partial update leaves the maps inconsistent for the remainder of the walk.
On the Redis side the highest immediate risk is open-cart repricing during the Summer Sale window. Carts currently stored under the old has_non_stackable logic contain a strictly smaller applied_discounts set than the new engine will compute for the same lines. If you reprice on read or on checkout, customers who already see a total will experience a sudden drop (margin leakage, but usually pleasant surprise) or, in the rarer configuration where priorities reverse an exclusive choice, a sudden rise (support tickets, abandoned checkouts). If you freeze the old applied_discounts with a version stamp, you create a second code path that must be kept alive until every pre-deploy cart expires; any checkout validation that re-runs the pricer will still diverge and either reject the cart or silently rewrite the total. The cleanest operational compromise is a cart schema version, an asynchronous reprice job that rewrites open carts under a feature flag, and a hard reprice-at-checkout that logs the delta for every order in the first 72 hours. Skipping that work before the Summer Sale launch guarantees either price flickering or silent margin errors at $2.3 M/week velocity.
Migration correctness is the other launch blocker. The audit that maps EMP50 and PARTNER100 to scope=global and everything else to scope=group is necessary, yet any promo that historically relied on the global flag for exclusion and is left at scope=group will now stack with seasonal and lifecycle codes. The reverse error (a true group exclusive promoted to global) will suppress legitimate combinations and recreate the 114-ticket class of complaints. Because the backfill is a one-shot script against production data, it needs a dry-run that emits the exact before/after applied set for a sampled week of carts, plus a kill-switch that reverts the exclusivity_scope column if the Summer Sale monitors show unexpected average-discount spikes.
Concrete highest-risk items to close before the three-day Summer Sale window:
- Codify the sort order and add golden tests for (a) two same-group non-stackables, (b) global vs. any group, (c) global + two groups, (d) stackable=true pair inside one group.
- Decide and document whether exclusive_group implies exclusion even for stackable=true; adjust rule 3 or 4 accordingly.
- Implement full tracker state (applied count, appliedInGroup, nonStackableByGroup, globalExclusiveApplied) and the exact mutations on apply.
- Add a value-based tie-breaker or clear-and-replace path for globals so EMP50 cannot lose to a lower-value seasonal.
- Version Redis carts, reprice asynchronously under flag, force reprice + delta log at checkout.
- Run the migration dry-run against last week’s carts and gate the Summer Sale codes behind the same flag.
The five rules are a solid direction, but without the ordering contract, the selection policy for exclusives, the complete state machine, and a rehearsed cart-migration plan they will mis-apply under the exact load you are about to receive. Fix those four gaps first; the rest of Black Friday prep can follow.
This path is $2.3M/wk - proposed fix is directionally right but has 4 highest risk holes for your exact traps.
1. Global + Group still misapplies due to priority ordering
Your rule 2:
If
scope='global'andlen(applied)>0-> skip global
This makes outcome order-dependent. If you sort by priority ASC and EMP50 priority=1, it applies first, then globalExclusiveApplied=true blocks groups. Good.
If Product configures EMP50 with priority=50 and SUMMER20 priority=10, processing order is SUMMER20 -> WELCOME10 -> EMP50. Then EMP50 is skipped because group promos already applied. Employee expects 50% alone, gets 20% + $10 instead. That's a silent internal escalation and margin-wrong in other direction.
Fix: Don't rely on sort for global exclusivity. Two-phase evaluation:
sort ASC by priority
// phase 1: any applicable global?
globalCandidates = filter scope==global
if len>0: apply highest priority global, return [global] // alone
// phase 2: group logic only
If business wants priority to decide between global vs group, you must enforce invariant: all global promos must have numerically lower priority than any group promo. Add DB check constraint or validation, not tribal knowledge.
Second case same group: Two non-stackable same exclusive_group='seasonal', priority 10 and 20. Which wins? With your rules, first wins and sets nonStackableByGroup[seasonal]=true, second blocked by rule 3. That's priority-wins if you sort correctly. Make sort explicit - today bug ticket doesn't state sort, very risky.
2. exclusive_group double meaning + stackable=true same group
You are using exclusive_group as the map key. Is exclusive_group intended as grouping, or are you now encoding exclusivity twice?
Current proposal allows: WELCOME10 lifecycle stackable=true + REFERRAL15 lifecycle stackable=true -> both apply because rule 3 only checks nonStackableByGroup and rule 4 only blocks if current.stackable==false.
Is that desired? For many retailers, exclusive_group means "at most one from this group regardless of stackable". If that's your intent, you need:
if len(appliedInGroup[group])>0 and scope=='group' -> skip unless you explicitly allow multi-stackable groups
You must decide and document. Otherwise Summer Sale + another seasonal stackable=true you launch in 3 days stacks 20% + 30% = 50% margin leak - worse than current bug. Add a cap: maxStackablePerGroup or allowMultipleStackable.
Also scope='none' is missing: should bypass nonStackableByGroup entirely and not set it. Your pseudocode doesn't branch on it.
Corrected skeleton:
if promo.Scope == "none" { apply; continue }
if globalApplied { skip }
if promo.Scope == "global" {
if len(applied)>0 { skip } else { apply; globalApplied=true }
continue
}
// group scope
if nonStackableByGroup[promo.Group] { skip }
if !promo.Stackable && len(appliedInGroup[promo.Group])>0 { skip }
apply; appliedInGroup[promo.Group] append; if !promo.Stackable { nonStackableByGroup[promo.Group]=true }
3. Redis open carts - repricing risk
Old engine: has_non_stackable global. So old was more restrictive than new - it blocked cross-group stacking. New allows cross-group, so repricing old carts will lower total (add WELCOME10), not raise. That's generally safe for trust.
Price jump up could still happen if you had a cart where old logic allowed 2 group non-stackable due to bug order? Check history. Low risk of increase, but not zero.
Don't do blind reprice on read during Summer Sale. Ship as:
- Add
cart.pricer_versionfield to Redis. On read: if version != current, recompute in shadow, emit metricpricer.diff_cents, log decision. - On checkout final step always reprice with latest logic inside transaction - that's canonical.
- For display, if new total < old total, show new (happy path). If new total > old total, keep old for 24h grace and flag for manual review. This avoids "$80 became $90 at pay" ticket.
Otherwise you will have inconsistent applied_discounts between Redis and cart_discounts table.
4. Migration race - highest immediate risk for Summer Sale
You found EMP50 + PARTNER100 = global. If any new promo is inserted between audit and deploy with stackable=false defaulting to old meaning, your backfill script misses it and it will be treated as group, allowing EMP50+seasonal stack.
Fix before deploy:
- DB migration adds
exclusivity_scopeNOT NULL with default, but also Add check:IF stackable=false AND scope IS NULL -> fail. - Feature flag
useGroupScoping- dark launch yesterday, shadow-evaluate both engines, compare for 1 day. You have 3 days to Summer Sale - do not big-bang deploy Friday. - Add unit golden snapshots from source example:
- [A] seasonal non-stack(10) + lifecycle stack(20) => both apply
- [B] seasonal non-stack(10) + seasonal non-stack(20) => only 10
- [C] seasonal stack(10) + seasonal non-stack(20) => only 10 (defines your policy)
- [D] global(1) + seasonal(10) + lifecycle(20) => only global if phase1, else priority dependent - assert your chosen policy
- [E] two stackable lifecycle same group => both? assert intended
- [F] scope=none always applies with global
For Summer Sale in 3 days: hotfix option is keep old engine but temporarily move WELCOME10 to exclusive_group='promo_bypass' with custom allowlist - less risk than full engine swap. If you must swap, flag-gate and monitor global_promo_skipped_total counter.
Bottom line: proposed map fix solves 114 tickets, but without global-first phase + sort guarantee + scope=none handling, you trade one exclusivity bug for another where employee discount silently disappears or two lifecycle stackables double dip.
The floor from round 1 is set: candidate evaluation gets a total, stable ordering (priority + ID tie-break) and input-order independence is a release blocker; the pairwise conflicts oracle is the spec and the trackers are a derived optimization that must match it; scope='none' bypasses group maps entirely; contradictory schema states get rejected by constraints; and the migration audit covers active AND scheduled promotions. Now the three product facts you've been asking for — **author stipulations, treat as fact:** (1) EMP50 and PARTNER100 mean "this code alone, always" — a global promotion, when eligible, must WIN, not merely refuse to join; the staff member gets 50% alone regardless of what applied first or what priority number someone typed. (2) Seasonal promotions never combine with each other — at most one seasonal per cart, regardless of the stackable bit. (3) Lifecycle promotions MAY combine with each other — two stackable lifecycle codes both apply. Note that facts 2 and 3 conflict with any single global rule for group semantics — resolve that deliberately, not by accident of the map key. Under those facts, settle the mechanism fight: two-phase evaluation, a priority invariant enforced by constraint, or a valid-set selection pass — pick one for the 3-day window, and say what the chosen mechanism does with the case that breaks it (a global that's eligible but arrives mid-walk after group promos already applied — replace-and-clear, or reject-the-cart-state?). The objection that two-phase silently decides policy is now moot on policy — but not on mechanism: say what ELSE two-phase decides that Product hasn't signed. Second decision: the stale-price grace period. The 24-hour honor-the-old-total proposal has a standing objection — it's a price guarantee with fraud, accounting, and expiry implications nobody approved. Resolve it: grace, disclose-and-confirm, or something narrower. Whoever wins, the checkout invariant must be stated in one line. Keep round-1 positions that survived in one line each. Deliverable: the evaluation algorithm as you'd ship it Friday, the golden-test matrix updated for the three stipulated facts, and your reprice policy.
We are shipping a two-phase engine that enforces global dominance as a hard phase-one rule, adopts explicit configuration for seasonal and lifecycle group behaviors, and mandates authoritative, re-confirmed pricing at checkout to eliminate unapproved price guarantees.
Ship the two-phase evaluator to guarantee global dominance
Phase one filters for global promotions; if any are eligible, they win alone, satisfying the "always win alone" requirement. This replaces unsafe priority-based hacks with a code-enforced, deterministic phase separation.
Stop guessing group rules; map explicit policy to each group
We need to use an explicit configuration map to distinguish between 'Singleton' (seasonal) and 'MultiStackable' (lifecycle) behaviors, rather than overloading the stackable boolean. This settles the conflict between Fact 2 and Fact 3 by treating group semantics as deliberately non-uniform.
Kill the 24-hour price guarantee; reprice authoritatively at checkout
We are rejecting the stale-price grace period to avoid fraud and accounting risks. The system must always perform a fresh, authoritative reprice at checkout, requiring customer re-confirmation if the total rises.
Default to priority-order if multiple globals compete
When two globals are both eligible, our mechanism defaults to using the existing priority-plus-ID sort to break the tie. This decision is provisional and should be flagged in the release note for Product to override if they later decide they prefer the largest discount.
I would ship two-phase evaluation Friday. The moderator’s stipulation removes my earlier policy objection: global now explicitly means dominant, not merely incompatible. @Muse’s mechanism is therefore the simplest safe implementation for the three-day window. I would not encode dominance indirectly through priority constraints; that makes a product invariant depend on every future author assigning magic numbers correctly. I would also not introduce valid-set optimization now because Product has already said the eligible global wins even if a group combination gives a larger discount.
Two-phase does make one additional decision Product has not signed: which promotion wins when multiple globals are eligible. For Friday, use the existing total order—priority, then immutable promotion ID—and document that as provisional. Do not choose the largest discount without authorization. It also requires a precise definition of “eligible”: perform eligibility checks using the same undiscounted eligibility base the current engine uses, and treat a promotion that calculates to zero after caps and rounding as non-applicable. Changing threshold semantics during this fix would unnecessarily broaden the release.
Friday algorithm
First construct a deduplicated, immutable candidate set and evaluate eligibility. Then:
candidates := EligiblePositivePromotions(cart) // dedupe by promotion ID
sort.SliceStable(candidates, byPriorityThenID)
// Phase 1: dominant globals
globals := filter(candidates, ScopeGlobal)
if len(globals) > 0 {
return ApplyAlone(cart, globals[0])
}
// Phase 2: non-global selection
accepted := []Promo{}
for _, candidate := range candidates {
if !conflictsWithAny(candidate, accepted, groupPolicies) {
accepted = append(accepted, candidate)
}
}
return PriceInOrder(cart, accepted)
The group policy cannot be inferred from exclusive_group plus stackable; the stipulated seasonal and lifecycle behavior proves the schema is missing a dimension. For Friday I would introduce an explicit, validated group policy:
SINGLE_WINNER // seasonal: at most one regardless of stackable
STACKABLE_IF_MARKED // lifecycle: coexist only when both are stackable
Every group must have a configured policy before its promotions can be activated. Seasonal is SINGLE_WINNER; lifecycle is STACKABLE_IF_MARKED. Other active and scheduled groups must be classified in the migration audit rather than receiving a guessed runtime default.
The pairwise oracle becomes:
func conflicts(a, b Promo, policies GroupPolicies) bool {
if a.Scope == Global || b.Scope == Global {
return true
}
if a.Scope == None || b.Scope == None {
return false
}
if a.Group != b.Group {
return false
}
switch policies[a.Group] {
case SingleWinner:
return true
case StackableIfMarked:
return !a.Stackable || !b.Stackable
default:
panic("unconfigured promotion group")
}
}
scope='none' bypasses group rules, but it does not bypass a global. “Global alone, always” means EMP50 excludes shipping, automatic, or otherwise ungrouped discounts too.
A global never legitimately “arrives mid-walk”: globals are resolved before the group walk, and adding a code causes full selection from the immutable candidate set rather than mutation of cached applied_discounts. If an internal incremental path somehow encounters an eligible global after accepting group promotions, I would replace and clear, then emit a high-severity invariant metric. Rejecting the cart would punish the customer for an engine-state defect. At checkout, Redis state is input/cache, never authority.
Trackers can optimize the second phase after tests prove equivalence with this pairwise oracle. The oracle remains executable test code.
Golden matrix
Assume lower numeric priority wins, then immutable ID breaks ties:
| Candidates | Expected |
|---|---|
| SUMMER20 seasonal non-stackable + WELCOME10 lifecycle stackable | Both |
| Two seasonal, both stackable | Higher-priority seasonal only |
| Two seasonal, both non-stackable | Higher-priority seasonal only |
| Seasonal stackable + seasonal non-stackable | Higher-priority seasonal only |
| Two lifecycle, both stackable | Both |
| Lifecycle stackable + lifecycle non-stackable | Higher-priority one only |
| Two lifecycle, both non-stackable | Higher-priority one only |
| Two compatible promotions in different groups | Both |
| EMP50 + SUMMER20 + WELCOME10 | EMP50 alone, under every priority and input permutation |
PARTNER100 + lifecycle codes + scope='none' code | PARTNER100 alone |
| EMP50 + PARTNER100 | Priority/ID winner alone |
| Ineligible EMP50 + SUMMER20 + WELCOME10 | Both group promotions |
| Eligible global calculating zero after rounding + group promotions | Global does not dominate; valid group promotions apply |
scope='none' + seasonal + lifecycle, no global | none plus the valid group winners |
| Equal-priority seasonal promotions | Lower immutable ID wins |
| Duplicate promotion ID/code | Applied once |
| Blank or unknown group policy | Configuration rejected |
| Every row above under all candidate permutations | Identical accepted set and breakdown |
Add golden monetary snapshots for sequential percentage/fixed calculations, caps, allocation, and rounding. Selection may be input-order independent while discount amounts still change if application order is unstable; pricing must therefore use the same priority-plus-ID order.
Reprice policy
No 24-hour grace. I agree with the standing objection: that would create an unapproved price guarantee and complicate expiry and abuse handling. Use disclose-and-confirm.
The checkout invariant is:
Payment authorization may occur only for the exact amount and quote version most recently displayed to and confirmed by the customer after authoritative repricing.
Version Redis carts with engine version, promotion revision, input hash, and discount breakdown. Lazily reprice on active-cart read or mutation and always reprice authoritatively at checkout. If the total rises, display the changed breakdown and require explicit reconfirmation; if it falls, display the new lower amount before authorization without inventing a stale-price guarantee. Existing authorized payments and completed orders remain immutable.
I disagree with proactively rewriting every open Redis cart. Lazy repricing limits work to active carts and avoids a deployment-driven cache storm while preserving checkout correctness. Shadow old/new pricing immediately, then canary the Summer Sale promotions behind a kill switch.
The surviving release blockers are one line each: stable priority-plus-ID ordering; permutation tests; pairwise oracle equivalence; positive-discount-only acceptance; explicit group-policy configuration; constraints rejecting contradictory states; audit of active and scheduled promotions; and authoritative versioned checkout repricing.
The three product facts close the policy gaps that made the original five rules unsafe. Global exclusives must win alone whenever eligible; seasonal is a hard mutex regardless of the stackable bit; lifecycle respects the stackable bit and may stack. That combination cannot be expressed by a single uniform rule over the exclusive_group map key, so the evaluator must treat group semantics as deliberately non-uniform rather than hoping the trackers invent the right answer.
@Muse’s two-phase shape is the mechanism I would ship Friday. A pure priority-sorted single walk with “skip if anything already applied” still lets a higher-priority seasonal steal the cart from EMP50 when someone typed the priority numbers wrong; that directly violates fact 1. A full valid-set search is correct in the abstract but is more code, more latency surface, and more review risk than we can absorb before Summer Sale. Two-phase is the narrowest implementation that makes “global always wins alone” true by construction and still re-uses the existing priority + ID ordering for everything else.
What else two-phase decides that Product has not explicitly signed: among multiple eligible globals it selects by the same priority + ID order used for groups (so EMP50 beats PARTNER100 only when its priority number is better); and no combination of group discounts is ever preferred over a single global even if the groups would produce a larger customer discount or lower merchant cost. Both are reasonable defaults given fact 1, but they should be called out in the release note so Product can override later with a value-based selector if they want.
Evaluation algorithm for Friday
// Pre-conditions (DB constraints + config load)
// - scope=global implies stackable=false
// - scope=group implies non-empty exclusive_group
// - scope=none implies stackable=true
// - mutex_groups = {"seasonal"} // config set; lifecycle absent
// - eligible already filtered for dates, usage, product match, etc.
func Evaluate(eligible []Promo) ([]Applied, []Rejection) {
sort.SliceStable(eligible, byPriorityAscThenID)
// Phase 1 — globals win alone
var bestGlobal *Promo
for i := range eligible {
p := &eligible[i]
if p.Scope != Global { continue }
if discount := compute(p); discount <= 0 { continue } // zero does not win
if bestGlobal == nil || better(p, bestGlobal) {
bestGlobal = p
}
}
if bestGlobal != nil {
return []Applied{apply(bestGlobal)}, nil
}
// Phase 2 — groups + none
var accepted []Applied
appliedInGroup := map[string]int{}
blockedGroup := map[string]bool{} // non-stackable or mutex already taken
var rejections []Rejection
for i := range eligible {
p := &eligible[i]
if p.Scope == Global { continue } // already handled
if p.Scope == None {
if d := compute(p); d > 0 {
accepted = append(accepted, apply(p))
}
continue
}
// scope == group
g := p.Group
if blockedGroup[g] {
rejections = append(rejections, Rejection{p, GROUP_CONSUMED})
continue
}
if mutex_groups[g] {
if appliedInGroup[g] > 0 {
rejections = append(rejections, Rejection{p, GROUP_MUTEX})
continue
}
} else {
// respect_stackable (lifecycle and any future non-mutex group)
if !p.Stackable && appliedInGroup[g] > 0 {
rejections = append(rejections, Rejection{p, GROUP_CONSUMED})
continue
}
}
d := compute(p)
if d <= 0 {
// zero after caps/rounding does not consume the group
continue
}
accepted = append(accepted, apply(p))
appliedInGroup[g]++
if mutex_groups[g] || !p.Stackable {
blockedGroup[g] = true
}
}
return accepted, rejections
}
The pairwise conflicts oracle that tests and shadow mode must implement exactly the same decisions; the maps above are only an O(n) optimization that is required to match the oracle on every golden case. “Applied” means a positive committed discount after rounding and caps; a zero result never sets blockedGroup or wins phase 1.
@GPT’s observation that exclusive_group is a combination bucket, not itself an exclusivity instruction, survives: the bucket supplies the key, while mutex_groups plus the stackable bit supply the exclusion rule. That is how facts 2 and 3 coexist without double-enforcement or a schema redesign this week.
Golden-test matrix (updated for the three facts)
- SUMMER20 (seasonal, any stackable) + WELCOME10 (lifecycle, stackable=true) → both, independent of input order.
- Two seasonal promotions (any combination of stackable bits) → exactly the higher-priority (then lower-ID) one.
- Two lifecycle stackable=true → both.
- One lifecycle stackable=true + one lifecycle stackable=false → only the higher-priority of the two; the non-stackable blocks the other if it arrives first under sort.
- EMP50 (global) + SUMMER20 + WELCOME10 → EMP50 alone, regardless of the three priority numbers and regardless of original slice order.
- EMP50 + PARTNER100 (both global, different priorities) → the better-priority global alone.
- Global whose computed discount rounds to zero → phase 1 produces nothing; fall through to groups.
- scope=none stackable with a global present → only the global.
- scope=none with no global → the none promo applies alongside any valid groups.
- Duplicate code in the eligible list → single application.
- Blank/missing group on a scope=group row → configuration rejection before evaluation.
- Ineligible/expired/exhausted promos never appear in eligible and therefore never touch trackers.
- Fixed + percentage in both priority orders → exact allocation and rounding snapshots match the oracle.
These replace the earlier matrix; the EMP50-always-alone and seasonal-mutex cases are now the executive assertions.
Reprice policy
The 24-hour honor-the-old-total idea is withdrawn. It creates an unapproved price guarantee, complicates revenue recognition, and interacts badly with coupon expiry and inventory. Instead:
- Every cart carries pricing_engine_version, promotion_config_revision, priced_at, input_hash, and the last discount_breakdown.
- Display path: lazy reprice on read/mutation. If the new total is lower, show it. If higher, show the new total with an explicit “price updated” notice and require the customer to re-confirm before payment.
- Checkout path (the only authoritative step): always recompute with the current engine inside the payment transaction; never trust a cached payable total.
- Completed orders and in-flight payment attempts remain immutable historical snapshots.
Checkout invariant (one line): the amount charged is always the result of running the current evaluator on the current cart contents; a previously displayed quote is never the payment amount.
Surviving round-1 positions (one line each)
Input-order independence via total stable sort (priority + ID) remains a release blocker.
The pairwise conflicts oracle is the specification; trackers are a derived optimization that must match it.
scope=none bypasses all group maps and never sets them.
Contradictory schema states are rejected by constraints before evaluation.
Migration audit and dry-run cover every active and scheduled promotion, not merely currently live rows.
Zero-after-rounding never consumes exclusivity.
Shadow evaluation + feature flag + kill switch that restores the old engine without rolling back the column are mandatory for the Summer Sale window.
Ship the two-phase evaluator behind the flag Friday, canary the SUMMER20/WELCOME10 pair first, and keep the global-skipped and group-mutex counters on the dashboard. That is the smallest change that satisfies the three facts, removes the 114-ticket class of bugs, and does not invent new silent losers for EMP50 or double-dip seasonals.
Mechanism decision: two-phase with replace-and-clear for the Friday window.
Product fact 1 is now EMP50/PARTNER100 = always win alone. That's not a sort decision, that's a phase decision. Priority-invariant enforced by DB constraint is fragile - one mistyped priority number gives staff 20% not 50% and no error. Two-phase implements the fact in code: if a global is eligible with positive discount, it clears everything.
Objection to two-phase noted as "decides policy silently" is now moot on global - but it still decides two things Product hasn't signed that you must document:
- Global wins even when the sum of group discounts is larger value than the global. Two-phase chooses policy "global always beats value" not "best discount wins".
- A global suppresses
scope=nonepromos too. If Product wants free-shipping-none to survive EMP50, two-phase must be amended. As written, "alone" = alone.
Mid-walk case: there is no mid-walk under two-phase. Global phase runs first. If eligible, we replace-and-clear - discard any speculative group state and return [globalWinner]. Rejecting group state is correct per stipulation. If no global, group phase runs.
Ship Friday algorithm
Surviving round-1 blockers:
- Total stable ordering - priority ASC, code ASC tie-break, input-order independent - blocker.
- Pairwise conflicts oracle is spec, maps are optimization - blocker.
scope=nonebypasses group maps for check and mutation - blocker.- Contradictory schema states rejected by constraint - blocker.
- Migration audit includes active AND scheduled - blocker.
// constraints to add now:
// CHECK (scope='group' AND exclusive_group IS NOT NULL AND length>0)
// CHECK (scope='global' => stackable=false)
// CHECK (scope='none' => stackable=true)
// FK exclusive_group -> promo_groups(group)
// dedup input by code before eval
type GroupRule int
const (
Singleton GroupRule = iota // at most one per group regardless of stackable flag
MultiStackable // multiple stackable=true allowed, non-stackable blocks
)
// For Friday - config table, not map key magic. Hardcode from Product facts:
rules := map[string]GroupRule{
"seasonal": Singleton, // fact 2
"lifecycle": MultiStackable, // fact 3
// employee/partner are global, not used here
}
func priceCart(candidates []Promo) []Applied {
// 1. dedup + calc positive discount first (>0 after rounding to cent)
eligible := filterPositive(candidates) // zero-value => does NOT consume exclusivity
// 2. stable sort for determinism
sort.SliceStable(eligible, func(i,j int) bool{
if eligible[i].Priority != eligible[j].Priority { return eligible[i].Priority < eligible[j].Priority }
return eligible[i].Code < eligible[j].Code
})
// 3. PHASE 1: global - must win if any positive
globals := filter(eligible, func(p Promo) bool{ return p.Scope==Global })
if len(globals)>0 {
winner := globals[0] // already priority+code sorted, or value tie-break if you choose
return []Applied{{Promo:winner, Reason: APPLIED_GLOBAL_WIN}}
}
// 4. PHASE 2: groups + none
var applied []Applied
nonStackableByGroup := map[string]bool{}
appliedInGroup := map[string][]Applied{}
var result []Applied
for _, cand := range eligible {
if cand.Scope == None {
result = append(result, Applied{cand, APPLIED_NONE})
continue // does not touch group trackers per blocker
}
if cand.Scope != Group { continue } // globals already handled
g := cand.Group
rule := rules[g] // default to Singleton if unknown for safety
if nonStackableByGroup[g] {
applied = append(applied, Applied{cand, REJECT_GROUP_CONSUMED_NONSTACKABLE}); continue
}
if len(appliedInGroup[g])>0 {
if rule==Singleton {
applied = append(applied, Applied{cand, REJECT_GROUP_SINGLETON}); continue
}
// MultiStackable
if !cand.Stackable {
applied = append(applied, Applied{cand, REJECT_GROUP_OCCUPIED}); continue
}
}
// apply
result = append(result, Applied{cand, APPLIED_GROUP})
appliedInGroup[g]=append(appliedInGroup[g], Applied{cand})
if !cand.Stackable {
nonStackableByGroup[g]=true
}
}
return result
}
Reference oracle for shadow & tests must use same pairwise conflicts() logic, then assert trackers match oracle on every cart.
Updated golden matrix for 3 facts
- SUMMER20 (seasonal false 10) + WELCOME10 (lifecycle true 20) => both, order independent after sort.
- Two non-stackable same group seasonal - FLASH30(5) + SUMMER20(10) => FLASH30 only [Singleton overrides stackable bit].
- Stackable + non-stackable same seasonal, both orders => higher priority wins, second rejected.
- Two stackable lifecycle same group - WELCOME10 + REFERRAL15 both stackable true => both apply [fact 3]. Same test with seasonal stackable+stackable => only one applies [fact 2].
- EMP50 global + SUMMER20 + WELCOME10, EMP50 any priority => EMP50 only, others REJECT_GLOBAL_WIN. Proves global wins not sort.
- Two globals EMP50 + PARTNER100 eligible => priority/code winner, other REJECT_GLOBAL_CONFLICT.
- Global calculates 0 after cap/rounding => does NOT win, groups apply.
- scope=none + global => none rejected when global wins.
- scope=none + seasonal => both, none does not populate GroupCon consumed.
- blank/missing group with scope=group => config rejection, not runtime collapse to "".
- duplicate code submission => one application.
- fixed + percent different priorities => exact allocation snapshot.
Reprice policy - no grace
24h honor-old-total is price guarantee with fraud/accounting risk - rejected.
Version cart:
pricing_engine_version, promotion_config_revision, priced_at, input_hash, discount_breakdown
- Not eager mass rewrite. Lazy reprice on read/mutation/checkout.
- Display: if new != old, show new total immediately with "Promotions updated" and breakdown diff.
- Checkout invariant (one line): Checkout always reprices authoritatively inside the transaction from current engine + inputs; cached
applied_discountsis display-only and never trusted for payable total. - If total changed between last display and pay click, block payment, require explicit confirmation of new total. Decreases auto-accept with new breakdown, increases require confirm.
This keeps margin correct for Summer Sale, avoids ghost discount freeze, and satisfies fact 1 without price-guarantee exposure. Ship flagged, shadow old vs new with rejection reason telemetry, kill switch = flag off restores old evaluator without DB rollback after column constraints added.
Closing round. Three threads to settle, then the audit and the verdict. First, the open challenges: the `default to Singleton if unknown` fallback has two objections standing — defend it or concede to fail-loud-at-activation. The `filterPositive` precompute has an order-sensitivity objection standing — if discount positivity can depend on what's already applied (thresholds, caps, sequential fixed/percentage), say where positivity is computed in the final algorithm and prove the phase-1 zero-check isn't circular. And the multi-global winner rule ships as priority-then-ID provisional — confirm you're all signing that as a documented default pending Product, or block on it. Second: you converged fast on two-phase + group-policy config + disclose-and-confirm. Spend a paragraph attacking your own convergence — what fails in the design you just agreed on that none of you has named? The config table is itself new surface. The candidate set is built once from a cart that checkout can mutate. Find what's yours; those are mine. Third, the concession audit: for each position you moved off this session, say whether the argument moved you or the 3-day clock did, and name the one concession you'd take back if the deadline vanished. Verdict: GO or NO-GO for the Friday window, your named refuse-to-ship conditions, and any dissent to preserve on the record as unresolved.
The team is GO for Friday with a two-phase evaluator, provided we enforce fail-loud configuration, authoritative re-evaluation at checkout, and treat our global-winner policies as temporary defaults rather than final requirements.
Fail-loud activation is the only acceptable safety barrier.
Relying on singleton defaults hides configuration errors; the engine must reject any promotion activation where the group policy is undefined, ensuring no runtime guessing occurs.
Authoritative checkout re-evaluation is mandatory to prevent stale-snapshot bugs.
Checkout can mutate the cart between display and payment, meaning the initial candidate set can go stale; the engine must re-validate the full quote atomically inside the final payment transaction, not rely on cached versions.
Positivity checks must be computed incrementally to avoid circular logic.
Pre-filtering candidates based on base-cart positivity is unsafe because real-world discount values depend on the sequence of applied promotions; validity must be verified at the moment of application.
Multi-global winner rules are provisional defaults pending Product review.
The decision to let globals dominate group sums and suppress others is an un-signed default; treat this as a temporary bridge for the Summer Sale rather than permanent business logic.
I’m signing the two-phase design, with three corrections: unknown group policy fails activation, positivity is evaluated in context rather than prefiltered, and priority-then-ID is the provisional multi-global rule.
Settling the open challenges
@Muse, I concede default to Singleton completely. An unknown policy must fail validation when a configuration revision is activated; the previously valid revision remains live. It must not silently acquire singleton behavior. A singleton fallback can suppress legitimate lifecycle-like combinations while making the configuration error appear to be a valid business decision. If an unknown policy somehow appears in an already-active runtime snapshot, evaluation raises an invariant error and rolls back to the last known-good configuration revision rather than guessing.
I also reject filterPositive(candidates) as a general pre-pass. A promotion’s realized value can depend on the discount state before it, including sequential percentage/fixed calculations, caps, allocation, and rounding. The final algorithm uses two distinct contexts:
- A global is computed against the immutable cart snapshot with no other promotions applied. That is not circular because the stipulated result, if selected, is that global alone. A global counts as dominant only if that standalone calculation produces a positive committed discount after allocation and rounding.
- A group or
nonepromotion is computed at its turn against the pricing state created by previously accepted promotions. It consumes exclusivity only after producing a positive committed discount in that state.
Threshold bases must be explicit. If a threshold is defined on pre-discount merchandise subtotal, it reads that immutable field; if defined on the running discounted subtotal, it reads the current phase-two state. No generic “eligible” flag may blur those two meanings.
The pairwise oracle therefore specifies compatibility, while a deterministic reference evaluator specifies selection and monetary application:
snapshot := FreezeCartAndConfig(cartRevision, promoRevision)
candidates := StructuralEligibility(snapshot)
dedupeAndSort(candidates, priorityThenImmutableID)
// Phase 1: globals are evaluated alone against the pristine snapshot.
positiveGlobals := []PricedPromo{}
for _, p := range candidates {
if p.Scope != Global {
continue
}
if priced := PriceAlone(snapshot, p); priced.Discount > 0 {
positiveGlobals = append(positiveGlobals, priced)
}
}
if len(positiveGlobals) > 0 {
return Quote{Applied: []PricedPromo{positiveGlobals[0]}}
}
// Phase 2: calculate in deterministic sequence.
state := NewPricingState(snapshot)
accepted := []PricedPromo{}
for _, p := range candidates {
if p.Scope == Global {
continue
}
if ConflictsWithAccepted(p, accepted, groupPolicies) {
reject(p)
continue
}
priced := PriceAgainst(state, p)
if priced.Discount <= 0 {
rejectNoEffect(p)
continue
}
state.Commit(priced)
accepted = append(accepted, priced)
}
return Quote{Applied: accepted}
For multiple positive globals, I sign priority, then immutable promotion ID as Friday’s documented provisional winner rule. It remains an unresolved Product policy, not a mathematically “correct” answer. No discount-value tie-break should be invented in code. If a usage reservation for the selected global fails atomically, remove that candidate and rerun selection; do not silently fall through using stale eligibility.
The failure in our converged design
Our consensus still fails if “immutable snapshot” is only rhetoric. The candidate set, cart lines, customer segment, promotion configuration, inventory price, and usage availability can change between evaluation and payment authorization. Without revision checks or locking, the engine can generate a correct quote for one cart state and charge it against another.
The Friday implementation therefore needs:
- A cart input revision and immutable input hash.
- One atomically activated promotion/configuration revision, including group policies.
- No mixed cache reads across configuration revisions.
- Compare-and-swap or equivalent revision verification before payment authorization.
- Atomic reservation of limited-use promotions at checkout, with deterministic reevaluation if reservation loses a race.
- A quote ID binding cart revision, promotion revision, breakdown, and payable amount.
The new group-policy table is also operational risk. A partially deployed application could understand the new promotion scope but not the policy revision. Activation must therefore validate all active and scheduled promotions, publish the complete revision atomically, and occur only after every checkout instance can read it. Publishing promotion rows and group policies independently can expose carts to a transient combination of semantics that passed no golden test.
Repricing and consent
There is no stale-price grace period. Any changed amount gets a new quote.
The checkout invariant is:
Payment authorization may occur only when the cart revision, promotion revision, payable amount, and quote ID exactly match the latest quote displayed to and explicitly confirmed by the customer.
That means an increase blocks and requires confirmation. A decrease after the pay click also returns the lower quote for confirmation rather than authorizing a quote the customer never saw. Lazy repricing before the pay action should make that second case uncommon. Completed orders and already-authorized payment attempts remain immutable.
Concession audit
I moved off my objection to two-phase because the Product stipulation changed the argument, not because of the deadline. Before that stipulation, two-phase silently made globals dominant; now dominance is the explicit requirement.
I moved from a uniform bucket interpretation of exclusive_group to explicit per-group policy because the seasonal and lifecycle facts proved that one interpretation cannot represent the business contract. That was argument-driven.
I accepted priority-then-ID for multiple globals because of the three-day clock. Without the deadline, I would take that concession back and require Product to specify whether EMP50, PARTNER100, or another global wins when several are simultaneously eligible.
I am also tolerating the existing scope + stackable + group-policy schema for Friday because of the clock. With a normal runway, I would replace its overlapping states with a compatibility model that cannot encode contradictory combinations.
I did not move on eager Redis rewriting or stale-price grace: active carts should be lazily repriced, checkout must be authoritative, and no unapproved guarantee should be created.
Verdict
GO for a flagged Friday canary, not an unconditional fleet-wide rollout.
My refuse-to-ship conditions are:
- Unknown group policy fails configuration activation; no runtime default.
- Seasonal is configured singleton and lifecycle stackable-if-marked.
- Globals are priced standalone in phase one and always win alone when positive.
- Group/
nonepositivity is calculated at the candidate’s deterministic turn. - Priority plus immutable ID provides a total order and all permutation tests pass.
- Tracker output matches the reference evaluator and pairwise conflict oracle.
- Cart and promotion revisions are atomically snapshotted and rechecked before authorization.
- Limited-use promotion reservation cannot race validation without reevaluation.
- Every changed payable amount creates a newly displayed and confirmed quote.
- Active and scheduled promotions pass migration dry-run.
- Shadow comparison, rejection telemetry, canary controls, and an evaluator kill switch are live.
The unresolved dissent to preserve is the multiple-global winner policy: priority-then-ID is deterministic and acceptable for Friday, but it is provisional. If any of those gates misses the window, my verdict changes to NO-GO for the new evaluator; keep the old engine and apply a narrower, explicitly tested Summer Sale mitigation instead.
The default-to-Singleton fallback is wrong and I concede it. @Muse’s runtime guess still invents policy for any group the migration missed; that is exactly how seasonal-style margin leaks reappear under a new code name three days later. Unconfigured groups must fail closed at promo activation or engine load: refuse to mark the promotion active, refuse to build the candidate set, and surface a configuration error before any cart sees it. Silent mutex is not safety; it is an unreviewed product decision executed in the hot path.
On filterPositive and order sensitivity: positivity is not a pure function of a single promotion against the raw cart once sequential thresholds, caps, or percentage-then-fixed interactions exist. Pre-filtering the entire candidate list to “positive on the base cart” therefore injects order dependence for phase 2 and can incorrectly drop a later promo that only becomes valuable after an earlier one, or retain one that becomes zero. The safe split is:
- Phase 1 (globals) computes each global’s discount solely against the undiscounted cart. Because a winning global applies alone, there are no prior discounts and the zero-check cannot be circular. A global that rounds to zero simply does not dominate; control falls through.
- Phase 2 walks the remaining candidates in stable priority-plus-ID order, computing each candidate’s discount against the running cart state after the promotions already accepted in this walk. Only a strictly positive result is accepted and mutates state; a zero or negative result is skipped without consuming group capacity.
That keeps the phase-1 dominance test non-circular while making sequential effects correct for groups and scope=none. The pairwise oracle must use the identical incremental rule so trackers cannot drift.
We are signing the multi-global rule as priority-then-ID, documented as a provisional default pending Product. It is the same total order already required for input-order independence; inventing a largest-discount tie-break this week would be a new unsigned policy. The release note must state both extra decisions two-phase still makes: global beats any value combination of groups, and a winning global also suppresses scope=none.
@GPT’s formulation of the checkout invariant is the one I will carry: payment authorization may occur only for the exact amount and quote version most recently displayed to and confirmed by the customer after authoritative repricing. That single sentence kills both stale-cache payment and any residual grace-period temptation.
Attack on our own convergence
We converged cleanly on two-phase plus an explicit group-policy table plus disclose-and-confirm, yet three failure modes remain unnamed. First, the policy config table is brand-new surface area: it must be populated for every active and scheduled group, versioned into the cart quote, and owned by someone who will answer pages when a marketing user creates “FLASH” without a policy row. A missing foreign key or a cache of the policy map that goes stale after a config push turns the fail-loud rule into a production outage. Second, the candidate set is not truly immutable for the lifetime of a checkout. Customers add lines, change quantities, and enter new codes; any evaluation that freezes candidates too early will miss a late-entered EMP50 or a newly eligible seasonal. Full re-evaluation on every mutation and again inside the payment transaction is mandatory; “immutable” applies only inside one call. Third, two-phase assumes eligibility itself is independent of other discounts. If any promotion’s eligibility threshold is defined on a post-discount total, phase 1 can select a global that would have been ineligible under the group combination that was discarded, or vice versa. None of us has written the eligibility base contract down, so the three-day canary will discover it in production. Disclose-and-confirm also lacks a quote TTL or lock; under flash-sale contention a customer can be asked to re-confirm oscillating totals, converting a pricing fix into checkout friction at peak volume.
Concession audit
I abandoned the original five-rule global-boolean replacement in favor of two-phase once the product facts made “always win alone” non-negotiable; the argument, not the clock, moved me. I dropped the 24-hour honor-old-total idea after the fraud and accounting objections; again the argument won. I accepted that exclusive_group is only a bucket and that a separate policy dimension is required once facts 2 and 3 conflicted with any uniform stackable rule; the facts forced it. If the three-day deadline vanished I would take back the decision to ship without a Product-signed choice between “global always beats value” and a true best-combination selector; that remains the largest unsigned policy still encoded in the mechanism.
Verdict
GO for the Friday window, narrowly, under these refuse-to-ship conditions:
- Unconfigured group policies fail at activation/load, never at evaluation time and never with a Singleton default.
- Golden matrix (including every permutation, monetary snapshots, zero-after-rounding, and sequential fixed/percentage cases) is green against the pairwise oracle; trackers match the oracle on the same inputs.
- Phase-1 positivity is computed only on the base cart; phase-2 positivity is incremental against running state; both are covered by tests.
- Migration dry-run and constraints cover every active and scheduled promotion and every group that appears in them.
- Cart quotes carry engine version, promotion-config revision (including group-policy version), input hash, and breakdown; checkout recomputes authoritatively and payment is allowed only for the exact confirmed quote version.
- Shadow evaluation is running, the canary is limited to the Summer Sale codes, and a kill switch restores the prior evaluator without a schema rollback.
Unresolved dissent I want on the record: the provisional multi-global priority-then-ID rule and the “global beats any group value combination” rule are documented defaults, not Product-signed requirements; sequential eligibility that depends on other discounts is still underspecified. If either surfaces as a business problem during the canary, we stop and do not widen the flag. Ship the flagged two-phase evaluator that satisfies the three facts, keep the old path one switch away, and treat every disagreement metric as a potential stop-ship until Summer Sale traffic proves otherwise.
GO for Friday, conditional on fail-loud group policy and authoritative reprice. Closing three threads:
1. Open challenges:
-
default to Singleton- I concede. Default-safe creates silent mutex where business expected stack. Ship fail-loud-at-activation instead: promotion creation/update rejects ifexclusive_groupnot ingroup_policiestable, and evaluator returns config error + metric, not fallback. @GPT/@Grok were right - no runtime guess. -
filterPositiveprecompute - objection sustained. Standalone compute is invalid if positivity depends on applied stack. Fixed + % in same group, caps, or threshold-on-discounted-total makecompute(candidate, ∅)!=compute(candidate, accepted).
Fix: eligibility = date/usage/product match on undiscounted cart only. Positivity checked inside loop at application time. Phase 1 zero-check is NOT circular because global alone means compute on bare cart. Phase 2 zero-check is sequential:
if compute(cart, accepted + candidate) <= 0 after rounding -> skip, do NOT set blockedGroup
- Multi-global winner: I sign priority-then-ID provisional with @GPT. Priority+ID is deterministic and auditable for 3-day window. Value-based winner is a product change post Summer Sale. Documented as provisional in release note.
2. Attacking our convergence:
We converged on two-phase + group-policy config + disclose-and-confirm. What we didn't name: the config table is new critical surface. Two-phase makes global dominant by construction, but now dominance depends on two independent stores: promotions and group_policies. Race: deploy adds mutex_groups={'seasonal'} cache, promo service still has stale empty cache, creates SUMMER20 clone with group='Seasonal' case-variant - passes text check, fails policy lookup, blocks activation in prod but looks fine in staging. We need single transaction for promo insert + policy existence and cache invalidation with version.
Second: candidate set immutable assumption breaks if checkout mutates cart between read and pricing transaction - gift options, shipping method, price update. If we capture eligible globals from old snapshot, Phase 1 decision is stale. Checkout must rebuild candidate set from current cart inside the payment transaction, not reuse display-path list. Our lazy reprice handles display but final authority must be re-select + re-evaluate + price atomically.
3. Concession audit:
- Moved from priority-invariant to two-phase: argument moved me. Fact 1 "global alone always" makes numeric priority encoding fragile by design - @GPT framing of dominant vs incompatible was decisive.
- 24h grace -> disclose-and-confirm: argument moved me. Unapproved price guarantee with fraud/accounting impact.
- filterPositive pre-filter -> inline positivity: argument moved me.
- Singleton default -> fail-loud: argument moved me.
- Full valid-set optimizer -> greedy two-phase + provisional global winner: clock moved me. With 20 weeks to Black Friday I would ship value-based optimal set search, not greedy.
If deadline vanished, I'd take back the greedy multi-stackable ordering concession. I'd replace lifecycle STACKABLE_IF_MARKED walk with best-value search and explicit pricing order distinct from selection order, and collapse stackable BOOL + scope + group into single policy enum.
Final ship Friday:
// constraints enforced before activation
// scope=global => stackable=false, scope=group => group NOT NULL normalized lower-trim
// scope=none => stackable=true
// FK: promotions.group -> group_policies.group, panic/reject if missing
// Rules per fact: seasonal=SINGLE_WINNER, lifecycle=STACKABLE_IF_MARKED
candidates := dedupByID(allEligibleOnUndiscountedCart(cart))
sort.SliceStable(candidates, byPriorityAscThenID)
globals := filter(candidates, ScopeGlobal)
winner := firstPositive(globals, cart, ∅) // compute on bare cart
if winner != nil {
return ApplyAlone(winner) // replace-and-clear, log suppressed groups
}
// Phase2
for cand in candidates where Scope != Global {
if cand.Scope == None {
if d:=compute(cart, accepted+[cand]); d>0 { result=append(result,cand) }
continue
}
if blockedGroup[cand.Group] { reject(GROUP_CONSUMED); continue }
if policies[cand.Group]==SINGLE_WINNER && appliedCount[cand.Group]>0 {
reject(GROUP_MUTEX); continue
}
if policies[cand.Group]==STACKABLE_IF_MARKED && !cand.Stackable && appliedCount[cand.Group]>0 {
reject(GROUP_OCCUPIED); continue
}
if d:=compute(cart, result+[cand]); d<=0 { continue } // zero does not consume
result.append(cand); appliedCount[group]++; if policy==SINGLE_WINNER||!cand.Stackable { blockedGroup[group]=true }
}
Checkout invariant per @GPT which moderator flagged: Payment authorization may occur only for exact amount and quote version most recently displayed to and confirmed after authoritative repricing.
Reprice: versioned cart engine_version, promo_rev, input_hash(prices,qty,fees), breakdown. Lazy reprice on read/mutation with metric. At checkout, always recompute inside txn. If total up -> show diff + require explicit reconfirm. No grace.
Refuse-to-ship if missing:
- stable sort priority+ID + permutation tests
- pairwise oracle == tracker proofs
- fail-loud group policy config, audit active+scheduled including EMP50/PARTNER100 fixtures
- zero-after-rounding does not consume + positive-only acceptance
- authoritative versioned checkout + shadow old/new + kill switch flag
- dashboard counters for GLOBAL_WIN, GROUP_MUTEX, GROUP_CONSUMED
Dissent preserved: two-phase silently decides global beats any group sum and global suppresses scope=none. Both are reasonable under Fact 1 but are provisional policy not signed product decisions - revisit post Summer Sale.