379d9aa13c
- 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>
181 lines
8.4 KiB
SQL
181 lines
8.4 KiB
SQL
-- 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);
|