Skip to main content

Qontak One | Billing & Subscriptions | Billing Expired Handling

Template: NEW PRD v1.2 · Bifrost Squad


HEADER BLOCK

FieldValue
PMAddo Hernando
PRD Version1.0
StatusDRAFT
PRD TypeNEW
EpicBIF-8640
SquadBifrost
RFC LinkN/A — no RFC required
Figma MasterN/A — no new screens
AnchorNo — standalone, single-squad
Labelsepic:qontak-one | module:billing-subscriptions | feature:billing-expired-handling
Last Updated2026-06-22

✅ Reformat Complete — No Flags

All mandatory sections were filled. This PRD meets the same standard as a write-prd output.


Table of Contents


3. One-liner + Problem

One-liner: A limited-access mode for expired Qontak One accounts that restricts operational features while preserving access to essential data and subscription renewal.

Problem: When a Qontak One client's subscription expires, the system immediately hard-locks all users out of their account — including access to historical reports and the billing page needed to renew. This abrupt lockout affects approximately 50 CIDs per month, with around 20% (10 clients/month) raising a CS support ticket to request manual data access. The resulting support burden, combined with the negative user experience at a critical renewal moment, creates churn risk and undermines the Qontak One launch.


4. What Happens If We Don't Build This

  • Users are abruptly locked out of their accounts on expiry, preventing access to historical data and reports — driving ~10 CS tickets/month for manual data retrieval.
  • The Customer Support team continues to bear the manual operational cost of handling expired-account data requests, with no self-service path available.
  • The negative offboarding experience at subscription expiry creates churn risk and damages the Qontak One brand perception ahead of and after launch.

5. Target Users + Persona Context

PersonaRoleGoalPainWorkaround
Primary — Qontak One User (expired CID)Any user in a Qontak One CID (unified_app = true) where billing_status = expiredAccess historical data and manage subscription renewal after expiryImmediately hard-locked out — cannot access reports, history, or the billing page to renewContacts Customer Support to request data retrieval; manually works with AM/CS to process renewal
Secondary — Qontak Internal CS AgentQontak internal Customer Support agent handling expired-account requestsResolve expired-account data requests without manual intervention~10 support tickets/month from expired CIDs requesting data access; high manual effort per ticketManually retrieves and sends data on behalf of the expired client; coordinates renewal with AM

(See Section 7 — Constraints for plan availability and feature flag scope.)


6. Non-Goals

  1. We will not build a new UI for the subscription-end notification banner — the existing banner component is reused as-is (see Open Question #3 for a discovered risk on this assumption).
  2. We will not change or create new dunning or warning email sequences as part of this project.
  3. We will not build a feature to allow expired users to export their data while in the limited-access mode.
  4. CIDs with unified_app = false (non-Qontak One clients) are not in scope — they are unaffected by this feature.
  5. Accounts with billing_status = freeze (the freeze status in organization_packages / company_accounts.status = freeze) are not in scope — they continue to follow the current hard-block behavior.

Scope Changes

  • Backendbilling_status expiry event handling; permission key evaluation logic against show_when_billing_expired column; billing_expired_limited_access flag enforcement
  • Frontend — Banner trigger on billing_status = expired; hide/disable UI elements per permission key evaluation
  • Mobile — Permission key enforcement on mobile (via Chat and CRM existing permission key implementation)

7. Constraints

FieldValue
PlatformWeb + Mobile — behavior determined by how each module (Chat, CRM, etc.) implements the permission_key enforcement
PerformancePermission key evaluation ≤ 100ms server-side. Banner render ≤ 500ms on page load.
Plan scopeQontak One clients only (unified_app = true) — applies to all tiers upon billing_status = expired. Non-Qontak One CIDs (unified_app = false) are unaffected.
Feature flagbilling_expired_limited_access | system-triggered per account when billing_status = expired. Default: OFF. This flag does not yet exist in the Flipper/Preference system (moderator-be) or qontak-preferences service (launchpad) — it is a net-new addition.
Read/writeDetermined per permission_key by show_when_billing_expired column: TRUE = access retained; FALSE = feature hidden/disabled for all users in the CID regardless of role. This column does not yet exist on any permissions table — it is a net-new schema addition (see Dependencies).
Billing status sourcebilling_status is derived from organization_packages.status (qontak-billing) → propagated to company_accounts.status (moderator-be, enum value 4 = expired) via BuildCache service. The unified_app flag is returned by the ModPanel API (GetUnifiedBillingResponse.UnifiedApp), not stored in a local DB field in launchpad.
Activation cadenceDailyDeactivatePackage cron (qontak-billing) runs once per day. Status transitions (active → grace → expired) happen in batch — limited-access activation may lag up to 24 hours after the exact subscription end timestamp. Real-time activation is not guaranteed without an additional event trigger.
Grace periodqontak-billing has a 7-day grace period between active and expired (active → grace → expired). It is an open question (see Open Question #4) whether limited-access activates at grace or only at expired.

8. Feature Changes

Change ID: CHG-001 — Subscription-End Banner (Global)

FieldDetail
Change TypeModified behavior — existing banner component triggered by new condition
ScopeAll pages across the Qontak One web and mobile app
Page IntentAny page a user navigates to after their CID's subscription expires
Before• Banner is not shown — account is active, billing_status = active
After• Persistent subscription-end banner is rendered at the top of all pages for all users in a CID where unified_app = true AND billing_status = expired
• Banner includes a renewal CTA navigating to the subscription page

Figma: N/A — existing banner component; no design changes required subject to Open Question #3.

Technical note (from repo exploration): The current expired.vue in qontak-launchpad-fe is a standalone full-page component (layout: landing) — not an inline reusable banner that can be inserted at the top of other pages. It contains static content (WhatsApp links, pricing cards, data retention copy) and has no props. If the intent is to show a persistent top-of-page banner while keeping the existing page visible beneath it, a new banner component must be built or extracted from this page. Engineering must validate in the discovery spike (Open Question #3) whether this constitutes design work before implementation begins.


Change ID: CHG-002 — Module Permission Enforcement (Global)

FieldDetail
Change TypeModified behavior — permission key evaluation extended to check billing_status
ScopeAll modules across the Qontak One app
Page IntentAny module page a user navigates to in a CID with billing_status = expired
Before• All permission_keys are active for all users in the CID regardless of billing_status
Afterpermission_keys where show_when_billing_expired = FALSE are hidden/disabled for all users
permission_keys where show_when_billing_expired = TRUE remain accessible (read access preserved)
• Full list of keys managed via the permission key sheet
Permission Key Exampleshow_when_billing_expiredBehavior on Expiry
usman_roles_manageFALSEHidden — all users cannot edit roles
tasks_general_uploadFALSEHidden — all users cannot upload tasks
subscriptions_general_viewTRUEAccessible — all users can open subscription page
deals_general_exportTRUEAccessible — all users can export deals

Figma: N/A — permission enforcement is behavioral; no new UI components.

Technical note (from repo exploration): The show_when_billing_expired boolean column does not exist on any permissions table today. It must be added as a new schema change to:

  • permissions table in moderator-be (fields: name, alias, feature — new show_when_billing_expired boolean needed)
  • Potentially permission_component_codes table in qontak-launchpad (permission_key, component_codes jsonb — schema ownership is an open question; see Open Question #5)

The permission check middleware in qontak-launchpad (internal/pkg/middleware/permission_check.go) currently evaluates permission_keys against static route/method mappings with an "everything" level check. It does not evaluate billing_status. A new billing-status check layer must be added to this middleware (or as a separate middleware) that reads the show_when_billing_expired value for each key and applies restrictions when billing_status = expired. Redis caching of the billing state (keyed by company ID) is already the pattern used in billings/get_unified_billing.go and should be reused here to meet the ≤100ms constraint.


9. New Features

N/A — no new screens. All UI changes are permission-key-driven modifications to existing screens (Section 8) and reuse of the existing subscription-end banner component.


10. System Flow + User Stories + ACs

10.1. System Flow

Flow: Billing Expiry → Limited Access Mode Activation Type: State Lifecycle

  1. Subscription end date passes → DailyDeactivatePackage cron (qontak-billing, runs once/day) transitions organization_packages.status: active → grace (7-day window), then grace → expired.
  2. qontak-billing notifies ModPanel (ModPanelUpdateStatus()); moderator-be BuildCache service propagates this to company_accounts.status = expired (enum value 4) and updates the Redis billing cache.
  3. System checks unified_app flag for the CID via ModPanel API (GetUnifiedBillingResponse.UnifiedApp). If unified_app = false → no change; current behavior applies.
  4. If unified_app = true AND billing_status = expiredbilling_expired_limited_access flag is auto-enabled for the CID (Flipper/Preference system in moderator-be; qontak-preferences service in launchpad).
  5. System evaluates all permission_keys against the show_when_billing_expired column (new column on permissions table, moderator-be).
  6. permission_keys where show_when_billing_expired = FALSE → marked as disabled for all users in the CID (role-agnostic).
  7. permission_keys where show_when_billing_expired = TRUE → remain active for all users.
  8. User logs in or navigates to any page → system checks billing_status for the CID (Redis cache first, fallback to ModPanel API).
  9. If billing_status check fails server-side → fail-closed: treat as expired, apply restriction.
  10. Subscription-end banner is rendered persistently on all pages with a renewal CTA.
  11. User clicks renewal CTA → navigates to the subscription page (subscriptions_general_view = TRUE).
  12. User completes renewal → billing_status transitions back to active.
  13. billing_expired_limited_access flag disabled → all permission_keys restored; banner removed.

Frozen accounts: If billing_status = freeze (company_accounts.status = freeze, or organization_packages.status = "freeze"), the current hard-block behavior applies — limited-access mode is NOT triggered. (Note: the PRD previously used "suspended" — the actual system status value is freeze.)

Grace period: The system has a 7-day grace period between active and expired. Whether limited-access mode activates at grace or only at expired is an open question (see Open Question #4).

Activation timing: Because DailyDeactivatePackage is a once-daily batch job, there may be up to a 24-hour lag between the exact subscription end timestamp and limited-access activation. See Open Question #6.

📊 System Flow — Billing Expired Handling

graph TD
A([Subscription end date passes]) --> A1[DailyDeactivatePackage cron: active → grace → expired]
A1 --> A2[qontak-billing notifies ModPanel → moderator-be BuildCache updates company_accounts.status]
A2 --> B{unified_app = true?\nvia ModPanel API}
B -- No --> C([Current behavior — unaffected])
B -- Yes --> D[Auto-enable billing_expired_limited_access flag\nFlipper/Preference system]
D --> E[Evaluate all permission_keys vs show_when_billing_expired column]
E --> F[FALSE keys: disabled for all users]
E --> G[TRUE keys: remain accessible]
F & G --> H([User navigates to any page])
H --> I{billing_status check OK?\nRedis cache → ModPanel API fallback}
I -- Fail --> J[Fail-closed: apply expired restrictions]
I -- OK, expired --> K[Render subscription-end banner + enforce permission restrictions]
I -- OK, active --> L([Normal access])
K --> M{User clicks renewal CTA?}
M -- Yes --> N([Navigate to subscription page])
N --> O{Renewal completed?}
O -- Yes --> P[billing_status = active → restore all permissions, remove banner]
O -- No --> K
M -- No --> K

10.2. User Stories

User StoryImportanceMockup / Technical NotesAcceptance Criteria
[BEH-S01] — Restrict permissions based on billing_status = expired

As a user in a Qontak One CID, I want to retain access to essential modules when my subscription expires, so that I can view historical data and manage my renewal without contacting support.
Must HaveFigma: N/A — existing screens, behavioral change only.

Data Fields:
billing_status (string, required) — billing engine
unified_app (boolean, required) — CID config
permission_key (string, required) — per-module
show_when_billing_expired (boolean, required) — permission sheet

Before-After Behavior: Before: all permission_keys active regardless of billing_status. After: permission_keys with show_when_billing_expired = FALSE are hidden/disabled for all users when billing_status = expired AND unified_app = true.
— Happy Path —
• AC-1: Given a CID with unified_app = true AND billing_status = expired, when a user navigates to a module with a permission_key where show_when_billing_expired = FALSE, then that permission_key is hidden and the associated action is disabled for all users regardless of role.
• AC-2: Given a CID with unified_app = true AND billing_status = expired, when a user navigates to a module with a permission_key where show_when_billing_expired = TRUE, then that permission_key remains visible and the action is accessible.
• AC-3: Given a CID with billing_status = active OR unified_app = false, when a user navigates to any module, then all permission_keys remain active as normal — no restrictions applied.

— Error / Unhappy Path —
• ERR-1: Given a CID with unified_app = true, when the billing_status check fails server-side (service unavailable), then the system fails-closed — all permission_keys where show_when_billing_expired = FALSE are treated as disabled.
• ERR-2: Given a CID with unified_app = true AND billing_status = expired, when a user directly navigates to a restricted URL, then access is denied and the user sees a prompt to renew — not a generic error page.

— Permission Model —
• CAN: All users in an expired CID (unified_app = true) may access features where show_when_billing_expired = TRUE.
• CANNOT: No user — regardless of role (admin, agent, supervisor) — may access features where show_when_billing_expired = FALSE when billing_status = expired.
• Unauthorized: Feature action button is hidden — not shown as disabled. No 403 error surfaced to the user.

— UI States —
• Loading: Permission state evaluated server-side before page render — no flash of restricted content.
• Empty: N/A — existing pages.
• Error: Fail-closed applied; restricted state shown with renewal prompt.
• Success: Correct permissions applied per show_when_billing_expired value.

— Negative Scenarios —
• NEG-1: Given a user in an expired CID (unified_app = true), when they attempt to perform an action tied to a permission_key where show_when_billing_expired = FALSE, then the action button is not rendered — user cannot trigger the action.
• NEG-2: Given a CID with billing_status = freeze, when a user attempts to log in, then the current hard-block behavior applies — limited-access mode is not triggered.
[BEH-S02] — Display subscription-end banner on expired CID

As a user in an expired Qontak One CID, I want to see a clear notification about my account status, so that I understand why features are restricted and know how to renew.
Must HaveFigma: N/A — existing banner component reused; no design changes.

Data Fields:
billing_status (string, required) — billing engine
unified_app (boolean, required) — CID config

Before-After Behavior: Before: banner not shown (billing_status = active). After: existing subscription-end banner rendered persistently on all pages when billing_status = expired AND unified_app = true.
— Happy Path —
• AC-1: Given a CID with unified_app = true AND billing_status = expired, when any user navigates to any page in the app, then the existing subscription-end banner is displayed persistently at the top of the page.
• AC-2: Given a CID with unified_app = false OR billing_status ≠ expired, when a user navigates to any page, then the subscription-end banner is NOT displayed.
• AC-3: Given a CID with billing_status = freeze, when a user attempts to log in, then the current hard-block behavior applies and the subscription-end banner is not shown.

— Error / Unhappy Path —
• ERR-1: Given a CID with unified_app = true AND billing_status = expired, when the banner component fails to render, then the page still loads normally with permission restrictions still enforced — banner failure does not block the limited-access state.

— Permission Model —
• CAN: All users in an expired CID (unified_app = true) see the subscription-end banner — role-agnostic.
• CANNOT: N/A — banner is view-only, no write action required.
• Unauthorized: N/A.

— UI States —
• Loading: Banner renders with page load — no additional delay beyond normal page render.
• Empty: N/A.
• Error: Banner fails gracefully — page and permission enforcement still function correctly.
• Success: Banner visible and persistent on all pages for all users in the expired CID.
[BEH-S03] — User clicks renewal CTA → navigates to subscription page

As a user in an expired Qontak One CID, I want to click a renewal CTA in the banner, so that I can navigate directly to the subscription page and complete my renewal without contacting support.
Must HaveFigma: N/A — existing banner CTA and subscription page reused.

Data Fields:
billing_status (string, required) — billing engine
unified_app (boolean, required) — CID config
subscriptions_general_view permission_key — show_when_billing_expired = TRUE

Before-After Behavior: Before: no in-app renewal path for expired users — they must contact CS/AM. After: banner CTA navigates user directly to the subscription page, which is fully accessible in expired state.
— Happy Path —
• AC-1: Given a user in an expired CID sees the subscription-end banner, when they click the renewal CTA, then they are navigated directly to the subscription/billing page.
• AC-2: Given a user is on the subscription page in an expired CID, when they view the page, then the page is fully accessible (subscriptions_general_view has show_when_billing_expired = TRUE).
• AC-3: Given a CID where billing_status transitions from expired back to active (after successful renewal), when a user navigates to any page, then the limited-access banner is removed and all permission_keys are restored to their active state.

— Error / Unhappy Path —
• ERR-1: Given a user clicks the renewal CTA, when the subscription page fails to load, then the user sees an error message with a retry option — not a blank or broken page.

— Permission Model —
• CAN: All users in an expired CID may click the renewal CTA and access the subscription page.
• CANNOT: N/A.
• Unauthorized: N/A.

— UI States —
• Loading: CTA click triggers navigation immediately.
• Empty: N/A.
• Error: Subscription page load failure shows retry option.
• Success: User lands on subscription page and can proceed with renewal.

— Negative Scenarios —
• NEG-1: Given a user in an expired CID, when they attempt to use an operational feature (e.g., create a deal, upload a task), then the action is hidden per the permission sheet — the renewal CTA is the only actionable path forward.

11. Rollout

FieldValue
Feature flagbilling_expired_limited_access — default: OFF; auto-enabled per CID when billing_status = expired AND unified_app = true
Stage 1 — Internal QABifrost team test CIDs with unified_app = true and forced billing_status = expired
GAGeneral release — all Qontak One CIDs (unified_app = true); flag auto-triggers on billing_status = expired
Backward compatYes — active accounts (billing_status = active) are completely unaffected
MigrationExisting hard-locked expired accounts (unified_app = true) will enter limited-access mode at GA — billing_expired_limited_access flag triggered on next billing_status evaluation

12. Observability

Key Events:

Event NameTriggerProperties
billing_expired_limited_access_activatedbilling_status = expired AND unified_app = true — system enables limited-access mode for CIDcompany_id, billing_status, timestamp
billing_expired_limited_access_deactivatedCID renews — billing_status transitions back to activecompany_id, timestamp
billing_expired_banner_renderedSubscription-end banner renders on page load for an expired CIDcompany_id, user_id, page_url, timestamp
billing_expired_renewal_cta_clickedUser clicks the renewal CTA in the bannercompany_id, user_id, timestamp
billing_expired_fail_closed_triggeredbilling_status check fails server-side — fail-closed restriction appliedcompany_id, error_reason, timestamp
billing_expired_permission_blockedWeekly aggregated count of restricted permission attempts per CIDcompany_id, attempt_count, top_permission_keys[], week_start_date
FieldDetail
Dashboard ownerBifrost team
Alert 1billing_expired_fail_closed_triggered count > 10 in any 1hr window → Slack: #bifrost-alerts (billing_status check service may be degraded)
Alert 2billing_expired_limited_access_activated fires but billing_expired_limited_access_deactivated never follows for the same company_id within 30 days → Slack: #bifrost-alerts (potential stuck expired state)

12.1 Post-Launch Monitoring Cadence

FieldDetail
Review cadenceWeekly for first 4 weeks post-GA, then monthly
OwnerBifrost PM
Review scopebilling_expired_renewal_cta_clicked rate, billing_expired_fail_closed_triggered count, CS ticket volume tagged "expired account"
Trigger threshold 1billing_expired_fail_closed_triggered > 5% of total billing_expired_limited_access_activated events in any 7-day window → engineering investigation same day
Trigger threshold 2Renewal CTA click rate (billing_expired_renewal_cta_clicked / billing_expired_banner_rendered) drops below 10% in any 14-day window post-GA → PM review (users may not be finding the renewal path)
Rollback considerationIf fail-closed triggers exceed 20% and cannot be resolved within 24 hours, PM disables billing_expired_limited_access globally pending root cause fix

13. Success Metrics

Adoption & Usage:

MetricDefinitionBaselineTarget
Support ticket reduction% drop in CS tickets tagged "expired account data request"~10 tickets/month (20% of ~50 monthly expired Qontak One CIDs)>80% reduction within 60 days of GA

Efficiency & Impact:

MetricDefinitionBaselineTarget
Renewal CTA click rate% of users in limited-access state who click the renewal CTA at least onceN/A — new metric, no historical dataEstablish baseline in first 30 days post-GA
Time-to-renewDays from billing_status = expired to account reactivationN/A — not currently measuredTrack trend; target reduction vs. pre-release cohort after 60 days

14. Launch Plan & Stage Gates

StageAudienceDurationSuccess Gate to AdvanceOwner
Internal QABifrost team test CIDs (unified_app = true, forced billing_status = expired)1–2 weeks0 P0/P1 bugs. All 3 story ACs pass. Permission key list verified against sheet. Banner renders correctly. Fail-closed behavior confirmed.PM + QA
GAAll Qontak One CIDs (unified_app = true) on billing_status = expiredOngoingInternal QA gate cleared. CS/AM teams briefed on new limited-access behavior — no longer need to manually handle data requests for expired Qontak One accounts.PM + Bifrost

15. Dependencies

DependencyOwning TeamRepo / ServiceDeliverable NeededBlocking?
billing_status = expired reliably triggeredBifrost (billing engine)qontak-billingDailyDeactivatePackage cronConfirmed active → grace → expired transitions working for Qontak One CIDs; ModPanel notified correctly via ModPanelUpdateStatus()YES
billing_status propagated to moderator-beBifrostmoderator-beBuildCache service; company_accounts.status enumcompany_accounts.status = expired (enum 4) set reliably when billing transitions, cached in Redis billing layerYES
show_when_billing_expired column — schemaBifrost Engmoderator-bepermissions tableNew boolean column show_when_billing_expired added to permissions table; default value defined; backfilled for all existing permission_keys before dev startYES
show_when_billing_expired column — values finalizedPM + CS/AMGoogle Sheet → permissions tableFull list of permission_keys with TRUE/FALSE values locked and reviewed before dev start — sheetYES
billing_expired_limited_access feature flag createdBifrost Engmoderator-be — Flipper/Preference system; qontak-launchpad — qontak-preferences serviceNet-new flag registered in both services; company-scoped enable/disable workingYES
unified_app flag available via APIBifrostqontak-launchpadbillings/get_unified_billing.go; ModPanel APIGetUnifiedBillingResponse.UnifiedApp reliable; Redis caching in place for performanceYES
Permission middleware extended for billing statusBifrost Engqontak-launchpadinternal/pkg/middleware/permission_check.goNew billing-status check layer added to permission middleware; reads show_when_billing_expired from cacheYES
Subscription-end banner componentBifrost / Frontendqontak-launchpad-feValidate whether a reusable inline banner component exists or must be built from expired.vue page — see Open Question #3NO (blocks CHG-001 scope confirmation)

16. Key Decisions + Alternatives Rejected

Decisions Made

DateDecisionRationale
Dec 2025Reuse existing subscription-end banner — no new UIFaster delivery; existing component already handles the visual notification; no design work required
Dec 2025Use existing permission_key system to enforce accessAvoids a parallel access-control layer; leverages existing enforcement mechanism across all modules
Dec 2025Scope to unified_app = true (Qontak One) onlyNon-Qontak One clients have a different billing lifecycle and are not part of this initiative
Dec 2025Permission enforcement is role-agnostic — all roles follow the show_when_billing_expired columnSimplifies implementation; all users in an expired CID are equally affected by the subscription state
Jun 2026billing_status = freeze follows current hard-block behavior — limited-access mode applies to expired onlySuspended accounts have a different lifecycle and are not in scope for this initiative

Alternatives Rejected

AlternativeWhy RejectedDate
Keep current hard lockout for expired accountsCreates negative UX, drives ~10 CS tickets/month, and jeopardizes Qontak One launch perceptionDec 2025
Full access grace period (7–30 days post-expiry)Removes renewal urgency; does not address the core problem of uncontrolled access and does not create a clear CTA pathDec 2025
Build new dedicated "expired account" UI pageOver-engineering — existing banner + permission system is sufficient for this phase; new UI adds design and dev cost without proportional benefitDec 2025

17. Open Questions

#TypeQuestionOwnerDeadlineMitigation
1Riskpermission_key list is more complex than anticipated — unintended side effects may block critical paths (e.g. billing page accidentally restricted)PM + Bifrost EngBefore dev startsEngineering audits all permission_keys against the sheet before implementation begins. PM + CS/AM validate show_when_billing_expired values with a dry-run checklist.
2RiskExisting banner component is not flexible enough to be triggered by billing_status = expired without additional UI workBifrost FrontendDiscovery phaseDesign + Engineering validate banner reusability in a technical discovery spike before committing to implementation. If inflexible, scope a minimal banner variant.
3Blocker — FrontendBanner component architecture: The current expired.vue in qontak-launchpad-fe is a standalone full-page component (layout: landing), not an inline reusable banner. It cannot be inserted at the top of other pages as-is. Is there a separate banner component available in another service (e.g. hub-chat, hub-service), or does one need to be built? If it needs to be built, this invalidates the "Figma: N/A — no new screens" assumption in CHG-001 and adds design scope.Bifrost FrontendDiscovery phaseScope a technical spike in the frontend to locate or prototype a minimal inline banner component before committing to CHG-001 as a no-design-work change.
4Decision — Grace PeriodLimited-access activation timing: qontak-billing has a 7-day grace period between active and expired (active → grace → expired). Should limited-access mode activate when status = grace (7 days before hard expiry) or only when status = expired? Activating at grace gives users more time to see the banner and renew; activating only at expired is a cleaner cut and avoids restricting users who will likely renew in the grace window.PM + BifrostBefore dev startsDefault proposal: activate only at expired, not grace. Grace period is a billing buffer — users still have active status semantically. Revisit if CS data shows most churn happens during grace.
5Decision — Schema Ownershipshow_when_billing_expired column location: The permission system spans two repos — permissions table in moderator-be (Ruby) and permission_component_codes table in qontak-launchpad (Go). Which service is the canonical owner of show_when_billing_expired config? If moderator-be owns it, launchpad must fetch it via API or a shared cache. If launchpad owns it locally, moderator-be must also be updated or removed from this feature's scope.Bifrost EngBefore dev startsEngineering decision spike needed. Proposed: moderator-be as canonical owner (it already owns the full permissions table), exposed to launchpad via the existing permission check API response.
6Risk — Activation LagCron cadence gap: DailyDeactivatePackage (qontak-billing) runs once per day. A CID whose subscription ends at noon may not enter limited-access until the next day's cron run — a gap of up to 24 hours. Is this acceptable? If not, a real-time event trigger (e.g. Kafka event from qontak-billing on status transition) is needed to activate limited-access mode promptly.PM + BifrostBefore dev startsAcceptable if the grace period (7 days) already provides sufficient buffer — a 24h cron lag within a 7-day grace window is low-risk. If limited-access activates at expired (no grace), the lag matters more. Decision links to Open Question #4.

PRD CHANGELOG

VersionDateBySectionTypeSummary
1.02026-06-22ClaudeAllREFORMATTEDReformatted from legacy Confluence PRD (Dec 2025) to Qontak PRD template v1.2. 12 sections extracted or filled via coaching interview; 1 flag remaining (S12 Observability).
1.12026-06-23ClaudeS12FILLEDFilled S12 Observability: 6 events defined, dashboard owner set to Bifrost team, 2 alerts added (fail-closed rate + stuck expired state at 30 days), post-launch monitoring cadence added. billing_expired_permission_blocked aggregated weekly per CID. Flag cleared.
1.22026-06-23ClaudeS6–S8, S10, S15, S17ENRICHEDTechnical enrichment from repo exploration (moderator-be, qontak-launchpad, qontak-launchpad-fe, qontak-billing). Key changes: (1) Terminology fix — "suspended" → freeze throughout; (2) Constraints expanded: unified_app source is ModPanel API, show_when_billing_expired is net-new schema, billing_expired_limited_access flag is net-new, 24h cron activation lag noted, 7-day grace period documented; (3) CHG-001 technical note: expired.vue is a standalone page not an inline banner — design scope risk flagged; (4) CHG-002 technical note: permission middleware in launchpad needs new billing-status check layer, Redis cache reuse recommended; (5) System flow updated: DailyDeactivatePackage cron chain, grace period, ModPanel API, freeze account note; (6) Dependencies expanded to 8 rows with specific repos and deliverables; (7) 4 new open questions added (#3–#6: banner architecture, grace period activation, schema ownership, cron lag).