Subscriptions & Plans — Code Map
API
apps/api/src/plans/
plans.module.tsplans.controller.ts—@Controller('organizations/:orgId/plans').RequiresFeature('membership_plans')on mutating routes.plans.service.ts—create | findAll | findById | update | remove | purchase | resumeCheckout. InternalrequireFeaturehelper for service-layer tier checks.resumeCheckoutis a thin front door ontopurchase(resumeSubscriptionIdpins the reuse branch to one row) so the checkout mechanics exist once.plans.service.unit.spec.ts— unit tests.dto/create-plan.dto.tsupdate-plan.dto.tspurchase-plan.dto.ts—{ successUrl, cancelUrl, replacesSubscriptionId }.
apps/api/src/subscriptions/
subscriptions.module.tssubscriptions.controller.ts— both member-scoped (/subscriptions/my/*) and admin-scoped (/subscriptions/:id/*) routes.subscriptions.service.ts— lifecycle. IncludessweepDueCancellations(plus itsrepairStuckScheduledCancellationspass) invoked by the cron,memberCancelPending,memberWithdrawScheduled/findWithdrawBlocker/applyScheduledWithdrawal,staffVoidPendingSubscriptionandrecordCheckoutReturn. The module-levelisMemberRolledBackCheckouthelper is what hides a member’s own rolled-back checkout fromgetMySubscriptions.member-action.ts—resolveMemberAction(legacy singular, one action) andresolveMemberActions(authoritative array). Both resolve from the same predicates the endpoints enforce, so a listed action is always one the API accepts.subscription-display-status.ts— the terminal-status split, plusPRESALE_WITHDRAWN_REASON,isPresaleWithdrawalandresolveCheckoutReleasedBy(which of the three writers released an abandoned checkout). Deliberately dependency-free (no Nest service imports) so label logic stays unit-testable.presale-withdraw.int.spec.ts— withdraw happy path, seat release, re-purchase, the three refusals, theuse_withdrawredirect, and the cron repair pass.checkout-decision.int.spec.ts— regret rollback + member-list hiding, the rows that must stay visible (swept, staff-dismissed), transaction metadata, staff void-pending, checkout-return telemetry.cancellation-cron.service.ts—@Cron('30 2 * * *')flipscancel_at_period_endrows tocancelled.cancellation-requests.controller.ts— request submission + owner approve/reject.cancellation-requests.service.ts— owns thecancellation_requeststable lifecycle.cancellation-flow.int.spec.ts— integration test for the full request → approve → refund handshake.plan-change.controller.ts(FIT-254 §4) — preview, org change, cancel-scheduled, member self-serve change. One controller, no class-level route prefix (member vs. staff routes have different shapes).plan-change.service.ts—PlanChangeService:previewChangePlan,orgChangePlan,memberChangePlan,cancelScheduledChange, plus the three immediate-variant private methods (applyImmediateCharge,applyImmediateComp,scheduleChange). Depends onPlansServiceviaforwardRef(member checkout delegates topurchase).plan-change-math.ts— pure proration math, zero framework dependency:computeDueNow,classifyDirection,PLAN_CHANGE_INTERVAL_DAYS.plan-change-eligibility.ts—enforcePlanChangeEligibility, the standalone §4.1 eligibility matrix shared by every plan-change entry point (includingPlansService.purchase’sreplacesSubscriptionIdpath) without a circular constructor dependency.plan-change.service.driver.ts,plan-change.service.unit.spec.ts,plan-change-math.unit.spec.ts— tests.dto/enroll-member.dto.tschange-plan.dto.ts— org-initiated{ newPlanId, timing, billing? }.member-change-plan.dto.ts— member self-serve{ newPlanId, successUrl?, cancelUrl? }.resume-checkout.dto.ts—{ successUrl?, cancelUrl?, failedUrl? }; same shape and same loose validation aspurchase-plan.dto.ts.cancel-pending.dto.ts— optional{ intent?, source? }; an empty body is today’s behavior.checkout-return.dto.ts—{ outcome, client }for the telemetry endpoint.
Web (apps/web/src/)
app/[lang]/(protected)/dashboard/plans/— plan CRUD UI.app/[lang]/(protected)/dashboard/payments/— owner-side payments console (includes a Cancellations tab driven bycomponents/payments/cancellation-requests-list.tsx).app/[lang]/(protected)/dashboard/members/— admin sub view, freeze/cancel actions.components/payments/cancellation-requests-list.tsx— owner approve/reject UI.components/payments/cancellations-widget.tsx— dashboard counts (pending requests + scheduled cancels).components/member/plan-card.tsx— member-facing subscription summary.components/member/change-plan-sheet.tsx(+.driver.tsx,.int.spec.tsx) — member self-serve “Change plan” sheet (FIT-254 §4.5): preview → upgrade redirect or downgrade confirm.components/member/scheduled-plan-change-banner.tsx(+.driver.tsx,.int.spec.tsx) — member-facing scheduled-change banner; showscurrentPeriodEnd(see behavior.md’s known W4 inconsistency vs. the staff badge).components/member/switch-plan-picker-dialog.tsx— shop “Switch to this plan” entry point when the member holds several subscription-type subs.components/overview/members/member-memberships-tab.tsx— staff member-detail Memberships tab: subscription list, scheduled-change badge (showsplanChangeScheduledAt), freeze/cancel/change-plan actions.components/overview/members/payment-methods-card.tsx— staff-facing card-on-file card (FIT-254 §3.1).components/members/manual-charge-dialog.tsx,components/members/collect-debt-dialog.tsx— staff desk-charge and debt-collection dialogs (FIT-254 §3.3/§3.4).app/[lang]/(protected)/(member)/profile/payments/page.tsx— member profile payments page; hosts the change-plan entry point and scheduled-change banner.
Booking-entitlement sweep (FIT-254 §4.6/§5)
apps/api/src/bookings/booking-entitlement-sweep.service.ts—BookingEntitlementSweepService:plan/planForTargetPlan(zero-write preview),apply/applyInPlace(writes). Flag keybooking-entitlement-sweep.apps/api/src/bookings/booking-entitlement-sweep.module.ts— module wiring (kept separate fromBookingsModuleto avoid the DI cycle documented in the service’s doc-comment).apps/api/src/bookings/booking-entitlement-sweep.service.driver.ts,.unit.spec.ts— tests.apps/api/src/bookings/booking-enforcement.util.ts— shared low-level primitives (dayBoundsUtc,weekBoundsUtc,sessionsOverlap,deductCreditAtomic,refundCreditAtomic,assertCreditAvailable,tryDeductCreditAtomic,promoteFromWaitlist,withSerializableRetry) extracted out ofBookingsServiceso the sweep engine can reuse them without a DI cycle back through it.- C3 (booking-horizon guard) lives inline in
apps/api/src/bookings/bookings.service.tsbook()— no separate file. - C4 call sites:
apps/api/src/subscriptions/subscriptions.service.ts(sweepOnEnd, immediate cancel + period-end cron),apps/api/src/payments/services/recurring-charge.service.ts(sweepOnDebt, 3rd renewal failure),apps/api/src/payments/services/webhook-processing.service.ts(sweepOnDebt, webhook-driven failure).
Shared
libs/shared/src/lib/schemas/plan.schema.ts— Zod schemas forPlanResponse+ the plan-change request/response types (ChangePlanRequest,MemberChangePlanRequest,MemberChangePlanResponse,ChangePlanPreviewResponse,PreviewBooking, direction/timing/billing enums).libs/shared/src/lib/schemas/membership.schema.ts—SubscriptionResponse,SubscriptionWithPlan.libs/shared/src/lib/constants/payment-error-codes.ts—PaymentErrorCodesregistry (FIT-254):NO_ACTIVE_PAYMENT_METHOD,MEMBERSHIP_NOT_ACTIVE,PROVIDER_CHARGE_UNVERIFIED,PAYMENT_METHOD_IN_USE,OUTSTANDING_BALANCE,PENDING_ACTION_CONFLICT,RESUME_FIRST,BOOKING_BEYOND_SUBSCRIPTION_END,RENEWAL_CHARGE_FAILED,PLAN_CHANGE_CHARGE_FAILED.libs/shared/src/lib/constants/feature-flags.ts—FeatureFlags.ADMIN_CARD_ON_FILE/.ADMIN_PLAN_CHANGE/.MEMBER_PLAN_CHANGE(thebooking-entitlement-sweepkey is defined directly onBookingEntitlementSweepService.FLAG_KEY, not duplicated here).
DB
libs/db/src/lib/schema/payments.ts—plans,subscriptions(+ FIT-254’sscheduled_plan_id/plan_change_scheduled_at/changed_from_subscription_id),cancellation_requests.libs/db/src/lib/schema/enums.ts—planType,planInterval,subscriptionStatus,cancellationRequestStatus,membershipPaymentStatus.libs/db/src/lib/schema/memberships.ts—memberships.paymentStatusis mutated by the renewal cron + webhook processor.libs/db/drizzle/0090_naive_roughhouse.sql— migration adding the three plan-change columns + their FKs. Not yet applied to any real environment (human to-do — seedata-model.md).
Notifications
apps/api/src/notifications/cancellation-notifications.service.ts—cancellationScheduled,cancellationRequestSubmitted,cancellationRequestApproved,cancellationRequestRejected,newCancellationRequestForOwner,refundCompleted.apps/api/src/notifications/templates/payment-failed.ts,debt-warning.ts,payment-receipt.ts.apps/api/src/notifications/sweep-notifications.service.ts(FIT-254) —SweepNotificationsService:sendPlanChangeSwept,sendSubscriptionEndedSwept,sendPlanChangeScheduled,sendPlanChangeScheduleCancelled. Resolves recipient locale (device → org →en) and org branding.apps/api/src/notifications/templates/entitlement-sweep.ts—planChangeSweepEmail,subscriptionEndedSweepEmail. Localized en/he/ru viadictionaries[locale].entitlementSweepEmail.copy.apps/api/src/notifications/templates/plan-change-scheduled.ts—planChangeScheduledEmail,planChangeScheduleCancelledEmail. Localized en/he/ru.
Cross-references
apps/api/src/payments/services/webhook-processing.service.ts— mutatessubscriptionson payment events; hosts the plan-change checkout activation hook (§4.3).apps/api/src/payments/services/recurring-charge.service.ts— mutatessubscriptionsandmemberships.paymentStatusdaily; hosts the scheduled-change boundary swap (§4.4, PD-B5a).apps/api/src/payments/services/manual-charge.service.ts—CHARGE_VERIFIED_PROVIDERS, imported byplan-change.service.tsfor the org saved-card charge variant.apps/api/src/automations/automation-scheduler.service.ts—sweepEnded’splan_change/ replacement-sub exclusions (§4.8, PD-B9).apps/api/src/bookings/— callssubscriptionsService.deductCredit / refundCredit;bookings.service.tshosts C3 inline.apps/api/src/courses/course-checkout.service.ts— usesplan.type='course'rows, bypassesplans.purchase.