Offers

Prev Next

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. indefinite UI 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.ts
  • src/__tests__/lib/helpers/Common.test.tscollectFormErrors

Steps:

  1. Open New Offer → Subscription.
  2. On Step 1 (Offer Type), leave the form blank.
  3. 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.tsREDUCED_CHARGE redemptionValue bounds

Steps:

  1. Create Subscription offer → REDUCED_CHARGE → Percentage.
  2. Enter 200, then -50, then 0, then 1000, then 100, then 15.5.
  3. 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.tsREDUCED_CHARGE redemptionValue bounds

Steps:

  1. Create Subscription offer → REDUCED_CHARGE → Fixed.
  2. 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.tsnumberOfCoupons string shape

Steps:

  1. Create Subscription → Redemption → Limited.
  2. Enter abc, 1.5, -10, 0, 1e9, 0100.
  3. 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.tsREDUCED_CHARGE redemptionLimit bounds

Steps:

  1. Create Subscription REDUCED_CHARGE offer.
  2. 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.tsScheduleDetails cross-field

Steps:

  1. On Availability step set From = Dec 31 2026, To = Jan 1 2024.
  2. Save.
  3. Set From = To (same instant). Save.
  4. 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.tsC7: "No End Date" coupling
  • src/__tests__/adapters/monetization/offers/deserializeOffer.adapter.test.tsC7: infer hasEndDate
  • src/__tests__/adapters/monetization/offers/serializeOffer.adapter.test.tsC7: strip UI-only scheduleDetails.hasEndDate
  • src/__tests__/adapters/monetization/offers/roundTrip.test.tsC7: round-trip guards
  • src/__tests__/modules/monetization/offers/addOrEdit/subscription-steps.test.tsxAvailabilityStep → C7:

Steps:

  1. Open New Offer. "No End Date" is checked by default; picker is disabled.
  2. Uncheck "No End Date". Picker becomes enabled, date is empty.
  3. Try to save without picking a date.
  4. 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 — always false)
  • src/__tests__/adapters/monetization/offers/serializeOffer.adapter.test.ts → drops indefinite
  • src/__tests__/adapters/monetization/offers/roundTrip.test.ts → never leaks indefinite

Steps:

  1. Create REDUCED_CHARGE offer with Indefinite checked. Save.
  2. Reopen in edit mode.
  3. Inspect the Indefinite checkbox.
  4. 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, offerTag preservation, empty-object preservation, server-owned key stripping.

Steps:

  1. For each variant (REDUCED_CHARGE+SINGLE_USE, FREE_TRIAL+UNLIMITED, FREE_TRIAL_UNTIL+PREPAID): open edit, change only the name, save.
  2. 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.tsxPricingStep (edit mode) — C10 confirm-before-edit gate
  • src/__tests__/modules/monetization/offers/addOrEdit/tvod-steps.test.tsxPricingStepCREATE mode: cases

Steps:

  1. Create a fresh TVOD offer. Confirm Prefix and # of Coupons are open and have no Edit icon.
  2. Save and reopen in edit mode. Confirm both inputs are now disabled and each shows an Edit icon.
  3. Click Edit on Prefix. Cancel the confirmation. Verify it stays locked.
  4. Click Edit on Prefix. Confirm. Verify it becomes editable.
  5. 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:

  1. Log in without the role — sidebar "Monetization" hidden.
  2. Log in with subscriptions:read only — can open an offer in edit mode; Save button hidden.
  3. Log in with subscriptions:write — full CRUD.
  4. 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.tsFREE_TRIAL_UNTIL endDate

Steps:

  1. FREE_TRIAL_UNTIL with No End Date checked + trial endDate null. Save.
  2. FREE_TRIAL_UNTIL with No End Date unchecked + trial endDate null. Save.
  3. endDate in the past. Save.
  4. endDate == From-date. Save.
  5. 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.tstoNumericString, toNumber
  • src/__tests__/adapters/monetization/offers/serializeOffer.adapter.test.ts
  • src/__tests__/adapters/monetization/offers/deserializeOffer.adapter.test.ts
  • src/__tests__/adapters/monetization/offers/roundTrip.test.ts

Steps:

  1. Load a subscription offer from staging (src/data/monetization/offerData.mock.json).
  2. Save without changes.
  3. Inspect the PUT body.
  4. 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.tsofferTag cases
  • src/__tests__/adapters/monetization/offers/roundTrip.test.ts → TVOD fixture preserves BZR

Steps:

  1. Load a REDEMPTION or PREPAID offer with a populated offerTag.
  2. Save without changes.
  3. 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.ts
  • src/__tests__/adapters/monetization/offers/roundTrip.test.ts

Steps:

  1. Load a TVOD offer (src/data/monetization/tvodOffer.mock.json) — no scheduledToDate, empty freeTrialUntil, no offerStrategyType.
  2. Save without changes.
  3. 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 subscription
  • src/__tests__/modules/monetization/offers/addOrEdit/subscription-steps.test.tsx → per-step render gates

Gap: no end-to-end wizard + save integration test.

Steps:

  1. Fill name/description/status.
  2. Step through Type → Benefit → Redemption → Billing → Availability → Marketing.
  3. 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 requires numberOfCoupons + promotionCodes
  • src/__tests__/modules/monetization/offers/addOrEdit/subscription-steps.test.tsxRedemptionStep renders LIMITED fields

Gap: Enter-to-add, dedupe, whitespace-only reject not covered.

Steps:

  1. Create Fixed REDUCED_CHARGE + LIMITED.
  2. Add codes via Enter, paste, and the Add button. Try duplicates and whitespace-only.
  3. 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 + UNLIMITED
  • src/__tests__/modules/monetization/offers/addOrEdit/subscription-steps.test.tsx → Benefit / Billing / Redemption branches

Steps:

  1. Create FREE_TRIAL offer with Multiplier + Period Type.
  2. Set Redemption = UNLIMITED, add at least one code.
  3. 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 validation
  • src/__tests__/modules/monetization/offers/addOrEdit/subscription-steps.test.tsx → period-type swap + reset

Gap: PREPAID pairing + end-to-end save not covered.

Steps:

  1. Create FREE_TRIAL_UNTIL + PREPAID offer.
  2. Pick a future trial end date.
  3. 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:

  1. For each content type, search a known title.
  2. Select, proceed to Pricing, fill Prefix + # of Coupons, set Availability.
  3. 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.tsxEdit mode — suites

Steps:

  1. Open a saved subscription offer.
  2. 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:

  1. Open a saved TVOD offer.
  2. 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.ts
  • src/__tests__/modules/monetization/offers/index.test.tsx

Steps:

  1. New Offer → Subscription → /monetization/offers/add/subscription.
  2. New Offer → Transactional → /monetization/offers/add/tvod.
  3. Add New Redemption Code → SVOD → /monetization/offers/add/subscription.
  4. 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:

  1. Create mode: try clicking step 3 from step 1.
  2. Complete steps 1–2, then jump back to 1.
  3. 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.tsxresetOnOfferTypeChange

Steps:

  1. Select REDUCED_CHARGE, fill fields.
  2. Switch to FREE_TRIAL.
  3. Switch back to REDUCED_CHARGE.
  4. 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.tsxresetOnOfferLimitTypeChange

Steps:

  1. Walk every transition combination.
  2. 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.tslist query shape

Gap: list UI component has no test — deep-linking and page-size changes untested.

Steps:

  1. Deep-link with ?searchTerm=foo&page=2&limit=25.
  2. Change page size. Back/forward through browser history.
  3. 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:

  1. Click a row body.
  2. Click the kebab menu → Edit.
  3. 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:

  1. 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.ts
  • src/__tests__/adapters/monetization/offers/roundTrip.test.ts
  • src/__tests__/hooks/monetization/offers/useSingleOffer.test.ts

Steps:

  1. Create an offer. Inspect the POST body.
  2. 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.ts
  • src/__tests__/services/monetization/OffersService.test.ts

Steps:

  1. Throttle or 500 the save endpoint. Submit from create and edit.
  2. 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()}s1 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.