feat: add admin billing system, SMTP email, rename to CloudLense
- Add payments, coupons, credits, invoices management to admin dashboard - Add 7 new admin tabs: Overview, Users, Orgs, Payments, Coupons, Credits, Invoices - Replace Resend with SMTP email via nodemailer (info@dk0.dev / mail.dk0.dev) - Add professional branded email templates (alerts, welcome, invoice, credit, password reset) - Add database migration for payments, coupons, coupon_redemptions, credit_transactions, invoices tables - Add credit_balance column to organizations - Add RLS policies for all new tables - Add 4 new API routes: /api/admin/{payments,coupons,credits,invoices} - Rename project from website-monitoring to CloudLense - Update all package.json names and README Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
-- Admin Dashboard: Payments, Coupons & Credits
|
||||
-- Adds tables for payment tracking, coupon/discount codes, and account credit system.
|
||||
-- Stripe fields are nullable (prepared for future integration).
|
||||
|
||||
-- ============================================================================
|
||||
-- PAYMENTS
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.payments (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
organization_id UUID NOT NULL REFERENCES public.organizations(id) ON DELETE CASCADE,
|
||||
amount INTEGER NOT NULL, -- amount in cents
|
||||
currency TEXT NOT NULL DEFAULT 'EUR',
|
||||
status TEXT NOT NULL DEFAULT 'completed' CHECK (status IN ('pending', 'completed', 'failed', 'refunded')),
|
||||
method TEXT CHECK (method IN ('manual', 'stripe', 'bank_transfer', 'paypal', 'other')),
|
||||
stripe_payment_id TEXT, -- nullable, for future Stripe integration
|
||||
description TEXT,
|
||||
notes TEXT, -- internal admin notes
|
||||
created_by UUID REFERENCES public.users(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_payments_organization_id ON public.payments(organization_id);
|
||||
CREATE INDEX idx_payments_created_at ON public.payments(created_at DESC);
|
||||
CREATE INDEX idx_payments_stripe_payment_id ON public.payments(stripe_payment_id) WHERE stripe_payment_id IS NOT NULL;
|
||||
|
||||
-- ============================================================================
|
||||
-- COUPONS
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.coupons (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
code TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
discount_type TEXT NOT NULL CHECK (discount_type IN ('percentage', 'fixed_amount', 'tier_upgrade')),
|
||||
discount_value INTEGER NOT NULL, -- percentage (0-100) or amount in cents
|
||||
currency TEXT NOT NULL DEFAULT 'EUR',
|
||||
max_redemptions INTEGER, -- NULL = unlimited
|
||||
current_redemptions INTEGER NOT NULL DEFAULT 0,
|
||||
applicable_tiers TEXT[] DEFAULT ARRAY['free', 'starter', 'professional'], -- which tiers can use this
|
||||
valid_from TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
valid_until TIMESTAMPTZ, -- NULL = no expiry
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
created_by UUID REFERENCES public.users(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX idx_coupons_code ON public.coupons(code);
|
||||
|
||||
-- ============================================================================
|
||||
-- COUPON REDEMPTIONS
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.coupon_redemptions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
coupon_id UUID NOT NULL REFERENCES public.coupons(id) ON DELETE CASCADE,
|
||||
organization_id UUID NOT NULL REFERENCES public.organizations(id) ON DELETE CASCADE,
|
||||
redeemed_by UUID REFERENCES public.users(id) ON DELETE SET NULL,
|
||||
discount_applied INTEGER NOT NULL, -- actual discount in cents
|
||||
redeemed_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_coupon_redemptions_coupon_id ON public.coupon_redemptions(coupon_id);
|
||||
CREATE INDEX idx_coupon_redemptions_organization_id ON public.coupon_redemptions(organization_id);
|
||||
|
||||
-- ============================================================================
|
||||
-- CREDIT TRANSACTIONS (ledger-style)
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.credit_transactions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
organization_id UUID NOT NULL REFERENCES public.organizations(id) ON DELETE CASCADE,
|
||||
amount INTEGER NOT NULL, -- positive = credit, negative = debit (in cents)
|
||||
balance_after INTEGER NOT NULL, -- running balance after this transaction
|
||||
type TEXT NOT NULL CHECK (type IN ('credit', 'debit')),
|
||||
reason TEXT NOT NULL, -- e.g. 'manual_credit', 'coupon_redemption', 'payment', 'usage_charge', 'refund', 'adjustment'
|
||||
reference_id UUID, -- optional reference to payment/coupon/invoice
|
||||
notes TEXT,
|
||||
created_by UUID REFERENCES public.users(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_credit_transactions_organization_id ON public.credit_transactions(organization_id);
|
||||
CREATE INDEX idx_credit_transactions_created_at ON public.credit_transactions(created_at DESC);
|
||||
|
||||
-- ============================================================================
|
||||
-- INVOICES
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.invoices (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
invoice_number TEXT NOT NULL UNIQUE,
|
||||
organization_id UUID NOT NULL REFERENCES public.organizations(id) ON DELETE CASCADE,
|
||||
amount INTEGER NOT NULL, -- total in cents
|
||||
currency TEXT NOT NULL DEFAULT 'EUR',
|
||||
status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'sent', 'paid', 'overdue', 'cancelled', 'refunded')),
|
||||
items JSONB NOT NULL DEFAULT '[]'::jsonb, -- line items: [{description, quantity, unit_price, total}]
|
||||
stripe_invoice_id TEXT, -- nullable, for future Stripe integration
|
||||
due_date TIMESTAMPTZ,
|
||||
paid_at TIMESTAMPTZ,
|
||||
notes TEXT,
|
||||
created_by UUID REFERENCES public.users(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_invoices_organization_id ON public.invoices(organization_id);
|
||||
CREATE INDEX idx_invoices_status ON public.invoices(status);
|
||||
CREATE INDEX idx_invoices_invoice_number ON public.invoices(invoice_number);
|
||||
|
||||
-- ============================================================================
|
||||
-- Add credit_balance column to organizations
|
||||
-- ============================================================================
|
||||
|
||||
ALTER TABLE public.organizations
|
||||
ADD COLUMN IF NOT EXISTS credit_balance INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
-- ============================================================================
|
||||
-- RLS Policies (admin-only access)
|
||||
-- ============================================================================
|
||||
|
||||
ALTER TABLE public.payments ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.coupons ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.coupon_redemptions ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.credit_transactions ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.invoices ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Admins (service role) can do everything; these policies allow authenticated admin access
|
||||
CREATE POLICY "Admin full access on payments" ON public.payments
|
||||
FOR ALL USING (
|
||||
EXISTS (SELECT 1 FROM public.users WHERE id = auth.uid() AND role IN ('owner', 'admin'))
|
||||
);
|
||||
|
||||
CREATE POLICY "Admin full access on coupons" ON public.coupons
|
||||
FOR ALL USING (
|
||||
EXISTS (SELECT 1 FROM public.users WHERE id = auth.uid() AND role IN ('owner', 'admin'))
|
||||
);
|
||||
|
||||
CREATE POLICY "Admin full access on coupon_redemptions" ON public.coupon_redemptions
|
||||
FOR ALL USING (
|
||||
EXISTS (SELECT 1 FROM public.users WHERE id = auth.uid() AND role IN ('owner', 'admin'))
|
||||
);
|
||||
|
||||
CREATE POLICY "Admin full access on credit_transactions" ON public.credit_transactions
|
||||
FOR ALL USING (
|
||||
EXISTS (SELECT 1 FROM public.users WHERE id = auth.uid() AND role IN ('owner', 'admin'))
|
||||
);
|
||||
|
||||
CREATE POLICY "Admin full access on invoices" ON public.invoices
|
||||
FOR ALL USING (
|
||||
EXISTS (SELECT 1 FROM public.users WHERE id = auth.uid() AND role IN ('owner', 'admin'))
|
||||
);
|
||||
|
||||
-- Org members can view their own organization's data (read-only)
|
||||
CREATE POLICY "Org members can view own payments" ON public.payments
|
||||
FOR SELECT USING (
|
||||
organization_id IN (
|
||||
SELECT organization_id FROM public.users WHERE id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "Org members can view own invoices" ON public.invoices
|
||||
FOR SELECT USING (
|
||||
organization_id IN (
|
||||
SELECT organization_id FROM public.users WHERE id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "Org members can view own credit transactions" ON public.credit_transactions
|
||||
FOR SELECT USING (
|
||||
organization_id IN (
|
||||
SELECT organization_id FROM public.users WHERE id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
-- Anyone can view active coupons (for redemption)
|
||||
CREATE POLICY "Anyone can view active coupons" ON public.coupons
|
||||
FOR SELECT USING (is_active = true);
|
||||
Reference in New Issue
Block a user