Skip to main content

haste_health/commands/
subscription.rs

1use clap::Subcommand;
2use haste_fhir_model::r4::generated::terminology::IssueType;
3use haste_fhir_operation_error::OperationOutcomeError;
4use std::path::{Path, PathBuf};
5
6/// Where the website reads the pricing table from, relative to `backend/`.
7const DEFAULT_OUTPUT: &str = "../frontend/packages/website/static/pricing/tiers.json";
8
9/// Publish what each subscription tier allows.
10#[derive(Subcommand, Debug)]
11pub(crate) enum SubscriptionCommands {
12    /// Write the subscription tiers to JSON for the website's pricing page.
13    Export {
14        /// Where to write the tiers.
15        #[arg(short, long, default_value = DEFAULT_OUTPUT)]
16        output: PathBuf,
17        /// Fail if the output is out of date instead of writing it.
18        #[arg(long)]
19        check: bool,
20    },
21    /// Print what each tier allows, as a table.
22    Show,
23}
24
25fn error(message: String) -> OperationOutcomeError {
26    OperationOutcomeError::error(IssueType::exception(), message)
27}
28
29/// The pricing table as JSON, newline-terminated so it is a well-formed text
30/// file and diffs cleanly.
31fn rendered() -> Result<String, OperationOutcomeError> {
32    let mut json = serde_json::to_string_pretty(&haste_subscription::export::build())
33        .map_err(|e| error(format!("Failed to serialize the subscription tiers: {e}")))?;
34    json.push('\n');
35
36    Ok(json)
37}
38
39async fn export(output: &Path, check: bool) -> Result<(), OperationOutcomeError> {
40    let contents = rendered()?;
41
42    if check {
43        let current = tokio::fs::read_to_string(output)
44            .await
45            .map_err(|e| error(format!("{}: {e}", output.display())))?;
46
47        return if current == contents {
48            println!("{} is up to date", output.display());
49            Ok(())
50        } else {
51            Err(error(format!(
52                "Out of date, run `cargo run subscription export` to regenerate: {}",
53                output.display()
54            )))
55        };
56    }
57
58    if let Some(parent) = output.parent() {
59        tokio::fs::create_dir_all(parent)
60            .await
61            .map_err(|e| error(format!("{}: {e}", parent.display())))?;
62    }
63
64    tokio::fs::write(output, contents)
65        .await
66        .map_err(|e| error(format!("{}: {e}", output.display())))?;
67    println!("Wrote {}", output.display());
68
69    Ok(())
70}
71
72fn show() {
73    let export = haste_subscription::export::build();
74
75    println!(
76        "Request costs: read {}, search {}, history {}, invocation {}, write {} points (budget per {})\n",
77        export.operation_points.read,
78        export.operation_points.search,
79        export.operation_points.history,
80        export.operation_points.invocation,
81        export.operation_points.write,
82        export.window,
83    );
84
85    for tier in &export.tiers {
86        let tier_id: String = tier.limits.tier.clone().into();
87
88        println!("{} ({})", tier.limits.display_name, tier_id);
89        println!(
90            "  {}{}  -  {}",
91            tier.limits.price,
92            tier.limits.cadence.unwrap_or(""),
93            tier.limits.audience,
94        );
95        println!("  Request budget    {}", tier.request_budget_label);
96        println!("  Total resources   {}", tier.total_resources_label);
97        println!("  Support           {}", tier.limits.support);
98        println!("  Uptime SLA        {}", tier.limits.uptime_sla);
99        println!(
100            "  Customization     {}",
101            if tier.limits.tenant_customization {
102                "allowed"
103            } else {
104                "not allowed"
105            }
106        );
107        println!(
108            "  BAA               {}",
109            if tier.limits.baa_available {
110                "available"
111            } else {
112                "not available"
113            }
114        );
115        println!();
116    }
117}
118
119pub(crate) async fn run(command: &SubscriptionCommands) -> Result<(), OperationOutcomeError> {
120    match command {
121        SubscriptionCommands::Export { output, check } => export(output, *check).await,
122        SubscriptionCommands::Show => {
123            show();
124            Ok(())
125        }
126    }
127}