Skip to content

Test Plan — Multiple Currency in Membership Plans

Base: origin/master Branch: Multiple-currency-in-Membership-plans Related design: multiple-currency-in-membership-plans.md Repo: rakletadmin/rakletv3

Headline rule: every per-plan currency / payment-account read must come from the plan, never the organisation, except where the design doc explicitly allows it (CC commission/fee currency, plan-create-time fallback).


Affected pages / routes

  • V2 API
  • POST /v2/organisations/{orgId}/membership/plans — plan create (R1)
  • PUT /v2/organisations/{orgId}/membership/plans — plan update (R2)
  • POST /v2/organisations/{orgId}/payment/scheduled — schedule create (R6.1)
  • PUT /v2/organisations/{orgId}/payment/scheduled — schedule update (R6.2)
  • POST /v2/organisations/{orgId}/payment/debt — manual invoice create (R6.4)
  • POST /v2/organisations/{orgId}/payment/debt/{debtId} — manual invoice update (R6.4)
  • MVC / Application
  • Application/Controllers/ApplyController — apply form (R5)
  • Application/Controllers/SignupController — signup form (R5)
  • Application/Controllers/PaymentController — saved-card selection (R5.2)
  • ApplicationFormService.ApproveApplication — multi-plan approve (R4)
  • AngularJS manager UI
  • admin.raklet.com/ng#/manager/membership/plans — plan list, create, edit (R7)
  • MVC member-facing views
  • Create.cshtml, MemberTypeEdit.cshtml, ChoosePlan.cshtml, Index.cshtml, SubscriptionCreate.cshtml, SubscriptionDetail.cshtml (R8)
  • WebJobs
  • Raklet.WebJobs.Secondary/Payment/ScheduledPaymentFunctions — recurring iteration (R6.3)

Codepaths to cover

# Flow / branch Location Test type Exists? Gap / action
R1.1 Plan create with Currency=null falls back to org currency V2MembershipController.cs:919-921 Integration Gap Add: POST without Currency, assert entity persisted with org default
R1.2 Plan create with PA on non-Stripe org → 400 V2MembershipController.cs:910-915 Integration Gap Add: POST with PaymentAccountId on Iyzico org, assert error message
R1.3 Plan create with empty PA leaves field null V2MembershipController.cs:951 Integration Gap Add: POST with PaymentAccountId=Guid.Empty, assert DB row is null
R1.4 Plan create with mismatched group sub-plan PA → 400 V2MembershipController.cs:962-975 Integration Gap Add: parent + sub-plan with different PA, assert reject
R1.4 Plan create with mismatched group sub-plan currency → 400 same Integration Gap Add: parent + sub-plan with different Currency, assert reject
R2.1 Plan update — PA Stripe gate runs first V2MembershipController.cs:1165-1170 Integration Gap Add: PUT changing PA on non-Stripe org, assert Stripe-gate message
R2.2 Plan update — PA immutability after gate V2MembershipController.cs:1172-1176 Integration Gap Add: PUT changing PA on Stripe org, assert immutability message
R2.2 Plan update — empty/null PA payload tolerated same Integration Gap Add: PUT with PaymentAccountId=null, assert no change, no error
R2.3 Plan update — Currency immutability V2MembershipController.cs:1178-1181 Integration Gap Add: PUT changing Currency, assert reject + plan unchanged
R2.4 Subscribed group plan — sub-plans append-only V2MembershipController.cs:1202-1220 Integration Gap Add: parent with subscriptions, attempt removing a sub-plan, assert preserved
R2.5 Unsubscribed group plan — full rebuild V2MembershipController.cs:1184-1201 Integration Gap Add: parent with no subscriptions, assert sub-plan list rewritten
R2.6 Rejected update leaves plan untouched same Integration Gap Add: PUT that fails R2.3, assert no DB write occurred
R3.1-3 CalculatedPaymentAccountId priority + fallback + Org-null safety CustomMemberType.cs:25-26 Unit Exists CustomMemberTypeTests covers these. Verify still passing.
R3.4 All CustomMemberTypeService reads Include(Organisation) CustomMemberTypeService.cs Static / code review Gap Grep audit; consider analyzer rule
R4.1 Multi-plan approve — distinct PA on non-Stripe → reject ApplicationFormService.cs:392-410 Integration Gap Add: 2 plans, different PA, non-Stripe org, assert "different payment gateways" message
R4.1 Multi-plan approve — distinct PA on Stripe → reject same Integration Gap Add: 2 plans, different PA, Stripe org, assert "different payment accounts" message
R4.2 Multi-plan approve — no-plan fallback, mixed currency reject ApplicationFormService.cs:1424-1445 Unit Exists ResolveSingleApplicationCurrencyTests covers no-plan org-currency fallback, single-plan, agreeing, and mixed-currency paths. Keep green.
R4.3 Approve writes Payment.Currency and PaymentAccountId from plan ApplicationFormService.cs:947-970 Integration Gap Approve a plan whose currency ≠ org currency, assert Payment row stamped with plan currency
R4.4 Sub-member payments use plan currency/PA ApplicationFormService.cs:706-738 Integration Gap Corporate membership with sub-members, assert each sub Payment uses plan PA
R4.5 CreditCardChargeRequest.Currency = resolvedCurrency ApplicationFormService.cs:610 Integration Gap Mock charger, assert charge request currency = plan currency
R4.6 Token bound to PA after charge ApplicationFormService.cs:989-1002 Integration Gap Approve, assert CreditCardToken.PaymentAccountId set
R4.7 Recurring loop guard (IsRecurring && token != null) || isDeferOrg ApplicationFormService.cs:1006 Integration Gap Approve recurring without token → no schedule. Approve as defer org → cleanup runs.
R5.2 DoesTokenMatchPaymentAccount rejects mismatched token PaymentController.cs:2639-2642 Unit Exists DoesTokenMatchPaymentAccountTests covers mismatch, wildcard, null-token, and null/empty-token-PA cases. Keep green.
R6.1 IsScheduledPaymentTokenValidAsync happy path V2PaymentController.cs:544-560 Integration Gap Create scheduled payment with matching token; assert created
R6.1 Token wrong org-membership → reject same Integration Gap Token belongs to other membership, assert reject
R6.1 Token wrong PA → reject same Integration Gap Token PA ≠ resolved PA, assert reject
R6.2 Update re-derives PA from plan, ignores DTO PA V2PaymentController.cs:499-516 Integration Gap PUT with DTO PA different from plan PA, assert plan PA wins
R6.3 WebJob recurring iteration re-derives PA ScheduledPaymentFunctions.cs Integration Gap Run iteration with mismatched stored PA, assert hard-fail (loud, not silent reroute)
R6.4 Manual invoice/debt create + update resolves currency from subscription plan or org fallback V2PaymentController.cs, DebtCurrencyResolver.cs Unit + Integration Partial DebtCurrencyResolutionTests covers pure selection. Add integration: POST without Currency, with USD plan on EUR org, assert Debt.Currency=USD; POST without subscription assert EUR, not TRY.
R7.1 isMultipleCurrencyPlanPAEnabled only true for processor 5 membership-plan-edit.controller.js:67-70 Karma / manual Gap Manual: switch processor in fixture, observe field visibility
R7.2 hasMatchingGroupMembershipPlanPA parity rules membership-plan-edit.controller.js:72-86 Karma / unit-js Gap Add JS unit covering Stripe vs non-Stripe parity
R7.3 $watch clears mismatched sub-plans on edit membership-plan-edit.controller.js:88-113 Manual / E2E Gap Manual: change parent currency, observe sub checkbox cleared
R7.4 getFilteredPlans filters mismatched plans membership-plan-edit.controller.js:589-610 Manual / E2E Gap Manual: parent in USD, sub-plan picker hides TRY plans
R7.5 Edit mode: Currency + PA disabled plan-edit.html Manual / E2E Gap Manual: open existing plan, assert ng-disabled
R7.6 Insert mode: Currency defaults to org currency membership-plan-edit.controller.js:231-235 Manual / E2E Gap Manual: new plan, observe default
R8.1 Member-facing views format with item.Currency commit bc200b9ec Manual Done in PR Visual: open membership profile in TRY plan org with USD plan, assert correct symbol
R8.2 JsonController plan select-list uses plan.Currency JsonController.cs Manual Gap Open select list, inspect rendered prices
R8.3 PlanDto.Currency nullable, TRY (=0) distinguishable from null PlanDto.cs:28 Unit Exists Verify PlanDto.MapFromEntity test path
R9.1 isDeferMembershipFeeOrg flag computation ApplicationFormService.cs:417-423 Unit Exists DeferMembershipFeeOrgTests covers org-set membership and before/after/boundary time-window behavior. Keep green.
R9.2 Defer-active path skips Payment, runs cleanup ApplicationFormService.cs:1015-1075 Integration Gap Approve with defer org, assert subscription Status=Scheduled, debt removed, no Payment row
R9.3 Multi-currency code does not run on defer branch merge resolution Integration Gap Same defer test: assert Payment.Currency not exercised (no row created)
R9.4 Cleanup TODO present TODO comment in source Static n/a Reviewer checklist only

Key interactions to verify (manual / E2E)

  • Multi-currency org: Plan A in USD on PA-A, Plan B in TRY on PA-B (StripeWithDirectCharge). Member can buy each separately. Buying both in one apply form → reject with "different payment accounts".
  • Same PA, different currency: Plan A USD, Plan B TRY, both on PA-A. Apply form with both → reject with currency mismatch.
  • Same PA, same currency: Plan A USD, Plan B USD, both on PA-A. Apply form with both → success, single charge, single Payment per plan, both stamped with USD + PA-A.
  • Recurring re-derive: Successful recurring purchase. Wait for next iteration (or fire WebJob manually). Stored ticket PA changed to a wrong value out-of-band → next iteration fails loudly with token/PA mismatch, does NOT silently reroute to a different sub-merchant.
  • Saved-card UX: Member with token on PA-A tries to pay invoice on PA-B → token hidden / rejected. Member with token on PA-A pays invoice on PA-A → token offered.
  • Manual invoice UX: Add Invoice with a selected subscription whose plan currency differs from the org; amount prefix and saved Debt.Currency must use the plan currency. Add Invoice without a subscription must save org currency, not enum default TRY.
  • Plan edit immutability: Open existing plan in manager UI. Currency dropdown disabled with warning. PaymentAccount selector disabled. Save with unchanged values → success. Attempt API PUT bypassing UI to change Currency or PA → 400.
  • Group plans: Parent USD/PA-A. Add USD/PA-A sub-plan → ok. Add TRY/PA-A sub-plan → blocked in UI + rejected by API. Add USD/PA-B sub-plan → blocked in UI + rejected by API. Subscribe a member, then try to remove a sub-plan → blocked.
  • Mumadoo defer (until 2026-06-01): Approve application on one of the 3 defer-org GUIDs. Subscription created with Status=Scheduled, StartDate=2026-06-01, no Payment row, no debt row. After mocking clock past 2026-06-01 → defer branch is dead, normal multi-currency path runs.
  • Mumadoo defer ROLLBACK readiness: Confirm a single-PR delete of the deferMembershipFeeOrgIds block compiles without touching multi-currency code paths.

Edge cases

  • Org with no DefaultPaymentAccountId setCalculatedPaymentAccountId returns Guid.Empty. Caller code must guard. Test: create plan with no PA on org with no default → resolved to Guid.Empty, charge attempt fails fast with clear error (not NRE).
  • Plan with stale Currency stored as 0 (TRY) — distinguishable from null after R8.3 nullable fix. Test legacy plan rows.
  • Lazy-load not triggered (Organisation null on the navigation property)CalculatedPaymentAccountId returns Guid.Empty rather than throw. Verify with AsNoTracking query that didn't Include(Organisation).
  • Token deleted between schedule create and charge timeIsScheduledPaymentTokenValidAsync should still reject on subsequent operations.
  • Org switches processor after plans exist — legacy plans created on StripeWithDirectCharge keep their PA; updates that touch other fields should not reject because of the now-failing Stripe gate. R2.1 only fires when PA value changes.
  • Currency immutability under round-trip — DTO returned by GET, posted back unchanged on PUT → R2.3 must accept (HasValue but equal).
  • Group parent plan currency change attempt — should fail R2.3 before any group reconciliation; sub-plans must remain untouched.

Critical paths (must pass before merge)

  1. Plan create / update guards (R1, R2) — all six guard combinations on a real RakletDb integration fixture.
  2. CalculatedPaymentAccountId unit suite green.
  3. Multi-plan approve hard-fail on PA conflict + currency conflict.
  4. Recurring iteration re-derives PA and rejects mismatched token.
  5. Member-facing views render plan currency, not session org currency, on a multi-currency org.
  6. Mumadoo defer org approve still works end-to-end and does NOT touch the multi-currency assignment lines.

Automated tests

  • Unit (DB-independent)
  • CustomMemberTypeTests — R3 — exists, keep green.
  • ResolveSingleApplicationCurrencyTests — R4.2 — exists, keep green.
  • DoesTokenMatchPaymentAccountTests — R5.2 — exists, keep green.
  • DeferMembershipFeeOrgTests — R9.1 — exists, keep green.
  • DebtCurrencyResolutionTests — R6.4 — exists, keep green.
  • PlanDto.MapFromEntity nullable currency — R8.3 — verify covered.
  • Integration (RakletDb fixture)
  • V2 plan create / update guard matrix — R1, R2.
  • V2 scheduled payment create / update — R6.1, R6.2.
  • ApplicationFormService.ApproveApplication happy path + multi-plan rejects + sub-member assignment — R4.
  • Frontend
  • JS unit / Karma for hasMatchingGroupMembershipPlanPA if a test runner is wired; otherwise add to manual checklist.
  • WebJob
  • Local invocation of ScheduledPaymentFunctions recurring iteration with seeded data — R6.3.

Build / regression

  • Run scripts/dev/build-fast.ps1 after changes in Models, Services, Raklet.Api, Application. UI-only AngularJS changes can be validated with browser refresh after gulp (per repo convention).
  • Full solution build before opening the merge PR — non-SDK .csproj registration is required for any new test files (see CLAUDE.md).

Sign-off

  • [ ] R1 — plan create guards green (4 cases)
  • [ ] R2 — plan update guards green (3 fail-fast + group append-only)
  • [ ] R3 — CalculatedPaymentAccountId unit suite green
  • [ ] R4 — multi-plan approve PA + currency rejects, payment rows stamped with plan values
  • [ ] R5 — saved-card token isolation enforced in checkout
  • [ ] R6 — scheduled payment create/update + WebJob iteration validate token + re-derive PA from plan
  • [ ] R7 — AngularJS plan-edit: gate, parity, watches, immutability
  • [ ] R8 — member-facing views render plan currency, not session org currency
  • [ ] R9 — Mumadoo defer-org approve still passes; multi-currency code path is not exercised on defer branch
  • [ ] Full solution build clean, all new .cs files registered in their .csproj
  • [ ] Manual QA on a StripeWithDirectCharge org with two sub-merchants and two currencies