boone-studios/cha-ching

Drop-in Laravel package that alerts you on Slack, Discord, or SMS when someone subscribes or purchases.

Maintainers

Package info

github.com/boone-studios/cha-ching

pkg:composer/boone-studios/cha-ching

Transparency log

Statistics

Installs: 19

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.1 2026-08-11 16:19 UTC

This package is auto-updated.

Last update: 2026-08-11 16:19:38 UTC


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

  1. Install Cashier and point Stripe webhooks at Cashier as usual (/stripe/webhook).
  2. Require Cha-Ching — auto-discovery is enough.
  3. 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 the sync connection 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