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:
- Build coupon invoice lines
- Filter those lines by plan restriction and application-fee rules
- Allocate fixed or percentage discounts
- Map the allocated discount lines back into the page or persistence model
- 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.htmlRaklet.Backend/.../settings-coupon-detail.htmlRaklet.Api/Controllers/V2/V2CouponsController.cs
Flow:
- Manager chooses discount type and amount.
- Manager chooses
OneTimeorOngoing. - For ongoing coupons, manager chooses
ForeverorMultiple. Multiplerequires a positive duration-period value.- Manager optionally enables application-fee coverage.
- Manager optionally limits the coupon to one or more plans.
- The V2 coupon API normalizes and persists:
ApplicationTypeOngoingDurationTypeOngoingDurationPeriodsApplicablePlanIdsAppliesToApplicationFee- Detail screen reloads those values and preserves them even if the plan list cannot be loaded safely.
- If the coupon is redeemed, only
EndDateandMaxRedemptionsremain editable.
Surface 2: Manager coupon list¶
Entry points:
Raklet.Backend/.../settings-coupons.controller.jsRaklet.Backend/.../settings-coupons.html
Flow:
- Coupon list loads membership coupons.
- Plan list is loaded in parallel when needed.
- Controller derives:
- normalized applicable plan ids
- application-fee inclusion
- human-readable plan scope summary
- Table row renders:
- main coupon identity / discount columns
- plan scope
- 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.csApplication/Views/Signup/CreditCardForm.cshtml
Flow:
- Signup builds preview invoice lines from:
- selected parent plans
- shared registration fee
- paid group sub-member selections
- Those lines are validated through
OngoingMembershipCouponHelper.ValidateSignupCouponForInvoices(...). - One-time coupons are delegated to
MembershipCouponHelper.ValidateCouponForInvoices(...). - Ongoing coupons are accepted only when an eligible membership-fee line exists.
- Returned
AppliedInvoiceDiscountsare mapped back to: - parent plan preview rows
- sub-member subscription preview rows
- registration fee preview row
- 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.csApplication/Views/Apply/CreditCardForm.cshtml
Flow:
- Build preview invoice lines using the same shape as signup.
- If the application comes from signup form context, validate through
OngoingMembershipCouponHelper. - Otherwise validate through
MembershipCouponHelper, which rejects ongoing coupons. - Map line-level discounts back into the apply view model.
- 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.jsRaklet.Backend/.../templates/application-edit.html
Flow:
- Admin page receives current application data.
- Angular controller reconstructs the same coupon-eligible line model client-side.
- Controller mirrors the helper rules for:
- plan restrictions
- application-fee inclusion
- fixed vs percentage targeting
- Discount values are written back into:
- main plan rows
- shared registration fee row
- signup-origin sub-member subscription rows
- 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:
- Approval rebuilds the same invoice-line set used in signup/apply preview.
- Signup-origin approvals validate through
OngoingMembershipCouponHelper; other approvals validate throughMembershipCouponHelper. - Helper output is split into ordered queues:
- primary membership queues
- sub-member membership queues
- Approval applies those queues to:
- inscription debt for registration fee discount
- primary membership debt/payment rows
- sub-member payment rows
- Redemption count is increased based on the actual discounted invoice count.
- If the applied coupon is ongoing and a real membership-fee discount was created, the matching
CustomMembership.CouponIdis set.
Important persistence rule:
- payment records must only carry
CouponIdwhen 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.csApplication/Controllers/CheckoutController.csApplication/Views/Shared/_SortedUnpaidInvoices.cshtmlApplication/Views/Shared/_StripeCheckout.cshtml
Flow:
- Checkout loads real unpaid debt rows for the member.
- Each debt row is projected into a coupon invoice item with both:
- debt id
MembershipPlanId- Helper validates the coupon against the selected debts.
AppliedInvoiceDiscountsbecomes the source of truth for:- row highlighting
- discounted totals
- saved-card processing
- Stripe processing
- zero-total coupon-covered completion
- coupon metadata persistence on affected debts
Special rule:
- checkout inscription debts stay plan-specific; a plan-restricted coupon only discounts inscription debts whose own
MembershipPlanIdmatches - 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.CouponIdto load the attached coupon EndDateis checked before duration/period rulesOngoingDurationType.Multiplederives applied period count from real membership-fee financial records, not fromMaxRedemptions- 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.CouponIdonly to subscriptions that received a real membership-fee discount