Qontak One | Billing | Integrate User Quota to Quota Management API
Product Requirements Document · NEW PRD v1.0
HEADER BLOCK
| Field | Value |
|---|---|
| PM | Addo Hernando |
| PRD Version | 1.2 |
| Status | DRAFT |
| PRD Type | NEW |
| Epic | BIF-8713 |
| Squad | Bifrost |
| RFC Link | TBD — pending PRD approval |
| Figma Master | N/A — no new UI screens |
| Anchor | No — standalone, single-squad |
| Labels | epic:qontak-one | module:billing | feature:user-quota-integration |
| Last Updated | 2026-06-23 |
Table of Contents
- HEADER BLOCK
- 3. One-liner + Problem
- 4. Target Users + Persona Context
- 5. Non-Goals
- Scope Changes
- 6. Constraints
- 7. API & Webhook Behavior
- 8. System Flow + User Stories + ACs
- 9. Rollout
- 10. Observability
- 11. Success Metrics
- 12. Launch Plan & Stage Gates
- 13. Dependencies
- 14. Key Decisions + Alternatives Rejected
- 15. Open Questions
- PRD CHANGELOG
3. One-liner + Problem
One-liner: Integrate Launchpad's user management with the centralized Quota Management API to enforce real-time quota checks, seat deductions, and refunds for all Qontak One clients globally.
Problem: Today, user seat limits in Launchpad are not connected to any centralized quota enforcement system, meaning a client admin can invite users beyond their purchased seat count without any real-time check or block. With the transition to Qontak One, quota tracking must be standardized across the platform to ensure a single source of truth for user seat consumption and prevent revenue leakage. Without this integration, there is no idempotent transaction tracking for user seat events, no automated balance recovery when users are deleted, and no mechanism to keep consultant provisioning from consuming client-paid quota.
Existing quota logic: Launchpad already has a
validate_user_quota.gobilling service that calls the ModpanelGetUnifiedBillingAPI to check seat counts before user creation. This integration replaces that Modpanel-based check with direct calls to the centralized Quota Management API inqontak-billing, giving Bifrost a single authoritative ledger for all quota transactions.
4. Target Users + Persona Context
| Persona | Role | Goal | Pain | Workaround |
|---|---|---|---|---|
| Primary — Qontak One Client Admin | Admin user of a Qontak One account managing team members | Invite users up to their purchased seat limit and have the system block attempts beyond that limit | No real-time enforcement exists — can invite users beyond the purchased quota, leading to billing discrepancies discovered only after the fact | Manually tracks seat count against the account's plan limit; contacts support when discrepancies appear |
| Secondary — Activation Team Member | Internal Mekari staff provisioning Consultant Mode access for clients via Modpanel | Access a client's Qontak One account for setup without consuming the client's purchased user seats | Consultant Mode accounts currently have no explicit bypass mechanism — provisioning them risks counting against the client's seat quota | Communicates manually with the client to track and reconcile any over-counted consultant seats |
(See Constraints for plan availability and feature flag scope.)
5. Non-Goals
- This does not expose the quota balance or remaining seat count in the Launchpad client-facing UI — balance visibility is owned by the Modpanel/billing layer.
- This does not enforce quota checks for any resource other than user seats (e.g. WhatsApp channel quota, message quota, storage quota are out of scope).
- This does not block an admin's access to the invite form before the check — the check happens at creation time, not at form load.
- This does not implement real-time push notifications or emails to admins when quota is approaching the limit or has been exceeded.
- This does not handle automatic quota top-up or upgrade prompts beyond displaying a "Quota Exceeded / Upgrade Required" message on block.
- This does not retroactively reconcile user counts for CIDs that were over-provisioned before this integration goes live — the backfill script only syncs accurate current usage forward into the quota system.
- This does not apply to non-Qontak One plans — standard Qontak clients are unaffected.
Scope Changes
- Backend — Launchpad user creation flow (pre-creation quota check + post-creation deduction), user deletion flow (refund call), Consultant Mode bypass logic, background retry worker for deduction failures, and one-time backfill rake task
6. Constraints
| Field | Value |
|---|---|
| Platform | Backend only — no new UI screens |
| Tech stack | Launchpad: Go 1.23+, PostgreSQL via SQLC, Redis, gocraft/work job queue. Quota Management API: Go 1.23+, same stack in qontak-billing. Moderator-be (Modpanel): Ruby 3.3 / Rails 7.2. |
| Performance | Quota check and deduction calls must not block user creation UX for > 500ms (engineering to validate in RFC) |
| Concurrency | Launchpad already serialises concurrent invitations for the same CID via Redis pessimistic lock lock:user_invite:{company_id} (10 s TTL) — quota check will execute inside this lock |
| Data limits | unique_code for deduction: create_user_{user_id}; for refund: delete_user_{user_id} — idempotency enforced by Quota Management API via billing_unique_logs table |
| Plan scope | Qontak One clients only — not applicable to standard Qontak plans |
| Feature flag | user_quota_integration_enabled | default: OFF. Enabled per CID via qontak-preferences service; checked using IsEnabledFunc(ctx, { FeatureName, UniqueID: companySsoID }) before any quota API interaction. |
| Authentication | Quota Management API — check and deduction calls require Bearer JWT token + X-Api-Key header. Refund calls require X-Api-Key header only. Launchpad must be provisioned with a service account JWT and internal API key. |
| Read/write | System calls Check API before creation. System calls Deduction API on successful creation. System calls Refund API on deletion. Background gocraft/work job retries failed deductions. Clients cannot directly call Quota Management API. |
| Existing quota validation | internal/app/service/billings/validate_user_quota.go in Launchpad (Modpanel-backed check) must be replaced or bypassed for Qontak One CIDs once this integration is live. Both must not run simultaneously for the same CID. |
7. API & Webhook Behavior
Quota Management API base path:
POST /iag/v1/quota-managements/{operation}All three endpoints are inqontak-billing.billing_code,deduction_code, andrefund_codevalues specific to user seats are TBD — see Open Questions §15 items 7–9.
Behavior 1: Check Quota Balance before user creation
| Field | Detail |
|---|---|
| Entity affected | Quota balance record for a Qontak One company |
| Triggered by | Client admin initiates a user invitation in Launchpad (POST /iag/v1/users/sso_invite) — inside the existing Redis lock lock:user_invite:{company_id} |
| Endpoint | POST /iag/v1/quota-managements/check-quota |
| Auth | Bearer JWT (service account) + X-Api-Key header |
| Request body | { "company_id": "<company UUID — see OQ-9>", "billing_code": "<TBD — user-seat billing code>", "extra_attrs": { "expectation_deduction": {} } } |
| Response | is_sufficient is returned inside extra_attrs: { "extra_attrs": { "is_sufficient": true|false, "is_unlimited": bool, "quota_info": { "total_remaining_credit_quota": float64, "total_remaining_balance_quota": float64 } } } |
| Expected behavior | • If extra_attrs.is_sufficient: true: user creation flow proceeds normally• If extra_attrs.is_sufficient: false: user creation is blocked; UI shows "Quota Exceeded / Upgrade Required" message• If extra_attrs.is_unlimited: true: treat as sufficient regardless of balance |
| Failure behavior | • If Quota Management API is unreachable or returns 5xx: allow with logging (resolved OQ-1 — see §15); log user_quota_check_error with company_id, error, fail_safe_action: "allow"• If CID is a Consultant Mode provisioning ( /private/users/create_consultant endpoint): skip check entirely — is_consultant flag will be set to true by that endpoint |
Behavior 2: Deduct Quota on successful user creation
| Field | Detail |
|---|---|
| Entity affected | Quota balance for the company; billing_logs + billing_unique_logs tables |
| Triggered by | New standard user record successfully saved to Launchpad database (after DB write, before response to caller) |
| Endpoint | POST /iag/v1/quota-managements/deduction |
| Auth | Bearer JWT (service account) + X-Api-Key header |
| Request body | { "company_id": "<company UUID>", "billing_code": "<TBD>", "deduction_code": "<TBD — user-seat deduction code>", "unique_code": "create_user_{user_id}", "quantity": 1, "extra_attrs": { "transaction_id": "{user_id}" } } |
| Response | { "company_id": "...", "billing_code": "...", "unique_code": "...", "credited_to": "<quota type deducted>", "value_before": float64, "value_after": float64 } |
| Idempotency | If the same unique_code is submitted twice, the API returns credited_to: "already-deducted" and makes no balance change — safe to retry |
| Expected behavior | • On success: 1 unit deducted from company's user quota balance; user_quota_deducted logged• User creation is never rolled back due to a deduction failure |
| Failure behavior | • On 5xx / timeout: push deduction request to gocraft/work background job with exponential backoff retry• On exhausted retries: log user_quota_deduction_failed for manual investigation |
Behavior 3: Refund Quota on user deletion
| Field | Detail |
|---|---|
| Entity affected | Quota balance for the company; billing_logs + billing_unique_logs tables |
| Triggered by | Admin successfully deletes a standard user from Launchpad (PUT /iag/v1/users/{sso_id}/delete) — after confirmed DB deletion |
| Endpoint | POST /iag/v1/quota-managements/refund |
| Auth | X-Api-Key header only (no Bearer token required for refund endpoint) |
| Request body | { "company_id": "<company UUID>", "billing_code": "<TBD>", "refund_code": "<TBD — user-seat refund code>", "unique_code": "delete_user_{user_id}", "quantity": 1 } |
| Response | { "company_id": "...", "billing_code": "...", "unique_code": "...", "refunded_to": "<initial|additional|already-refunded>", "value_before": float64, "value_after": float64 } |
| Idempotency | unique_code is optional but strongly recommended — if submitted twice, returns refunded_to: "already-refunded" with no balance change |
| Expected behavior | • On success: 1 unit restored to company's user quota balance; user_quota_refunded logged |
| Failure behavior | • On API failure: log user_quota_refund_failed with user_id, company_id, and reason — retry behavior to be defined in RFC (see OQ-3) |
8. System Flow + User Stories + ACs
8.1 System Flow
Flow: User Quota Integration — Check, Deduct, Refund, and Consultant Bypass Type: State Lifecycle / API Sequence
Standard User Creation Flow:
- Client admin initiates a user invitation in Launchpad (
POST /iag/v1/users/sso_invite) - System acquires existing Redis lock
lock:user_invite:{company_id}(pessimistic concurrency control already in place) - System checks: is this company on
user_quota_integration_enabled(viaqontak-preferences)? - If NO → proceed with creation, no quota interaction
- If YES → call Quota Management
check-quotaAPI (POST /iag/v1/quota-managements/check-quota) - If
extra_attrs.is_sufficient: false(and notis_unlimited: true) → block creation; return "Quota Exceeded / Upgrade Required" to UI - If
extra_attrs.is_sufficient: true→ create user record in Launchpad database - After successful DB save → call Deduction API (
POST /iag/v1/quota-managements/deduction) withunique_code: create_user_{user_id},quantity: 1 - If Deduction API returns success → quota deducted, flow complete
- If Deduction API returns 5xx / timeout → push to
gocraft/workbackground job for retry; user creation is not rolled back
Consultant Mode Flow (Internal via Modpanel):
11. Activation Team requests Consultant Mode access via Modpanel (moderator-be)
12. Modpanel calls Launchpad POST /private/users/create_consultant endpoint (with Basic Auth)
13. Launchpad sets is_consultant = true on the created user record — this flag is the bypass signal
14. Quota Check and Deduction API calls are skipped entirely — the /private/users/create_consultant endpoint never enters the quota check path
User Deletion Flow:
15. Admin initiates user deletion in Launchpad (PUT /iag/v1/users/{sso_id}/delete)
16. System checks: does the user have is_consultant = true?
17. If YES → delete user, skip Refund API call
18. If NO → delete user from Launchpad database
19. After confirmed deletion → call Refund API (POST /iag/v1/quota-managements/refund) with unique_code: delete_user_{user_id}, quantity: 1
20. On success → 1 unit restored to quota balance
graph TD
C1[Activation Team Requests Access via Modpanel] --> C2["Modpanel calls POST /private/users/create_consultant"]
C2 --> C3["Launchpad sets is_consultant = true on user record"]
C3 --> C4[Quota Check and Deduction Skipped — not in quota path]
A[Client Admin initiates POST /iag/v1/users/sso_invite] --> LOCK[Acquire Redis lock — lock:user_invite:company_id]
LOCK --> FF{Is CID on user_quota_integration_enabled?}
FF -- No --> FE[Create User — No Quota Interaction]
FF -- Yes --> F["Call POST /iag/v1/quota-managements/check-quota"]
F --> G{"extra_attrs.is_sufficient?"}
G -- No --> H[Block Creation — Show Quota Exceeded Message]
G -- Yes --> I[Create Standard User in Launchpad DB]
I --> J["Call POST /iag/v1/quota-managements/deduction — unique_code: create_user_{user_id}"]
J --> K{Deduction Response}
K -- Success --> L[Quota Deducted]
K -- "5xx / Timeout" --> M[Push to gocraft/work background job]
M --> J
N["Admin initiates PUT /iag/v1/users/{sso_id}/delete"] --> O{"is_consultant = true?"}
O -- Yes --> P[Delete User — Skip Refund API]
O -- No --> Q[Delete Standard User in Launchpad DB]
Q --> R["Call POST /iag/v1/quota-managements/refund — unique_code: delete_user_{user_id}"]
R --> S[Quota Refunded]
8.2 User Stories
| User Story | Importance | Mockup / Technical Notes | Acceptance Criteria |
|---|---|---|---|
| [UQI-S01] — Pre-validate User Quota Before Invitation As a Launchpad System, I want to check the user quota balance before processing a new user invitation, so that client admins are prevented from exceeding their paid user seat limits in real-time. | Must Have | Before State: Launchpad calls Modpanel GetUnifiedBilling via validate_user_quota.go to check seat counts. No centralized deduction/refund ledger exists.After Delta: For Qontak One CIDs with user_quota_integration_enabled = ON, replace the Modpanel-based check with a call to POST /iag/v1/quota-managements/check-quota. The existing Redis pessimistic lock lock:user_invite:{company_id} already serialises concurrent invitations — quota check executes inside that lock.Endpoint called: POST /iag/v1/quota-managements/check-quotaAuth: Bearer JWT + X-Api-KeyNote: is_sufficient is nested inside extra_attrs in the response body — not a top-level field.Figma: N/A | — Happy Path — • AC-1: Given a client admin initiates a user invitation and user_quota_integration_enabled = ON for the CID, when the system calls POST /iag/v1/quota-managements/check-quota and extra_attrs.is_sufficient: true, then the user creation flow proceeds normally.• AC-2: Given a client admin initiates a user invitation and the response returns extra_attrs.is_sufficient: false (and is_unlimited is not true), then the system blocks the invitation and displays a "Quota Exceeded / Upgrade Required" message in the UI.• AC-3: Given the response returns extra_attrs.is_unlimited: true, then the system treats quota as sufficient regardless of balance and proceeds with creation.— Error / Unhappy Path — • ERR-1: Given the Quota Check API returns a 5xx or is unreachable, when the system cannot determine quota availability, then the invitation is allowed (fail-open) and user_quota_check_error is logged with company_id, error, and fail_safe_action: "allow" — consistent with resolved OQ-1.— Permission Model — • CAN: System (internal, triggered by client admin invitation action) • CANNOT: No external actor can bypass this check for non-Consultant Mode users • Unauthorized: N/A — internal system call — UI States — • Loading: N/A — quota check is synchronous inside the existing lock • Error: Fail-open — invitation allowed with logging • Success: N/A — user creation proceeds if sufficient |
| [UQI-S02] — Deduct Quota on Successful User Creation As a Launchpad System, I want to deduct one user quota upon successful standard user creation, so that the company's available seat balance is accurately updated in the centralized billing system. | Must Have | Before State: No deduction call exists — user creation has no effect on any quota balance. After Delta: After a standard user is saved to the Launchpad database, the system calls POST /iag/v1/quota-managements/deduction. On 5xx/timeout the request is enqueued in a gocraft/work background job for async retry — user creation is never rolled back.Endpoint called: POST /iag/v1/quota-managements/deductionAuth: Bearer JWT + X-Api-KeyRequest payload: { "company_id": "<UUID>", "billing_code": "<TBD>", "deduction_code": "<TBD>", "unique_code": "create_user_{user_id}", "quantity": 1, "extra_attrs": { "transaction_id": "{user_id}" } }Background Job: gocraft/work (Go, Redis-backed) — same job infrastructure as SyncUserDetailJob in worker_service.go | — Happy Path — • AC-1: Given a new standard user is successfully saved to the Launchpad database, when the system calls the Deduction API with unique_code: create_user_{user_id} and quantity: 1, then 1 unit is deducted from the company's user quota balance and user_quota_deducted is logged with company_id, user_id, and unique_code.• AC-2: Given the same unique_code is submitted twice (retry scenario), when the API processes the second call, then it returns credited_to: "already-deducted" and no double-deduction occurs — idempotency is enforced via the billing_unique_logs table in qontak-billing.— Error / Unhappy Path — • ERR-1: Given the Deduction API returns a 5xx or timeout after the initial call, when the error is caught, then the system pushes the deduction request to a gocraft/work background job with exponential backoff retry — user creation is not rolled back and user_quota_deduction_retry is logged with company_id, user_id, unique_code, and attempt count.• ERR-2: Given the background job exhausts all retries without a successful deduction, when the final retry fails, then user_quota_deduction_failed is logged with user_id, company_id, and reason for manual investigation.— Permission Model — • CAN: System (internal, triggered on successful user creation DB write) • CANNOT: No external actor can trigger or cancel a deduction directly • Unauthorized: N/A — internal system event — UI States — • Loading: N/A — backend only • Error: N/A — retried in background; user creation not affected • Success: N/A — transparent to client admin |
| [UQI-S03] — Refund Quota on User Deletion As a Launchpad System, I want to refund one user quota when an existing standard user is deleted, so that the company regains their available seat capacity immediately. | Must Have | Before State: No refund call exists — deleting a user has no effect on the quota balance. After Delta: After a standard user is confirmed deleted from the Launchpad database, the system calls POST /iag/v1/quota-managements/refund.Endpoint called: POST /iag/v1/quota-managements/refundAuth: X-Api-Key header only (no Bearer token required for this endpoint)Request payload: { "company_id": "<UUID>", "billing_code": "<TBD>", "refund_code": "<TBD>", "unique_code": "delete_user_{user_id}", "quantity": 1 }Note: unique_code is optional in the API schema but must be supplied here for idempotency — duplicate refund calls return refunded_to: "already-refunded" with no balance change. | — Happy Path — • AC-1: Given an admin successfully deletes a standard user from Launchpad, when the deletion is confirmed in the database, then the system calls the Refund API with unique_code: delete_user_{user_id} and quantity: 1, 1 unit is restored to the company's user quota balance, and user_quota_refunded is logged.• AC-2: Given the Refund API returns refunded_to: "already-refunded" (duplicate call), then no balance change occurs and no error is raised — idempotent behaviour is correct.— Error / Unhappy Path — • ERR-1: Given the Refund API returns an error, when the call fails, then user_quota_refund_failed is logged with user_id, company_id, and reason — retry behaviour to be defined in RFC (see OQ-3).— Permission Model — • CAN: System (internal, triggered on confirmed user deletion) • CANNOT: No external actor can call the Refund API directly • Unauthorized: N/A — internal system event — UI States — • Loading: N/A — backend only • Error: N/A — logged for manual review • Success: N/A — transparent to admin |
| [UQI-S04] — Consultant Mode Quota Exemption As a Launchpad System, I want to bypass quota checking and deduction for Consultant Mode users provisioned via Modpanel, so that the Activation Team can access a client's CID for setup without consuming the client's paid user seats. | Must Have | Before State: No explicit bypass mechanism exists — provisioning a consultant via Modpanel risks counting against the client's seat quota. After Delta: Consultant Mode provisioning is routed through a separate endpoint in Launchpad — POST /private/users/create_consultant (Basic Auth, called from moderator-be). Users created via this endpoint have is_consultant = true set on their database record. The quota check/deduction path in POST /iag/v1/users/sso_invite is never entered for consultant creation.Bypass mechanism: is_consultant BOOLEAN field already exists in the users table (db/schema.sql). The CountUser SQL query used for quota counting already excludes consultants (AND (is_consultant IS NULL OR is_consultant = false)). No code change is needed in create_consultant.go for bypass — the bypass is structural.Deletion bypass: The delete.go handler already checks is_consultant before applying company membership validation. The Refund API call in the deletion flow must also gate on is_consultant = false.Figma: N/A | — Happy Path — • AC-1: Given the Activation Team initiates a Consultant Mode access request from Modpanel, when Modpanel calls POST /private/users/create_consultant, then Launchpad creates the user with is_consultant = true and neither the Quota Check nor the Deduction API is called — the company's seat balance is unaffected.• AC-2: Given a Consultant Mode user ( is_consultant = true) is deleted from Launchpad, when the deletion flow executes, then the Refund API call is skipped entirely — the balance does not change.— Error / Unhappy Path — • ERR-1: Given a provisioning request arrives at POST /private/users/create_consultant but fails to set is_consultant = true (e.g. database error during user write), then the user creation must fail atomically — no partial record with an incorrect quota deduction can result. Log user_quota_consultant_flag_missing.— Permission Model — • CAN: Activation Team (via Modpanel internal tooling — moderator-be)• CANNOT: Client admins cannot self-provision via the /private/users/create_consultant endpoint (Basic Auth, internal only)• Unauthorized: N/A — internal tooling only — UI States — • Loading: N/A — backend only • Error: N/A — creation fails atomically • Success: N/A — transparent to Activation Team |
| [UQI-S05] — Backfill Existing User Usage As a Backend Developer, I want a one-time script to backfill current user usage into the Quota Management API, so that existing Qontak One clients have an accurate starting balance without manual intervention at the time of integration go-live. | Must Have | Before State: Quota Management API has no record of existing user seat consumption for Qontak One CIDs — balances would start at zero if not backfilled. After Delta: A one-time Go task iterates through all Qontak One CIDs, counts active non-consultant users per CID, and pushes the aggregate usage to the Quota Management API. Counting logic already exists: The CountUser SQL query in Launchpad (db/query/users.sql) already excludes both consultant users (is_consultant IS NULL OR is_consultant = false) and soft-deleted users (deleted_at IS NULL). The backfill script can reuse this query directly.User states to include: Only status = 'active' users (not pending, not deleted). Consultant users (is_consultant = true) must be excluded — consistent with resolved OQ-6.Figma: N/A — internal tooling Execution: One-time Go command to be run as a migration step during deployment. Must log discrepancies and failed syncs per CID. | — Happy Path — • AC-1: Given the backfill script is executed, when it processes a CID, then it accurately counts the number of active non-consultant users (reusing the CountUser query logic) and syncs this exact number as consumed quota to the Quota Management API via the Deduction endpoint.• AC-2: Given the script processes all CIDs, when it completes, then it outputs a summary log of: total CIDs processed, total CIDs with discrepancies, total failed syncs, and a list of CID IDs that failed for manual follow-up. — Error / Unhappy Path — • ERR-1: Given the backfill script encounters a failed API call for a specific CID, when the sync fails, then it logs user_quota_backfill_failed with company_id and reason, skips to the next CID, and continues — it does not halt the entire run.— Permission Model — • CAN: Engineering team members (Go command via deployment pipeline) • CANNOT: No client-facing interface exposes this action • Unauthorized: N/A — internal tooling — UI States — • Loading: N/A — CLI output only • Error: Logs failed CIDs with reason; does not stop execution • Success: Outputs per-CID sync confirmation to console |
AC ids are numbered per story (each story restarts at AC-1). Composite ids minted here:
UQI-S01/AC-1,UQI-S01/AC-2,UQI-S01/AC-3,UQI-S01/ERR-1,UQI-S02/AC-1,UQI-S02/AC-2,UQI-S02/ERR-1,UQI-S02/ERR-2,UQI-S03/AC-1,UQI-S03/AC-2,UQI-S03/ERR-1,UQI-S04/AC-1,UQI-S04/AC-2,UQI-S04/ERR-1,UQI-S05/AC-1,UQI-S05/AC-2,UQI-S05/ERR-1.
9. Rollout
| Field | Value |
|---|---|
| Feature flag | user_quota_integration_enabled — default: OFF (checked via qontak-preferences service per CID, using company sso_id as the preference UniqueID) |
| Stage 1 | Internal Bifrost test CIDs only (≤5 CIDs, manually enabled) — validate check, deduction, refund, and Consultant bypass |
| Stage 2 | Closed pilot: 5–10 Qontak One CIDs — confirm deduction reliability and backfill accuracy |
| Stage 3 | All Qontak One CIDs in batches (25% → 50% → 100%) after backfill script confirmed successful |
| GA | All Qontak One CIDs with user_quota_integration_enabled ON by default |
| Backward compat | Yes — CIDs with flag OFF continue using existing Modpanel-based validate_user_quota.go check; existing user creation flows are unaffected |
| Migration | Backfill Go command (UQI-S05) must run before or at GA to seed accurate usage counts into the Quota Management API |
10. Observability
Key Events:
| Event Name | Trigger | Properties |
|---|---|---|
user_quota_check_passed | Quota check returns is_sufficient: true for a user invitation | company_id, remaining_quota |
user_quota_check_failed | Quota check returns is_sufficient: false; invitation blocked | company_id, remaining_quota |
user_quota_check_error | Quota Check API is unreachable or returns 5xx | company_id, error, fail_safe_action |
user_quota_deducted | Deduction API call succeeds after user creation | company_id, user_id, unique_code |
user_quota_deduction_retry | Deduction API returned 5xx; pushed to gocraft/work job | company_id, user_id, unique_code, attempt |
user_quota_deduction_failed | gocraft/work job exhausted retries; deduction unconfirmed | company_id, user_id, unique_code, error |
user_quota_refunded | Refund API call succeeds after user deletion | company_id, user_id, unique_code |
user_quota_refund_failed | Refund API call fails after user deletion | company_id, user_id, unique_code, error |
user_quota_consultant_bypass | is_consultant = true detected; quota interaction skipped | company_id, user_id, source: create_consultant_endpoint |
user_quota_backfill_completed | Backfill script successfully synced quota for a CID | company_id, synced_user_count |
user_quota_backfill_failed | Backfill script failed to sync quota for a CID | company_id, error |
| Field | Detail |
|---|---|
| Dashboard owner | Bifrost team |
| Alert 1 | user_quota_deduction_failed count > 0 in any 1hr window → Slack: #bifrost-alerts (every failed deduction is a potential revenue leak) |
| Alert 2 | user_quota_check_error rate > 5% of total check calls in 30min window → Slack: #bifrost-alerts (Quota Management API may be degraded) |
10.1 Post-Launch Monitoring Cadence
| Field | Detail |
|---|---|
| Review cadence | Weekly for first 4 weeks post-GA, then monthly |
| Owner | Bifrost PM |
| Review scope | user_quota_deducted, user_quota_refunded, user_quota_deduction_failed, user_quota_refund_failed, user_quota_check_error |
| Trigger threshold 1 | user_quota_deduction_failed count > 0 in any 7-day window → immediate investigation (zero tolerance for unconfirmed deductions post-GA) |
| Trigger threshold 2 | user_quota_check_error rate > 1% sustained over 48 hours → Quota Management API health check and Bifrost Eng escalation |
| Rollback consideration | If user_quota_deduction_failed rate exceeds 5% of total creations and cannot be resolved within 24 hours, PM disables user_quota_integration_enabled globally pending root cause fix |
11. Success Metrics
Reliability:
| Metric | Definition | Baseline | Target |
|---|---|---|---|
| ⭐ Deduction success rate | % of standard user creations followed by a confirmed successful Quota Management deduction (including gocraft/work background retries) | N/A — new feature | ≥ 99.9% within 30 days of GA |
| Refund success rate | % of standard user deletions followed by a confirmed successful Quota Management refund | N/A — new feature | ≥ 99.5% within 30 days of GA |
Correctness:
| Metric | Definition | Baseline | Target |
|---|---|---|---|
| Quota enforcement accuracy | % of user invitations that are correctly blocked when CID quota is at zero | N/A — new feature | 100% (zero leakage) within 14 days of GA |
| Consultant bypass accuracy | % of Consultant Mode provisioning events (via POST /private/users/create_consultant) that correctly skip quota interaction | N/A — new feature | 100% (zero false deductions) |
Migration:
| Metric | Definition | Baseline | Target |
|---|---|---|---|
| Backfill coverage | % of active Qontak One CIDs successfully backfilled before GA | 0% | 100% before Stage 3 begins |
12. Launch Plan & Stage Gates
| Stage | Audience | Duration | Success Gate to Advance | Owner |
|---|---|---|---|---|
| Internal Alpha | Bifrost test CIDs (≤5) | 1 week | 0 P0 bugs. Quota check, deduction, and refund all confirmed working. Consultant bypass verified via is_consultant flag. | Bifrost PM + QA |
| Closed Pilot | 5–10 Qontak One CIDs | 1 week | Deduction success rate ≥ 99.9%. Refund success rate ≥ 99.5%. 0 user_quota_deduction_failed events. | Bifrost PM |
| Backfill Execution | All active Qontak One CIDs | Pre-Stage 3 (one-time) | 100% of CIDs backfilled. 0 unresolved user_quota_backfill_failed events. | Bifrost Eng Lead |
| Staged Rollout | All Qontak One CIDs in batches (25% → 50% → 100%) | 2 weeks | Deduction failure rate ≤ 0.1% sustained for 1 week per batch. No quota enforcement bypass detected. | Bifrost PM + Eng Lead |
| GA | All Qontak One CIDs | Ongoing | All staged rollout gates sustained for 2 consecutive weeks. | Bifrost PM |
13. Dependencies
| Dependency | Owning Team | Deliverable Needed | Blocking? |
|---|---|---|---|
Quota Management API — POST /iag/v1/quota-managements/check-quota | Bifrost (qontak-billing) | Stable, documented endpoint available in staging; confirmed billing_code value for user seats | YES |
Quota Management API — POST /iag/v1/quota-managements/deduction | Bifrost (qontak-billing) | Stable endpoint; confirmed billing_code + deduction_code values; idempotency via unique_code + billing_unique_logs | YES |
Quota Management API — POST /iag/v1/quota-managements/refund | Bifrost (qontak-billing) | Stable endpoint; confirmed billing_code + refund_code values | YES |
| Service account credentials for Quota Management API | Bifrost / Platform | Bearer JWT (service account) + X-Api-Key header provisioned for Launchpad in all environments | YES |
sync_consultant_to_launchpad feature flag in moderator-be | Modpanel team | Feature flag must be ON for Qontak One CIDs so Consultant Mode provisioning routes through POST /private/users/create_consultant in Launchpad (not legacy Chat/CRM direct) | YES |
qontak-preferences feature flag entry for user_quota_integration_enabled | Bifrost | Preference entry registered and testable in staging | YES |
14. Key Decisions + Alternatives Rejected
Decisions Made:
| Date | Decision | Rationale |
|---|---|---|
| 2026-06-23 | Use user_id as the idempotency key (unique_code) for both deduction and refund calls | user_id is unique per Launchpad user and permanent — ensures idempotent retries for both deduction and refund without risk of double-counting. The billing_unique_logs table in qontak-billing enforces uniqueness at the DB level. |
| 2026-06-23 | Use gocraft/work background job (not Sidekiq) for deduction API failures | Launchpad is a Go service using gocraft/work with Redis as the job store — the same infrastructure that powers SyncUserDetailJob. There is no Sidekiq in Launchpad; introducing it would add an unjustified dependency. |
| 2026-06-23 | Block creation on deduction failure is wrong — use async retry | Blocking creation on a Quota Management API failure would degrade the user invitation UX for an internal billing failure; user is already created in the DB, so the deduction must succeed eventually via async retry |
| 2026-06-23 | Consultant Mode bypass is structural, not flag-based | The /private/users/create_consultant endpoint (called from Modpanel) sets is_consultant = true on the user record. The quota check/deduction path (/iag/v1/users/sso_invite) is never entered for consultant creation. No runtime flag check is needed — the bypass is guaranteed by endpoint routing. |
| 2026-06-23 | Run backfill as a one-time Go command at deployment, not as an ongoing sync | Quota Management API is the live system of record post-integration; ongoing sync would create a second source of truth and risk double-counting |
| 2026-06-23 | Fail-open (allow with logging) when Quota Check API is unreachable | Blocking all invitations on an API outage is too disruptive for an internal billing failure — consistent with the async retry pattern for Deduction |
Alternatives Rejected:
| Alternative | Why Rejected | Date |
|---|---|---|
| Enforce quota via a nightly batch job rather than real-time API check | Does not prevent over-provisioning in real-time — admins could exceed quota intraday before the batch catches up, creating the same revenue leakage problem this integration aims to solve | 2026-06-23 |
| Block user creation entirely when Deduction API returns 5xx | Too disruptive — a transient billing service failure should not prevent legitimate user operations; async retry preserves quota accuracy without degrading the invitation experience | 2026-06-23 |
| Skip the backfill and let quota balances start from zero at go-live | Existing Qontak One CIDs with current users would appear to have full available seats even though seats are already consumed — first-day quota checks would give incorrect results and allow over-provisioning | 2026-06-23 |
| Use a runtime Consultant Mode flag in the request payload for bypass detection | Launchpad already has structural separation via the /private/users/create_consultant endpoint and the is_consultant DB column — adding a runtime flag would duplicate this and introduce a new attack surface if the flag could be spoofed by external callers | 2026-06-23 |
15. Open Questions
| # | Type | Question | Owner | Deadline |
|---|---|---|---|---|
| 1 | Open Question | What is the fail-safe behavior when the Quota Check API is unreachable — block the invitation (safe but disruptive) or allow with logging (risky but non-blocking)? | Bifrost PM + Eng | Before RFC |
| 2 | Open Question | What is the retry strategy for the Deduction gocraft/work background job — max attempts, backoff curve, and dead-letter queue handling? | Bifrost Eng | Before RFC |
| 3 | Open Question | Does the Refund API call on user deletion also need gocraft/work background retry, or is a single attempt with logging sufficient? | Bifrost Eng | Before RFC |
| 4 | Assumption | The Quota Management API billing_unique_logs table correctly deduplicates concurrent calls for the same unique_code — if not, rapid create-retry flows could double-deduct | Bifrost Eng | Before Stage 1 |
| 5 | Risk | If sync_consultant_to_launchpad feature flag in moderator-be is OFF for a given CID, Modpanel routes consultant provisioning to Chat/CRM directly — Launchpad is never called and is_consultant is never set. These users would not exist in Launchpad at all, so no quota impact. But if the flag is subsequently toggled ON mid-tenancy, historical consultant users would have no is_consultant flag in Launchpad. | Bifrost PM + Modpanel team | Before RFC |
| 6 | Open Question | Should the backfill script exclude users in any state other than "active" (e.g. deactivated, suspended) or only exclude Consultant Mode users? | Bifrost PM | Before RFC |
| 7 | Open Question | What is the billing_code value to use for user seat quota in the Quota Management API? This is required in all three API calls. | Bifrost (qontak-billing team) | Before RFC |
| 8 | Open Question | What are the deduction_code and refund_code values for user seat operations in the Quota Management API? Both are required fields in their respective request payloads. | Bifrost (qontak-billing team) | Before RFC |
| 9 | Open Question | Which company identifier should be passed as company_id in Quota Management API calls — Launchpad's internal companies.id (UUID), the Mekari SSO company sso_id (UUID), or the external_company_id (integer cast to string)? The billing API expects a UUID string. | Bifrost Eng | Before RFC |
| 10 | Open Question | Should the expectation_deduction field in the check-quota extra_attrs be populated with any data for user seat checks, or passed as an empty object {}? The Quota Management API requires this field. | Bifrost Eng | Before RFC |
| 11 | Open Question | Launchpad already uses validate_user_quota.go (Modpanel-backed) for non-Qontak One CIDs. For Qontak One CIDs going through this new integration, should the Modpanel-based check be explicitly disabled (removed from the code path under feature flag) or does the feature flag on the new path implicitly bypass it? | Bifrost Eng | Before RFC |
PRD CHANGELOG
| Version | Date | By | Section | Type | Summary |
|---|---|---|---|---|---|
| 1.0 | 2026-06-22 | Claude | All | CREATED | Initial NEW PRD generated from Confluence page (ID 51184074959); all 5 user stories from source preserved and expanded to full Qontak PRD format with Gherkin ACs, observability events, and rollout plan. |
| 1.1 | 2026-06-23 | Claude | S14, S15 | MODIFIED | Set all S14 decision and alternative dates to 2026-06-23. Resolved S15 OQ1 (allow with logging on Quota Check API failure) and OQ6 (backfill excludes Consultant Mode + deactivated/suspended users). OQ2, OQ3, OQ4, OQ5 remain open pending Engineering/cross-team input. |
| 1.2 | 2026-06-23 | Claude | S3, S6, S7, S8, S13, S14, S15 | MODIFIED | Enriched with findings from qontak-launchpad, qontak-billing, and moderator-be repos. Key changes: (1) Corrected background job technology from "Sidekiq" to gocraft/work (Launchpad is Go). (2) Filled in exact Quota Management API endpoint paths, request/response JSON schemas, and auth headers (was deferred to RFC). (3) Clarified Consultant Mode bypass is structural via endpoint routing + is_consultant DB field — not a runtime flag. (4) Noted existing validate_user_quota.go (Modpanel-based) that this integration replaces. (5) Documented existing Redis pessimistic lock lock:user_invite:{company_id}. (6) Added OQ7–OQ11 for billing_code, deduction_code, refund_code, company_id UUID type, and expectation_deduction payload. (7) Updated backfill story with knowledge that CountUser SQL already excludes consultants and soft-deleted users. |