boone-studios / cha-ching
Drop-in Laravel package that alerts you on Slack, Discord, or SMS when someone subscribes or purchases.
Requires
- php: ^8.2
- illuminate/events: ^11.0|^12.0|^13.0
- illuminate/http: ^11.0|^12.0|^13.0
- illuminate/notifications: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
Requires (Dev)
- mockery/mockery: ^1.6
- orchestra/testbench: ^9.0|^10.0|^11.0
- pestphp/pest: ^3.8
- pestphp/pest-plugin-laravel: ^3.2
- phpunit/phpunit: ^11.0
Suggests
- laravel/cashier: Enable automatic revenue alerts from Stripe Cashier webhooks.
README
Drop-in Laravel package that pings you on Slack, Discord, and/or Twilio SMS whenever someone subscribes or purchases on your platform.
Works with Cashier, Spark, Paddle, custom billing — anything. You fire an event (or call a facade); Cha-Ching handles delivery.
Coding agents: start with AGENTS.md (full integration guide) and llms.txt (capability index). Do not reverse-engineer APIs from source when those docs cover the task.
Requirements
- PHP 8.2+
- Laravel 11, 12, or 13
Installation
composer require boone-studios/cha-ching php artisan vendor:publish --tag=cha-ching-config
Auto-discovery registers the service provider and ChaChing facade.
Configuration
Set the channels you want. Any channel with credentials will be auto-enabled; you can also set channels explicitly in config/cha-ching.php.
CHA_CHING_ENABLED=true # Slack incoming webhook CHA_CHING_SLACK_WEBHOOK=https://hooks.slack.com/services/... # Discord webhook CHA_CHING_DISCORD_WEBHOOK=https://discord.com/api/webhooks/... # Twilio SMS (you/your team — not the customer) CHA_CHING_TWILIO_SID=ACxxxxxxxx CHA_CHING_TWILIO_TOKEN=your-auth-token CHA_CHING_TWILIO_FROM=+15551234567 CHA_CHING_TWILIO_TO=+15557654321,+15559876543 # Queue (default: true — run a queue worker in production) CHA_CHING_QUEUE=true
| Variable | Purpose |
|---|---|
CHA_CHING_ENABLED |
Master switch (false = no-op) |
CHA_CHING_SLACK_WEBHOOK |
Slack incoming webhook URL |
CHA_CHING_DISCORD_WEBHOOK |
Discord channel webhook URL |
CHA_CHING_TWILIO_* |
Twilio credentials + recipient list |
CHA_CHING_QUEUE |
Queue the listener (recommended in production) |
CHA_CHING_CASHIER_ENABLED |
Auto-listen to Cashier Stripe webhooks when Cashier is installed (default true) |
CHA_CHING_CASHIER_RENEWALS |
Also alert on subscription renewals (default false) |
CHA_CHING_CUSTOMER_URL |
Admin URL template with {id} for the billable model |
Usage
Facade (simplest)
use BooneStudios\ChaChing\AlertType; use BooneStudios\ChaChing\Data\RevenueAlert; use BooneStudios\ChaChing\Facades\ChaChing; ChaChing::subscription(new RevenueAlert( type: AlertType::Subscription, customerName: $user->name, customerEmail: $user->email, productName: 'Pro', amountInCents: 2900, currency: 'USD', interval: 'month', source: 'stripe', url: route('admin.users.show', $user), metadata: ['plan_id' => 'price_pro_monthly'], )); ChaChing::purchase(new RevenueAlert( type: AlertType::Purchase, customerName: $user->name, customerEmail: $user->email, productName: 'Lifetime Pack', amountInCents: 9900, source: 'paddle', )); // Or route by the alert's type: ChaChing::alert($alert);
Events
use BooneStudios\ChaChing\Events\SubscriptionCreated; use BooneStudios\ChaChing\Events\PurchaseCompleted; event(new SubscriptionCreated($alert)); event(new PurchaseCompleted($alert));
Both paths share the same listener and channels.
Laravel Cashier (Stripe) auto-listeners
If laravel/cashier is installed, Cha-Ching automatically listens to Cashier's WebhookHandled event and maps Stripe webhooks into revenue alerts. No Cashier dependency is required for the package itself — the integration only boots when Cashier is present.
What fires alerts
| Stripe event | Alert | Notes |
|---|---|---|
customer.subscription.created |
Subscription | New subs (Checkout or API) |
checkout.session.completed |
Purchase | Only mode=payment + paid (subscription mode skipped to avoid dupes) |
invoice.payment_succeeded (manual) |
Purchase | One-off invoices / charges |
invoice.payment_succeeded (subscription_cycle) |
Subscription (renewal) | Off by default |
invoice.payment_succeeded (subscription_create) |
— | Skipped (covered by subscription.created) |
Setup
- Install Cashier and point Stripe webhooks at Cashier as usual (
/stripe/webhook). - Require Cha-Ching — auto-discovery is enough.
- Optional env:
CHA_CHING_CASHIER_ENABLED=true CHA_CHING_CASHIER_RENEWALS=false # Deep link to the billable record in your admin ({id} = local primary key) CHA_CHING_CUSTOMER_URL="${APP_URL}/nova/resources/users/{id}"
When a webhook includes a Stripe customer id, Cha-Ching resolves the billable model via config('cashier.model') (fallback App\Models\User) using the stripe_id column, and prefers that model's name / email on the alert.
Disable auto-listeners
CHA_CHING_CASHIER_ENABLED=false
You can still call ChaChing::subscription() / ::purchase() manually from your own listeners.
Manual mapping (non-Cashier or custom)
use BooneStudios\ChaChing\AlertType; use BooneStudios\ChaChing\Data\RevenueAlert; use BooneStudios\ChaChing\Facades\ChaChing; ChaChing::subscription(new RevenueAlert( type: AlertType::Subscription, customerName: $user->name, customerEmail: $user->email, productName: 'Pro', amountInCents: 2900, source: 'stripe', ));
Message previews
Slack / Discord
💰 New subscription
Customer: Alice Smith <alice@example.com>
Plan: Pro · $29.00/month
Source: stripe
Link: https://app.example.com/admin/users/1
SMS
Cha-Ching: Alice Smith subscribed to Pro — $29.00/month
Queueing
The SendRevenueAlert listener is queueable so checkout requests are not blocked by Slack/Discord/Twilio latency.
CHA_CHING_QUEUE=true(default) — uses your app queue connection (run a worker in production).CHA_CHING_QUEUE=false— forces thesyncconnection so alerts still send without a worker.
Optional overrides:
CHA_CHING_QUEUE_CONNECTION=redis CHA_CHING_QUEUE_NAME=notifications
Smoke-test after deploy
When you wire a new project, send a fake alert without a real sale:
php artisan cha-ching:test php artisan cha-ching:test --type=subscription php artisan cha-ching:test --channel=slack
The command forces sync delivery so you do not need a queue worker just to verify credentials.
Celebrations & localization
Chat/SMS openers pick a random “you got paid” line from the app locale (config('app.locale')).
Ship with en, es, fr, and de. Publish and customize:
php artisan vendor:publish --tag=cha-ching-lang
Edit lang/vendor/cha-ching/{locale}/messages.php — especially the celebrations and celebrations_sms arrays. Placeholders: :amount, :customer, :product.
CHA_CHING_RANDOM_CELEBRATIONS=true
App name (multi-project)
Messages are prefixed with the host app name so you can tell projects apart:
💰 Canderbox: You got paid!
Defaults to config('app.name'). Override or disable:
CHA_CHING_APP_NAME=Canderbox CHA_CHING_SHOW_APP_NAME=true
Webhook deduplication
Stripe retries. Cashier auto-listeners claim each Stripe event id (evt_…) in cache for 24h so the same event does not double-ping you.
CHA_CHING_DEDUPE=true CHA_CHING_DEDUPE_TTL=86400 # CHA_CHING_DEDUPE_STORE=redis # optional cache store
Use a shared cache (Redis) in production so multiple app servers share the claim.
Behavior notes
- Operator alerts only — messages go to your Slack/Discord/phones, not the customer.
- Missing credentials — that channel is skipped with a warning; others still send.
- Channel failures — errors are logged per channel and do not block the rest.
- No channels configured — silent no-op (debug log).
- Metadata on chat messages — off by default (
CHA_CHING_INCLUDE_METADATA); Stripe ids stay in structured logs/metadata only.
Testing
Tests are written with Pest.
composer install composer test # or ./vendor/bin/pest
License
MIT