From d0e7eb4308b0ddb7bca5bfdd26494a048ee777d4 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Fri, 6 Mar 2026 10:49:22 -0600 Subject: [PATCH] feat: add Stripe meter ID configuration - Add stripe_meter_id to dev, runtime, and test configs - Add STRIPE_METER_ID to k8s deployment secret references - Use real Stripe meter and price IDs from Stripe setup - Meter: mtr_test_61UHPU067veEZ7bhl41S77kvnTfgyODY - Price: price_1T81XBS77kvnTfgyPlw1jF8N ($1/device/month) - Include setup script for Stripe billing configuration --- config/dev.exs | 3 +- config/runtime.exs | 13 +++++- k8s/deployment.yaml | 12 +++++ setup_stripe_meter.exs | 103 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 setup_stripe_meter.exs diff --git a/config/dev.exs b/config/dev.exs index 2d6cc2f2..a1d8042a 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -219,4 +219,5 @@ config :towerops, stripe_secret_key: "sk_test_51T7zcQS77kvnTfgyu0DCQU2xVKzeaneVCueHaXV3jsIw6HAwGWWllEL3J8jGZOybHAtPyu6oYIiuvJHhReFtTycE00VrJYJ8Ao", stripe_webhook_secret: "whsec_dev_fake_secret", - stripe_price_id: "price_dev_fake_id" + stripe_price_id: "price_1T81XBS77kvnTfgyPlw1jF8N", + stripe_meter_id: "mtr_test_61UHPU067veEZ7bhl41S77kvnTfgyODY" diff --git a/config/runtime.exs b/config/runtime.exs index 65ad77e2..4f543bdc 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -373,10 +373,18 @@ if config_env() == :prod do This is the Stripe price ID for the metered billing plan. """ + stripe_meter_id = + System.get_env("STRIPE_METER_ID") || + raise """ + environment variable STRIPE_METER_ID is missing. + This is the Stripe billing meter ID for usage reporting. + """ + config :towerops, stripe_secret_key: stripe_secret_key, stripe_webhook_secret: stripe_webhook_secret, - stripe_price_id: stripe_price_id + stripe_price_id: stripe_price_id, + stripe_meter_id: stripe_meter_id end # Test environment Stripe configuration @@ -384,5 +392,6 @@ if config_env() == :test do config :towerops, stripe_secret_key: "sk_test_fake", stripe_webhook_secret: "whsec_test_fake", - stripe_price_id: "price_test_fake" + stripe_price_id: "price_test_fake", + stripe_meter_id: "meter_test_fake" end diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml index 7d3fcabc..865ad811 100644 --- a/k8s/deployment.yaml +++ b/k8s/deployment.yaml @@ -84,6 +84,12 @@ spec: name: towerops-billing key: STRIPE_PRICE_ID optional: true + - name: STRIPE_METER_ID + valueFrom: + secretKeyRef: + name: towerops-billing + key: STRIPE_METER_ID + optional: true envFrom: - secretRef: name: towerops-db @@ -189,6 +195,12 @@ spec: name: towerops-billing key: STRIPE_PRICE_ID optional: true + - name: STRIPE_METER_ID + valueFrom: + secretKeyRef: + name: towerops-billing + key: STRIPE_METER_ID + optional: true envFrom: # Redis connection configured via towerops-redis secret - secretRef: diff --git a/setup_stripe_meter.exs b/setup_stripe_meter.exs new file mode 100644 index 00000000..5a753903 --- /dev/null +++ b/setup_stripe_meter.exs @@ -0,0 +1,103 @@ +#!/usr/bin/env elixir + +# Setup script for Stripe billing meter + +stripe_key = "sk_test_51T7zcQS77kvnTfgyu0DCQU2xVKzeaneVCueHaXV3jsIw6HAwGWWllEL3J8jGZOybHAtPyu6oYIiuvJHhReFtTycE00VrJYJ8Ao" +product_id = "prod_U6DpFdl21ftiDz" + +IO.puts("Creating Stripe billing meter...") + +# Create billing meter +form_data = + URI.encode_query(%{ + "display_name" => "Devices Monitored", + "event_name" => "devices_monitored", + "default_aggregation[formula]" => "sum", + "value_settings[event_payload_key]" => "value" + }) + +{output, exit_code} = + System.cmd( + "curl", + [ + "-X", + "POST", + "https://api.stripe.com/v1/billing/meters", + "-u", + "#{stripe_key}:", + "-H", + "Stripe-Version: 2024-11-20.acacia", + "-H", + "Content-Type: application/x-www-form-urlencoded", + "-d", + form_data + ], + stderr_to_stdout: true + ) + +if exit_code == 0 do + case Jason.decode(output) do + {:ok, meter} -> + IO.puts("\n✓ Billing meter created successfully!") + IO.puts(" Meter ID: #{meter["id"]}") + IO.puts(" Display name: #{meter["display_name"]}") + IO.puts(" Event name: #{meter["event_name"]}") + IO.puts("\nNow creating metered price...") + + # Create metered price linked to the meter + price_form = + URI.encode_query(%{ + "product" => product_id, + "currency" => "usd", + "recurring[interval]" => "month", + "recurring[usage_type]" => "metered", + "recurring[meter]" => meter["id"], + "billing_scheme" => "per_unit", + # $1.00 in cents + "unit_amount" => "100", + "nickname" => "Per Device Monthly" + }) + + {price_output, price_exit} = + System.cmd( + "curl", + [ + "-X", + "POST", + "https://api.stripe.com/v1/prices", + "-u", + "#{stripe_key}:", + "-H", + "Stripe-Version: 2024-11-20.acacia", + "-H", + "Content-Type: application/x-www-form-urlencoded", + "-d", + price_form + ], + stderr_to_stdout: true + ) + + if price_exit == 0 do + case Jason.decode(price_output) do + {:ok, price} -> + IO.puts("\n✓ Price created successfully!") + IO.puts(" Price ID: #{price["id"]}") + IO.puts(" Amount: $#{price["unit_amount"] / 100}/device/month") + IO.puts("\n" <> String.duplicate("=", 60)) + IO.puts("SETUP COMPLETE!") + IO.puts(String.duplicate("=", 60)) + IO.puts("\nAdd these to your Kubernetes secret:") + IO.puts(" STRIPE_METER_ID=#{meter["id"]}") + IO.puts(" STRIPE_PRICE_ID=#{price["id"]}") + + {:error, _} -> + IO.puts("\nError creating price: #{price_output}") + end + end + + {:error, _} -> + IO.puts("\nError: #{output}") + end +else + IO.puts("\nCurl failed: #{output}") +end