Monetization Offers — QA Test Plan
Pre-release checklist for the Offers module. Money-handling — treat Critical items as release blockers until verified.
Totals
| Severity | Count |
|---|---|
| Critical | 15 |
| High | 16 |
| Medium | 13 |
| Low | 6 |
| Total | 50 |
Coverage summary (Critical + High)
22 — schema + adapter layer, save-path wiring, permission gating, TVOD edit gate. 6 — e.g. indefiniteUI load (C8), full wizard E2E (H1–H4).3 — H5, H13, H14.
Critical — Release blockers
C1 — Global "Save" bypasses form validation
Scope: The header Save button must run the schema before calling the API.
Tests:
src/__tests__/modules/monetization/offers/addOrEdit/schema.test.tssrc/__tests__/lib/helpers/Common.test.ts→collectFormErrors
Steps:
- Open New Offer → Subscription.
- On Step 1 (Offer Type), leave the form blank.
- Click Save in the page header.
Expected: No network request. One toast per failed field.
Risk: Corrupted/invalid offer records if Save bypasses validation.
Ref: src/modules/monetization/offers/addOrEdit/index.tsx
C2 — Percentage discount value clamped to (0, 100]
Scope: redemptionValue must satisfy 0 < value <= 100 when redemptionType === 'PERCENTAGE'.
Tests:
src/__tests__/modules/monetization/offers/addOrEdit/schema.test.ts→REDUCED_CHARGE redemptionValue bounds
Steps:
- Create Subscription offer → REDUCED_CHARGE → Percentage.
- Enter 200, then -50, then 0, then 1000, then 100, then 15.5.
- Advance and Save.
Expected: 200 / -50 / 0 / 1000 rejected. 100 and 15.5 accepted.
Risk: 200% discount = platform pays customer; negative = price increase; zero = no-op offer persisted.
Ref: src/modules/monetization/offers/addOrEdit/schema.ts
C3 — Fixed-amount discount has no lower bound
Scope: redemptionValue > 0 when redemptionType === 'FIXED'. No upper bound (depends on plan price).
Tests:
src/__tests__/modules/monetization/offers/addOrEdit/schema.test.ts→REDUCED_CHARGE redemptionValue bounds
Steps:
- Create Subscription offer → REDUCED_CHARGE → Fixed.
- Enter 0, -100, 2.5, a number larger than any plan price.
Expected: 0 and -100 rejected. 2.5 and large values accepted.
Risk: Negative charge = refund. Zero = silent no-op.
Ref: src/modules/monetization/offers/addOrEdit/schema.ts
C4 — numberOfCoupons accepts non-numeric strings
Scope: numberOfCoupons must match /^[1-9]\d*$/ on both Subscription and TVOD.
Tests:
src/__tests__/modules/monetization/offers/addOrEdit/schema.test.ts→numberOfCoupons string shape
Steps:
- Create Subscription → Redemption → Limited.
- Enter
abc,1.5,-10,0,1e9,0100. - Repeat on TVOD Pricing step.
Expected: All rejected. Plain positive integers accepted.
Risk: BE errors, generates 0 codes, or generates a billion codes.
Ref: src/modules/monetization/offers/addOrEdit/schema.ts
C5 — Billing periods allows 0 / negative / decimals
Scope: redemptionLimit must be a positive integer.
Tests:
src/__tests__/modules/monetization/offers/addOrEdit/schema.test.ts→REDUCED_CHARGE redemptionLimit bounds
Steps:
- Create Subscription REDUCED_CHARGE offer.
- On Billing step enter 0, -3, 1.5, 1, then a very large integer.
Expected: 0, -3, 1.5 rejected. 1 and large integers accepted.
Risk: Customer billed incorrectly for discount duration.
Ref: src/modules/monetization/offers/addOrEdit/schema.ts
C6 — To-Date not validated against From-Date
Scope: scheduledToDate > scheduledFromDate when end date is set.
Tests:
src/__tests__/modules/monetization/offers/addOrEdit/schema.test.ts→ScheduleDetails cross-field
Steps:
- On Availability step set From = Dec 31 2026, To = Jan 1 2024.
- Save.
- Set From = To (same instant). Save.
- Set To > From. Save.
Expected: Cases 1 and 2 blocked. Case 3 succeeds.
Risk: Offer created with expiry before start — instantly-expired or redemption-time errors.
Ref: src/modules/monetization/offers/addOrEdit/schema.ts
C7 — Unchecking "No End Date" silently sets To-Date to NOW
Scope: The "No End Date" checkbox owns scheduleDetails.hasEndDate. Toggling it never stamps the date picker with now().
Tests:
src/__tests__/modules/monetization/offers/addOrEdit/schema.test.ts→C7: "No End Date" couplingsrc/__tests__/adapters/monetization/offers/deserializeOffer.adapter.test.ts→C7: infer hasEndDatesrc/__tests__/adapters/monetization/offers/serializeOffer.adapter.test.ts→C7: strip UI-only scheduleDetails.hasEndDatesrc/__tests__/adapters/monetization/offers/roundTrip.test.ts→C7:round-trip guardssrc/__tests__/modules/monetization/offers/addOrEdit/subscription-steps.test.tsx→AvailabilityStep → C7:
Steps:
- Open New Offer. "No End Date" is checked by default; picker is disabled.
- Uncheck "No End Date". Picker becomes enabled, date is empty.
- Try to save without picking a date.
- Re-check "No End Date" after picking a date.
Expected: Step 3 blocked with "End date is required when 'No End Date' is unchecked". Step 4 clears any picked date.
Risk: Offer DOA — marketing campaign launches with a dead offer.
Ref: src/modules/monetization/offers/addOrEdit/sections/offer-availability/index.tsx
C8 — indefinite flag is hardcoded to false on edit load
Scope: On edit, the Billing step's Indefinite checkbox must reflect the saved value. Serialize must never leak the UI-only flag.
Tests:
src/__tests__/adapters/monetization/offers/deserializeOffer.adapter.test.ts(codifies current bug — alwaysfalse)src/__tests__/adapters/monetization/offers/serializeOffer.adapter.test.ts→ dropsindefinitesrc/__tests__/adapters/monetization/offers/roundTrip.test.ts→ never leaksindefinite
Steps:
- Create REDUCED_CHARGE offer with Indefinite checked. Save.
- Reopen in edit mode.
- Inspect the Indefinite checkbox.
- Without changes, click Save and inspect the PUT body.
Expected: Step 3 checkbox reflects saved value (currently wrong — always unchecked). Step 4 payload has no indefinite key.
Risk: Editor misled about saved config. Save path is safe; UI load is lying.
Ref: src/adapters/monetization/offers/deserializeOffer.adapter.ts
C9 — Edit save ships the full form body — locked fields may overwrite
Scope: Load + save without edits must produce an equivalent PUT body for every deserialize-captured field.
Tests:
src/__tests__/adapters/monetization/offers/roundTrip.test.ts→ round-trip identity, numeric type preservation,offerTagpreservation, empty-object preservation, server-owned key stripping.
Steps:
- For each variant (REDUCED_CHARGE+SINGLE_USE, FREE_TRIAL+UNLIMITED, FREE_TRIAL_UNTIL+PREPAID): open edit, change only the name, save.
- Diff the PUT body against the pre-edit record.
Expected: Byte-for-byte equivalence on offerDetails except the edited field.
Risk: Any new BE field not added to both adapters will silently drop from saves — caught by extending round-trip tests.
Ref: src/adapters/monetization/offers/
C10 — TVOD Pricing: Prefix & # of Coupons gated behind a confirmation
Scope: TVOD-only and edit-mode-only. In CREATE mode both fields are open and the Edit icon is hidden. In EDIT mode campaignTag and numberOfCoupons are locked by default and each shows an inline Edit icon that opens a ConfirmationDialog. Confirming unlocks that one field; cancelling keeps it locked. Unlocking one field re-locks the other (mutual exclusion), so the user can never silently flip both at once. Subscription's same-named SINGLE_USE / PREPAID fields are hard-locked in edit mode (H6).
Tests:
src/__tests__/modules/monetization/offers/addOrEdit/tvod-steps.test.tsx→PricingStep (edit mode) — C10 confirm-before-edit gatesrc/__tests__/modules/monetization/offers/addOrEdit/tvod-steps.test.tsx→PricingStep→CREATE mode:cases
Steps:
- Create a fresh TVOD offer. Confirm Prefix and # of Coupons are open and have no Edit icon.
- Save and reopen in edit mode. Confirm both inputs are now disabled and each shows an Edit icon.
- Click Edit on Prefix. Cancel the confirmation. Verify it stays locked.
- Click Edit on Prefix. Confirm. Verify it becomes editable.
- Click Edit on # of Coupons. Confirm. Verify it becomes editable and Prefix snaps back to locked.
Expected: Each step behaves as described. Dialog title / body match the field being edited.
Risk: Without the gate, distributed promo codes could be silently invalidated by a stray edit. Mitigated.
Ref: src/modules/monetization/offers/addOrEdit/sections/offer-pricing/index.tsx
C11 — Role-based access control — Subscriptions & Offers
Scope: Users without subscriptions:read never see the nav entry; users without subscriptions:write cannot submit from the editor; every outgoing call carries RoleKeyEnum.SubscriptionsAndOffers.
Tests:
src/__tests__/services/monetization/OffersService.test.ts→ role key on every call.src/__tests__/adapters/permissions/landingPathResolver.test.ts→ sidebar gate.
Steps:
- Log in without the role — sidebar "Monetization" hidden.
- Log in with
subscriptions:readonly — can open an offer in edit mode; Save button hidden. - Log in with
subscriptions:write— full CRUD. - Hit the API directly without the role cookie.
Expected: 1–3 per scope. 4 — BE rejects with 401/403.
Risk: Privilege escalation on a money-handling resource. Direct-API enforcement is backend-owned.
Ref: src/modules/monetization/offers/addOrEdit/index.tsx
C12 — FREE_TRIAL_UNTIL end date has no upper bound
Scope: endDate must be > scheduledFromDate. Nullability is coupled to "No End Date". Upper bound still open (tracked via TODO in schema).
Tests:
src/__tests__/modules/monetization/offers/addOrEdit/schema.test.ts→FREE_TRIAL_UNTIL endDate
Steps:
- FREE_TRIAL_UNTIL with No End Date checked + trial endDate null. Save.
- FREE_TRIAL_UNTIL with No End Date unchecked + trial endDate null. Save.
- endDate in the past. Save.
- endDate == From-date. Save.
- endDate 50 years in the future. Save.
Expected: 1 succeeds. 2–4 rejected. 5 currently accepted (no upper bound).
Risk: Unbounded free access (case 5) or instantly-expired trial.
Ref: src/modules/monetization/offers/addOrEdit/schema.ts
C13 — Numeric staging fields must round-trip as numbers
Scope: numberOfCoupons, renewalCycleMultiplier, marketing.cookieValidDays come from the BE as number and must go back as number.
Tests:
src/__tests__/lib/helpers/Common.test.ts→toNumericString,toNumbersrc/__tests__/adapters/monetization/offers/serializeOffer.adapter.test.tssrc/__tests__/adapters/monetization/offers/deserializeOffer.adapter.test.tssrc/__tests__/adapters/monetization/offers/roundTrip.test.ts
Steps:
- Load a subscription offer from staging (
src/data/monetization/offerData.mock.json). - Save without changes.
- Inspect the PUT body.
- Repeat with the TVOD fixture.
Expected: All three fields are JSON numbers, not quoted strings. Explicit null and empty-string inputs round-trip as null.
Risk: BE 4xx on save; user loses wizard state.
Ref: src/adapters/monetization/offers/
C14 — offerTag must survive load + save
Scope: BE-generated offerTag on offerLimit must round-trip unchanged.
Tests:
src/__tests__/modules/monetization/offers/addOrEdit/schema.test.ts→offerTagcasessrc/__tests__/adapters/monetization/offers/roundTrip.test.ts→ TVOD fixture preservesBZR
Steps:
- Load a REDEMPTION or PREPAID offer with a populated
offerTag. - Save without changes.
- Inspect the PUT body.
Expected: offerDetails.offerLimit.offerTag present and equal to the loaded value.
Risk: Silent data loss on every edit-save. Breaks campaign analytics and code-invalidation.
Ref: src/modules/monetization/offers/addOrEdit/schema.ts
C15 — TVOD load with missing optional keys must not crash
Scope: Absent scheduledToDate, offerStrategyType, and empty-object freeTrialUntil / reduceCharge must parse without inventing defaults.
Tests:
src/__tests__/modules/monetization/offers/addOrEdit/schema.test.ts→ C17 (Subscription + TVOD)src/__tests__/adapters/monetization/offers/deserializeOffer.adapter.test.tssrc/__tests__/adapters/monetization/offers/roundTrip.test.ts
Steps:
- Load a TVOD offer (
src/data/monetization/tvodOffer.mock.json) — noscheduledToDate, emptyfreeTrialUntil, noofferStrategyType. - Save without changes.
- Inspect the PUT body.
Expected: Form loads without errors. Payload preserves {}, null, and absent keys exactly as received.
Risk: Load-time crash or silent data invention that poisons downstream consumers.
Ref: src/adapters/monetization/offers/
High — Core flows
H1 — Happy path: Subscription REDUCED_CHARGE (Percentage) + SINGLE_USE
Scope: Full 6-step create-and-save for a percentage single-use offer.
Tests:
src/__tests__/modules/monetization/offers/addOrEdit/schema.test.ts→ valid REDUCED_CHARGE subscriptionsrc/__tests__/modules/monetization/offers/addOrEdit/subscription-steps.test.tsx→ per-step render gates
Gap: no end-to-end wizard + save integration test.
Steps:
- Fill name/description/status.
- Step through Type → Benefit → Redemption → Billing → Availability → Marketing.
- Save.
Expected: Toast, navigation back to list, entry shows Name + Active badge + Reduced Charge + Single Use.
Risk: Core happy path — regressions here break the feature's primary job.
Ref: src/modules/monetization/offers/addOrEdit/
H2 — Happy path: Subscription REDUCED_CHARGE (Fixed) + LIMITED (multi-code)
Scope: Fixed-discount LIMITED flow with the multi-promo-code input.
Tests:
src/__tests__/modules/monetization/offers/addOrEdit/schema.test.ts→ LIMITED requiresnumberOfCoupons+promotionCodessrc/__tests__/modules/monetization/offers/addOrEdit/subscription-steps.test.tsx→RedemptionSteprenders LIMITED fields
Gap: Enter-to-add, dedupe, whitespace-only reject not covered.
Steps:
- Create Fixed REDUCED_CHARGE + LIMITED.
- Add codes via Enter, paste, and the Add button. Try duplicates and whitespace-only.
- Remove one. Save.
Expected: Duplicates and whitespace-only rejected; removed codes not in payload.
Risk: Bad promo-code data corrupts redemption.
Ref: src/modules/monetization/offers/addOrEdit/sections/offer-redemption/
H3 — Happy path: Subscription FREE_TRIAL (Days/Months/Years) + UNLIMITED
Scope: FREE_TRIAL with UNLIMITED promo codes.
Tests:
src/__tests__/modules/monetization/offers/addOrEdit/schema.test.ts→ valid FREE_TRIAL + UNLIMITEDsrc/__tests__/modules/monetization/offers/addOrEdit/subscription-steps.test.tsx→ Benefit / Billing / Redemption branches
Steps:
- Create FREE_TRIAL offer with Multiplier + Period Type.
- Set Redemption = UNLIMITED, add at least one code.
- Save.
Expected: Billing step shows info message. Save succeeds.
Risk: Free-trial misconfiguration = unlimited free access.
Ref: src/modules/monetization/offers/addOrEdit/
H4 — Happy path: Subscription FREE_TRIAL_UNTIL + PREPAID
Scope: "Until a Date" trial swaps Multiplier for a date picker; freeTrial cycle fields null out.
Tests:
src/__tests__/modules/monetization/offers/addOrEdit/schema.test.ts→ FREE_TRIAL_UNTIL validationsrc/__tests__/modules/monetization/offers/addOrEdit/subscription-steps.test.tsx→ period-type swap + reset
Gap: PREPAID pairing + end-to-end save not covered.
Steps:
- Create FREE_TRIAL_UNTIL + PREPAID offer.
- Pick a future trial end date.
- Save.
Expected: offerStrategyType === 'FREE_TRIAL_UNTIL', freeTrial.renewalCycleMultiplier/Type === null in the payload.
Risk: Trial duration misconfigured.
Ref: src/modules/monetization/offers/addOrEdit/sections/offer-benefit/
H5 — Happy path: TVOD across every content type
Scope: Content search and selection for video, series, bundle, article, audio, event.
Tests: None. ContentService is mocked but search/select interactions are not driven.
Steps:
- For each content type, search a known title.
- Select, proceed to Pricing, fill Prefix + # of Coupons, set Availability.
- Save.
Expected: Selected content attached with correct type; list entry renders it.
Risk: TVOD creation broken for one or more content types.
Ref: src/modules/monetization/offers/addOrEdit/sections/offer-content-selection/
H6 — Edit: Subscription wizard locks immutable fields
Scope: Subscription-only. In edit mode, the Subscription wizard locks: OfferType radios, Benefit (discount type + value, period type, multiplier), Billing periods + Indefinite, and on the Redemption step — promo codes, # of Coupons (LIMITED), Prefix + # of Codes (SINGLE_USE / PREPAID). TVOD's same-named Pricing fields are tracked separately by C10 because they are not locked there.
Tests:
src/__tests__/modules/monetization/offers/addOrEdit/subscription-steps.test.tsx→Edit mode —suites
Steps:
- Open a saved subscription offer.
- Scan each step.
Expected: Locked fields greyed out. Name, Description, Status, Availability dates, Marketing remain editable.
Risk: Config drift on immutable fields.
Ref: src/modules/monetization/offers/addOrEdit/sections/offer-redemption/index.tsx
H7 — Edit: TVOD content locked, search hidden
Scope: TVOD edit renders a read-only content tile, not the search UI.
Tests:
src/__tests__/modules/monetization/offers/addOrEdit/tvod-steps.test.tsx→ "does not render search input in edit mode"
Steps:
- Open a saved TVOD offer.
- Inspect the Content step.
Expected: Title + type tile visible; no search input, no clear/reselect control.
Risk: Content swap on an existing TVOD offer invalidates distributed codes.
Ref: src/modules/monetization/offers/addOrEdit/sections/offer-content-selection/
H8 — Entry points: all four Create-dialog paths
Scope: List header "New Offer" and "Add New Redemption Code" buttons open the right init dialogs and routes.
Tests:
src/__tests__/config/monetization/offers/offerInitConfig.test.tssrc/__tests__/modules/monetization/offers/index.test.tsx
Steps:
- New Offer → Subscription →
/monetization/offers/add/subscription. - New Offer → Transactional →
/monetization/offers/add/tvod. - Add New Redemption Code → SVOD →
/monetization/offers/add/subscription. - Add New Redemption Code → TVOD →
/monetization/offers/add/tvod.
Expected: Each path routes correctly and the wizard title matches the entry.
Risk: Users land in the wrong wizard.
Ref: src/modules/monetization/offers/
H9 — Stepper navigation: forward locked, backward free; edit mode full access
Scope: Create mode gates forward on step completion; edit mode pre-unlocks all steps.
Tests:
src/__tests__/modules/monetization/offers/addOrEdit/stepper-and-config.test.tsx→ stepper gating
Gap: edit-mode preloading of completed steps has no direct test.
Steps:
- Create mode: try clicking step 3 from step 1.
- Complete steps 1–2, then jump back to 1.
- Edit mode: click any step directly.
Expected: 1 blocked. 2 and 3 succeed.
Risk: Users stuck or able to skip required fields.
Ref: src/modules/monetization/offers/addOrEdit/Stepper.tsx
H10 — Offer-type switching resets stale branch fields
Scope: REDUCED_CHARGE ↔ FREE_TRIAL clears the other branch's fields. Re-selecting the same type is a no-op.
Tests:
src/__tests__/modules/monetization/offers/addOrEdit/subscription-steps.test.tsx→resetOnOfferTypeChange
Steps:
- Select REDUCED_CHARGE, fill fields.
- Switch to FREE_TRIAL.
- Switch back to REDUCED_CHARGE.
- Re-select REDUCED_CHARGE.
Expected: 2 clears reduceCharge/indefinite. 3 clears freeTrial fields. 4 preserves values.
Risk: Stale payload fields from abandoned branch.
Ref: src/lib/helpers/monetization/
H11 — Redemption-type switching resets stale limit fields
Scope: SINGLE_USE / LIMITED / UNLIMITED / PREPAID transitions clear numberOfCoupons, promotionCodes, campaignTag. Re-selecting the same type preserves.
Tests:
src/__tests__/modules/monetization/offers/addOrEdit/subscription-steps.test.tsx→resetOnOfferLimitTypeChange
Steps:
- Walk every transition combination.
- Re-select the same type after filling.
Expected: Resets fire on change; no-op on same-type re-select.
Risk: Stale limit fields leak into payload.
Ref: src/lib/helpers/monetization/
H12 — List: search, pagination, item-limit
Scope: ?searchTerm, ?page, ?limit are URL-driven.
Tests:
src/__tests__/services/monetization/OffersService.test.ts→listquery shape
Gap: list UI component has no test — deep-linking and page-size changes untested.
Steps:
- Deep-link with
?searchTerm=foo&page=2&limit=25. - Change page size. Back/forward through browser history.
- Clear the search.
Expected: URL and table stay in sync. Empty-state renders on no matches.
Risk: Lost search state or broken deep-links.
Ref: src/modules/monetization/offers/list/
H13 — List: row click navigates; action menu does not double-fire
Scope: Row click routes to edit. Kebab menu must stopPropagation.
Tests: None.
Steps:
- Click a row body.
- Click the kebab menu → Edit.
- Click the kebab menu → Version History.
Expected: 1 and 2 navigate to edit exactly once. 3 routes to versions without a prior edit navigation.
Risk: Double navigation / wrong route.
Ref: src/modules/monetization/offers/list/
H14 — List: columns render correctly
Scope: Status badge, formatted type, formatted limit, dash fallbacks.
Tests: None.
Steps:
- Inspect a list with a mix of Active/Expired offers across all strategy and limit types.
Expected: Status = Expired (destructive) or Active (success). Type = formatted offerStrategyType. Limit = formatted offerLimitType. Null fields show —.
Risk: Misleading list that hides or miscommunicates offer state.
Ref: src/modules/monetization/offers/list/columns.tsx
H15 — Save payload: contentStatus is renamed to status
Scope: UI uses contentStatus; BE expects status. Rename lives in the serializer.
Tests:
src/__tests__/adapters/monetization/offers/serializeOffer.adapter.test.tssrc/__tests__/adapters/monetization/offers/roundTrip.test.tssrc/__tests__/hooks/monetization/offers/useSingleOffer.test.ts
Steps:
- Create an offer. Inspect the POST body.
- Edit same offer. Inspect the PUT body.
Expected: Both payloads contain status, never contentStatus.
Risk: BE drops the field and status silently defaults.
Ref: src/adapters/monetization/offers/serializeOffer.adapter.ts
H16 — Error paths: save failure and load failure
Scope: Network failures surface a toast and preserve form state.
Tests:
src/__tests__/hooks/monetization/offers/useSingleOffer.test.tssrc/__tests__/services/monetization/OffersService.test.ts
Steps:
- Throttle or 500 the save endpoint. Submit from create and edit.
- 404/500 the GET endpoint. Open edit.
Expected: Error toasts; form values preserved on save failure; layout does not crash on load failure.
Risk: Data loss on transient network errors; blank-screen crash on bad load.
Ref: src/hooks/monetization/offers/useSingleOffer.ts
Medium — Edge cases and UX gaps
M1 — Promo code dedupe: case, whitespace, character set
Scope: codes.includes(trimmed) dedupes only exact matches.
Steps: Test PROMO vs promo, leading/trailing spaces, mid-string spaces, unicode, very long codes.
Risk: Duplicate-looking codes accepted; analytics and redemption confused.
Ref: src/modules/monetization/offers/addOrEdit/sections/offer-redemption/
M2 — Summary panel live preview
Scope: Right-side Summary panel reflects form state across every step.
Steps: Fill each step; verify Name, Description, Type, Value / Trial Period, Redemption, Billing, Starts, Ends update live.
Risk: Users save based on a stale preview.
Ref: src/modules/monetization/offers/addOrEdit/SummaryPanel.tsx
M3 — Summary panel pluralisation bug
Scope: Renders ${multiplier} ${cycleType.toLowerCase()}s → 1 days.
Steps: Trigger 1 day / 1 month / 1 year cases.
Risk: Low — copy only. Confirm product acceptance.
Ref: src/modules/monetization/offers/addOrEdit/SummaryPanel.tsx
M4 — Cookie duration: edge values
Scope: Defaults to "30"; stored as string even though input is type=number.
Steps: Test blank, 0, negative, decimals, very large values.
Risk: BE accepts garbage or display glitches.
Ref: src/modules/monetization/offers/addOrEdit/sections/offer-marketing/
M5 — Name & description: special chars, long input
Scope: No client-side max length.
Steps: Test emoji, RTL, quotes/backslashes/HTML, 500+ char description. Verify list, detail, and JSON serialisation.
Risk: Rendering breaks or payload injection vectors.
Ref: src/modules/monetization/offers/addOrEdit/sections/offer-basic-details/
M6 — Content search: debounce & type filtering
Scope: 500 ms debounce; switching content type while mid-search re-runs.
Steps: Rapid typing, mid-search type switch, clear keyword.
Risk: Burst network calls; stale results.
Ref: src/modules/monetization/offers/addOrEdit/sections/offer-content-selection/
M7 — Unsaved-changes guard is missing
Scope: No router-leave or beforeunload confirmation.
Steps: Fill part of the wizard, navigate away, confirm no warning appears.
Risk: Silent data loss.
Ref: src/modules/monetization/offers/addOrEdit/
M8 — Timezone handling
Scope: Dates stored as ISO UTC.
Steps: Save in one timezone; reopen in another and verify displayed date matches user intent.
Risk: Off-by-one-day bugs at midnight boundaries.
Ref: src/modules/monetization/offers/addOrEdit/sections/offer-availability/
M9 — Enter key should not save the wizard
Scope: Wizard is wrapped in <Form>; Enter inside an input must not submit the outer form.
Steps: Press Enter in the promo code input, name input, cookie duration input.
Risk: Accidental submission of invalid forms.
Ref: src/modules/monetization/offers/addOrEdit/
M10 — "Download Promo Codes" (.xls) file is actually plain text
Scope: Blob type is text/plain but filename is .xls.
Steps: Download from a LIMITED/PREPAID offer; open in Excel.
Risk: Excel complains or misformats; user sees broken download.
Ref: src/modules/monetization/offers/addOrEdit/sections/offer-pricing/
M11 — Per-step Next gate
Scope: Only the OfferType step gates Next on local state; others rely on global save.
Steps: Skip through with empty steps; confirm QA-expected behaviour.
Risk: Users reach Save with invalid intermediate steps.
Ref: src/modules/monetization/offers/addOrEdit/
M12 — Percentage ↔ Fixed switch preserves value
Scope: Toggling redemptionType does not reset redemptionValue.
Steps: Enter 25 (as %) then toggle to Fixed.
Risk: User submits 25 dollars instead of 25%.
Ref: src/modules/monetization/offers/addOrEdit/sections/offer-benefit/
M13 — Mobile / narrow viewport
Scope: Summary panel is hidden lg:block.
Steps: Run the full wizard on a narrow viewport.
Risk: Wizard unusable on tablets/phones.
Ref: src/modules/monetization/offers/addOrEdit/
Low — Polish and cleanup
L1 — console.log leak in offer layout
Scope: Unconditional console.log in edit/add layout.
Ref: src/app/(protected)/monetization/offers/[...offer]/layout.tsx
L2 — TVOD defaults include stray FREE_TRIAL values
Scope: transactionalOfferDefaults.offerDetails.freeTrial preloaded with values TVOD never uses.
Ref: src/modules/monetization/offers/addOrEdit/schema.ts
L3 — cookieValidDays string-vs-number mismatch
Scope: Minor type inconsistency in the form schema.
Ref: src/modules/monetization/offers/addOrEdit/schema.ts
L4 — Empty-list CTA tone
Scope: NoData block copy and icon.
Ref: src/modules/monetization/offers/list/
L5 — Wizard title copy differs by entry point
Scope: "Create New Offer" vs "Create New Content Redemption Code" — verify consistency.
Ref: src/modules/monetization/offers/addOrEdit/
L6 — Promo code chip wrap / overflow
Scope: Many codes should wrap cleanly.
Ref: src/modules/monetization/offers/addOrEdit/sections/offer-redemption/
Out of scope
Redemption-time flows (customer-side application), payment-gateway integration, invoice math, tax handling, promo-code generation backend — those live outside this admin module.