Skip to content

Where this file lives

  • This path (docs/coupons/membership-coupon-surface-flows.md) is inside the rakletv3 repo.
  • This document describes how membership coupons move through each page and service surface, including one-time invoice/debt coupons and ongoing subscription coupons.

Membership coupon surface flows

Generated: 2026-05-08 Repo: rakletadmin/rakletv3 Focus: page-by-page coupon data flow

Why this file exists

The business rules are defined in membership-coupon-process.md, but reviewers and QA also need a page-level map:

  • where the coupon is entered
  • where preview totals come from
  • where line-level discounts are written back
  • where discounts are persisted on approval or payment

This file is the operational flow map for those surfaces.

Shared mental model

Every migrated surface follows the same conceptual steps:

  1. Build coupon invoice lines
  2. Filter those lines by plan restriction and application-fee rules
  3. Allocate fixed or percentage discounts
  4. Map the allocated discount lines back into the page or persistence model
  5. Persist redemptions when the flow actually completes

The main implementation for one-time invoice/debt coupon steps 2 and 3 lives in Services/MembershipCouponHelper.cs.

Ongoing coupon behavior lives in Services/OngoingMembershipCouponHelper.cs. That helper delegates one-time coupons back to MembershipCouponHelper and applies ongoing-only rules for recurring coupon attachment.

Surface 1: Manager coupon create / detail

Entry points:

  • Raklet.Backend/.../settings-coupon-create.html
  • Raklet.Backend/.../settings-coupon-detail.html
  • Raklet.Api/Controllers/V2/V2CouponsController.cs

Flow:

  1. Manager chooses discount type and amount.
  2. Manager chooses OneTime or Ongoing.
  3. For ongoing coupons, manager chooses Forever or Multiple.
  4. Multiple requires a positive duration-period value.
  5. Manager optionally enables application-fee coverage.
  6. Manager optionally limits the coupon to one or more plans.
  7. The V2 coupon API normalizes and persists:
  8. ApplicationType
  9. OngoingDurationType
  10. OngoingDurationPeriods
  11. ApplicablePlanIds
  12. AppliesToApplicationFee
  13. Detail screen reloads those values and preserves them even if the plan list cannot be loaded safely.
  14. If the coupon is redeemed, only EndDate and MaxRedemptions remain editable.

Surface 2: Manager coupon list

Entry points:

  • Raklet.Backend/.../settings-coupons.controller.js
  • Raklet.Backend/.../settings-coupons.html

Flow:

  1. Coupon list loads membership coupons.
  2. Plan list is loaded in parallel when needed.
  3. Controller derives:
  4. normalized applicable plan ids
  5. application-fee inclusion
  6. human-readable plan scope summary
  7. Table row renders:
  8. main coupon identity / discount columns
  9. plan scope
  10. application-fee inclusion

This surface is read-only and does not allocate discounts. It exists to make coupon scope obvious before opening detail.

Surface 3: Signup credit card preview

Entry points:

  • Application/Controllers/SignupController.cs
  • Application/Views/Signup/CreditCardForm.cshtml

Flow:

  1. Signup builds preview invoice lines from:
  2. selected parent plans
  3. shared registration fee
  4. paid group sub-member selections
  5. Those lines are validated through OngoingMembershipCouponHelper.ValidateSignupCouponForInvoices(...).
  6. One-time coupons are delegated to MembershipCouponHelper.ValidateCouponForInvoices(...).
  7. Ongoing coupons are accepted only when an eligible membership-fee line exists.
  8. Returned AppliedInvoiceDiscounts are mapped back to:
  9. parent plan preview rows
  10. sub-member subscription preview rows
  11. registration fee preview row
  12. Total discount and grand total are recalculated from the mapped line discounts.

Special rule:

  • in group signup/apply flows, the registration fee is shared; if any selected matching plan qualifies and application-fee coverage is enabled, the shared fee is discountable
  • ongoing fixed coupons still target membership fee first; the shared registration fee cannot cover an oversized fixed membership discount

Surface 4: Apply credit card preview

Entry points:

  • Application/Controllers/ApplyController.cs
  • Application/Views/Apply/CreditCardForm.cshtml

Flow:

  1. Build preview invoice lines using the same shape as signup.
  2. If the application comes from signup form context, validate through OngoingMembershipCouponHelper.
  3. Otherwise validate through MembershipCouponHelper, which rejects ongoing coupons.
  4. Map line-level discounts back into the apply view model.
  5. Render parent plan, sub-member, and registration fee discounts consistently with signup.

Surface 5: Admin waiting-application preview

Entry points:

  • Raklet.Backend/.../membership-application-edit-controller.js
  • Raklet.Backend/.../templates/application-edit.html

Flow:

  1. Admin page receives current application data.
  2. Angular controller reconstructs the same coupon-eligible line model client-side.
  3. Controller mirrors the helper rules for:
  4. plan restrictions
  5. application-fee inclusion
  6. fixed vs percentage targeting
  7. Discount values are written back into:
  8. main plan rows
  9. shared registration fee row
  10. signup-origin sub-member subscription rows
  11. Total discount and total amount are recalculated from those line values.

Important note:

  • this is a mirrored implementation, not a direct server helper call
  • when helper business rules change, this controller/template pair must be kept in sync

Surface 6: Application approval

Entry point:

  • Services/ApplicationFormService.cs

Flow:

  1. Approval rebuilds the same invoice-line set used in signup/apply preview.
  2. Signup-origin approvals validate through OngoingMembershipCouponHelper; other approvals validate through MembershipCouponHelper.
  3. Helper output is split into ordered queues:
  4. primary membership queues
  5. sub-member membership queues
  6. Approval applies those queues to:
  7. inscription debt for registration fee discount
  8. primary membership debt/payment rows
  9. sub-member payment rows
  10. Redemption count is increased based on the actual discounted invoice count.
  11. If the applied coupon is ongoing and a real membership-fee discount was created, the matching CustomMembership.CouponId is set.

Important persistence rule:

  • payment records must only carry CouponId when a discount was actually applied in that branch of the flow
  • one-time coupons must not be written to CustomMembership.CouponId
  • ongoing coupons attach to the subscription only when that subscription received a membership-fee discount

Surface 7: Checkout / pay

Entry points:

  • Services/CheckoutService.cs
  • Application/Controllers/CheckoutController.cs
  • Application/Views/Shared/_SortedUnpaidInvoices.cshtml
  • Application/Views/Shared/_StripeCheckout.cshtml

Flow:

  1. Checkout loads real unpaid debt rows for the member.
  2. Each debt row is projected into a coupon invoice item with both:
  3. debt id
  4. MembershipPlanId
  5. Helper validates the coupon against the selected debts.
  6. AppliedInvoiceDiscounts becomes the source of truth for:
  7. row highlighting
  8. discounted totals
  9. saved-card processing
  10. Stripe processing
  11. zero-total coupon-covered completion
  12. coupon metadata persistence on affected debts

Special rule:

  • checkout inscription debts stay plan-specific; a plan-restricted coupon only discounts inscription debts whose own MembershipPlanId matches
  • checkout can accept ongoing coupons through the ongoing helper when a selected membership-fee debt is eligible
  • checkout attaches the ongoing coupon only to subscriptions whose membership-fee debt actually received a discount

Surface 8: Scheduled billing WebJob

Entry point:

  • Raklet.WebJobs.Secondary/Payment/ScheduledPaymentFunctions.cs

Current state:

  • scheduled debt generation uses CustomMembership.CouponId to load the attached coupon
  • EndDate is checked before duration/period rules
  • OngoingDurationType.Multiple derives applied period count from real membership-fee financial records, not from MaxRedemptions
  • generated membership-fee debts carry coupon id and discount amount when the ongoing coupon is applied
  • scheduled payment amounts and before-due reminder balance checks should use the same discounted membership-fee calculation

QA handoff checklist

  • verify the same coupon produces the same discount shape on:
  • signup/apply public preview
  • admin waiting-application preview
  • approval persistence
  • checkout/pay
  • verify fixed coupons still hit only one row
  • verify percentage coupons fan out to all qualifying rows
  • verify group sub-member rows render line-level discounts where applicable
  • verify manager coupon list communicates scope without opening detail
  • verify ongoing fixed coupons target membership fee before application fee
  • verify signup approval attaches CustomMembership.CouponId only to subscriptions that received a real membership-fee discount