rok-cli 0.3.6

Developer CLI for rok-based Axum applications
use super::{write_file, Scaffold, ScaffoldArgs, ScaffoldResult};
use anyhow::Result;

pub struct BillingScaffold;

impl Scaffold for BillingScaffold {
    fn name(&self) -> &'static str {
        "billing"
    }
    fn description(&self) -> &'static str {
        "Billing: subscription plans, checkout, webhook handler, invoice model"
    }

    fn generate(&self, args: &ScaffoldArgs) -> Result<ScaffoldResult> {
        let mut r = ScaffoldResult::default();
        let d = args.dry_run;
        write_file(
            &mut r,
            "src/app/controllers/billing_controller.rs",
            CONTROLLER,
            d,
        )?;
        write_file(&mut r, "migrations/create_billing_tables.sql", MIGRATION, d)?;
        r.warnings
            .push("Set STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET in .env".into());
        r.warnings
            .push("Register billing routes in src/app/routes.rs".into());
        Ok(r)
    }
}

const CONTROLLER: &str = r#"use axum::{extract::{Path, State}, response::IntoResponse, Json};
use rok_auth::axum::{Ctx, Response};

pub async fn plans(State(pool): State<sqlx::PgPool>) -> impl IntoResponse {
    // TODO: list active subscription plans
    Response::json(serde_json::json!({ "data": [] }))
}

pub async fn checkout(ctx: Ctx, Json(body): Json<serde_json::Value>) -> impl IntoResponse {
    // TODO: create Stripe checkout session, return URL
    Response::json(serde_json::json!({ "checkout_url": "" }))
}

pub async fn portal(ctx: Ctx) -> impl IntoResponse {
    // TODO: create Stripe customer portal session
    Response::json(serde_json::json!({ "portal_url": "" }))
}

pub async fn webhook(Json(payload): Json<serde_json::Value>) -> impl IntoResponse {
    // TODO: handle stripe webhook events (checkout.completed, invoice.paid, etc.)
    Response::json(serde_json::json!({ "received": true }))
}

pub async fn invoices(ctx: Ctx, State(pool): State<sqlx::PgPool>) -> impl IntoResponse {
    // TODO: list invoices for current user
    Response::json(serde_json::json!({ "data": [] }))
}
"#;

const MIGRATION: &str = r#"CREATE TABLE subscription_plans (
    id           BIGSERIAL PRIMARY KEY,
    name         TEXT NOT NULL,
    price_cents  INTEGER NOT NULL,
    interval     TEXT NOT NULL DEFAULT 'month',
    stripe_id    TEXT,
    active       BOOLEAN NOT NULL DEFAULT true,
    created_at   TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE subscriptions (
    id                  BIGSERIAL PRIMARY KEY,
    user_id             BIGINT NOT NULL,
    plan_id             BIGINT NOT NULL REFERENCES subscription_plans(id),
    stripe_sub_id       TEXT,
    stripe_customer_id  TEXT,
    status              TEXT NOT NULL DEFAULT 'active',
    current_period_end  TIMESTAMPTZ,
    created_at          TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE invoices (
    id              BIGSERIAL PRIMARY KEY,
    user_id         BIGINT NOT NULL,
    subscription_id BIGINT REFERENCES subscriptions(id),
    amount_cents    INTEGER NOT NULL,
    currency        TEXT NOT NULL DEFAULT 'usd',
    status          TEXT NOT NULL DEFAULT 'open',
    stripe_id       TEXT,
    paid_at         TIMESTAMPTZ,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);
"#;