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 — reasonare 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
| Field | Value | Notes |
|---|---|---|
| Status | IDEA | YAML status: carries the remapped linter enum draft |
| DRI | addo.hernando@mekari.com | Single accountable owner (carried from PRD; reassign to Bifrost eng lead on handoff) |
| Team | bifrost | Carried from source PRD |
| Author(s) | addo.hernando@mekari.com | Initial draft author |
| Reviewers | bifrost-backend, bifrost-frontend, modpanel-team | Tech reviewers across affected layers |
| Approver(s) | bifrost-tech-lead | Tech lead approval |
| Submitted Date | 2026-06-24 | Date RFC opened for discussion |
| Last Updated | 2026-06-24-r2 | Bump on every material edit |
| Target Release | 2026-Q3 | Carried from PRD |
| Target Quarter | 2026-Q3 | Carried from source PRD |
| Delivery | not yet handed to delivery | No delivery/ artifacts exist yet |
| Related | ../prds/self-subs.md | Source PRD (v1.2, 2026-06-23) |
| Discussion | #bifrost-self-subs | Slack channel |
Type: full-stack Frontend sub-type: enhancement Backend sub-type: new-feature
Sections at a Glance
- Overview (incl. Design References — FE half, and PRD-to-Schema Derivation — BE half)
- Technical Design (Repo Reading Guide both layers → topology → ADRs → end-to-end mermaid → DDL → APIs → cross-layer contract)
- High-Availability & Security
- Backwards Compatibility and Rollout Plan
- Concerns, Questions, or Known Limitations
- Comment Log
- 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-chat — New 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 byx-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 realsales_order_id), not after payment; the paid callback then converts SO → Sales Invoice via Billing'sSyncAccountingJob. 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 hostedpayment_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 = OFFsee 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 guardDESCOPED (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_renewalhides 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 = OFForbilling_version ≠ v3,/subscriptions/packagesis byte-identical to the current state.
Out of Scope
Carried from PRD §5 Non-Goals:
- Non-V3 clients — completely hidden, no opt-in.
- New Subscription flow for non-trial V3 clients —
is_trial = truegate is hard. - Add-on package upgrades — main package only.
- Self-service downgrade or contract cancellation.
- Paid SO creation without completed payment.
- 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).
Related Documents
- PRD: bifrost/self-subs/prds/self-subs.md (v1.2, 2026-06-23)
- Initiative README: bifrost/self-subs/README.md
Assumptions
- A-1:
billing_versionis already a persisted field on the organization/subscription model in hub-service (confirmed: thePOST /billings/subscriptionendpoint accepts it as a param —billing_versionis stored, not computed). The implementing agent must locate the exact model and column viagrep -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 aflow_type-keyed/self-subscription. It is keyed bybilling_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, scopepost_invoiceables; idempotency viaexternal_ref_id;sales_order_sourcefor Qontak-family =jurnal_qontak. Flow mapping: New Subscription →new_subscription, Upgrade (main) →upgrade_main. GAP: there is norenewalbilling_type in the contract — SS-S03 Renewal cannot be mapped without Billing confirmation (new Open Q11). GAP: the request body requirescompany_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_ididempotency +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_enabledanddisable_self_renewalare 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 noreturn_urlfield — 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
| Dependency | Owning team | Deliverable needed | Status | Blocking? |
|---|---|---|---|---|
| Mekari Billing — self-checkout create contract | Mekari Billing | Request/response schema; auth; idempotency | RESOLVED (create): POST /api/v4/invoiceables/self-checkout, see contract | partial — see Q11/Q12 |
Mekari Billing — renewal billing_type | Mekari Billing | Which billing_type (or alternate path) represents a renewal | needs confirmation | YES (Open Q11) |
| Mekari Billing — create payload data sourcing | Mekari Billing / Bifrost | Where hub-service gets company_record, subscribed_packages, prices, dates | needs confirmation | YES (Open Q12) |
| Pending-SO / Invoicable list | BI team | Owned by BI; Qontak needs no API — SS-S05 descoped | RESOLVED (descoped) 2026-07-01 | no (Open Q1 closed) |
Mekari Billing — billing_version field values | Mekari Billing | V3 string value | RESOLVED: '3.0.0' (repo billing_v3? helper) | no |
| Post-payment return target | Bifrost FE | Redirect to hub-chat invoices page; no param parsing | RESOLVED 2026-07-01 | no (Open Q4 closed) |
| Mekari Pay → Billing paid callback | Mekari Billing | Webhook ownership | RESOLVED: POST /api/v1/customer/payment/receipts, Billing-side only; no Qontak webhook | no (Open Q5 closed) |
| SO provisioning / reconciliation | Mekari Billing | SO lifecycle vs payment | REFRAMED: SO provisioned synchronously at create-time; date-guard active_from ≤ paid_date (Open Q2/Q13) | YES |
| Prorated upgrade calculation basis | Mekari Billing | Calendar months vs billing-cycle months (PRD OQ3) | needs confirmation | no (PRD §4) |
| moderator-be — CID Billing Settings page path | moderator-be team | Confirm view file path where DisableSelfRenewalToggle lands | needs confirmation | YES (FE chunk 6) |
| Figma frames | Design | Frame links for all 3 flow CTAs and modal (PRD flag: pending) | design in progress | YES (blocks FE visual QA) |
Design References (frontend half — required)
| PRD-named surface | Figma / design link | Frame name | Design system | Design QA contact | Notes |
|---|---|---|---|---|---|
New Subscription / Renew / Upgrade CTAs in PackageInfoComponent | n/a — design pending (PRD §7/§8) | pending | @mekari/pixel3 (verified package.json) | pending — Bifrost design | Reuses MpButton (existing in SubscriptionsLayout); CTA visibility computed from eligibility flags; frame required before FE visual QA |
| Pending SO Warning Modal | n/a — design pending (PRD §8) | pending | same | pending | New component PendingSOWarningModal.vue; reuses MpModal / MpButton pattern from other modals in hub-chat |
| Disable Self-Renewal Toggle in moderator-be | n/a — design pending (PRD §8) | pending | moderator-be component library | pending — moderator-be team | Toggle 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 / rule | Persisted as | Exposed via | Enforced where | Source (PRD §) |
|---|---|---|---|---|
| Billing V3 client check | billing_version on existing subscription/org model (col already exists — A-1); new hub-service response field | GET /api/core/v1/billings/info (extend response) | BE: Interactors::Billings::BillingInfo (extended) | SS-S01, §9 behavior 1 |
| Self-subs feature flag per CID | self_subs_enabled boolean (new col, default false, set by Bifrost team) | GET /api/core/v1/billings/info (extend response) | BE: extend BillingInfo interactor | SS-S01, §6 constraints |
| Disable self-renewal per CID | disable_self_renewal boolean (new col, default false) on CID billing settings model | GET /api/core/v1/billings/info (extend) + PUT /api/core/v1/modpanels/subscriptions/:id (extend params) | BE: modpanel endpoint; FE: CTA visibility check | SS-S06, §8 |
| Self-service flow initiation | not persisted in hub-service — proxy call to Mekari Billing | POST /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 API | GET /api/core/v1/self_subscriptions/invoicable (new) | BE: SelfSubscriptions resource (new) | SS-S05, §9 behavior 3 |
| Observability events | FE Mixpanel tracks (existing $mixpanel plugin in hub-chat) | hub-chat Mixpanel | FE: composable on initiation/error | §12 |
Detail 1.A — PRD Traceability (cross-layer)
Forward (PRD AC → RFC):
| PRD composite AC id | FE section / component | BE section / endpoint |
|---|---|---|
SS-S01/AC-1 | PackageInfoComponent.vue eligibility gate: no CTAs if non-V3 or flag OFF | GET /billings/info extended response (billing_version, self_subs_enabled) |
SS-S01/AC-2 | PackageInfoComponent.vue: CTAs rendered per trial/active state | same endpoint + BillingStore extension |
SS-S01/ERR-1 | Fail-safe: all CTAs hidden if billings/info call fails | BE: 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-2 | Redirect to Mekari Pay; on return fetch billing summary | POST /billings/info refresh on page mount |
SS-S02/AC-3 | CTA hidden when packages.is_trial === false | FE computed eligibility |
SS-S02/ERR-1 | Error toast "Something went wrong. Please try again." | BE 4xx/5xx → FE catch block |
SS-S02/ERR-2 | Client returned to page after Mekari Pay cancel/fail; no CTA state change | FE: Mekari Pay redirect includes status=failed param — FE reads and shows toast |
SS-S03/AC-1 | "Renew" CTA → useCheckInvoicable() → proceed or show modal | GET /api/core/v1/self_subscriptions/invoicable (new) |
SS-S03/AC-2 | active_date = current end date — set by Mekari Billing (not Qontak) | n/a — external |
SS-S03/ERR-1 | Invoicable API fail/timeout → proceed to checkout + log track | BE: 5xx/timeout → FE treats as no pending SO (fail-safe) |
SS-S03/ERR-2 | Mekari Pay failure redirect → toast | FE: redirect param detection |
SS-S04/AC-1 | "Upgrade Main Package" CTA → useInitiateSelfSubs(flow_type='upgrade') | POST /api/core/v1/self_subscriptions |
SS-S04/AC-2 | Immediate package upgrade — set by Mekari Billing | n/a — external |
SS-S04/AC-3 | No Invoicable check for upgrade (hardcoded bypass) | FE: no useCheckInvoicable call in upgrade path |
SS-S04/ERR-1 | Error toast on API failure | BE 4xx/5xx → FE catch |
SS-S04/ERR-2 | Mekari Pay failure/cancel → toast, no SO | FE redirect param |
SS-S05/AC-1 | PendingSOWarningModal.vue shown when Invoicable returns ≥1 pending SO | GET /self_subscriptions/invoicable returns list |
SS-S05/AC-2 | "Pay Existing" → redirect to Mekari Pay for existing SO | FE: 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-1 | Mekari Pay failure for existing SO → return to page | FE: redirect param |
SS-S06/AC-1 | CTAs hidden for CIDs with disable_self_renewal = true | GET /billings/info returns disable_self_renewal; FE BillingStore consumes |
SS-S06/AC-2 | CTAs render normally when toggle OFF | FE computed |
SS-S06/ERR-1 | Toggle save failure → error toast, state reverted | BE 4xx → FE error handler in moderator-be |
Reverse (RFC → PRD AC):
| New / extended FE component or BE artifact | PRD composite AC ids it serves |
|---|---|
BillingStore.BillingInfo — new fields billing_version, self_subs_enabled, disable_self_renewal | SS-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 logic | SS-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 DisableSelfRenewalToggle | SS-S06/AC-1,AC-2,ERR-1 |
UI / Consumer Surface Coverage
| PRD-named surface | Consumer | Required reads (BE) | Required writes (BE) | FE component | Status surface |
|---|---|---|---|---|---|
/subscriptions/packages — CTA area | web (V3 client) | GET /billings/info, GET /reports/billing/summary | POST /self_subscriptions, GET /self_subscriptions/invoicable | PackageInfoComponent.vue | Loading skeleton; error hidden; success: CTAs |
| Pending SO Warning Modal | web (V3 active client during renewal) | GET /self_subscriptions/invoicable | — | PendingSOWarningModal.vue | Loading on Renew CTA; modal on ≥1 pending SO |
| Modpanel CID Billing Settings — Toggle | web (admin) | CID billing settings fetch (existing) | PUT /modpanels/subscriptions/:id | moderator-be DisableSelfRenewalToggle (path TBD) | Skeleton; error toast; success toast |
Role Coverage
| PRD role | Authorization mechanism | Endpoints 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_subscriptions | New Subscription CTA | Mixpanel track |
| Billing V3 Client (active) | same | same + GET /self_subscriptions/invoicable | Renew + Upgrade CTAs | Mixpanel track |
| Qontak Admin | oauth2 :modpanel | PUT /modpanels/subscriptions/:id | DisableSelfRenewalToggle in moderator-be | BE log |
| Non-V3 Client | same client scopes | none of above (gate returns hidden) | no CTAs | — |
PRD Section Coverage
| PRD § | Title | Where covered (RFC) |
|---|---|---|
| HEADER BLOCK | Header | §1 Metadata |
| 3 | One-liner + Problem | §1 Overview |
| 4 | Target Users + Persona | §1 Detail 1.A Role Coverage |
| 5 | Non-Goals | §1 Out of Scope |
| Scope Changes | BE + FE scope | §1 Overview |
| 6 | Constraints | §3 (performance), §4 (feature flag) |
| 7 | Feature Changes (CHG-001) | §2.A UI Contract, §2.4 APIs |
| 8 | New Features (Modal, Toggle) | §2.A, §2.C |
| 9 | API & Webhook Behavior | §2.4 APIs — closes PRD gap |
| 10.1 | System Flow | §2.1 branch flow, §2.2 sequences |
| 10.2 | User Stories | Detail 1.C |
| 11 | Rollout | §4 Rollout Strategy |
| 12 | Observability | §3 Monitoring |
| 13 | Success Metrics | §1 Success Criteria |
| 14 | Launch Plan & Stage Gates | §4 Rollout Strategy |
| 15 | Dependencies | §1 Dependencies |
| 16 | Key Decisions + Alternatives | §2 Technical Decisions (ADRs) |
| 17 | Open Questions | §5 Concerns/Questions |
Detail 1.B — Decisions Closed (cross-layer)
| Decision | Chosen option | Alternatives rejected | Why rejected | Layer |
|---|---|---|---|---|
| Eligibility data source | Extend GET /billings/info response with billing_version, self_subs_enabled, disable_self_renewal; consume via existing BillingStore (ADR-1) | New dedicated eligibility endpoint | BillingStore already calls /billings/info on every page load; extra round-trip for same data; no precedent for a standalone eligibility endpoint in this stack | BE + FE |
Storage of self_subs_enabled / disable_self_renewal | New 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 flags | hub-service is the single source of truth for billing settings; FE stores are ephemeral; Mekari Billing flag delivery latency is uncontrolled | BE |
| Self-subscription proxy architecture | New API::Core::V1::SelfSubscriptions Grape resource namespace in hub-service; proxies Mekari Billing API server-side (ADR-3) | Direct FE call to Mekari Billing | Mekari 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 auth | BE |
disable_self_renewal management endpoint | Extend existing PUT /api/core/v1/modpanels/subscriptions/:moderator_account_id with new disable_self_renewal optional param (ADR-4) | New dedicated endpoint | Existing endpoint already handles CID billing subscription settings with :modpanel scope; extending avoids new route registration and auth wiring | BE |
| Invoicable check: blocking vs. non-blocking | Non-blocking: API failure/timeout → silently proceed to checkout (ADR-5) | Blocking: fail the renewal if Invoicable API is down | PRD §9 behavior 3 explicitly requires fail-safe non-blocking; preventing renewal on Invoicable downtime would degrade the core billing flow | BE + FE |
| FE feature-flag gating | Check 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 gating | self_subs_enabled is a per-CID boolean, not a component code; BillingStore already has is_trial, status, and similar boolean flags from the same endpoint | FE |
| Mekari Pay webhook handling | Out 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 endpoint | PRD §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 id | Story title | Layer scope | FE changes | BE changes | Composite AC ids | RFC anchors |
|---|---|---|---|---|---|---|
SS-S01 | V3 Flow Visibility Gate | FE + BE | BillingStore.BillingInfo +3 fields; PackageDetails.vue passes eligibility props; PackageInfoComponent.vue conditional CTA block | Extend Interactors::Billings::BillingInfo to expose billing_version, self_subs_enabled, disable_self_renewal | SS-S01/AC-1,AC-2,ERR-1 | §2.0 anchors 1–3, §2.4 endpoint 1 |
SS-S02 | New Subscription from Trial | FE + BE | New "New Subscription" button in PackageInfoComponent.vue; useInitiateSelfSubs(flow_type='new_sub') call; loading + error states | POST /api/core/v1/self_subscriptions new endpoint in API::Core::V1::SelfSubscriptions::Resources::SelfSubscriptions | SS-S02/AC-1,AC-2,AC-3,ERR-1,ERR-2 | §2.2 seq 1, §2.4 endpoint 2 |
SS-S03 | Subscription Renewal | FE + BE | New "Renew" button; useCheckInvoicable() call before initiation; loading spinner; useInitiateSelfSubs(flow_type='renewal') | GET /api/core/v1/self_subscriptions/invoicable proxy; POST /self_subscriptions reused | SS-S03/AC-1,AC-2,ERR-1,ERR-2 | §2.2 seq 2, §2.4 endpoints 2–3 |
SS-S04 | Upgrade Main Package | FE + BE | New "Upgrade Main Package" button; useInitiateSelfSubs(flow_type='upgrade') directly (no invoicable check) | POST /self_subscriptions reused with flow_type=upgrade | SS-S04/AC-1,AC-2,AC-3,ERR-1,ERR-2 | §2.2 seq 3, §2.4 endpoint 2 |
SS-S05 | — | Not built (owned by BI; double-payment handled upstream) | Not built | — | descope note §1, §5 Q1 | |
SS-S06 | Disable Self-Renewal for Partnerships | FE + BE (BE + moderator-be) | BillingStore disable_self_renewal field; CTA visibility gate in PackageInfoComponent.vue; DisableSelfRenewalToggle in moderator-be | Extend PUT /modpanels/subscriptions/:id with disable_self_renewal param; new column + migration | SS-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
| Layer | Path | Why the agent reads it | What pattern it teaches |
|---|---|---|---|
| BE | app/services/api/core/v1/billings/resources/billings.rb | The resource to extend for GET /billings/info | Grape resource pattern: oauth2 :admin, ..., get '/info', params do, interactor invocation + present(response:) error 422 |
| BE | app/services/api/core/v1/modpanels/resources/subscriptions.rb | The resource to extend with disable_self_renewal | Same pattern; :modpanel scope; PUT '/:moderator_account_id'; optional :field, type: extension |
| BE | app/services/api/core/v1/billings/routes.rb | Route mounting pattern to replicate for self_subscriptions/routes.rb | mount API::Core::V1::Billings::Resources::* |
| BE | app/services/api/core/v1/modpanels/routes.rb | Route mounting to confirm where SelfSubscriptions routes are registered | mount ...Routes pattern; mount location for new resource |
| BE | spec/services/api/core/v1/billings/resources/billings_spec.rb | The RSpec request spec pattern for new specs | RSpec.describe '...', type: :request; let(:organization), stub_auth, get '/path', headers:, expect(response_as_json) |
| FE | common/store/BillingStore.ts | BillingInfo interface to extend; $customFetch pattern; store action shape | $customFetch('/api/core/v1/billings/info') → {data: BillingInfo}; Pinia defineStore("billing", () => {...}) |
| FE | features/subscriptions/packages/PackageDetails.vue | The parent that orchestrates PackageInfoComponent and useSummaryQuotaInfo | useSubscriptionPackages() + useSummaryQuotaInfo() on onBeforeMount; computed props passed to child |
| FE | features/subscriptions/packages/PackageInfoComponent.vue | The component to extend with CTAs | MpFlex/MpText/MpBadge/MpSpinner from @mekari/pixel3; props pattern; loading skeleton |
| FE | features/subscriptions/composables/usePackages.ts | The composable to extend with useInitiateSelfSubs and useCheckInvoicable | $customFetch + ref<T> state + status enum pattern; useNuxtApp() access |
| FE | features/subscriptions/SubscriptionsLayout.vue | Existing 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)
| Layer | Anchor / pattern / contract | Verified by | Evidence |
|---|---|---|---|
| BE | GET /billings/info exists, scoped to client roles | read | billings.rb line 607–629: oauth2 :admin, :owner, :supervisor, :agent, :member get '/info' do; interactor = Interactors::Billings::BillingInfo |
| BE | billing_version is an accepted param on subscription creation | read | billings.rb line 47: optional :billing_version, type: String in POST /subscription params → field exists in hub-service |
| BE | PUT /modpanels/subscriptions/:moderator_account_id exists with optional params pattern | read | modpanels/resources/subscriptions.rb lines 13–37: multiple optional :field params; oauth2 :modpanel; put '/:moderator_account_id' |
| BE | Grape mount pattern for new resource | read | billings/routes.rb: mount API::Core::V1::Billings::Resources::Billings |
| BE | RSpec request spec pattern | read | billings_spec.rb: RSpec.describe '...', type: :request; stub_auth; get '/path', headers:; response_as_json helper |
| BE | Interactors pattern (class + class methods) | read | billings.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" . |
| FE | BillingStore calls /api/core/v1/billings/info and exposes BillingInfo | read | BillingStore.ts lines 134–136: $customFetch('/api/core/v1/billings/info').then(r => { const {data} = r; billing.value = data }) |
| FE | BillingInfo interface does NOT currently have billing_version, self_subs_enabled, disable_self_renewal | read | BillingStore.ts lines 4–18: interface BillingInfo listing — none of the three fields present |
| FE | is_trial already in BillingInfo | read | BillingStore.ts line 6: is_trial: boolean |
| FE | useSubscriptionPackages composable fetches GET /api/core/v1/reports/billing/summary | read | usePackages.ts lines 59–73: $customFetch('/api/core/v1/reports/billing/summary') → PackagesType { is_trial, package_status, ... } |
| FE | PackageInfoComponent.vue currently has no CTAs | read | PackageInfoComponent.vue lines 1–98: renders only package name badge and renewal date; no buttons |
| FE | PackageDetails.vue orchestrates PackageInfoComponent and UsageDetail | read | PackageDetails.vue lines 1–10: <PackageInfoComponent :datas="packageInfo" :isLoading="status" />; <UsageDetail .../> |
| FE | $customFetch is the HTTP client pattern | read | BillingStore.ts line 134: const { $customFetch } = useNuxtApp() |
| FE | SubscriptionsLayout.vue uses billingStore.billing.is_trial, billing_enabled, etc. | read | SubscriptionsLayout.vue lines 128–135: billing.value.billing_enabled && billing.value.payment_type?.toLowerCase() === 'prepaid' && !billing.value.is_trial && billing.value.status === 'active' |
| FE | Feature flag check pattern in SubscriptionsLayout | read | SubscriptionsLayout.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.* |
| FE | Test command | read | package.json scripts: test: 'vitest --dom --pool=forks'; type-check: 'vue-tsc --noEmit'; lint: 'pnpm lint:js && pnpm lint:prettier' |
Patterns to Follow
| Layer | Concern | Pattern in repo | Reference file | Deviation? |
|---|---|---|---|---|
| BE | Grape resource endpoint | oauth2 :scope, params do, interactor invoke, present(response:), 422 error | billings.rb, subscriptions.rb | none |
| BE | New Grape resource namespace | module API::Core::V1::X::Resources::X < API::Core::V1::ApplicationResource + routes.rb | billings/routes.rb | none — replicate for self_subscriptions/ |
| BE | Optional param extension | optional :field, type: Boolean in existing params do block | subscriptions.rb lines 13–37 | none |
| BE | RSpec request spec | describe '...', type: :request; let(:organization), stub_auth, get/post/put, expect(response_as_json) | billings_spec.rb | none |
| FE | BillingStore field extension | add field to BillingInfo interface + DEFAULT_BILLING_INFO constant | BillingStore.ts | none |
| FE | New composable in usePackages.ts | export `const useX = (): UseX => { const status = ref<'idle' | 'pending' | 'resolved' |
| FE | Conditional rendering in component | v-if="eligibilityFlag" on button elements | SubscriptionsLayout.vue handleShowingTopupButton | none |
| FE | Loading state | MpSpinner inside v-if="isLoading === 'pending'" block | PackageInfoComponent.vue lines 71–87 | none |
| FE | Error / toast | useNuxtApp().$toast?.error(...) or equivalent toast pattern used in hub-chat | search features/ for toast usage (agent must verify exact import) | none |
| FE | Modal | Verify MpModal usage in another hub-chat feature for correct props/slots pattern (agent must grep MpModal in features/) | pending verification | none |
Reading Order for the Agent
app/services/api/core/v1/billings/resources/billings.rb— GET /info handler shape to extend.app/services/api/core/v1/modpanels/resources/subscriptions.rb— PUT handler shape to extend.grep -rn "class.*BillingInfo" .then read the located interactor — understand what fields the BillingInfo interactor returns and how to add new ones.app/services/api/core/v1/billings/routes.rb— mount pattern for newself_subscriptions/routes.rb.common/store/BillingStore.ts—BillingInfointerface +$customFetchcall to extend.features/subscriptions/packages/PackageDetails.vue— orchestration layer; where eligibility props are computed and passed down.features/subscriptions/packages/PackageInfoComponent.vue— the component to extend with CTAs.features/subscriptions/composables/usePackages.ts— the composable to extend withuseInitiateSelfSubsanduseCheckInvoicable.features/subscriptions/SubscriptionsLayout.vue— existing feature-flag conditional rendering pattern.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
| Service | Use case in this RFC | Third-party connection |
|---|---|---|
| hub-service Billings resource | Extend GET /info to return V3 gate fields | PostgreSQL read |
| hub-service SelfSubscriptions resource | Proxy flow initiation + Invoicable check to Mekari Billing | Mekari Billing API (HTTP) |
| hub-service Modpanels::Subscriptions resource | Persist disable_self_renewal per CID | PostgreSQL write |
| Mekari Billing API | Accepts flow initiation, returns checkout_url; runs Invoicable check | Mekari Pay |
| moderator-be | Admin UI for DisableSelfRenewalToggle | Kong → 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 isPOST /api/v4/invoiceables/self-checkout(keyed bybilling_type), the SO is provisioned synchronously at create-time (returnssales_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. Therenewalcreate 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/andgrep -rn "self_subs_enabled" app/models/to identify the exact model and table for these columns. The expectation (A-1, A-4) is:
billing_versionalready exists on some model (confirm col name and table).self_subs_enabledanddisable_self_renewaldo 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. TheNOT NULL + default falsepattern 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 · Path | Status | Change | Auth scope | Request | Response / Error | Proxy timeout / retry |
|---|---|---|---|---|---|---|
GET /api/core/v1/billings/info | extend | Add 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 unchanged | n/a (DB read only) |
POST /api/core/v1/self_subscriptions | new | Flow initiation proxy → Mekari Billing POST /api/v4/invoiceables/self-checkout | :admin, :owner, :supervisor, :agent, :member | FE → 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 unavailable | 5 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/invoicable | ❌ descoped 2026-07-01 | Pending-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_id | extend | Add disable_self_renewal optional param | :modpanel (unchanged) | {disable_self_renewal: true | false, ...existing params} | 200 success; 422 validation error | n/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.
| Property | Value |
|---|---|
| Method · Path | POST /api/v4/invoiceables/self-checkout (Mekari Billing) |
| Auth | Bearer Mekari access token; scope post_invoiceables (401 if missing/invalid) |
| Idempotency | external_ref_id (unique; duplicate → 422 external_ref_id has already been taken) |
| Required body | billing_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 / optional | active_from, end_at (strict YYYY-MM-DD), net_amount (IDR int), discount_percentage per line |
billing_type enum | new_subscription, legacy_subscription, free_subscription, non_recurring, upgrade_main, upgrade_additional — no renewal (Open Q11) |
| Success 200 | {status:"OK", message, invoiceable_id, payment_link} |
| Errors 422 | errors:[{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
renewalbilling_typeexists; 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_frommust be ≤paid_dateto 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.vuerows 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.
| Surface | Element | Behavior | Eligibility guard | AC |
|---|---|---|---|---|
PackageInfoComponent.vue | New Subscription button | v-if="isTrial && selfSubsEnabled && !disableSelfRenewal" | billing_version=v3, self_subs_enabled, is_trial=true, disable_self_renewal=false | SS-S02/AC-1,AC-3 |
PackageInfoComponent.vue | Renew button | v-if="!isTrial && isActive && selfSubsEnabled && !disableSelfRenewal" | billing_version=v3, self_subs_enabled, is_trial=false, package_status=active, disable_self_renewal=false | SS-S03/AC-1 |
PackageInfoComponent.vue | Upgrade Main Package button | v-if="!isTrial && isActive && selfSubsEnabled" (not blocked by disable_self_renewal per PRD SS-S06/AC-1) | billing_version=v3, self_subs_enabled, active | SS-S04/AC-1 |
| All 3 CTAs | Loading state | MpSpinner replacing button while API call is in-flight | — | SS-S02/ERR-1, SS-S03/ERR-1, SS-S04/ERR-1 |
| All 3 CTAs | Error state | Error toast with "Something went wrong. Please try again." + retry option | — | SS-S02/ERR-1, SS-S03/ERR-1, SS-S04/ERR-1 |
PendingSOWarningModal.vue | Modal container | v-if="showPendingSOModal" — MpModal or equivalent; rendered inside PackageDetails.vue | triggered by useCheckInvoicable() result pending_sos.length > 0 | SS-S05/AC-1 |
PendingSOWarningModal.vue | Pay Existing button | Redirects to pendingSODetails.checkout_url (from Invoicable API) | — | SS-S05/AC-2 |
PendingSOWarningModal.vue | Create New button | Dismisses modal; calls useInitiateSelfSubs(flow_type='renewal') | — | SS-S05/AC-3 |
| moderator-be CID Billing Settings | DisableSelfRenewalToggle | Boolean toggle; calls PUT /modpanels/subscriptions/:id on change | admin role | SS-S06/AC-1,AC-2 |
Deviation note: The Upgrade CTA is intentionally not blocked by
disable_self_renewalper 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, andcheckout_urlare proposed names matching PRD §9 behavior 3. Exact field names from the Mekari Billing Invoicable API response must be confirmed (Open Q1) before implementinguseCheckInvoicable— 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.
| Data | Source endpoint | Composable / store | Fetch timing |
|---|---|---|---|
billing_version, self_subs_enabled, disable_self_renewal | GET /api/core/v1/billings/info | BillingStore.getDetail() (existing, extended) | On page mount (already fetched by layout) |
is_trial, package_status | GET /api/core/v1/reports/billing/summary | useSubscriptionPackages() (existing) | onBeforeMount in PackageDetails.vue |
| Self-subscription initiation | POST /api/core/v1/self_subscriptions | useInitiateSelfSubs() (new, in usePackages.ts) | On CTA click |
| Invoicable check | GET /api/core/v1/self_subscriptions/invoicable | useCheckInvoicable() (new, in usePackages.ts) | On Renew CTA click only |
Detail 2.C — UI State Matrix
| State | CTA area (PackageInfoComponent) | PendingSOWarningModal | DisableSelfRenewalToggle (moderator-be) |
|---|---|---|---|
| Loading (billing info) | CTA area hidden / skeleton until BillingStore resolved | n/a | Skeleton for toggle area |
| Loading (CTA click in-flight) | MpSpinner on clicked button | Spinner on Renew CTA during Invoicable check | — |
| Empty (non-V3 or flag OFF) | No CTAs — page renders identically to today | n/a | n/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
| System | This RFC changes | Unchanged |
|---|---|---|
| hub-service | GET /billings/info response (extend); PUT /modpanels/subscriptions/:id (extend param); new SelfSubscriptions resource (POST + GET) | All other billing endpoints; hub-worker; hub-core |
| hub-chat | BillingStore.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-be | CID Billing Settings view: DisableSelfRenewalToggle (exact path TBD — Open Q6) | All other moderator-be functionality |
| Mekari Billing | Consumed read-only as external dependency (no schema changes in Qontak) | n/a |
3. High-Availability & Security
Performance Requirement
| Constraint | Source | How met |
|---|---|---|
| Checkout page load ≤ 2s | PRD §6 | GET /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 ≤ 1s | PRD §6 | hub-service proxy adds minimal overhead; Invoicable API timeout must be set (e.g. 800ms) with non-blocking fallback on exceed |
Monitoring & Alerting
| Signal | Source | Owner |
|---|---|---|
self_subs_flow_initiated | FE Mixpanel track (on CTA click, before API call) | Bifrost PM |
self_subs_payment_completed | FE Mixpanel track (on Mekari Pay success redirect) | Bifrost PM |
self_subs_payment_failed | FE Mixpanel track (on Mekari Pay failure redirect) | Bifrost PM |
self_subs_initiation_failed | FE Mixpanel track (on POST /self_subscriptions error) | Bifrost Eng |
self_subs_pending_so_check_failed | FE Mixpanel track (on GET /invoicable failure) | Bifrost Eng |
self_subs_partnership_toggle_failed | BE Rails.logger.error in modpanel endpoint handler | Bifrost Eng |
self_subs_pending_so_popup_shown | FE Mixpanel track | Bifrost 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:
| Level | Event | Required fields |
|---|---|---|
info | Initiation proxied to Mekari Billing | company_id, flow_type, mekari_billing_response_ms |
info | Invoicable check proxied | company_id, pending_so_count (0 on fail-safe path) |
error | Mekari Billing initiation failed | company_id, flow_type, http_status, error_message, attempt_count |
error | Invoicable API failed / timed out | company_id, timeout_ms, error_class |
error | disable_self_renewal enforcement rejected initiation | company_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
| Risk | Mitigation |
|---|---|
| Unauthorized client initiating a self-sub flow for another CID | POST /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 directly | hub-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 exposure | Credentials stored as hub-service env vars; never sent to FE; proxy pattern keeps them server-side |
| Invoicable API fail-open design | Intentional 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-admins | PUT /modpanels/subscriptions/:id scoped to :modpanel only (verified in subscriptions.rb line 38) |
Detail 3.A — Failure Mode Catalog
| Failure | Affected flow | System behavior | Client experience |
|---|---|---|---|
GET /billings/info fails | All flows | BillingStore empty → all eligibility flags default false | Standard page renders — no CTAs shown (fail-safe) |
POST /self_subscriptions 4xx/5xx | New Sub / Renewal / Upgrade | Hub-service returns error; FE catches | Error toast; CTA re-enabled for retry |
| Mekari Billing unavailable (timeout) | New Sub / Renewal / Upgrade | Hub-service returns 503 | Error toast "Could not start. Please try again." |
GET /invoicable timeout / 5xx | Renewal only | Hub-service returns 200 with pending_sos: [] (non-blocking) | Renewal proceeds to checkout as if no pending SO |
| Mekari Pay payment failure | All flows | Mekari Pay redirects back with failure indicator | Client returned to /subscriptions/packages with error toast |
| SO creation failure after payment | All flows | Mekari Billing's responsibility; not in Qontak scope for Phase 1 | Client shown error from Mekari Pay redirect; manual reconciliation (Open Q2) |
PUT /modpanels/subscriptions/:id fails | Toggle save | 422 from hub-service | Admin sees "Could not save. Try again."; toggle reverted |
Detail 3.B — Error Message Catalog
| Error event | User-facing message | Logged 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.vuemust setaria-modal="true"androle="dialog"(verifyMpModaldefault behavior — agent must grepMpModalin 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)
| Aspect | Target | Source | Notes |
|---|---|---|---|
| Page load (checkout redirect) | ≤ 2s from CTA click to Mekari Pay page | PRD §6 | Satisfied by lightweight proxy; no new blocking render path |
| Invoicable check latency | ≤ 1s end-to-end | PRD §6 | hub-service enforces 800ms Invoicable API timeout |
| Browser support | Inherits existing hub-chat matrix (Chrome, Safari, Firefox, Edge — last 2 major versions; iOS Safari 16+) | hub-chat package.json browserslist / existing QA baseline | No new APIs used that would require wider support declaration |
| Bundle size delta | Negligible — no new npm packages; new files are ~3 SFCs + 1 composable extension | — | Agent should not add new dependencies for this feature |
| Responsive / mobile | hub-chat is desktop-primary; new CTAs use MpButton (pixel3) which is responsive by default | @mekari/pixel3 component behavior | No special mobile breakpoint handling required |
| i18n | English-only for Phase 1 (all new strings are English literals per PRD §8) | PRD §8 | No i18n refactoring required |
4. Backwards Compatibility and Rollout Plan
Compatibility
GET /billings/inforesponse is additive — new fieldsbilling_version,self_subs_enabled,disable_self_renewaladded; no existing fields removed or renamed. All existing callers unaffected.PUT /modpanels/subscriptions/:idchange is additive — new optional paramdisable_self_renewal; existing callers unaffected.- DB migration is additive (
NOT NULL + default falsecolumns); no data migration required. - Non-V3 clients: zero change to
/subscriptions/packagesrendering. The existingBillingStore.billing.is_trial/status/is_qontak_one_package_uiflags are untouched.
Deploy Order and Cross-Layer Compatibility
Deploy order: hub-service BE first, then hub-chat FE, then moderator-be.
| Scenario | FE version | BE version | Works? | Notes |
|---|---|---|---|---|
| Pre-deploy (baseline) | Old | Old | ✓ Yes | Current state |
| BE first | Old (no CTAs) | New (new fields, new endpoints) | ✓ Yes | Old FE ignores new /billings/info fields (BillingStore defaults undefined → false). New endpoints are never called. Zero breakage. |
| FE first | New (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) | New | New | ✓ Yes | Full feature active for enabled CIDs |
| BE rollback | New FE | Old BE (rolled back) | ✓ Yes (safe degradation) | Same as "FE first" scenario above — CTAs hidden, no data corruption |
| FE rollback | Old FE | New BE | ✓ Yes | Old 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:
| Stage | Audience | Gate |
|---|---|---|
| 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 Pilot | 5–10 V3 CIDs with active subscriptions | SO creation success ≥ 99%; 0 duplicate SO incidents; modal triggers correctly |
| Staged Rollout | All V3 CIDs in batches (25% → 50% → 100%) | SO creation ≥ 99% per batch; no P0/P1 payment incidents |
| GA | All V3 CIDs | Staged 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
| Config | Location | Description | Default |
|---|---|---|---|
self_subs_enabled | hub-service DB (per-CID column) | V3 self-service gate per CID | false |
disable_self_renewal | hub-service DB (per-CID column) | Admin override to block renewal + new sub | false |
| Mekari Billing API base URL | hub-service env var (e.g. MEKARI_BILLING_BASE_URL) | Self-sub API and Invoicable API host | set per env |
| Mekari Billing API auth | hub-service env var (e.g. MEKARI_BILLING_API_KEY) | API key / OAuth token for Mekari Billing | set 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.
| Chunk | Layer | Files | Action | Acceptance criteria |
|---|---|---|---|---|
| 1 — DB migration | BE | New migration file | Add 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 BillingInfo | BE | Locate Interactors::Billings::BillingInfo via grep; extend response | Add billing_version, self_subs_enabled, disable_self_renewal to response | bundle exec rspec spec/services/api/core/v1/billings/resources/billings_spec.rb — new test for /info response includes all 3 fields |
| 3 — New SelfSubscriptions resource | BE | app/services/api/core/v1/self_subscriptions/resources/self_subscriptions.rb, routes.rb, interactors | POST /self_subscriptions + GET /invoicable proxy endpoints | bundle exec rspec spec/services/api/core/v1/self_subscriptions/ — happy path 200, Mekari Billing 5xx → 503, auth missing → 401 |
| 4 — Extend modpanel subscriptions | BE | app/services/api/core/v1/modpanels/resources/subscriptions.rb, interactor | Add disable_self_renewal optional param to PUT | bundle exec rspec spec/services/api/core/v1/modpanels/ — toggle ON saves; toggle OFF saves; non-modpanel auth → 401 |
| 5 — Extend BillingStore | FE | common/store/BillingStore.ts | Add billing_version, self_subs_enabled, disable_self_renewal to BillingInfo interface + DEFAULT_BILLING_INFO | pnpm type-check — no TypeScript errors |
| 6 — Extend PackageDetails eligibility | FE | features/subscriptions/packages/PackageDetails.vue | Consume BillingStore fields; compute eligibility booleans; pass as props to PackageInfoComponent | pnpm type-check; existing page renders identically for non-V3 client |
| 7 — Extend PackageInfoComponent CTAs | FE | features/subscriptions/packages/PackageInfoComponent.vue, usePackages.ts | Add 3 CTA buttons with eligibility guards; useInitiateSelfSubs composable | pnpm test features/subscriptions/packages/PackageInfoComponent; CTAs hidden for non-V3; CTA click triggers self-checkout call; redirect to payment_link |
| — | — | DESCOPED 2026-07-01 — pending-SO/Invoicable owned by BI; no modal, no useCheckInvoicable | — | |
| 9 — moderator-be toggle | FE | moderator-be app/views/billing/accounts/show.html.erb (Open Q6 resolved) | Add DisableSelfRenewalToggle UI; calls PUT /modpanels/subscriptions | Toggle 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):
- Set
self_subs_enabled = truefor test CID via DB. - Log in as trial V3 client →
/subscriptions/packages→ "New Subscription" CTA visible → click → verify redirect to Mekari Pay. - Log in as active V3 client → "Renew" CTA visible → click → if test pending SO exists: modal shown with "Pay Existing" + "Create New".
- Log in as active V3 client → "Upgrade Main Package" CTA visible → click → verify redirect.
- In moderator-be: set
disable_self_renewal = ONfor test CID → verify Renew + New Sub CTAs hidden, Upgrade CTA still visible. - 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.
| # | Type | Question | Owner | Blocking? | PRD ref |
|---|---|---|---|---|---|
| 1 | 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 team | no | PRD OQ1 | |
| 2 | Reconciliation: 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 Eng | partial | PRD OQ2 | |
| 3 | Create contract = POST /api/v4/invoiceables/self-checkout (auth, body, 200/422 documented). Rate limits still unstated. | Mekari Billing | no | PRD Assumption 4 | |
| 4 | Post-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 FE | no | — | |
| 5 | Paid callback is MekariPay → Billing (POST /api/v1/customer/payment/receipts); no Qontak webhook in Phase 1. | Mekari Billing | no | — | |
| 6 | moderator-be path = app/views/billing/accounts/show.html.erb → accounts_controller#update_status → use_cases/accounts/update_account.rb → …/update_remaining_balance.rb (verified via recon). | moderator-be team | no | PRD §8 | |
| 7 | billing_version V3 value = '3.0.0' (repo billing_v3? helper, hub_core OrganizationPackage). | Bifrost Eng | no | — | |
| 8 | billing_version lives on organization_packages in hub_core (not hub-service app/models/). | Bifrost Eng | no | PRD A-1 | |
| 9 | Open Question | Is the prorated upgrade amount computed by calendar months remaining or billing cycle months remaining? | Mekari Billing | no (computed externally) | PRD OQ3 |
| 10 | Risk | If Mekari Billing API is unavailable, all self-service flows break with no fallback. No circuit-breaker / graceful degradation in Phase 1. | Bifrost Eng | no (Phase 2) | PRD Risk 5 |
| 11 | BLOCKER (new) | The contract has no renewal billing_type (upgrade_main/new_subscription etc. only). How is SS-S03 Renewal expressed upstream? | Mekari Billing | YES | A-2 |
| 12 | BLOCKER (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 Eng | YES | A-2 |
| 13 | Open 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 Billing | no (affects renewal correctness) | SS-S03/AC-2 |
6. Comment Log
| Date | Author | Comment |
|---|---|---|
| 2026-06-24 | Claude (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-24 | Claude (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-30 | Claude (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-01 | Claude (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):
- Open Q11 — no
renewalbilling_typein the contract → SS-S03 Renewal cannot be mapped - 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-reviewerfor a second-pass score and readiness assessment.