Skip to main content

RFC: Qontak One | Billing | Self-Subs — Phase 1: New Subscription, Renewal and Upgrade

Document Conventions (do not remove)

This RFC follows the Qontak RFC Template format for governance — the metadata table, sections 1–6, and Comment logs are mandatory. Sections marked N/A — reason are intentional, not omissions.

It is also agent-execution-ready: the §1 Design References (FE half) + §1 PRD-to-Schema Derivation (BE half), §2 Repo Reading Guide (Detail 2.0) for both layers, mermaid diagrams, §2.G Cross-Layer Contract Verification, and §4 Agent Execution Plan + Verification & Rollback Recipe are present.

Delivery & project management live elsewhere. This RFC is the technical artifact only — no staffing, effort estimates, timeline, or rollout schedule. Delivery not yet handed to delivery.

The YAML frontmatter at the very top is the machine-readable index. The metadata table below is the human-readable governance record. Both agree on every shared field.

Metadata

FieldValueNotes
StatusIDEAYAML status: carries the remapped linter enum draft
DRIaddo.hernando@mekari.comSingle accountable owner (carried from PRD; reassign to Bifrost eng lead on handoff)
TeambifrostCarried from source PRD
Author(s)addo.hernando@mekari.comInitial draft author
Reviewersbifrost-backend, bifrost-frontend, modpanel-teamTech reviewers across affected layers
Approver(s)bifrost-tech-leadTech lead approval
Submitted Date2026-06-24Date RFC opened for discussion
Last Updated2026-06-24-r2Bump on every material edit
Target Release2026-Q3Carried from PRD
Target Quarter2026-Q3Carried from source PRD
Deliverynot yet handed to deliveryNo delivery/ artifacts exist yet
Related../prds/self-subs.mdSource PRD (v1.2, 2026-06-23)
Discussion#bifrost-self-subsSlack channel

Type: full-stack Frontend sub-type: enhancement Backend sub-type: new-feature

Sections at a Glance

  1. Overview (incl. Design References — FE half, and PRD-to-Schema Derivation — BE half)
  2. Technical Design (Repo Reading Guide both layers → topology → ADRs → end-to-end mermaid → DDL → APIs → cross-layer contract)
  3. High-Availability & Security
  4. Backwards Compatibility and Rollout Plan
  5. Concerns, Questions, or Known Limitations
  6. Comment Log
  7. Ready for Agent Execution

1. Overview

Today, every billing lifecycle action (new subscription, renewal, upgrade) for Billing V3 clients requires manual intervention by the sales or CSM team. Clients cannot initiate any self-service path; every transaction generates internal operational work regardless of complexity.

Self-Subs Phase 1 introduces three self-service CTAs on /subscriptions/packages in hub-chatNew Subscription, Renew, and Upgrade Main Package — gated exclusively to Billing V3 clients with self_subs_enabled = ON. A Pending SO Warning Modal guards the Renewal flow against duplicate payments. A Disable Self-Renewal toggle in Modpanel lets admins block partnership CIDs from self-serving.

This is a delta on existing subscription and modpanel machinery, not a rewrite. It reuses: the existing GET /api/core/v1/billings/info endpoint and BillingStore (FE) for eligibility data; the existing GET /api/core/v1/reports/billing/summary and useSubscriptionPackages composable for trial/active state; the existing PUT /api/core/v1/modpanels/subscriptions/:id endpoint and API::Core::V1::Modpanels::Resources::Subscriptions resource for the toggle. It adds: two new hub-service resources (SelfSubscriptions for flow initiation and Invoicable check), new fields on GET /billings/info response, new CTA components in PackageInfoComponent.vue, a new PendingSOWarningModal.vue, a new useInitiateSelfSubs composable, and the disable_self_renewal toggle UI in moderator-be.

Architectural clarification on the Mekari Pay callback — CONFIRMED by the upstream contract. The Self-Checkout API Contract (v4 create + MekariPay paid callback) confirms the integration: Mekari Pay → Mekari Billing is a server-to-server webhook (POST /api/v1/customer/payment/receipts, authed by x-callback-token) handled entirely by Mekari Billing — Qontak (hub-service / hub-worker) does not receive or process this webhook for Phase 1 (Open Q5 resolved: no Qontak webhook). One correction to the prior mental model: the sales order is provisioned synchronously at PI-create time (the create call returns a real sales_order_id), not after payment; the paid callback then converts SO → Sales Invoice via Billing's SyncAccountingJob. So "SO creation / active_date / subscription state are Mekari Billing's responsibility" still holds, but the SO exists from initiation, not from payment. Qontak only initiates the flow (proxy call) and the client is redirected to the hosted payment_link. On completion the client lands on the hub-chat invoices page (features/subscriptions/invoices/) — Open Q4 resolved (2026-07-01): the return is a plain navigation to the invoices surface, so the FE needs no success/cancel query-param parsing on /subscriptions/packages. The Qontak-side observability events (self_subs_flow_initiated, etc.) are FE-layer Mixpanel tracks on initiation/redirect; post-payment state is refreshed by re-fetching billing info.

Success Criteria

  • SC-1 (V3 gate, PRD §10.2 SS-S01): 100% of non-V3 clients or CIDs with self_subs_enabled = OFF see no self-service CTAs on /subscriptions/packages.
  • SC-2 (Flow initiation, PRD §9): New Subscription + Upgrade redirect the client to Mekari Pay within the PRD performance envelope (page load ≤ 2s). (Renewal pending Open Q11.)
  • SC-3 — Pending SO guard DESCOPED (2026-07-01): SS-S05 is owned by the BI team; double-payment is handled upstream by Billing. No Qontak-side pending-SO modal or Invoicable check.
  • SC-4 (Partnership control, PRD §10.2 SS-S06): Admin-toggled disable_self_renewal hides Renewal + New Subscription CTAs for the affected CID within one page reload cycle (no cache invalidation delay).
  • SC-5 (No regression): With self_subs_enabled = OFF or billing_version ≠ v3, /subscriptions/packages is byte-identical to the current state.

Out of Scope

Carried from PRD §5 Non-Goals:

  1. Non-V3 clients — completely hidden, no opt-in.
  2. New Subscription flow for non-trial V3 clients — is_trial = true gate is hard.
  3. Add-on package upgrades — main package only.
  4. Self-service downgrade or contract cancellation.
  5. Paid SO creation without completed payment.
  6. Proactive renewal reminders or payment notifications.

Additionally out of scope for this RFC: any Mekari Pay → Qontak webhook handler (Phase 2); moderator-be internal admin subscriptions (handled by Jumpstart, separate flow).

Assumptions

  • A-1: billing_version is already a persisted field on the organization/subscription model in hub-service (confirmed: the POST /billings/subscription endpoint accepts it as a param — billing_version is stored, not computed). The implementing agent must locate the exact model and column via grep -rn "billing_version" app/models/ before writing DDL.
  • A-2 (REVISED — Open Q3 resolved for create, gaps remain): The upstream create endpoint is POST /api/v4/invoiceables/self-checkout (Mekari Billing), not a flow_type-keyed /self-subscription. It is keyed by billing_type ∈ {new_subscription, legacy_subscription, free_subscription, non_recurring, upgrade_main, upgrade_additional} and returns { status, message, invoiceable_id, payment_link }. Auth is a Bearer Mekari access token, scope post_invoiceables; idempotency via external_ref_id; sales_order_source for Qontak-family = jurnal_qontak. Flow mapping: New Subscription → new_subscription, Upgrade (main) → upgrade_main. GAP: there is no renewal billing_type in the contract — SS-S03 Renewal cannot be mapped without Billing confirmation (new Open Q11). GAP: the request body requires company_record (company/contact name + email), subscribed_packages (product_code, quantity, total_price), billing_period, active_from/end_at — where hub-service sources this contact + line-item + price data is unspecified (new Open Q12). Rate limits unstated.
  • A-3 (RESOLVED by descoping — 2026-07-01): The pending-SO / Invoicable list is owned by the BI team; Qontak builds no Invoicable API. Double-payment is handled upstream by Billing (external_ref_id idempotency + rls_double_payment_validation). SS-S05 (Pending SO Warning Modal) is descoped from Qontak — the renewal flow proceeds straight to initiate. (Open Q1 closed.)
  • A-4: self_subs_enabled and disable_self_renewal are new per-CID boolean fields stored in hub-service DB. No pre-existing column — a migration is required.
  • A-5 (RESOLVED — 2026-07-01): On payment completion the client is redirected to the hub-chat invoices page (features/subscriptions/invoices/). No success/cancel query-param parsing is required on /subscriptions/packages. (Open Q4 closed. Mechanism for configuring the return target — the create contract has no return_url field — is a minor implementation follow-up, not a blocker.)
  • A-6: The prorated upgrade amount is computed by Mekari Billing, not Qontak — Qontak only proxies the create call and redirects the client to the resulting payment_link.

Dependencies

DependencyOwning teamDeliverable neededStatusBlocking?
Mekari Billing — self-checkout create contractMekari BillingRequest/response schema; auth; idempotencyRESOLVED (create): POST /api/v4/invoiceables/self-checkout, see contractpartial — see Q11/Q12
Mekari Billing — renewal billing_typeMekari BillingWhich billing_type (or alternate path) represents a renewalneeds confirmationYES (Open Q11)
Mekari Billing — create payload data sourcingMekari Billing / BifrostWhere hub-service gets company_record, subscribed_packages, prices, datesneeds confirmationYES (Open Q12)
Pending-SO / Invoicable listBI teamOwned by BI; Qontak needs no API — SS-S05 descopedRESOLVED (descoped) 2026-07-01no (Open Q1 closed)
Mekari Billing — billing_version field valuesMekari BillingV3 string valueRESOLVED: '3.0.0' (repo billing_v3? helper)no
Post-payment return targetBifrost FERedirect to hub-chat invoices page; no param parsingRESOLVED 2026-07-01no (Open Q4 closed)
Mekari Pay → Billing paid callbackMekari BillingWebhook ownershipRESOLVED: POST /api/v1/customer/payment/receipts, Billing-side only; no Qontak webhookno (Open Q5 closed)
SO provisioning / reconciliationMekari BillingSO lifecycle vs paymentREFRAMED: SO provisioned synchronously at create-time; date-guard active_from ≤ paid_date (Open Q2/Q13)YES
Prorated upgrade calculation basisMekari BillingCalendar months vs billing-cycle months (PRD OQ3)needs confirmationno (PRD §4)
moderator-be — CID Billing Settings page pathmoderator-be teamConfirm view file path where DisableSelfRenewalToggle landsneeds confirmationYES (FE chunk 6)
Figma framesDesignFrame links for all 3 flow CTAs and modal (PRD flag: pending)design in progressYES (blocks FE visual QA)

Design References (frontend half — required)

PRD-named surfaceFigma / design linkFrame nameDesign systemDesign QA contactNotes
New Subscription / Renew / Upgrade CTAs in PackageInfoComponentn/a — design pending (PRD §7/§8)pending@mekari/pixel3 (verified package.json)pending — Bifrost designReuses MpButton (existing in SubscriptionsLayout); CTA visibility computed from eligibility flags; frame required before FE visual QA
Pending SO Warning Modaln/a — design pending (PRD §8)pendingsamependingNew component PendingSOWarningModal.vue; reuses MpModal / MpButton pattern from other modals in hub-chat
Disable Self-Renewal Toggle in moderator-ben/a — design pending (PRD §8)pendingmoderator-be component librarypending — moderator-be teamToggle component; exact location TBD (Open Q6)

No Figma frames exist yet (PRD §7/§8 marks all UI "Pending"). The CTA additions and modal can be structurally implemented from the PRD spec; visual QA is blocked on frames.

PRD-to-Schema Derivation (backend half — required)

PRD-described entity / attribute / rulePersisted asExposed viaEnforced whereSource (PRD §)
Billing V3 client checkbilling_version on existing subscription/org model (col already exists — A-1); new hub-service response fieldGET /api/core/v1/billings/info (extend response)BE: Interactors::Billings::BillingInfo (extended)SS-S01, §9 behavior 1
Self-subs feature flag per CIDself_subs_enabled boolean (new col, default false, set by Bifrost team)GET /api/core/v1/billings/info (extend response)BE: extend BillingInfo interactorSS-S01, §6 constraints
Disable self-renewal per CIDdisable_self_renewal boolean (new col, default false) on CID billing settings modelGET /api/core/v1/billings/info (extend) + PUT /api/core/v1/modpanels/subscriptions/:id (extend params)BE: modpanel endpoint; FE: CTA visibility checkSS-S06, §8
Self-service flow initiationnot persisted in hub-service — proxy call to Mekari BillingPOST /api/core/v1/self_subscriptions (new)BE: SelfSubscriptions resource (new)SS-S02/S03/S04, §9 behaviors 2–4
Invoicable list (pending SO check)not persisted — proxy to Mekari Billing Invoicable APIGET /api/core/v1/self_subscriptions/invoicable (new)BE: SelfSubscriptions resource (new)SS-S05, §9 behavior 3
Observability eventsFE Mixpanel tracks (existing $mixpanel plugin in hub-chat)hub-chat MixpanelFE: composable on initiation/error§12

Detail 1.A — PRD Traceability (cross-layer)

Forward (PRD AC → RFC):

PRD composite AC idFE section / componentBE section / endpoint
SS-S01/AC-1PackageInfoComponent.vue eligibility gate: no CTAs if non-V3 or flag OFFGET /billings/info extended response (billing_version, self_subs_enabled)
SS-S01/AC-2PackageInfoComponent.vue: CTAs rendered per trial/active statesame endpoint + BillingStore extension
SS-S01/ERR-1Fail-safe: all CTAs hidden if billings/info call failsBE: BillingInfo interactor returns nil/error → FE defaults to hidden
SS-S02/AC-1"New Subscription" button → useInitiateSelfSubs(flow_type='new_sub')POST /api/core/v1/self_subscriptions (new)
SS-S02/AC-2Redirect to Mekari Pay; on return fetch billing summaryPOST /billings/info refresh on page mount
SS-S02/AC-3CTA hidden when packages.is_trial === falseFE computed eligibility
SS-S02/ERR-1Error toast "Something went wrong. Please try again."BE 4xx/5xx → FE catch block
SS-S02/ERR-2Client returned to page after Mekari Pay cancel/fail; no CTA state changeFE: Mekari Pay redirect includes status=failed param — FE reads and shows toast
SS-S03/AC-1"Renew" CTA → useCheckInvoicable() → proceed or show modalGET /api/core/v1/self_subscriptions/invoicable (new)
SS-S03/AC-2active_date = current end date — set by Mekari Billing (not Qontak)n/a — external
SS-S03/ERR-1Invoicable API fail/timeout → proceed to checkout + log trackBE: 5xx/timeout → FE treats as no pending SO (fail-safe)
SS-S03/ERR-2Mekari Pay failure redirect → toastFE: redirect param detection
SS-S04/AC-1"Upgrade Main Package" CTA → useInitiateSelfSubs(flow_type='upgrade')POST /api/core/v1/self_subscriptions
SS-S04/AC-2Immediate package upgrade — set by Mekari Billingn/a — external
SS-S04/AC-3No Invoicable check for upgrade (hardcoded bypass)FE: no useCheckInvoicable call in upgrade path
SS-S04/ERR-1Error toast on API failureBE 4xx/5xx → FE catch
SS-S04/ERR-2Mekari Pay failure/cancel → toast, no SOFE redirect param
SS-S05/AC-1PendingSOWarningModal.vue shown when Invoicable returns ≥1 pending SOGET /self_subscriptions/invoicable returns list
SS-S05/AC-2"Pay Existing" → redirect to Mekari Pay for existing SOFE: pending_so.checkout_url or equivalent field from Invoicable API (pending A-3)
SS-S05/AC-3"Create New" → call POST /self_subscriptions (flow_type=renewal)FE: call initiate after modal confirmation
SS-S05/ERR-1Mekari Pay failure for existing SO → return to pageFE: redirect param
SS-S06/AC-1CTAs hidden for CIDs with disable_self_renewal = trueGET /billings/info returns disable_self_renewal; FE BillingStore consumes
SS-S06/AC-2CTAs render normally when toggle OFFFE computed
SS-S06/ERR-1Toggle save failure → error toast, state revertedBE 4xx → FE error handler in moderator-be

Reverse (RFC → PRD AC):

New / extended FE component or BE artifactPRD composite AC ids it serves
BillingStore.BillingInfo — new fields billing_version, self_subs_enabled, disable_self_renewalSS-S01/*, SS-S06/*
GET /api/core/v1/billings/info (extended response)SS-S01/*, SS-S06/*
POST /api/core/v1/self_subscriptions (new)SS-S02/AC-1, SS-S03/AC-1, SS-S04/AC-1
GET /api/core/v1/self_subscriptions/invoicable (new)SS-S03/AC-1, SS-S05/AC-1,AC-2
PUT /api/core/v1/modpanels/subscriptions/:id (extended)SS-S06/AC-1,AC-2,ERR-1
PackageInfoComponent.vue — CTA rendering logicSS-S01/AC-1,AC-2, SS-S02/AC-1,AC-3, SS-S03/AC-1, SS-S04/AC-1, SS-S06/AC-1
PendingSOWarningModal.vue (new)SS-S05/AC-1,AC-2,AC-3,ERR-1
useInitiateSelfSubs composable (new, in usePackages.ts or dedicated file)SS-S02/AC-1, SS-S03/AC-1, SS-S04/AC-1
useCheckInvoicable composable (new)SS-S03/AC-1, SS-S05/AC-1
PackageDetails.vue (extended — passes eligibility props to child)SS-S01/*
moderator-be DisableSelfRenewalToggleSS-S06/AC-1,AC-2,ERR-1

UI / Consumer Surface Coverage

PRD-named surfaceConsumerRequired reads (BE)Required writes (BE)FE componentStatus surface
/subscriptions/packages — CTA areaweb (V3 client)GET /billings/info, GET /reports/billing/summaryPOST /self_subscriptions, GET /self_subscriptions/invoicablePackageInfoComponent.vueLoading skeleton; error hidden; success: CTAs
Pending SO Warning Modalweb (V3 active client during renewal)GET /self_subscriptions/invoicablePendingSOWarningModal.vueLoading on Renew CTA; modal on ≥1 pending SO
Modpanel CID Billing Settings — Toggleweb (admin)CID billing settings fetch (existing)PUT /modpanels/subscriptions/:idmoderator-be DisableSelfRenewalToggle (path TBD)Skeleton; error toast; success toast

Role Coverage

PRD roleAuthorization mechanismEndpoints permitted (BE)UI surface visibility (FE)Audit trail
Billing V3 Client (trial)oauth2 :admin, :owner, :supervisor, :agent, :member (existing hub-service scopes)GET /billings/info, GET /reports/billing/summary, POST /self_subscriptionsNew Subscription CTAMixpanel track
Billing V3 Client (active)samesame + GET /self_subscriptions/invoicableRenew + Upgrade CTAsMixpanel track
Qontak Adminoauth2 :modpanelPUT /modpanels/subscriptions/:idDisableSelfRenewalToggle in moderator-beBE log
Non-V3 Clientsame client scopesnone of above (gate returns hidden)no CTAs

PRD Section Coverage

PRD §TitleWhere covered (RFC)
HEADER BLOCKHeader§1 Metadata
3One-liner + Problem§1 Overview
4Target Users + Persona§1 Detail 1.A Role Coverage
5Non-Goals§1 Out of Scope
Scope ChangesBE + FE scope§1 Overview
6Constraints§3 (performance), §4 (feature flag)
7Feature Changes (CHG-001)§2.A UI Contract, §2.4 APIs
8New Features (Modal, Toggle)§2.A, §2.C
9API & Webhook Behavior§2.4 APIs — closes PRD gap
10.1System Flow§2.1 branch flow, §2.2 sequences
10.2User StoriesDetail 1.C
11Rollout§4 Rollout Strategy
12Observability§3 Monitoring
13Success Metrics§1 Success Criteria
14Launch Plan & Stage Gates§4 Rollout Strategy
15Dependencies§1 Dependencies
16Key Decisions + Alternatives§2 Technical Decisions (ADRs)
17Open Questions§5 Concerns/Questions

Detail 1.B — Decisions Closed (cross-layer)

DecisionChosen optionAlternatives rejectedWhy rejectedLayer
Eligibility data sourceExtend GET /billings/info response with billing_version, self_subs_enabled, disable_self_renewal; consume via existing BillingStore (ADR-1)New dedicated eligibility endpointBillingStore already calls /billings/info on every page load; extra round-trip for same data; no precedent for a standalone eligibility endpoint in this stackBE + FE
Storage of self_subs_enabled / disable_self_renewalNew boolean columns on hub-service DB model (most likely organization_packages or an organization_settings model — agent must grep; A-4); default false (ADR-2)Store as Mekari Billing attribute; store only in FE flagshub-service is the single source of truth for billing settings; FE stores are ephemeral; Mekari Billing flag delivery latency is uncontrolledBE
Self-subscription proxy architectureNew API::Core::V1::SelfSubscriptions Grape resource namespace in hub-service; proxies Mekari Billing API server-side (ADR-3)Direct FE call to Mekari BillingMekari Billing credentials must stay server-side; consistent with existing billing proxy pattern in hub-service; FE cannot call Mekari Billing directly due to CORS and authBE
disable_self_renewal management endpointExtend existing PUT /api/core/v1/modpanels/subscriptions/:moderator_account_id with new disable_self_renewal optional param (ADR-4)New dedicated endpointExisting endpoint already handles CID billing subscription settings with :modpanel scope; extending avoids new route registration and auth wiringBE
Invoicable check: blocking vs. non-blockingNon-blocking: API failure/timeout → silently proceed to checkout (ADR-5)Blocking: fail the renewal if Invoicable API is downPRD §9 behavior 3 explicitly requires fail-safe non-blocking; preventing renewal on Invoicable downtime would degrade the core billing flowBE + FE
FE feature-flag gatingCheck billingStore.billing.billing_version === 'v3' and billingStore.billing.self_subs_enabled and billingStore.billing.disable_self_renewal — derived from BillingStore (already populated) (ADR-6)usePackageStore package-code gatingself_subs_enabled is a per-CID boolean, not a component code; BillingStore already has is_trial, status, and similar boolean flags from the same endpointFE
Mekari Pay webhook handlingOut of scope for Phase 1 — no Qontak-side webhook handler; SO creation is fully Mekari Billing's responsibility (ADR-7)Hub-service or hub-worker webhook endpointPRD §9 does not require Qontak to receive the Mekari Pay callback; adding webhook increases scope and requires idempotency/retry infra; Phase 2 (Open Q5)BE

Detail 1.C — Per-Story Change Map

Story idStory titleLayer scopeFE changesBE changesComposite AC idsRFC anchors
SS-S01V3 Flow Visibility GateFE + BEBillingStore.BillingInfo +3 fields; PackageDetails.vue passes eligibility props; PackageInfoComponent.vue conditional CTA blockExtend Interactors::Billings::BillingInfo to expose billing_version, self_subs_enabled, disable_self_renewalSS-S01/AC-1,AC-2,ERR-1§2.0 anchors 1–3, §2.4 endpoint 1
SS-S02New Subscription from TrialFE + BENew "New Subscription" button in PackageInfoComponent.vue; useInitiateSelfSubs(flow_type='new_sub') call; loading + error statesPOST /api/core/v1/self_subscriptions new endpoint in API::Core::V1::SelfSubscriptions::Resources::SelfSubscriptionsSS-S02/AC-1,AC-2,AC-3,ERR-1,ERR-2§2.2 seq 1, §2.4 endpoint 2
SS-S03Subscription RenewalFE + BENew "Renew" button; useCheckInvoicable() call before initiation; loading spinner; useInitiateSelfSubs(flow_type='renewal')GET /api/core/v1/self_subscriptions/invoicable proxy; POST /self_subscriptions reusedSS-S03/AC-1,AC-2,ERR-1,ERR-2§2.2 seq 2, §2.4 endpoints 2–3
SS-S04Upgrade Main PackageFE + BENew "Upgrade Main Package" button; useInitiateSelfSubs(flow_type='upgrade') directly (no invoicable check)POST /self_subscriptions reused with flow_type=upgradeSS-S04/AC-1,AC-2,AC-3,ERR-1,ERR-2§2.2 seq 3, §2.4 endpoint 2
SS-S05Pending SO Warning Modal — DESCOPED 2026-07-01Not built (owned by BI; double-payment handled upstream)Not builtdescope note §1, §5 Q1
SS-S06Disable Self-Renewal for PartnershipsFE + BE (BE + moderator-be)BillingStore disable_self_renewal field; CTA visibility gate in PackageInfoComponent.vue; DisableSelfRenewalToggle in moderator-beExtend PUT /modpanels/subscriptions/:id with disable_self_renewal param; new column + migrationSS-S06/AC-1,AC-2,ERR-1§2.3 DDL, §2.4 endpoint 4

2. Technical Design

Detail 2.0 — Repo Reading Guide

Repo Map (mermaid, both layers)

flowchart LR
subgraph fe["hub-chat (Nuxt 4 / Vue 3 / Pinia)"]
page["pages/subscriptions/index.vue"]
layout["features/subscriptions/SubscriptionsLayout.vue"]
pd["features/subscriptions/packages/PackageDetails.vue"]
pic["features/subscriptions/packages/PackageInfoComponent.vue (extend)"]
modal["features/subscriptions/packages/PendingSOWarningModal.vue (new)"]
up["features/subscriptions/composables/usePackages.ts (extend)"]
bs["common/store/BillingStore.ts (extend)"]
end
subgraph hs["hub-service (Rails 7 / Grape / PostgreSQL)"]
bi_res["app/services/api/core/v1/billings/resources/billings.rb (extend GET /info)"]
mp_subs["app/services/api/core/v1/modpanels/resources/subscriptions.rb (extend PUT)"]
ss_res["app/services/api/core/v1/self_subscriptions/resources/self_subscriptions.rb (new)"]
ss_rts["app/services/api/core/v1/self_subscriptions/routes.rb (new)"]
bi_int["Interactors::Billings::BillingInfo (extend — locate via grep)"]
ss_int["Interactors::SelfSubscriptions::* (new)"]
db[("PostgreSQL")]
end
subgraph ext["External (Mekari Platform)"]
mb["Mekari Billing Self-Sub API"]
inv["Mekari Billing Invoicable API"]
mpay["Mekari Pay"]
end
mod_be["moderator-be (Rails — CID Billing Settings UI, path TBD)"]

page --> layout --> pd --> pic
pd --> modal
pd --> up
pd --> bs
up --> bi_res
bs --> bi_res
up --> ss_res
mod_be --> mp_subs
ss_res --> mb
ss_res --> inv
mb --> mpay
bi_res --> bi_int --> db
mp_subs --> db
ss_res --> ss_int

Existing Code Anchors

LayerPathWhy the agent reads itWhat pattern it teaches
BEapp/services/api/core/v1/billings/resources/billings.rbThe resource to extend for GET /billings/infoGrape resource pattern: oauth2 :admin, ..., get '/info', params do, interactor invocation + present(response:) error 422
BEapp/services/api/core/v1/modpanels/resources/subscriptions.rbThe resource to extend with disable_self_renewalSame pattern; :modpanel scope; PUT '/:moderator_account_id'; optional :field, type: extension
BEapp/services/api/core/v1/billings/routes.rbRoute mounting pattern to replicate for self_subscriptions/routes.rbmount API::Core::V1::Billings::Resources::*
BEapp/services/api/core/v1/modpanels/routes.rbRoute mounting to confirm where SelfSubscriptions routes are registeredmount ...Routes pattern; mount location for new resource
BEspec/services/api/core/v1/billings/resources/billings_spec.rbThe RSpec request spec pattern for new specsRSpec.describe '...', type: :request; let(:organization), stub_auth, get '/path', headers:, expect(response_as_json)
FEcommon/store/BillingStore.tsBillingInfo interface to extend; $customFetch pattern; store action shape$customFetch('/api/core/v1/billings/info'){data: BillingInfo}; Pinia defineStore("billing", () => {...})
FEfeatures/subscriptions/packages/PackageDetails.vueThe parent that orchestrates PackageInfoComponent and useSummaryQuotaInfouseSubscriptionPackages() + useSummaryQuotaInfo() on onBeforeMount; computed props passed to child
FEfeatures/subscriptions/packages/PackageInfoComponent.vueThe component to extend with CTAsMpFlex/MpText/MpBadge/MpSpinner from @mekari/pixel3; props pattern; loading skeleton
FEfeatures/subscriptions/composables/usePackages.tsThe composable to extend with useInitiateSelfSubs and useCheckInvoicable$customFetch + ref<T> state + status enum pattern; useNuxtApp() access
FEfeatures/subscriptions/SubscriptionsLayout.vueExisting feature flag checks (is_centralized_package, is_qontak_one_package_ui)Pattern for reading billingStore.billing.* boolean checks + conditional rendering

Source Verification (anti-hallucination — required)

LayerAnchor / pattern / contractVerified byEvidence
BEGET /billings/info exists, scoped to client rolesreadbillings.rb line 607–629: oauth2 :admin, :owner, :supervisor, :agent, :member get '/info' do; interactor = Interactors::Billings::BillingInfo
BEbilling_version is an accepted param on subscription creationreadbillings.rb line 47: optional :billing_version, type: String in POST /subscription params → field exists in hub-service
BEPUT /modpanels/subscriptions/:moderator_account_id exists with optional params patternreadmodpanels/resources/subscriptions.rb lines 13–37: multiple optional :field params; oauth2 :modpanel; put '/:moderator_account_id'
BEGrape mount pattern for new resourcereadbillings/routes.rb: mount API::Core::V1::Billings::Resources::Billings
BERSpec request spec patternreadbillings_spec.rb: RSpec.describe '...', type: :request; stub_auth; get '/path', headers:; response_as_json helper
BEInteractors pattern (class + class methods)readbillings.rb lines 618–619: interactor = Interactors::Billings::BillingInfo; parameters = interactor.parameters(params); result = interactor.new(parameters).result — Interactors live in a non-app/ path; agent must locate via grep -rn "class.*BillingInfo" .
FEBillingStore calls /api/core/v1/billings/info and exposes BillingInforeadBillingStore.ts lines 134–136: $customFetch('/api/core/v1/billings/info').then(r => { const {data} = r; billing.value = data })
FEBillingInfo interface does NOT currently have billing_version, self_subs_enabled, disable_self_renewalreadBillingStore.ts lines 4–18: interface BillingInfo listing — none of the three fields present
FEis_trial already in BillingInforeadBillingStore.ts line 6: is_trial: boolean
FEuseSubscriptionPackages composable fetches GET /api/core/v1/reports/billing/summaryreadusePackages.ts lines 59–73: $customFetch('/api/core/v1/reports/billing/summary')PackagesType { is_trial, package_status, ... }
FEPackageInfoComponent.vue currently has no CTAsreadPackageInfoComponent.vue lines 1–98: renders only package name badge and renewal date; no buttons
FEPackageDetails.vue orchestrates PackageInfoComponent and UsageDetailreadPackageDetails.vue lines 1–10: <PackageInfoComponent :datas="packageInfo" :isLoading="status" />; <UsageDetail .../>
FE$customFetch is the HTTP client patternreadBillingStore.ts line 134: const { $customFetch } = useNuxtApp()
FESubscriptionsLayout.vue uses billingStore.billing.is_trial, billing_enabled, etc.readSubscriptionsLayout.vue lines 128–135: billing.value.billing_enabled && billing.value.payment_type?.toLowerCase() === 'prepaid' && !billing.value.is_trial && billing.value.status === 'active'
FEFeature flag check pattern in SubscriptionsLayoutreadSubscriptionsLayout.vue lines 138–153: if (!appConfig.value.is_centralized_package) and !billing.value.is_qontak_one_package_ui && !isUseUnifiedApp.value → boolean checks on billingStore.billing.*
FETest commandreadpackage.json scripts: test: 'vitest --dom --pool=forks'; type-check: 'vue-tsc --noEmit'; lint: 'pnpm lint:js && pnpm lint:prettier'

Patterns to Follow

LayerConcernPattern in repoReference fileDeviation?
BEGrape resource endpointoauth2 :scope, params do, interactor invoke, present(response:), 422 errorbillings.rb, subscriptions.rbnone
BENew Grape resource namespacemodule API::Core::V1::X::Resources::X < API::Core::V1::ApplicationResource + routes.rbbillings/routes.rbnone — replicate for self_subscriptions/
BEOptional param extensionoptional :field, type: Boolean in existing params do blocksubscriptions.rb lines 13–37none
BERSpec request specdescribe '...', type: :request; let(:organization), stub_auth, get/post/put, expect(response_as_json)billings_spec.rbnone
FEBillingStore field extensionadd field to BillingInfo interface + DEFAULT_BILLING_INFO constantBillingStore.tsnone
FENew composable in usePackages.tsexport `const useX = (): UseX => { const status = ref<'idle''pending''resolved'
FEConditional rendering in componentv-if="eligibilityFlag" on button elementsSubscriptionsLayout.vue handleShowingTopupButtonnone
FELoading stateMpSpinner inside v-if="isLoading === 'pending'" blockPackageInfoComponent.vue lines 71–87none
FEError / toastuseNuxtApp().$toast?.error(...) or equivalent toast pattern used in hub-chatsearch features/ for toast usage (agent must verify exact import)none
FEModalVerify MpModal usage in another hub-chat feature for correct props/slots pattern (agent must grep MpModal in features/)pending verificationnone

Reading Order for the Agent

  1. app/services/api/core/v1/billings/resources/billings.rb — GET /info handler shape to extend.
  2. app/services/api/core/v1/modpanels/resources/subscriptions.rb — PUT handler shape to extend.
  3. grep -rn "class.*BillingInfo" . then read the located interactor — understand what fields the BillingInfo interactor returns and how to add new ones.
  4. app/services/api/core/v1/billings/routes.rb — mount pattern for new self_subscriptions/routes.rb.
  5. common/store/BillingStore.tsBillingInfo interface + $customFetch call to extend.
  6. features/subscriptions/packages/PackageDetails.vue — orchestration layer; where eligibility props are computed and passed down.
  7. features/subscriptions/packages/PackageInfoComponent.vue — the component to extend with CTAs.
  8. features/subscriptions/composables/usePackages.ts — the composable to extend with useInitiateSelfSubs and useCheckInvoicable.
  9. features/subscriptions/SubscriptionsLayout.vue — existing feature-flag conditional rendering pattern.
  10. spec/services/api/core/v1/billings/resources/billings_spec.rb — RSpec pattern for new request specs.

Detail 2.1 — Architecture (mermaid)

Infrastructure / Deployment Topology

flowchart TB
subgraph client["Browser (V3 client)"]
hc["hub-chat (Nuxt 4 SSR/SPA)"]
end
subgraph admin_browser["Browser (admin)"]
modbe["moderator-be (Rails views)"]
end
kong["Kong API Gateway"]
subgraph hs["hub-service (Rails 7 + Grape)"]
billing_res["Billings resource (GET /info extended)"]
reports_res["Reports::Billing resource (GET /summary — unchanged)"]
ss_res["SelfSubscriptions resource (POST + GET — new)"]
modpanel_res["Modpanels::Subscriptions resource (PUT extended)"]
end
pg[("PostgreSQL")]
subgraph ext["Mekari Platform"]
mb["Mekari Billing Self-Sub API"]
inv_api["Mekari Billing Invoicable API"]
mpay["Mekari Pay checkout"]
end

hc --> kong
modbe --> kong
kong --> billing_res
kong --> reports_res
kong --> ss_res
kong --> modpanel_res
billing_res --> pg
modpanel_res --> pg
ss_res --> mb
ss_res --> inv_api
mb --> mpay
ServiceUse case in this RFCThird-party connection
hub-service Billings resourceExtend GET /info to return V3 gate fieldsPostgreSQL read
hub-service SelfSubscriptions resourceProxy flow initiation + Invoicable check to Mekari BillingMekari Billing API (HTTP)
hub-service Modpanels::Subscriptions resourcePersist disable_self_renewal per CIDPostgreSQL write
Mekari Billing APIAccepts flow initiation, returns checkout_url; runs Invoicable checkMekari Pay
moderator-beAdmin UI for DisableSelfRenewalToggleKong → hub-service

Branch Flow — Eligibility Gate and CTA Rendering

flowchart TD
A["Client loads /subscriptions/packages"] --> B{"billing_version = v3?"}
B -- No --> C["Render standard page — no self-service CTAs"]
B -- Yes --> D{"self_subs_enabled = true?"}
D -- No --> C
D -- Yes --> E{"disable_self_renewal = true?"}
E -- Yes --> F["Show Upgrade CTA only (if active sub)"]
E -- No --> G{"is_trial = true?"}
G -- Yes --> H["Show New Subscription CTA"]
G -- No --> I{"package_status = active?"}
I -- Yes --> J["Show Renew CTA + Upgrade CTA"]
I -- No --> K["No self-service CTAs (inactive/expired)"]

Detail 2.2 — Sequence Diagrams

Correction (2026-06-30, per the confirmed contract): The diagrams below predate the Self-Checkout contract and still show POST /self-subscription (flow_type=…) and the SO being created after payment. Per the contract, the create call is POST /api/v4/invoiceables/self-checkout (keyed by billing_type), the SO is provisioned synchronously at create-time (returns sales_order_id + payment_link), and the paid callback (MekariPay → Billing) converts SO → SI. Read the "MB creates paid SO" notes as "MB converts the already-provisioned SO to a paid SI". Seq 2 (Renewal with Pending SO Modal) is superseded (2026-07-01): the Pending-SO modal / Invoicable check is descoped (owned by BI; double-payment handled upstream), so renewal goes straight to initiate — ignore the modal branch. The renewal create call itself is not yet mappable upstream (Open Q11). On completion the client returns to the invoices page (Q4).

Seq 1 — New Subscription (Trial → Paid) happy path

sequenceDiagram
participant FE as hub-chat
participant HS as hub-service
participant MB as Mekari Billing API
participant MP as Mekari Pay

FE->>HS: POST /api/core/v1/self_subscriptions (flow_type=new_sub)
HS->>MB: POST /self-subscription (flow_type=new_sub, company_id)
MB-->>HS: 200 checkout_url
HS-->>FE: 200 data.checkout_url
FE->>MP: redirect to checkout_url
Note over FE,MP: Client completes payment on Mekari Pay
MP-->>FE: redirect to /subscriptions/packages
Note over MB: MB creates paid SO — active_date=immediate
FE->>HS: GET /api/core/v1/billings/info (page reload refresh)
HS-->>FE: updated billing_info — is_trial=false

Seq 2 — Renewal with Pending SO Modal

sequenceDiagram
participant FE as hub-chat
participant HS as hub-service
participant MB as Mekari Billing API
participant MP as Mekari Pay

FE->>HS: GET /api/core/v1/self_subscriptions/invoicable (company_id)
alt Invoicable API timeout or error
HS-->>FE: 200 pending_sos=[] (fail-safe empty)
Note right of FE: Logs self_subs_pending_so_check_failed
else No pending SO
HS-->>FE: 200 pending_sos=[]
else Pending SO found
HS-->>FE: 200 pending_sos=[...so details...]
Note right of FE: Show PendingSOWarningModal
alt Client clicks Pay Existing
FE->>MP: redirect to existing SO checkout_url
Note over MP: Client pays existing SO
MP-->>FE: redirect to /subscriptions/packages
else Client clicks Create New
Note right of FE: Proceed to renewal initiation
end
end
FE->>HS: POST /api/core/v1/self_subscriptions (flow_type=renewal)
HS->>MB: POST /self-subscription (flow_type=renewal, company_id)
MB-->>HS: 200 checkout_url
HS-->>FE: 200 data.checkout_url
FE->>MP: redirect to checkout_url
MP-->>FE: redirect to /subscriptions/packages
Note over MB: MB creates paid SO — active_date=current subscription end

Seq 3 — Upgrade Main Package happy path

sequenceDiagram
participant FE as hub-chat
participant HS as hub-service
participant MB as Mekari Billing API
participant MP as Mekari Pay

FE->>HS: POST /api/core/v1/self_subscriptions (flow_type=upgrade)
HS->>MB: POST /self-subscription (flow_type=upgrade, company_id)
MB-->>HS: 200 checkout_url (prorated amount embedded by MB)
HS-->>FE: 200 data.checkout_url
FE->>MP: redirect to checkout_url (prorated amount shown on Mekari Pay)
MP-->>FE: redirect to /subscriptions/packages
Note over MB: MB creates paid SO — package updated immediately

Seq 4 — Mekari Billing API failure (all flows)

sequenceDiagram
participant FE as hub-chat
participant HS as hub-service
participant MB as Mekari Billing API

FE->>HS: POST /api/core/v1/self_subscriptions (any flow_type)
HS->>MB: POST /self-subscription
MB-->>HS: 4xx or 5xx or timeout
HS-->>FE: 422 or 503 (proxied error)
Note right of FE: Show error toast — "Something went wrong. Please try again."
Note right of FE: Logs self_subs_initiation_failed

Detail 2.3 — Database Model (DDL)

hub-service uses Rails ActiveRecord migrations (bundle exec rails db:migrate). Migration naming convention: YYYYMMDDHHMMSS_description.rb.

Pre-condition: Before writing migrations, the implementing agent must run: grep -rn "billing_version" app/models/ and grep -rn "self_subs_enabled" app/models/ to identify the exact model and table for these columns. The expectation (A-1, A-4) is:

  • billing_version already exists on some model (confirm col name and table).
  • self_subs_enabled and disable_self_renewal do not yet exist (confirm with grep).

Proposed migration (adjust table name after above verification):

# YYYYMMDDHHMMSS_add_self_subs_fields_to_organization_packages.rb
class AddSelfSubsFieldsToOrganizationPackages < ActiveRecord::Migration[7.0]
def change
# Confirm table name via: grep -rn "self_renewal\|self_subs" app/models/ — then set correct table
add_column :organization_packages, :self_subs_enabled, :boolean, null: false, default: false
add_column :organization_packages, :disable_self_renewal, :boolean, null: false, default: false
add_index :organization_packages, :self_subs_enabled, name: 'idx_org_packages_self_subs_enabled'
end
end

If the target table is different (e.g. subscriptions, organization_settings), adjust the migration accordingly. The NOT NULL + default false pattern ensures no nil-check issues and no backfill needed for existing rows.


Detail 2.4 — APIs

All endpoints use the bilingual response envelope pattern verified in hub-service: { status: "success", data: {...} } on success; { status: "error", message: "..." } (422) on failure. New endpoints follow the existing Grape resource + Interactor pattern.

Method · PathStatusChangeAuth scopeRequestResponse / ErrorProxy timeout / retry
GET /api/core/v1/billings/infoextendAdd billing_version, self_subs_enabled, disable_self_renewal to response body:admin, :owner, :supervisor, :agent, :member (unchanged)200 {data: {billing_version, self_subs_enabled, disable_self_renewal, ...existing fields}}; 422 unchangedn/a (DB read only)
POST /api/core/v1/self_subscriptionsnewFlow initiation proxy → Mekari Billing POST /api/v4/invoiceables/self-checkout:admin, :owner, :supervisor, :agent, :memberFE → hub-service: {flow_type: "new_sub" | "upgrade", company_id derived from token} (renewal pending Open Q11). hub-service → Billing: maps to billing_type + assembles company_record / subscribed_packages / billing_period / active_from / end_at / sales_order_source: "jurnal_qontak" (sourcing pending Open Q12)200 {data: {payment_link, invoiceable_id}} (upstream returns payment_link, not checkout_url); 422 (upstream errors[] array, e.g. duplicate external_ref_id); 503 on Mekari Billing unavailable5 s timeout; 1 retry with 1 s linear backoff on network error or 5xx only (not on 4xx; 422 is not retried); after retry exhaustion → 503; no circuit breaker in Phase 1 (Risk 10)
GET /api/core/v1/self_subscriptions/invoicabledescoped 2026-07-01Pending-SO check owned by BI; Qontak builds no Invoicable API (Open Q1 closed). SS-S05 descoped; double-payment handled upstream by Billing.
PUT /api/core/v1/modpanels/subscriptions/:moderator_account_idextendAdd disable_self_renewal optional param:modpanel (unchanged){disable_self_renewal: true | false, ...existing params}200 success; 422 validation errorn/a (DB write only)

Confirmed upstream contract — Mekari Billing Self-Checkout (v4)

Source: Self-Checkout API Contract (v4 create + MekariPay paid callback), generated from source 2026-06-24. This is the endpoint the hub-service POST /self_subscriptions proxy wraps.

PropertyValue
Method · PathPOST /api/v4/invoiceables/self-checkout (Mekari Billing)
AuthBearer Mekari access token; scope post_invoiceables (401 if missing/invalid)
Idempotencyexternal_ref_id (unique; duplicate → 422 external_ref_id has already been taken)
Required bodybilling_type, billing_period (months), sales_order_source (jurnal_qontak), company_record{company_name, billing_contact_name, billing_contact_email}, subscribed_packages[]{product_code, company_id, quantity, total_price}
Conditional / optionalactive_from, end_at (strict YYYY-MM-DD), net_amount (IDR int), discount_percentage per line
billing_type enumnew_subscription, legacy_subscription, free_subscription, non_recurring, upgrade_main, upgrade_additionalno renewal (Open Q11)
Success 200{status:"OK", message, invoiceable_id, payment_link}
Errors 422errors:[{field: message}] array (see contract for the full table)
Side effects (200)PI persisted (creation_type=self_checkout); Jurnal contact + sales order provisioned synchronously (real sales_order_id); payment_link generated. SO-provision failure rolls back the whole PI (422).

Paid settlement: MekariPay → Billing POST /api/v1/customer/payment/receipts (x-callback-token); Qontak is not a party (Open Q5 closed). Date guard: SO→SI conversion fails if active_from > paid_date — relevant to renewal's future active_date (Open Q13).

Remaining contract gaps (still blocking — require external confirmation):

  • Open Q11 — no renewal billing_type exists; SS-S03 Renewal cannot be mapped.
  • Open Q12 — sourcing of company_record + subscribed_packages + billing_period + dates the create call requires (hub-service does not assemble this today).
  • Open Q13 (non-blocking) — renewal active_from must be ≤ paid_date to clear the SO→SI date guard.

Closed 2026-07-01: Open Q1 (Invoicable list — owned by BI, SS-S05 descoped) and Open Q4 (post-payment return = hub-chat invoices page, no param parsing).


Detail 2.A — UI Contract

Descoped 2026-07-01: the PendingSOWarningModal.vue rows below are not built (SS-S05 owned by BI). They remain here as historical detail only; the same applies to the modal entries in Detail 2.A.1 (TypeScript), 2.B (data-fetching), 2.C (UI state matrix), §3 monitoring, and the accessibility note.

SurfaceElementBehaviorEligibility guardAC
PackageInfoComponent.vueNew Subscription buttonv-if="isTrial && selfSubsEnabled && !disableSelfRenewal"billing_version=v3, self_subs_enabled, is_trial=true, disable_self_renewal=falseSS-S02/AC-1,AC-3
PackageInfoComponent.vueRenew buttonv-if="!isTrial && isActive && selfSubsEnabled && !disableSelfRenewal"billing_version=v3, self_subs_enabled, is_trial=false, package_status=active, disable_self_renewal=falseSS-S03/AC-1
PackageInfoComponent.vueUpgrade Main Package buttonv-if="!isTrial && isActive && selfSubsEnabled" (not blocked by disable_self_renewal per PRD SS-S06/AC-1)billing_version=v3, self_subs_enabled, activeSS-S04/AC-1
All 3 CTAsLoading stateMpSpinner replacing button while API call is in-flightSS-S02/ERR-1, SS-S03/ERR-1, SS-S04/ERR-1
All 3 CTAsError stateError toast with "Something went wrong. Please try again." + retry optionSS-S02/ERR-1, SS-S03/ERR-1, SS-S04/ERR-1
PendingSOWarningModal.vueModal containerv-if="showPendingSOModal"MpModal or equivalent; rendered inside PackageDetails.vuetriggered by useCheckInvoicable() result pending_sos.length > 0SS-S05/AC-1
PendingSOWarningModal.vuePay Existing buttonRedirects to pendingSODetails.checkout_url (from Invoicable API)SS-S05/AC-2
PendingSOWarningModal.vueCreate New buttonDismisses modal; calls useInitiateSelfSubs(flow_type='renewal')SS-S05/AC-3
moderator-be CID Billing SettingsDisableSelfRenewalToggleBoolean toggle; calls PUT /modpanels/subscriptions/:id on changeadmin roleSS-S06/AC-1,AC-2

Deviation note: The Upgrade CTA is intentionally not blocked by disable_self_renewal per PRD SS-S06/AC-1 ("Upgrade Main Package CTA remains available if the client is eligible").

Detail 2.A.1 — TypeScript Contract (frontend layer)

New TypeScript interfaces the implementing agent must declare (in or alongside usePackages.ts and PackageInfoComponent.vue). These are derived from the API contract above and the composable pattern confirmed in usePackages.ts lines 55–73.

// Return type for useInitiateSelfSubs — add to usePackages.ts
type SelfSubsFlowType = 'new_sub' | 'renewal' | 'upgrade'

interface InitiateResult {
checkout_url: string
}

interface UseInitiateSelfSubs {
status: Ref<'idle' | 'pending' | 'resolved' | 'rejected'>
initiate: (flowType: SelfSubsFlowType) => Promise<InitiateResult | null>
error: Ref<string | null>
}

// Return type for useCheckInvoicable — add to usePackages.ts
interface PendingSO {
so_id: string // exact field names pending Open Q1 (Invoicable API contract)
amount: number
checkout_url: string
// additional fields TBD
}

interface InvoicableResult {
pending_sos: PendingSO[]
}

interface UseCheckInvoicable {
status: Ref<'idle' | 'pending' | 'resolved' | 'rejected'>
check: () => Promise<InvoicableResult> // never rejects — returns empty list on failure (fail-safe)
pendingSOs: Ref<PendingSO[]>
}

// Props for PendingSOWarningModal.vue (new component)
interface PendingSOWarningModalProps {
visible: boolean // controls MpModal open state
pendingSOs: PendingSO[] // list from useCheckInvoicable; length > 0 guaranteed when modal shown
}
// Emits: 'close' (modal dismissed), 'pay-existing' (Pay Existing clicked), 'create-new' (Create New clicked)

// New eligibility props added to PackageInfoComponent.vue (extend existing Props interface)
// Existing props: datas: Record<string, unknown>[], isLoading: string
// New props (all boolean, all with default false):
interface PackageInfoEligibilityProps {
isV3SelfSubsEnabled: boolean // billing_version === v3 && self_subs_enabled === true
isSelfRenewalDisabled: boolean // disable_self_renewal === true
}
// The parent (PackageDetails.vue) computes these booleans from billingStore and passes them down.
// isV3SelfSubsEnabled = billingStore.billing.billing_version === 'v3' && billingStore.billing.self_subs_enabled === true
// isSelfRenewalDisabled = billingStore.billing.disable_self_renewal === true

Note on PendingSO field names: so_id, amount, and checkout_url are proposed names matching PRD §9 behavior 3. Exact field names from the Mekari Billing Invoicable API response must be confirmed (Open Q1) before implementing useCheckInvoicable — update the interface once the external contract is received.


Detail 2.B — Data-Fetching Strategy

New API calls use $customFetch from useNuxtApp() — the same pattern used by BillingStore and useSubscriptionPackages. No new HTTP client layer is introduced.

DataSource endpointComposable / storeFetch timing
billing_version, self_subs_enabled, disable_self_renewalGET /api/core/v1/billings/infoBillingStore.getDetail() (existing, extended)On page mount (already fetched by layout)
is_trial, package_statusGET /api/core/v1/reports/billing/summaryuseSubscriptionPackages() (existing)onBeforeMount in PackageDetails.vue
Self-subscription initiationPOST /api/core/v1/self_subscriptionsuseInitiateSelfSubs() (new, in usePackages.ts)On CTA click
Invoicable checkGET /api/core/v1/self_subscriptions/invoicableuseCheckInvoicable() (new, in usePackages.ts)On Renew CTA click only

Detail 2.C — UI State Matrix

StateCTA area (PackageInfoComponent)PendingSOWarningModalDisableSelfRenewalToggle (moderator-be)
Loading (billing info)CTA area hidden / skeleton until BillingStore resolvedn/aSkeleton for toggle area
Loading (CTA click in-flight)MpSpinner on clicked buttonSpinner on Renew CTA during Invoicable check
Empty (non-V3 or flag OFF)No CTAs — page renders identically to todayn/an/a
Error (initiation API fails)Error toast "Something went wrong. Please try again."
Error (Invoicable API fails)Proceed silently to renewal checkout (non-blocking)Modal not shown
Error (toggle save fails)"Could not save setting. Try again." toast; toggle reverted
Success (CTA clicked)Redirect to Mekari Pay
Success (modal shown)Modal with pending SO details + 2 CTAs
Success (toggle saved)Confirmation toast; toggle persisted

Detail 2.D — Scope Boundaries

SystemThis RFC changesUnchanged
hub-serviceGET /billings/info response (extend); PUT /modpanels/subscriptions/:id (extend param); new SelfSubscriptions resource (POST + GET)All other billing endpoints; hub-worker; hub-core
hub-chatBillingStore.BillingInfo interface + DEFAULT_BILLING_INFO; PackageDetails.vue (eligibility prop pass-down); PackageInfoComponent.vue (CTA block); PendingSOWarningModal.vue (new); usePackages.ts (useInitiateSelfSubs, useCheckInvoicable added)SubscriptionsLayout.vue; UsageDetail.vue; all invoice/usage pages; usePackageStore
moderator-beCID Billing Settings view: DisableSelfRenewalToggle (exact path TBD — Open Q6)All other moderator-be functionality
Mekari BillingConsumed read-only as external dependency (no schema changes in Qontak)n/a

3. High-Availability & Security

Performance Requirement

ConstraintSourceHow met
Checkout page load ≤ 2sPRD §6GET /billings/info is already in-flight on page mount; CTA initiation (POST /self_subscriptions) is a lightweight proxy; no new blocking calls added to the critical rendering path
Invoicable list API ≤ 1sPRD §6hub-service proxy adds minimal overhead; Invoicable API timeout must be set (e.g. 800ms) with non-blocking fallback on exceed

Monitoring & Alerting

SignalSourceOwner
self_subs_flow_initiatedFE Mixpanel track (on CTA click, before API call)Bifrost PM
self_subs_payment_completedFE Mixpanel track (on Mekari Pay success redirect)Bifrost PM
self_subs_payment_failedFE Mixpanel track (on Mekari Pay failure redirect)Bifrost PM
self_subs_initiation_failedFE Mixpanel track (on POST /self_subscriptions error)Bifrost Eng
self_subs_pending_so_check_failedFE Mixpanel track (on GET /invoicable failure)Bifrost Eng
self_subs_partnership_toggle_failedBE Rails.logger.error in modpanel endpoint handlerBifrost Eng
self_subs_pending_so_popup_shownFE Mixpanel trackBifrost PM

Logging

hub-service: use existing Rails.logger.info / Rails.logger.error pattern (confirmed in reports/resources/billing.rb). Structured log fields for the new SelfSubscriptions interactor:

LevelEventRequired fields
infoInitiation proxied to Mekari Billingcompany_id, flow_type, mekari_billing_response_ms
infoInvoicable check proxiedcompany_id, pending_so_count (0 on fail-safe path)
errorMekari Billing initiation failedcompany_id, flow_type, http_status, error_message, attempt_count
errorInvoicable API failed / timed outcompany_id, timeout_ms, error_class
errordisable_self_renewal enforcement rejected initiationcompany_id, flow_type, reason: "disable_self_renewal"

hub-chat: use existing $mixpanel.track(...) pattern confirmed in usePackages.ts (lines 257–264) — $mixpanel?.track("event_name", {props}).

Security Implications

RiskMitigation
Unauthorized client initiating a self-sub flow for another CIDPOST /self_subscriptions uses oauth2 :admin, :owner, :supervisor, :agent, :member scope — hub-service derives organization_id from the authenticated token (me.organization_id); never accept company_id from the request body
Partnership CID bypassing disable_self_renewal by calling hub-service directlyhub-service must enforce disable_self_renewal at the API layer (return 422 if disable_self_renewal = true and flow_type != upgrade), not just on the FE
Mekari Billing API credential exposureCredentials stored as hub-service env vars; never sent to FE; proxy pattern keeps them server-side
Invoicable API fail-open designIntentional per PRD (fail-safe non-blocking); risk is minimal since the worst case is a duplicate renewal SO which the client can cancel via Mekari Pay
Modpanel toggle accessible to non-adminsPUT /modpanels/subscriptions/:id scoped to :modpanel only (verified in subscriptions.rb line 38)

Detail 3.A — Failure Mode Catalog

FailureAffected flowSystem behaviorClient experience
GET /billings/info failsAll flowsBillingStore empty → all eligibility flags default falseStandard page renders — no CTAs shown (fail-safe)
POST /self_subscriptions 4xx/5xxNew Sub / Renewal / UpgradeHub-service returns error; FE catchesError toast; CTA re-enabled for retry
Mekari Billing unavailable (timeout)New Sub / Renewal / UpgradeHub-service returns 503Error toast "Could not start. Please try again."
GET /invoicable timeout / 5xxRenewal onlyHub-service returns 200 with pending_sos: [] (non-blocking)Renewal proceeds to checkout as if no pending SO
Mekari Pay payment failureAll flowsMekari Pay redirects back with failure indicatorClient returned to /subscriptions/packages with error toast
SO creation failure after paymentAll flowsMekari Billing's responsibility; not in Qontak scope for Phase 1Client shown error from Mekari Pay redirect; manual reconciliation (Open Q2)
PUT /modpanels/subscriptions/:id failsToggle save422 from hub-serviceAdmin sees "Could not save. Try again."; toggle reverted

Detail 3.B — Error Message Catalog

Error eventUser-facing messageLogged event
POST /self_subscriptions any error (new sub)"Something went wrong. Please try again."self_subs_initiation_failed (flow_type=new_sub)
POST /self_subscriptions any error (renewal)"Something went wrong. Please try again."self_subs_initiation_failed (flow_type=renewal)
POST /self_subscriptions any error (upgrade)"Something went wrong. Please try again."self_subs_initiation_failed (flow_type=upgrade)
Mekari Pay payment failure (any flow)"Payment failed. Please try again."self_subs_payment_failed
GET /invoicable failure(silent — proceed to checkout)self_subs_pending_so_check_failed
Modpanel toggle save failure"Could not save setting. Try again."self_subs_partnership_toggle_failed

Detail 3.C — Accessibility (frontend)

  • All new CTA buttons (MpButton) carry accessible labels from their text content.
  • PendingSOWarningModal.vue must set aria-modal="true" and role="dialog" (verify MpModal default behavior — agent must grep MpModal in hub-chat for existing modal usage pattern).
  • Focus trapping inside the modal on open; return focus to the Renew button on modal dismiss.

Detail 3.D — Non-Functional Specificity (frontend)

AspectTargetSourceNotes
Page load (checkout redirect)≤ 2s from CTA click to Mekari Pay pagePRD §6Satisfied by lightweight proxy; no new blocking render path
Invoicable check latency≤ 1s end-to-endPRD §6hub-service enforces 800ms Invoicable API timeout
Browser supportInherits existing hub-chat matrix (Chrome, Safari, Firefox, Edge — last 2 major versions; iOS Safari 16+)hub-chat package.json browserslist / existing QA baselineNo new APIs used that would require wider support declaration
Bundle size deltaNegligible — no new npm packages; new files are ~3 SFCs + 1 composable extensionAgent should not add new dependencies for this feature
Responsive / mobilehub-chat is desktop-primary; new CTAs use MpButton (pixel3) which is responsive by default@mekari/pixel3 component behaviorNo special mobile breakpoint handling required
i18nEnglish-only for Phase 1 (all new strings are English literals per PRD §8)PRD §8No i18n refactoring required

4. Backwards Compatibility and Rollout Plan

Compatibility

  • GET /billings/info response is additive — new fields billing_version, self_subs_enabled, disable_self_renewal added; no existing fields removed or renamed. All existing callers unaffected.
  • PUT /modpanels/subscriptions/:id change is additive — new optional param disable_self_renewal; existing callers unaffected.
  • DB migration is additive (NOT NULL + default false columns); no data migration required.
  • Non-V3 clients: zero change to /subscriptions/packages rendering. The existing BillingStore.billing.is_trial / status / is_qontak_one_package_ui flags are untouched.

Deploy Order and Cross-Layer Compatibility

Deploy order: hub-service BE first, then hub-chat FE, then moderator-be.

ScenarioFE versionBE versionWorks?Notes
Pre-deploy (baseline)OldOld✓ YesCurrent state
BE firstOld (no CTAs)New (new fields, new endpoints)✓ YesOld FE ignores new /billings/info fields (BillingStore defaults undefined → false). New endpoints are never called. Zero breakage.
FE firstNew (CTAs + BillingStore extension)Old (no new endpoints, no new fields)✓ Yes (safe degradation)New BillingStore fields are undefined → default false → no CTAs shown. New endpoints (POST /self_subscriptions, GET /invoicable) don't exist → if somehow called: 404 → error toast. Feature is effectively dark.
Both deployed (target)NewNew✓ YesFull feature active for enabled CIDs
BE rollbackNew FEOld BE (rolled back)✓ Yes (safe degradation)Same as "FE first" scenario above — CTAs hidden, no data corruption
FE rollbackOld FENew BE✓ YesOld FE never calls new endpoints; new BE fields are ignored

Feature flag coordination: Single flag (self_subs_enabled per-CID in hub-service DB). No separate FE flag needed — the FE reads the value from the BE response at runtime.

Coordinated rollback: BE rollback requires no FE rollback (FE degrades safely). FE rollback requires no BE rollback. Independent. Flag kill-switch (self_subs_enabled = false) takes precedence over code rollback for fastest response.

Rollout Strategy

Carried from PRD §11:

StageAudienceGate
Internal Alpha≤5 Bifrost test CIDs (self_subs_enabled ON via DB)0 P0 bugs; all 3 flows complete end-to-end; SO created successfully
Closed Pilot5–10 V3 CIDs with active subscriptionsSO creation success ≥ 99%; 0 duplicate SO incidents; modal triggers correctly
Staged RolloutAll V3 CIDs in batches (25% → 50% → 100%)SO creation ≥ 99% per batch; no P0/P1 payment incidents
GAAll V3 CIDsStaged gates sustained ≥ 2 consecutive weeks

Feature flag self_subs_enabled defaults to false in the migration. Bifrost team enables per-CID via DB (internal tooling or Modpanel) as the staged rollout progresses.

Detail 4.A — Configuration Contract

ConfigLocationDescriptionDefault
self_subs_enabledhub-service DB (per-CID column)V3 self-service gate per CIDfalse
disable_self_renewalhub-service DB (per-CID column)Admin override to block renewal + new subfalse
Mekari Billing API base URLhub-service env var (e.g. MEKARI_BILLING_BASE_URL)Self-sub API and Invoicable API hostset per env
Mekari Billing API authhub-service env var (e.g. MEKARI_BILLING_API_KEY)API key / OAuth token for Mekari Billingset per env

Exact env var names must be confirmed with the Bifrost infra/devops team — the above are proposed names following the existing pattern for other external service env vars in hub-service.

Detail 4.B — Test Plan (commands the agent will run)

hub-service (RSpec):

# Run new request specs for the self_subscriptions resource
bundle exec rspec spec/services/api/core/v1/self_subscriptions/ --format documentation

# Run existing billings info spec to verify no regression
bundle exec rspec spec/services/api/core/v1/billings/resources/billings_spec.rb --format documentation

# Run existing modpanels subscriptions spec
bundle exec rspec spec/services/api/core/v1/modpanels/ --format documentation

# Full spec suite (CI gate)
bundle exec rspec

hub-chat (vitest + TypeScript):

# Type check (must pass clean)
pnpm type-check

# Lint
pnpm lint

# Unit tests
pnpm test

# Run only subscription-related tests
pnpm test features/subscriptions/

Detail 4.C — Agent Execution Plan

Ordered implementation chunks. Each chunk is independently mergeable and testable.

ChunkLayerFilesActionAcceptance criteria
1 — DB migrationBENew migration fileAdd self_subs_enabled, disable_self_renewal columns (verify table name first via grep)bundle exec rails db:migrate succeeds; rails db:rollback restores; columns exist on correct table
2 — Extend BillingInfoBELocate Interactors::Billings::BillingInfo via grep; extend responseAdd billing_version, self_subs_enabled, disable_self_renewal to responsebundle exec rspec spec/services/api/core/v1/billings/resources/billings_spec.rb — new test for /info response includes all 3 fields
3 — New SelfSubscriptions resourceBEapp/services/api/core/v1/self_subscriptions/resources/self_subscriptions.rb, routes.rb, interactorsPOST /self_subscriptions + GET /invoicable proxy endpointsbundle exec rspec spec/services/api/core/v1/self_subscriptions/ — happy path 200, Mekari Billing 5xx → 503, auth missing → 401
4 — Extend modpanel subscriptionsBEapp/services/api/core/v1/modpanels/resources/subscriptions.rb, interactorAdd disable_self_renewal optional param to PUTbundle exec rspec spec/services/api/core/v1/modpanels/ — toggle ON saves; toggle OFF saves; non-modpanel auth → 401
5 — Extend BillingStoreFEcommon/store/BillingStore.tsAdd billing_version, self_subs_enabled, disable_self_renewal to BillingInfo interface + DEFAULT_BILLING_INFOpnpm type-check — no TypeScript errors
6 — Extend PackageDetails eligibilityFEfeatures/subscriptions/packages/PackageDetails.vueConsume BillingStore fields; compute eligibility booleans; pass as props to PackageInfoComponentpnpm type-check; existing page renders identically for non-V3 client
7 — Extend PackageInfoComponent CTAsFEfeatures/subscriptions/packages/PackageInfoComponent.vue, usePackages.tsAdd 3 CTA buttons with eligibility guards; useInitiateSelfSubs composablepnpm test features/subscriptions/packages/PackageInfoComponent; CTAs hidden for non-V3; CTA click triggers self-checkout call; redirect to payment_link
8 — PendingSOWarningModalDESCOPED 2026-07-01 — pending-SO/Invoicable owned by BI; no modal, no useCheckInvoicable
9 — moderator-be toggleFEmoderator-be app/views/billing/accounts/show.html.erb (Open Q6 resolved)Add DisableSelfRenewalToggle UI; calls PUT /modpanels/subscriptionsToggle ON/OFF saves; error state reverts; success toast

Detail 4.D — Verification & Rollback Recipe

Pre-merge (CI must pass):

# hub-service
bundle exec rspec # 0 failures
bundle exec rubocop # 0 offenses (or existing baseline)

# hub-chat
pnpm type-check # 0 errors
pnpm lint # 0 errors
pnpm test # 0 failures

Post-deploy smoke test (Staging, using a test V3 CID):

  1. Set self_subs_enabled = true for test CID via DB.
  2. Log in as trial V3 client → /subscriptions/packages → "New Subscription" CTA visible → click → verify redirect to Mekari Pay.
  3. Log in as active V3 client → "Renew" CTA visible → click → if test pending SO exists: modal shown with "Pay Existing" + "Create New".
  4. Log in as active V3 client → "Upgrade Main Package" CTA visible → click → verify redirect.
  5. In moderator-be: set disable_self_renewal = ON for test CID → verify Renew + New Sub CTAs hidden, Upgrade CTA still visible.
  6. Non-V3 client → /subscriptions/packages → confirm no CTAs visible (no regression).

Rollback:

# If a P0 is found post-deploy:
# Option A — feature flag (preferred, zero downtime):
# Set self_subs_enabled = false for all affected CIDs via DB
bundle exec rails runner "OrganizationPackage.update_all(self_subs_enabled: false)"

# Option B — DB rollback (last resort):
bundle exec rails db:rollback STEP=1 # reverts the migration

# Option C — code deploy revert:
git revert <merge-commit-sha>
# redeploy

5. Concerns, Questions, or Known Limitations

Status update (2026-07-01): After the contract integration + product direction, only 2 blockers remain: Q11 (renewal billing_type) and Q12 (create-payload sourcing) — both on the Renewal path. Resolved: Q3, Q5, Q6, Q7, Q8, Q1 (Invoicable → BI-owned, SS-S05 descoped), Q4 (return → hub-chat invoices page). Reframed: Q2. Non-blocking: Q9, Q10, Q13.

#TypeQuestionOwnerBlocking?PRD ref
1BLOCKERRESOLVED (descoped)Pending-SO/Invoicable list is owned by the BI team; Qontak builds no Invoicable API and SS-S05 (Pending SO Modal) is descoped. Double-payment handled upstream (Billing external_ref_id + rls_double_payment_validation).BI teamnoPRD OQ1
2BLOCKERReframedReconciliation: SO is now known to be provisioned synchronously at create-time (PI rolls back on SO-create failure → 422). Confirm client-retry UX for a rolled-back create, and recovery if the paid callback misfires.Mekari Billing + Bifrost EngpartialPRD OQ2
3BLOCKERRESOLVED (create)Create contract = POST /api/v4/invoiceables/self-checkout (auth, body, 200/422 documented). Rate limits still unstated.Mekari BillingnoPRD Assumption 4
4BLOCKERRESOLVEDPost-payment return = redirect to the hub-chat invoices page (features/subscriptions/invoices/); no success/cancel param parsing on /subscriptions/packages. (Return-target config mechanism is a minor follow-up — the create contract has no return_url field.)Bifrost FEno
5BLOCKERRESOLVEDPaid callback is MekariPay → Billing (POST /api/v1/customer/payment/receipts); no Qontak webhook in Phase 1.Mekari Billingno
6BLOCKERRESOLVEDmoderator-be path = app/views/billing/accounts/show.html.erbaccounts_controller#update_statususe_cases/accounts/update_account.rb…/update_remaining_balance.rb (verified via recon).moderator-be teamnoPRD §8
7BLOCKERRESOLVEDbilling_version V3 value = '3.0.0' (repo billing_v3? helper, hub_core OrganizationPackage).Bifrost Engno
8OpenRESOLVEDbilling_version lives on organization_packages in hub_core (not hub-service app/models/).Bifrost EngnoPRD A-1
9Open QuestionIs the prorated upgrade amount computed by calendar months remaining or billing cycle months remaining?Mekari Billingno (computed externally)PRD OQ3
10RiskIf Mekari Billing API is unavailable, all self-service flows break with no fallback. No circuit-breaker / graceful degradation in Phase 1.Bifrost Engno (Phase 2)PRD Risk 5
11BLOCKER (new)The contract has no renewal billing_type (upgrade_main/new_subscription etc. only). How is SS-S03 Renewal expressed upstream?Mekari BillingYESA-2
12BLOCKER (new)The create call requires company_record (company/contact name + email), subscribed_packages (product_code, quantity, total_price), billing_period, active_from/end_at. Where does hub-service source this data per CID?Mekari Billing + Bifrost EngYESA-2
13Open Question (new)SO→SI conversion is blocked if active_from > paid_date. Renewal future-dates active_date to the current end — how is the date guard satisfied?Mekari Billingno (affects renewal correctness)SS-S03/AC-2

6. Comment Log

DateAuthorComment
2026-06-24Claude (rfc-starter)Initial draft generated from PRD v1.2. All 6 stories traced. 10 open questions — 7 are blockers that must be resolved before implementation. Mermaid diagrams validated (see §7).
2026-06-24Claude (rfc-reviewer R1 → iterate)Applied 5 fixes from R1 review: (REV-2) deploy order matrix added to §4; (REV-3) TypeScript interfaces for new composables and components added to §2.A.1; (REV-4) proxy timeout/retry column added to §2.4 API table (5s/1-retry for initiation, 800ms/0-retry fail-open for invoicable); (REV-5) structured log fields table added to §3 Logging; (REV-6) NFS table added to §3.D with browser matrix and bundle size note. REV-1 (Mekari Billing contract), REV-7 (migration table), REV-8 (billing_version string) remain as external blockers.
2026-06-30Claude (contract integration)Incorporated the Self-Checkout API Contract v4. Resolved Q3 (create endpoint POST /api/v4/invoiceables/self-checkout), Q5 (no Qontak webhook — Billing receipts webhook), Q6/Q7/Q8 (via repo recon: moderator-be path, V3='3.0.0', organization_packages in hub_core). Reframed Q2 (SO provisioned synchronously at create-time, not post-payment) and §1 overview note + §2.4 contract block. Q1 (no Invoicable list endpoint) and Q4 (browser redirect params) remain blockers. Added Q11 (no renewal billing_type), Q12 (create-payload data sourcing), Q13 (SO→SI date guard vs renewal future-dating). No assumptions introduced — unconfirmed items kept as open questions.
2026-07-01Claude (scope change)Product direction resolved the last two contract gaps: Q4 — post-payment return redirects to the hub-chat invoices page (no param parsing); Q1 — pending-SO/Invoicable list is owned by the BI team, so Qontak builds no Invoicable API and SS-S05 (Pending SO Warning Modal) is descoped (double-payment handled upstream). Updated §1 overview + A-3/A-5, Dependencies, §2.2 Seq-2 note, §2.4 (invoicable row struck, gaps note), §5, §7. 2 blockers remain: Q11, Q12 (Renewal path). Task breakdown updated in parallel (Task 1.2 removed; invoicable dropped from 2.3/2.5; effort 17.5 → 14 md).

7. Ready for Agent Execution

The readiness gate. Check off each item; rfc-reviewer scores this RFC PROCEED / HOLD / BLOCKED.

  • Every PRD story + composite AC id is traced in §1.A (SS-S01 through SS-S06, all AC + ERR ids)
  • Architecture + sequence diagrams cover happy paths (Seq 1, 2, 3) and failure path (Seq 4)
  • Test plan commands are concrete and runnable (§4.B)
  • Rollback recipe is verified (§4.D)
  • BLOCKED on 2 open questions (§5 items 11, 12) before the Renewal path can execute (down from 7 → 4 → 2 after the 2026-06-30 contract integration and 2026-07-01 scope change)

Ready for agent execution: PARTIAL — most of the plan is now unblocked; New Subscription + Upgrade are buildable end-to-end (pending only Q12 payload sourcing). Resolved since R1: Q3 (create contract), Q5 (webhook), Q6 (moderator-be path), Q7 (V3='3.0.0'), Q8 (organization_packages/hub_core), Q1 (Invoicable → BI-owned, SS-S05 descoped), Q4 (return → hub-chat invoices page).

Still blocking (Renewal path only):

  1. Open Q11 — no renewal billing_type in the contract → SS-S03 Renewal cannot be mapped
  2. Open Q12 — sourcing of the create payload (company_record, subscribed_packages, billing_period, dates)

Chunks 1, 2, 4, 5, 6 (DB migration, BillingInfo extension, modpanel toggle BE, BillingStore FE extension, PackageDetails eligibility) can proceed. Chunk 3 (SelfSubscriptions resource) builds the New Subscription + Upgrade create proxy against the confirmed POST /api/v4/invoiceables/self-checkout contract; only the Renewal path (Q11) and payload sourcing (Q12) stay blocked. The Invoicable proxy and Pending SO modal (old chunks) are descoped (BI-owned). Chunk 7 (CTAs) builds the full UI; the renewal initiate stays guarded.

Optional next step: hand this RFC to rfc-reviewer for a second-pass score and readiness assessment.