Billing & Usage Cron Jobs #3724
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Billing & Usage Cron Jobs | |
| on: | |
| schedule: | |
| # Aggregate & Debit Usage - Every 15 minutes (Instant Billing) | |
| - cron: '*/15 * * * *' | |
| # Storage snapshots — midnight UTC every day (MUST run before aggregate-daily) | |
| - cron: '0 0 * * *' | |
| # Process billing cycle - 1st of each month at 3 AM UTC | |
| - cron: '0 3 1 * *' | |
| # Enforce grace periods - daily at 1 AM UTC | |
| - cron: '0 1 * * *' | |
| # Send reminders - daily at 9 AM UTC | |
| - cron: '0 9 * * *' | |
| # Expire trials - daily at 2 AM UTC | |
| - cron: '0 2 * * *' | |
| # Evaluate usage alerts - hourly at the 30th minute | |
| - cron: '30 * * * *' | |
| # Sync AI model pricing - every 30 minutes at :05 and :35 | |
| - cron: '5,35 * * * *' | |
| workflow_dispatch: | |
| inputs: | |
| job: | |
| description: 'Job to run' | |
| required: true | |
| default: 'enforce-grace' | |
| type: choice | |
| options: | |
| - process-cycle | |
| - enforce-grace | |
| - expire-trials | |
| - send-reminders | |
| - evaluate-alerts | |
| - aggregate-daily | |
| - storage-snapshot | |
| - sync-pricing | |
| # Note: All billing operations use USD as the primary currency. | |
| # Razorpay payments are processed in INR with live conversion rates. | |
| env: | |
| API_BASE_URL: ${{ secrets.NEXT_PUBLIC_APP_URL }} | |
| CRON_SECRET: ${{ secrets.CRON_SECRET }} | |
| jobs: | |
| billing-cron: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Determine job to run | |
| id: job | |
| run: | | |
| if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then | |
| echo "job=${{ github.event.inputs.job }}" >> $GITHUB_OUTPUT | |
| else | |
| # Determine based on schedule | |
| HOUR=$(date -u +%H) | |
| MINUTE=$(date -u +%M) | |
| DAY=$(date -u +%d) | |
| # 1. MAINTENANCE JOBS (Prioritized at specific hours) | |
| if [ "$MINUTE" == "00" ]; then | |
| if [ "$HOUR" == "00" ]; then | |
| echo "job=storage-snapshot" >> $GITHUB_OUTPUT | |
| elif [ "$HOUR" == "01" ]; then | |
| echo "job=enforce-grace" >> $GITHUB_OUTPUT | |
| elif [ "$HOUR" == "02" ]; then | |
| echo "job=expire-trials" >> $GITHUB_OUTPUT | |
| elif [ "$HOUR" == "03" ] && [ "$DAY" == "01" ]; then | |
| echo "job=process-cycle" >> $GITHUB_OUTPUT | |
| elif [ "$HOUR" == "09" ]; then | |
| echo "job=send-reminders" >> $GITHUB_OUTPUT | |
| else | |
| # Default to aggregation at top of most hours | |
| echo "job=aggregate-daily" >> $GITHUB_OUTPUT | |
| fi | |
| # 2. EVALUATE ALERTS (Every hour at :30) | |
| elif [ "$MINUTE" == "30" ]; then | |
| echo "job=evaluate-alerts" >> $GITHUB_OUTPUT | |
| # 3. INSTANT AGGREGATION & DEBIT (Every 15, 45 minutes) | |
| elif [ "$MINUTE" == "15" ] || [ "$MINUTE" == "45" ]; then | |
| echo "job=aggregate-daily" >> $GITHUB_OUTPUT | |
| # 4. AI PRICING SYNC (Every 30 minutes at :05 and :35) | |
| elif [ "$MINUTE" == "05" ] || [ "$MINUTE" == "35" ]; then | |
| echo "job=sync-pricing" >> $GITHUB_OUTPUT | |
| else | |
| echo "job=none" >> $GITHUB_OUTPUT | |
| fi | |
| fi | |
| - name: Capture daily storage snapshots | |
| if: steps.job.outputs.job == 'storage-snapshot' | |
| run: | | |
| echo "📦 Capturing daily storage snapshots for all workspaces..." | |
| response=$(curl -s -w "\n%{http_code}" -X POST \ | |
| -H "Authorization: Bearer $CRON_SECRET" \ | |
| -H "Content-Type: application/json" \ | |
| "$API_BASE_URL/api/cron/storage/snapshot") | |
| http_code=$(echo "$response" | tail -n1) | |
| body=$(echo "$response" | sed '$d') | |
| echo "Response: $body" | |
| if [ "$http_code" != "200" ]; then | |
| echo "❌ Storage snapshot failed with status $http_code" | |
| exit 1 | |
| fi | |
| echo "✅ Storage snapshots captured successfully" | |
| - name: Run billing cycle processing | |
| if: steps.job.outputs.job == 'process-cycle' || steps.job.outputs.run_billing_cycle == 'true' | |
| run: | | |
| echo "📅 Running monthly billing cycle processing..." | |
| response=$(curl -s -w "\n%{http_code}" -X POST \ | |
| -H "Authorization: Bearer $CRON_SECRET" \ | |
| -H "Content-Type: application/json" \ | |
| "$API_BASE_URL/api/cron/billing/process-cycle") | |
| http_code=$(echo "$response" | tail -n1) | |
| body=$(echo "$response" | sed '$d') | |
| echo "Response: $body" | |
| if [ "$http_code" != "200" ]; then | |
| echo "❌ Failed with status $http_code" | |
| exit 1 | |
| fi | |
| echo "✅ Billing cycle processed successfully" | |
| - name: Run grace period enforcement | |
| if: steps.job.outputs.job == 'enforce-grace' | |
| run: | | |
| echo "⏰ Running daily grace period enforcement..." | |
| response=$(curl -s -w "\n%{http_code}" -X POST \ | |
| -H "Authorization: Bearer $CRON_SECRET" \ | |
| -H "Content-Type: application/json" \ | |
| "$API_BASE_URL/api/cron/billing/enforce-grace") | |
| http_code=$(echo "$response" | tail -n1) | |
| body=$(echo "$response" | sed '$d') | |
| echo "Response: $body" | |
| if [ "$http_code" != "200" ]; then | |
| echo "❌ Failed with status $http_code" | |
| exit 1 | |
| fi | |
| echo "✅ Grace periods enforced successfully" | |
| - name: Expire trials | |
| if: steps.job.outputs.job == 'expire-trials' | |
| run: | | |
| echo "⏳ Running daily trial expiry check..." | |
| response=$(curl -s -w "\n%{http_code}" -X POST \ | |
| -H "Authorization: Bearer $CRON_SECRET" \ | |
| -H "Content-Type: application/json" \ | |
| "$API_BASE_URL/api/cron/billing/expire-trials") | |
| http_code=$(echo "$response" | tail -n1) | |
| body=$(echo "$response" | sed '$d') | |
| echo "Response: $body" | |
| if [ "$http_code" != "200" ]; then | |
| echo "❌ Failed with status $http_code" | |
| exit 1 | |
| fi | |
| echo "✅ Trials expired successfully" | |
| - name: Send reminder emails | |
| if: steps.job.outputs.job == 'send-reminders' | |
| run: | | |
| echo "📧 Sending grace period reminder emails..." | |
| response=$(curl -s -w "\n%{http_code}" -X POST \ | |
| -H "Authorization: Bearer $CRON_SECRET" \ | |
| -H "Content-Type: application/json" \ | |
| "$API_BASE_URL/api/cron/billing/send-reminders") | |
| http_code=$(echo "$response" | tail -n1) | |
| body=$(echo "$response" | sed '$d') | |
| echo "Response: $body" | |
| if [ "$http_code" != "200" ]; then | |
| echo "❌ Failed with status $http_code" | |
| exit 1 | |
| fi | |
| echo "✅ Reminders sent successfully" | |
| - name: Evaluate usage alerts | |
| if: steps.job.outputs.job == 'evaluate-alerts' | |
| run: | | |
| echo "🔔 Evaluating usage alerts..." | |
| response=$(curl -s -w "\n%{http_code}" -X POST \ | |
| -H "Authorization: Bearer $CRON_SECRET" \ | |
| -H "Content-Type: application/json" \ | |
| "$API_BASE_URL/api/cron/billing/evaluate-alerts") | |
| http_code=$(echo "$response" | tail -n1) | |
| body=$(echo "$response" | sed '$d') | |
| echo "Response: $body" | |
| if [ "$http_code" != "200" ]; then | |
| echo "❌ Failed with status $http_code" | |
| exit 1 | |
| fi | |
| echo "✅ Usage alerts evaluated successfully" | |
| - name: Aggregate daily usage | |
| if: steps.job.outputs.job == 'aggregate-daily' | |
| run: | | |
| echo "📊 Aggregating yesterday's usage events into daily summary..." | |
| response=$(curl -s -w "\n%{http_code}" -X POST \ | |
| -H "Authorization: Bearer $CRON_SECRET" \ | |
| -H "Content-Type: application/json" \ | |
| "$API_BASE_URL/api/cron/usage/aggregate-daily") | |
| http_code=$(echo "$response" | tail -n1) | |
| body=$(echo "$response" | sed '$d') | |
| echo "Response: $body" | |
| if [ "$http_code" != "200" ]; then | |
| echo "❌ Failed with status $http_code" | |
| exit 1 | |
| fi | |
| echo "✅ Daily usage aggregated successfully" | |
| - name: Sync AI model pricing | |
| if: steps.job.outputs.job == 'sync-pricing' | |
| run: | | |
| echo "🤖 Syncing AI model pricing from Google sources..." | |
| response=$(curl -s -w "\n%{http_code}" -X POST \ | |
| -H "Authorization: Bearer $CRON_SECRET" \ | |
| -H "Content-Type: application/json" \ | |
| "$API_BASE_URL/api/cron/ai/sync-pricing") | |
| http_code=$(echo "$response" | tail -n1) | |
| body=$(echo "$response" | sed '$d') | |
| echo "Response: $body" | |
| if [ "$http_code" != "200" ]; then | |
| echo "❌ Failed with status $http_code" | |
| exit 1 | |
| fi | |
| echo "✅ AI model pricing synced successfully" | |
| - name: Skip (no matching job) | |
| if: steps.job.outputs.job == 'none' | |
| run: echo "⏭️ No job matched current schedule, skipping..." |