Skip to content

Option A — Phase 5: Billing & Usage — Remaining Tasks

This document lists only the remaining work for Phase 5 of AppiFire AI Chat (Option A). The full Phase 5 plan is in Option-A-Phase-5-Billing-and-Usage.md.

Status: Most of Phase 5 is implemented (plans, billing routes, subscription confirm, buy-credits, webhooks, Prisma models, usage dashboard, limit enforcement, cancel flow, reconciliation, success banners). The items below are the gaps and follow-ups to complete or document.


1. Monthly $10 credits on subscription renewal

Section titled “1. Monthly $10 credits on subscription renewal”

Current behaviour: Shops receive the included $10 credits once when they first confirm the paid subscription. They do not receive a fresh $10 at the start of each billing cycle.

Required: When a paid subscription renews (new billing period), add the monthly included credits to shop.creditBalanceCents.

Implementation approach:

  • In app/routes/webhooks.app_subscriptions.update.jsx: when the webhook payload has status === "ACTIVE" and you have a new currentPeriodEnd (e.g. different from the one stored in ShopSubscription for that shop), treat it as a new period and add SUBSCRIPTION_CREDITS_CENTS (e.g. 1000 = $10) to shop.creditBalanceCents.
  • To avoid double-granting (e.g. multiple webhooks for the same period), use a single source of truth for “last period we granted credits for”:
    • Option A: Compare incoming currentPeriodEnd from the webhook with ShopSubscription.currentPeriodEnd. If the incoming value is later than the stored one, grant credits and then update ShopSubscription.currentPeriodEnd to the new value.
    • Option B: Add a field such as lastCreditsGrantedPeriodEnd (DateTime, nullable) on Shop or ShopSubscription. When you grant credits, set it to the period end for which you granted. Next time, only grant if the webhook’s currentPeriodEnd is after lastCreditsGrantedPeriodEnd.

Acceptance: After a merchant’s subscription renews (Shopify sends app_subscriptions/update with ACTIVE and updated currentPeriodEnd), the shop’s creditBalanceCents increases by the monthly included amount (e.g. $10), once per period.


2. Billing UI: show “Total replies this month” for paid users

Section titled “2. Billing UI: show “Total replies this month” for paid users”

Current behaviour: For paid users, the Billing page shows AI credit balance and the “Usage stats (last 30 days)” section (API calls, OpenRouter cost, charged cost, tokens, last 10 questions). It does not show a simple “Total replies this month” for the current calendar month.

Required: For paid users, display total replies this month (same metric as for free: assistant messages in the current calendar month) in the “Current usage this month” or “Subscription” section.

Implementation: The loader already computes repliesUsed (current month). In the Billing page UI, when currentPlan === "paid", add a line such as: “Replies this month: {repliesUsed}” (and optionally “Each reply uses a variable amount of credits.”) so paid merchants see both credit balance and reply count.

Acceptance: Paid users see their current-month reply count on the Billing page alongside credit balance and usage stats.


3. Store subscriptionLineItemId when easy (optional)

Section titled “3. Store subscriptionLineItemId when easy (optional)”

Current behaviour: Shop.subscriptionLineItemId exists in the schema and is cleared on cancel, but it is never set when the subscription is created or updated.

Required: If the Shopify API readily returns a subscription line item ID when confirming or fetching the subscription, store it in shop.subscriptionLineItemId. If it requires extra round-trips or non-standard handling, leave this as a future enhancement and do not change logic elsewhere.

Implementation: In app/routes/app.billing.confirm.jsx, when you fetch the subscription via node(id: AppSubscription/...), check whether the GraphQL response includes a line-item identifier (e.g. lineItems { id } or similar on AppSubscription). If yes, set subscriptionLineItemId on the shop update. If the Admin API does not expose it in the same query you already use, skip this task.

Acceptance: When the API response includes a subscription line item ID, it is stored on the shop; otherwise behaviour is unchanged.


4. Low-balance email: separate infrastructure task (Phase 5.1)

Section titled “4. Low-balance email: separate infrastructure task (Phase 5.1)”

Current behaviour: Low-balance alert logic is implemented: thresholds, once-per-day throttling (lastLowBalanceAlertAt), and free vs paid (remaining replies vs remaining balance) are correct. sendLowBalanceAlert in app/lib/email.server.js is a stub (e.g. logs only; no real email sent).

Required: Treat “send real email when low-balance alert fires” as a separate, future task (e.g. Phase 5.1 or “Email infrastructure”), not as a blocker for Phase 5 completion. No change to Phase 5 scope.

Implementation: When you are ready, wire sendLowBalanceAlert to your chosen provider (e.g. Resend, SendGrid) and configure credentials (env vars). No changes to the remaining-tasks list for Phase 5.

Acceptance: Phase 5 is considered complete without real email delivery; real email is a later infrastructure task.


Daily spend limit: Not implemented. Paid plan allows a reply whenever creditBalanceCents > 0. The last reply may bring the balance below $0 (soft cap); no daily spend cap is enforced.


Current behaviour: Plan names, prices, and caps (e.g. $20/mo, 50 free replies, $10 included credits) are defined in code in app/lib/plans.server.js.

Required: Implement plan definitions in the database so pricing and caps can be changed without a code deploy. Plans (Free, Paid) must be stored in the DB and used by billing and limits.

Implementation approach:

  • Add a Plan (or plans) table in Prisma, e.g.: id, name, monthlyPriceCents, includedReplies (nullable), creditsIncludedCents, isActive, createdAt, updatedAt.
  • Seed Free and Paid rows (e.g. via migration or seed script).
  • Update app/lib/plans.server.js (or replace with a server module) to read plans from the DB instead of a static object — e.g. getPlans(), getPlanById(id).
  • Update the Billing page loader and any code that uses PLANS to use the DB-backed plans (with fallback or cache as needed).
  • Update app/lib/limits.server.js to use the free plan’s includedReplies from the DB (or keep a single “free reply cap” config if only one free plan exists).

Acceptance: Plan names, monthly price, free reply cap, and included credits are stored in the DB and used by billing and limits; changes do not require a code deploy.


6. Make plan constants configurable (environment variables)

Section titled “6. Make plan constants configurable (environment variables)”

Current behaviour: Monthly price (e.g. $20) and free reply cap (e.g. 50) are hard-coded in app/lib/plans.server.js.

Required: Make paid plan monthly price and free plan reply cap configurable via environment variables so they can be changed without a code deploy.

Implementation approach:

  • Add PAID_PLAN_MONTHLY_PRICE, FREE_PLAN_REPLIES_CAP, and optionally PAID_PLAN_CREDITS_INCLUDED_CENTS to .env (e.g. PAID_PLAN_MONTHLY_PRICE=20, FREE_PLAN_REPLIES_CAP=50, PAID_PLAN_CREDITS_INCLUDED_CENTS=1000).
  • In app/lib/plans.server.js, read these with parseInt(process.env.…, 10) and use fallbacks to current values (e.g. 20, 50, 1000) when env is missing or invalid.
  • Export PLANS (or a getter) so billing and limits use the same values.

Acceptance: Changing the monthly price (e.g. from $20 to another value) or the free reply cap (e.g. from 50 to another value) does not require a code change; it is done via env vars in .env.


7. Behaviour already agreed (no code change)

Section titled “7. Behaviour already agreed (no code change)”
  • Paid plan credit check: Keep soft behaviour: allow a reply whenever creditBalanceCents > 0; deduct actual cost up to the current balance (so the last reply can consume the remaining cents). No strict “balance ≥ estimated cost” check required.

  • Monthly credits on renewal: When app_subscriptions/update webhook has ACTIVE and a new currentPeriodEnd, add SUBSCRIPTION_CREDITS_CENTS to shop.creditBalanceCents once per period (use stored period or lastCreditsGrantedPeriodEnd to avoid double-grant).
  • Paid UI — replies this month: On Billing page, for paid users, show “Total replies this month” (use existing repliesUsed from loader).
  • subscriptionLineItemId (if easy): When confirming subscription, if Shopify API returns a line item ID in the subscription payload/query, set shop.subscriptionLineItemId; otherwise leave as-is.
  • Low-balance email: Left as separate infrastructure task (Phase 5.1); Phase 5 complete without it.
  • Plan definitions in DB: Add Plan table, seed Free/Paid, and have billing + limits read plans from DB so pricing/caps can change without deploy.
  • Configurable plan constants: Make paid monthly price and free reply cap configurable via env vars (e.g. PAID_PLAN_MONTHLY_PRICE, FREE_PLAN_REPLIES_CAP) so $20 and 50 (or other values) can be changed without code deploy.

Next: Option-A-Phase-6-Launch-and-Publishing-Guide.md (when moving to Phase 6).