Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Costs: Cloudflare's subscriptions read from Cloudflare each day, the estimate only until then; sudo's costs split into Costs & margin and Bill & pricing | 1 | //! Cloudflare's subscriptions: what g1t pays each month whatever it uses |
| 2 | //! (Workers Paid, add-ons), read from Cloudflare each day. | |
| 3 | //! | |
| 4 | //! They are not on the billable-usage bill, so the costs statement and | |
| 5 | //! g1t's own spend add them as a fixed cost. Read with the bill's token | |
| 6 | //! (`CLOUDFLARE_BILLING_TOKEN`, Account: Billing Read) and kept in | |
| 7 | //! `cf_subscriptions`; until a read has worked, `CLOUDFLARE_FIXED_MONTHLY_MICROS` | |
| 8 | //! stands in as an estimate. | |
| 9 | ||
| 10 | use g1t_contracts::billing::FixedCost; | |
| 11 | use g1t_contracts::time::rfc3339; | |
| 12 | use g1t_kit::now_ms; | |
| 13 | use serde::Deserialize; | |
| 14 | use serde_json::Value; | |
| 15 | use worker::Result; | |
| 16 | ||
| 17 | use crate::Billing; | |
| 18 | ||
| 19 | /// The states of a subscription that is paid for (or will be). | |
| 20 | const PAID: [&str; 4] = ["Paid", "Provisioned", "AwaitingPayment", "Trial"]; | |
| 21 | ||
| 22 | /// Each subscription that costs money, at what it comes to a month. | |
| 23 | pub(crate) fn monthly(body: &Value) -> Vec<FixedCost> { | |
| 24 | let mut out = Vec::new(); | |
| 25 | for s in body["result"].as_array().into_iter().flatten() { | |
| 26 | let state = s["state"].as_str().unwrap_or("Paid"); | |
| 27 | if !PAID.contains(&state) { | |
| 28 | continue; | |
| 29 | } | |
| 30 | let price = s["price"].as_f64().unwrap_or(0.0); | |
| 31 | let months = match s["frequency"].as_str().unwrap_or("monthly") { | |
| 32 | "weekly" => 12.0 / 52.0, | |
| 33 | "quarterly" => 3.0, | |
| 34 | "yearly" => 12.0, | |
| 35 | _ => 1.0, | |
| 36 | }; | |
| 37 | let monthly_micros = (price / months * 1_000_000.0).round() as i64; | |
| 38 | if monthly_micros <= 0 { | |
| 39 | continue; | |
| 40 | } | |
| 41 | let name = s["rate_plan"]["public_name"] | |
| 42 | .as_str() | |
| 43 | .or_else(|| s["product"]["name"].as_str()) | |
| 44 | .or_else(|| s["rate_plan"]["id"].as_str()) | |
| 45 | .unwrap_or("Subscription") | |
| 46 | .to_owned(); | |
| 47 | out.push(FixedCost { name, monthly_micros }); | |
| 48 | } | |
| 49 | out | |
| 50 | } | |
| 51 | ||
| 52 | /// What the fixed cost is, and where the figure came from. | |
| 53 | pub(crate) struct Fixed { | |
| 54 | pub monthly_micros: i64, | |
| 55 | /// `cloudflare` (read from Cloudflare) or `estimate` (the variable). | |
| 56 | pub source: &'static str, | |
| 57 | pub read_at: Option<String>, | |
| 58 | pub items: Vec<FixedCost>, | |
| 59 | } | |
| 60 | ||
| 61 | impl Billing { | |
| 62 | /// Reads the subscriptions and keeps them; an error is returned for the | |
| 63 | /// run to log, and the last read (or the estimate) stays. | |
| 64 | pub(crate) async fn read_subscriptions(&self, keeper: &crate::keeper::Keeper) -> Result<usize> { | |
| 65 | let body = keeper.subscriptions_body().await?; | |
| 66 | let items = monthly(&body); | |
| 67 | let total: i64 = items.iter().map(|i| i.monthly_micros).sum(); | |
| 68 | self.db | |
| 69 | .prepare( | |
| 70 | "INSERT INTO cf_subscriptions (id, monthly_micros, detail, read_at) VALUES ('current', ?1, ?2, ?3) | |
| 71 | ON CONFLICT (id) DO UPDATE SET monthly_micros = excluded.monthly_micros, detail = excluded.detail, read_at = excluded.read_at", | |
| 72 | ) | |
| 73 | .bind(&[(total as f64).into(), serde_json::to_string(&items)?.into(), rfc3339(now_ms()).into()])? | |
| 74 | .run() | |
| 75 | .await?; | |
| 76 | Ok(items.len()) | |
| 77 | } | |
| 78 | ||
| 79 | /// Cloudflare's subscriptions as last read, else the estimate. | |
| 80 | pub(crate) async fn fixed_monthly(&self, estimate: i64) -> Result<Fixed> { | |
| 81 | #[derive(Deserialize)] | |
| 82 | struct Row { | |
| 83 | monthly_micros: i64, | |
| 84 | detail: String, | |
| 85 | read_at: String, | |
| 86 | } | |
| 87 | let row = self | |
| 88 | .db | |
| 89 | .prepare("SELECT monthly_micros, detail, read_at FROM cf_subscriptions WHERE id = 'current'") | |
| 90 | .first::<Row>(None) | |
| 91 | .await?; | |
| 92 | Ok(match row { | |
| 93 | Some(row) => Fixed { | |
| 94 | monthly_micros: row.monthly_micros, | |
| 95 | source: "cloudflare", | |
| 96 | read_at: Some(row.read_at), | |
| 97 | items: serde_json::from_str(&row.detail).unwrap_or_default(), | |
| 98 | }, | |
| 99 | None => Fixed { monthly_micros: estimate, source: "estimate", read_at: None, items: Vec::new() }, | |
| 100 | }) | |
| 101 | } | |
| 102 | } | |
| 103 | ||
| 104 | #[cfg(test)] | |
| 105 | mod tests { | |
| 106 | use super::*; | |
| 107 | use serde_json::json; | |
| 108 | ||
| 109 | #[test] | |
| 110 | fn paid_subscriptions_come_to_a_month_each() { | |
| 111 | let body = json!({ "result": [ | |
| 112 | { "state": "Paid", "price": 5.0, "frequency": "monthly", "rate_plan": { "public_name": "Workers Paid" } }, | |
| 113 | { "state": "Paid", "price": 240.0, "frequency": "yearly", "product": { "name": "Zone Pro" } }, | |
| 114 | { "state": "Cancelled", "price": 20.0, "frequency": "monthly", "rate_plan": { "public_name": "Old" } }, | |
| 115 | { "state": "Paid", "price": 0.0, "frequency": "monthly", "rate_plan": { "public_name": "Free" } }, | |
| 116 | ] }); | |
| 117 | let items = monthly(&body); | |
| 118 | assert_eq!( | |
| 119 | items, | |
| 120 | vec![ | |
| 121 | FixedCost { name: "Workers Paid".into(), monthly_micros: 5_000_000 }, | |
| 122 | FixedCost { name: "Zone Pro".into(), monthly_micros: 20_000_000 }, | |
| 123 | ] | |
| 124 | ); | |
| 125 | } | |
| 126 | ||
| 127 | #[test] | |
| 128 | fn nothing_answered_is_nothing_paid() { | |
| 129 | assert!(monthly(&json!({ "result": [] })).is_empty()); | |
| 130 | assert!(monthly(&json!({})).is_empty()); | |
| 131 | } | |
| 132 | } |