hazo_pay
v0.4.3
Published
Batteries-included Stripe billing for hazo apps: pricing UI, subscriptions, lifetime, coupons, dunning, refunds, invoices, tax, metering.
Maintainers
Readme
hazo_pay
Batteries-included Stripe billing for hazo apps — pricing UI, subscriptions, lifetime
purchases, coupons, dunning/grace, refunds, invoices, tax, and metering. Stripe is the
source of truth; hazo_pay keeps a webhook-synced mirror for fast rendering and feature
gating.
See design/PRD.md and design/architecture.md for the full spec.
Status
Phase 3 (coupons & admin). Complete billing foundation with checkout, subscriptions, entitlements, coupons, promotion codes, refunds, invoices, tax, metering, and webhooks. Admin UI for subscriptions, customers, coupons, and refunds. See CHANGELOG.md for version history and design/PRD.md for the full product spec.
Entry points
hazo_pay/client— pure, browser-safe logic (no Node/Stripe):formatMoney,toMinorUnits,computeOrderTotal(discount before tax),couponDiscount,validateCoupon/effectiveMaxRedemptions,resolveEntitlement,buildPricingModel. Plus React components:PricingTable,BillingPanel,BillingAdminPanel,CouponAdminPanel, and theUpgradePromptProvider/useUpgradePrompt/UpgradePromptDialogupgrade-prompt trio (see below).hazo_pay(server) — re-exports the client surface plusloadPayConfig(secrets from env) andcreatePayServer({ onEntitlementChange })(the FR-15 entitlement-change seam +getEntitlement/hasActivePlan).
Test-app
A Next.js test-app demonstrates each scenario in a shadcn sidebar, with an /autotest
route running the browser test suite via hazo_ui/test-harness.
npm run dev:test-app # builds the package, then starts the test-app on :3300
PORT=3091 npm run dev:test-app # custom portSidebar pages: Overview, Pricing model, Entitlement, Coupons & totals, Billing panel, Admin, Webhooks, Coupons Admin, Upgrade prompt, Autotest, Live Stripe.
Upgrade prompt
UpgradePromptProvider is a headless-checkout upsell dialog: mount it once near the app
root, then call useUpgradePrompt().showUpgradePrompt({ feature, requiredTier }) from
anywhere beneath it to open a dialog offering monthly/yearly/lifetime price options for the
gated tier. It never initiates checkout itself — you supply resolveCheckout(priceId, mode)
and decide how to start the Stripe flow (redirect, server action, API call, etc.).
// app/providers.tsx
'use client';
import { UpgradePromptProvider } from 'hazo_pay/client';
import { pricingConfig } from './pricing-config';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<UpgradePromptProvider
pricingConfig={pricingConfig}
resolveCheckout={async (priceId, mode) => {
const res = await fetch('/api/checkout', {
method: 'POST',
body: JSON.stringify({ priceId, mode }),
});
const { url } = await res.json();
window.location.href = url;
}}
>
{children}
</UpgradePromptProvider>
);
}// anywhere beneath the provider
'use client';
import { useUpgradePrompt } from 'hazo_pay/client';
export function BackgroundsGate() {
const { showUpgradePrompt } = useUpgradePrompt();
return (
<button onClick={() => showUpgradePrompt({ feature: 'composer_backgrounds', requiredTier: 'pro' })}>
Unlock backgrounds
</button>
);
}UpgradePromptDialog (rendered internally by the provider) is theme-neutral — it's built on
hazo_ui's HazoUiDialog/Button primitives and shadcn tokens (bg-primary,
text-muted-foreground, …), so it inherits the consuming app's brand palette. All copy is
overridable via the optional labels prop (headline, description, monthlyLabel,
yearlyLabel, lifetimeLabel, savingsLabel, ctaLabel, closeLabel, noOptionsLabel).
Admin Panel
Mounting BillingAdminPanel in a Next.js page
// app/admin/billing/page.tsx
import { BillingAdminPanel } from 'hazo_pay/client';
import { createListSubscriptions, createGetCustomerDetail } from 'hazo_pay/server';
export default function BillingAdminPage() {
return (
<BillingAdminPanel
getSubscriptions={createListSubscriptions({ getHazoConnect })}
getCustomerDetail={createGetCustomerDetail({ getHazoConnect })}
cancelSubscription={createCancelSubscription({ getHazoConnect, stripe })}
changePlan={createChangePlan({ getHazoConnect, stripe })}
issueRefund={createIssueRefund({ getHazoConnect, stripe })}
/>
);
}Wiring into hazo_admin
// admin/manifest.tsx
import { billingAdminSection } from 'hazo_pay/admin';
import { BillingAdminPanel } from 'hazo_pay/client';
// import type { AdminManifest } from 'hazo_admin/index.ui';
const manifest /* : AdminManifest */ = {
sections: [
billingAdminSection({
component: <BillingAdminPanel getSubscriptions={...} ... />,
// optional overrides:
// path: '/admin/billing',
// label: 'Billing',
// permission: 'hazo_pay.admin.firm',
// group: 'billing',
// order: 100,
}),
],
};billingAdminSection is also available from hazo_pay/client for client bundles.
Admin API route factories
Mount these in your Next.js app/api/admin/billing/ directory:
| Factory | Import | Route |
|---|---|---|
| createListSubscriptionsHandler | hazo_pay/api | GET /api/admin/billing/subscriptions |
| createGetCustomerDetailHandler | hazo_pay/api | GET /api/admin/billing/customers/[id] |
| createCancelSubscriptionHandler | hazo_pay/api | POST /api/admin/billing/cancel |
| createChangePlanHandler | hazo_pay/api | POST /api/admin/billing/change-plan |
| createIssueRefundHandler | hazo_pay/api | POST /api/admin/billing/refund |
Permission strings
hazo_pay.admin.all— full billing admin (refunds, cancellations, plan changes)hazo_pay.admin.firm— firm-scoped admin (default forbillingAdminSection; cannot act on other firms' subscriptions)
