Cashier (Payments)

Cashier is a multi-gateway billing plugin for Nimbus, modeled on Laravel Cashier but gateway-agnostic. One app can register several payment gateways (Stripe, Razorpay, PayU, …), choose a default, select a gateway per request, verify webhooks with signature checks, and gate access behind a paywall.

Installation

Register the plugin in bin/server.go. With FromEnv it auto-registers any gateway whose credentials are present in the environment:

import (
    "github.com/CodeSyncr/nimbus/plugins/cashier"
    "github.com/CodeSyncr/nimbus/plugins/cashier/events"
)

app.Use(cashier.NewPlugin(cashier.Config{
    FromEnv: true,        // register gateways whose env keys are set
    Default: "razorpay",  // default gateway
    OnWebhook: func(e cashier.WebhookEvent) error {
        switch events.Normalize(e) {
        case events.PaymentSucceeded:      // grant paywall access
        case events.SubscriptionCancelled: // revoke access
        }
        return nil
    },
}))

The plugin also runs migrations for its cashier_transactions and cashier_subscriptions tables, and binds cashier, cashier.manager, and cashier.paywall into the container.

Gateways & environment variables

GatewayRegionEnv vars
StripeInternationalSTRIPE_KEY, STRIPE_WEBHOOK_SECRET
RazorpayIndiaRAZORPAY_KEY_ID, RAZORPAY_KEY_SECRET, RAZORPAY_WEBHOOK_SECRET
PayUIndiaPAYU_MERCHANT_KEY, PAYU_MERCHANT_SALT, PAYU_PAYMENT_URL

The default gateway is Config.Default, else PAYMENTS_DEFAULT_GATEWAY, else the first registered. Register gateways explicitly instead of FromEnv when you need custom config:

m := cashier.NewGatewayManager()
m.Register(gateways.NewRazorpay(gateways.RazorpayConfig{KeyID: id, KeySecret: secret, WebhookSecret: whs}))
m.Register(gateways.NewStripe(gateways.StripeConfig{SecretKey: sk, WebhookSecret: whs}))
m.SetDefault("razorpay")
app.Use(cashier.NewPlugin(cashier.Config{Manager: m}))

Starting a payment

Resolve the facade from the container and create a charge. Pass an empty gateway name to use the default, or a specific name to route to a chosen gateway:

cash := app.Container.MustMake("cashier").(*cashier.Cashier)

// Default gateway (e.g. Razorpay in India):
charge, err := cash.Charge(ctx, "", cashier.ChargeParams{
    Amount:   49900,       // smallest unit — paise for INR, cents for USD
    Currency: "INR",
    Reference: "order_123",
    CustomerEmail: "user@example.com",
})

// A specific gateway for an international customer:
charge, err = cash.Charge(ctx, "stripe", cashier.ChargeParams{
    Mode: "subscription", PriceID: "price_abc",
    SuccessURL: "https://app/ok", CancelURL: "https://app/cancel",
})

Stripe returns charge.RedirectURL (redirect the browser). Razorpay/PayU return an order/txn id the frontend opens via the checkout widget (see views/checkout.nimbus).

Webhooks

The plugin mounts a signature-verified endpoint per gateway at /payments/<gateway>/webhook (configurable via WebhookPrefix). Each gateway verifies its own signature (Stripe/Razorpay HMAC-SHA256; PayU SHA-512 hash) before your OnWebhook runs. Point each provider's dashboard webhook at the matching URL and set the corresponding *_WEBHOOK_SECRET. Use events.Normalize(evt) to branch on canonical events across all gateways.

Paywall

Grant access on a successful payment and gate routes with RequirePlan (returns HTTP 402 when denied). The default store is in-memory; implement EntitlementStore for a database-backed paywall.

pw := app.Container.MustMake("cashier.paywall").(*cashier.Paywall)

// In OnWebhook, on events.PaymentSucceeded:
pw.Grant(userID, "pro", time.Now().Add(30*24*time.Hour))

// Gate a route group:
subject := func(c *http.Context) string { return currentUserID(c) }
app.Router.Group("/pro", pw.RequirePlan("pro", subject)).Get("/report", handler)

// Anywhere:
if pw.HasAccess(userID, "pro") { /* ... */ }

Adding a gateway

Drop a new file in plugins/cashier/gateways/ implementing contracts.PaymentGateway (Name, CreateCharge, VerifyPayment, VerifyWebhook) and register it in the manager. PhonePe, Cashfree, Paytm, and PayPal slot in the same way.