Add Stripe fee tracking on paid invoices + backfill function
- Store stripe_fee on invoices when webhook receives checkout.session.completed - Display Stripe fee and net received in InvoiceDetail when paid via Stripe - Add backfill-stripe-fees edge function to populate fee on existing paid invoices - Migration: add stripe_fee column to invoices table - Includes all pending portal changes (brand book, sign survey, task/project/company updates, etc.) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
|
||||
import Stripe from 'https://esm.sh/stripe@14?target=deno';
|
||||
|
||||
const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY')!, { apiVersion: '2023-10-16' });
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
};
|
||||
|
||||
serve(async (req) => {
|
||||
if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders });
|
||||
|
||||
const supabase = createClient(
|
||||
Deno.env.get('SUPABASE_URL')!,
|
||||
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!,
|
||||
);
|
||||
|
||||
// Find all paid invoices that don't have a stripe_fee yet
|
||||
const { data: invoices, error } = await supabase
|
||||
.from('invoices')
|
||||
.select('id, invoice_number, total')
|
||||
.eq('status', 'paid')
|
||||
.is('stripe_fee', null);
|
||||
|
||||
if (error) {
|
||||
return new Response(JSON.stringify({ error: error.message }), { status: 500, headers: corsHeaders });
|
||||
}
|
||||
|
||||
if (!invoices?.length) {
|
||||
return new Response(JSON.stringify({ message: 'No invoices missing fees', updated: 0 }), { headers: corsHeaders });
|
||||
}
|
||||
|
||||
const results: { invoice_id: string; invoice_number: string; status: string; fee?: number }[] = [];
|
||||
|
||||
for (const invoice of invoices) {
|
||||
try {
|
||||
// Search Stripe checkout sessions by invoice_id metadata
|
||||
const sessions = await stripe.checkout.sessions.search({
|
||||
query: `metadata["invoice_id"]:"${invoice.id}"`,
|
||||
limit: 1,
|
||||
expand: ['data.payment_intent.latest_charge.balance_transaction'],
|
||||
});
|
||||
|
||||
const session = sessions.data[0];
|
||||
if (!session) {
|
||||
results.push({ invoice_id: invoice.id, invoice_number: invoice.invoice_number, status: 'no_session' });
|
||||
continue;
|
||||
}
|
||||
|
||||
const paymentIntent = session.payment_intent as Stripe.PaymentIntent | null;
|
||||
const charge = paymentIntent?.latest_charge as Stripe.Charge | null;
|
||||
const balanceTx = charge?.balance_transaction as Stripe.BalanceTransaction | null;
|
||||
|
||||
if (balanceTx?.fee == null) {
|
||||
results.push({ invoice_id: invoice.id, invoice_number: invoice.invoice_number, status: 'no_balance_tx' });
|
||||
continue;
|
||||
}
|
||||
|
||||
const stripe_fee = balanceTx.fee / 100;
|
||||
await supabase.from('invoices').update({ stripe_fee }).eq('id', invoice.id);
|
||||
results.push({ invoice_id: invoice.id, invoice_number: invoice.invoice_number, status: 'updated', fee: stripe_fee });
|
||||
} catch (err) {
|
||||
results.push({ invoice_id: invoice.id, invoice_number: invoice.invoice_number, status: `error: ${err.message}` });
|
||||
}
|
||||
}
|
||||
|
||||
const updated = results.filter(r => r.status === 'updated').length;
|
||||
return new Response(
|
||||
JSON.stringify({ updated, total: invoices.length, results }),
|
||||
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } },
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
|
||||
import Stripe from 'https://esm.sh/stripe@14?target=deno';
|
||||
|
||||
const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY')!, { apiVersion: '2023-10-16' });
|
||||
const webhookSecret = Deno.env.get('STRIPE_WEBHOOK_SECRET')!;
|
||||
|
||||
serve(async (req) => {
|
||||
const body = await req.text();
|
||||
const sig = req.headers.get('stripe-signature');
|
||||
|
||||
let event: Stripe.Event;
|
||||
try {
|
||||
event = await stripe.webhooks.constructEventAsync(body, sig!, webhookSecret);
|
||||
} catch (err) {
|
||||
console.error('Webhook signature failed:', err.message);
|
||||
return new Response(`Webhook Error: ${err.message}`, { status: 400 });
|
||||
}
|
||||
|
||||
if (event.type === 'checkout.session.completed') {
|
||||
const session = event.data.object as Stripe.Checkout.Session;
|
||||
const invoice_id = session.metadata?.invoice_id;
|
||||
|
||||
if (invoice_id) {
|
||||
const supabase = createClient(
|
||||
Deno.env.get('SUPABASE_URL')!,
|
||||
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!,
|
||||
);
|
||||
|
||||
// Retrieve the Stripe processing fee from the balance transaction
|
||||
let stripe_fee: number | null = null;
|
||||
try {
|
||||
const paymentIntentId = session.payment_intent as string;
|
||||
if (paymentIntentId) {
|
||||
const paymentIntent = await stripe.paymentIntents.retrieve(paymentIntentId, {
|
||||
expand: ['latest_charge.balance_transaction'],
|
||||
});
|
||||
const charge = paymentIntent.latest_charge as Stripe.Charge | null;
|
||||
const balanceTx = charge?.balance_transaction as Stripe.BalanceTransaction | null;
|
||||
if (balanceTx?.fee != null) {
|
||||
stripe_fee = balanceTx.fee / 100;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to retrieve Stripe fee:', err.message);
|
||||
}
|
||||
|
||||
const updateData: Record<string, unknown> = { status: 'paid' };
|
||||
if (stripe_fee !== null) updateData.stripe_fee = stripe_fee;
|
||||
|
||||
await supabase.from('invoices').update(updateData).eq('id', invoice_id);
|
||||
}
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ received: true }), { status: 200 });
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Add stripe_fee column to store the Stripe processing fee on paid invoices
|
||||
alter table public.invoices add column if not exists stripe_fee numeric(10,2);
|
||||
@@ -0,0 +1,13 @@
|
||||
-- Add cover page fields to brand_books table
|
||||
ALTER TABLE brand_books
|
||||
ADD COLUMN IF NOT EXISTS creation_date date,
|
||||
ADD COLUMN IF NOT EXISTS revision_date date,
|
||||
ADD COLUMN IF NOT EXISTS customer_name text,
|
||||
ADD COLUMN IF NOT EXISTS customer_address text,
|
||||
ADD COLUMN IF NOT EXISTS project_logo_path text,
|
||||
ADD COLUMN IF NOT EXISTS client_logo_url text,
|
||||
ADD COLUMN IF NOT EXISTS client_contact_name text,
|
||||
ADD COLUMN IF NOT EXISTS client_contact_email text,
|
||||
ADD COLUMN IF NOT EXISTS client_contact_phone text,
|
||||
ADD COLUMN IF NOT EXISTS approved_date date,
|
||||
ADD COLUMN IF NOT EXISTS approval_notes text;
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Add template and inventory map fields to brand_books table
|
||||
ALTER TABLE brand_books
|
||||
ADD COLUMN IF NOT EXISTS template text DEFAULT 'fourge',
|
||||
ADD COLUMN IF NOT EXISTS inventory_map_path text;
|
||||
@@ -0,0 +1,29 @@
|
||||
-- Add brand book / cover page fields to companies
|
||||
ALTER TABLE companies
|
||||
ADD COLUMN IF NOT EXISTS address text,
|
||||
ADD COLUMN IF NOT EXISTS contact_name text,
|
||||
ADD COLUMN IF NOT EXISTS contact_email text,
|
||||
ADD COLUMN IF NOT EXISTS contact_phone text,
|
||||
ADD COLUMN IF NOT EXISTS client_logo_url text;
|
||||
|
||||
-- Create public bucket for company logos
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM storage.buckets WHERE id = 'company-logos') THEN
|
||||
INSERT INTO storage.buckets (id, name, public) VALUES ('company-logos', 'company-logos', true);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Storage policies for company-logos
|
||||
DROP POLICY IF EXISTS "Authenticated users can manage company logos" ON storage.objects;
|
||||
DROP POLICY IF EXISTS "Public can read company logos" ON storage.objects;
|
||||
|
||||
CREATE POLICY "Authenticated users can manage company logos"
|
||||
ON storage.objects FOR ALL
|
||||
TO authenticated
|
||||
USING (bucket_id = 'company-logos')
|
||||
WITH CHECK (bucket_id = 'company-logos');
|
||||
|
||||
CREATE POLICY "Public can read company logos"
|
||||
ON storage.objects FOR SELECT
|
||||
USING (bucket_id = 'company-logos');
|
||||
@@ -0,0 +1,123 @@
|
||||
-- ============================================================
|
||||
-- Migration: Add external role and project_members table
|
||||
-- Run this in Supabase → SQL Editor → Run
|
||||
-- ============================================================
|
||||
|
||||
-- 1. Update profiles.role check constraint to include 'external'
|
||||
do $$
|
||||
declare
|
||||
cname text;
|
||||
begin
|
||||
select constraint_name into cname
|
||||
from information_schema.table_constraints
|
||||
where table_schema = 'public'
|
||||
and table_name = 'profiles'
|
||||
and constraint_type = 'CHECK'
|
||||
and constraint_name ilike '%role%';
|
||||
if cname is not null then
|
||||
execute 'alter table public.profiles drop constraint ' || quote_ident(cname);
|
||||
end if;
|
||||
end;
|
||||
$$;
|
||||
|
||||
alter table public.profiles
|
||||
add constraint profiles_role_check check (role in ('team', 'client', 'external'));
|
||||
|
||||
-- 2. project_members table
|
||||
create table public.project_members (
|
||||
id uuid default gen_random_uuid() primary key,
|
||||
project_id uuid references public.projects(id) on delete cascade not null,
|
||||
profile_id uuid references public.profiles(id) on delete cascade not null,
|
||||
created_at timestamptz default now() not null,
|
||||
unique(project_id, profile_id)
|
||||
);
|
||||
alter table public.project_members enable row level security;
|
||||
|
||||
-- 3. Helper function
|
||||
create or replace function public.is_external()
|
||||
returns boolean as $$
|
||||
select get_my_role() = 'external';
|
||||
$$ language sql security definer stable;
|
||||
|
||||
-- 4. RLS: project_members
|
||||
create policy "Team all project_members" on public.project_members
|
||||
for all using (get_my_role() = 'team');
|
||||
create policy "External reads own memberships" on public.project_members
|
||||
for select using (profile_id = auth.uid());
|
||||
|
||||
-- 5. RLS: projects (external reads assigned only)
|
||||
create policy "External reads assigned projects" on public.projects
|
||||
for select using (
|
||||
get_my_role() = 'external' and
|
||||
id in (select project_id from public.project_members where profile_id = auth.uid())
|
||||
);
|
||||
|
||||
-- 6. RLS: tasks (external reads + updates assigned projects)
|
||||
create policy "External reads assigned tasks" on public.tasks
|
||||
for select using (
|
||||
get_my_role() = 'external' and
|
||||
project_id in (select project_id from public.project_members where profile_id = auth.uid())
|
||||
);
|
||||
create policy "External updates assigned tasks" on public.tasks
|
||||
for update using (
|
||||
get_my_role() = 'external' and
|
||||
project_id in (select project_id from public.project_members where profile_id = auth.uid())
|
||||
);
|
||||
|
||||
-- 7. RLS: submissions
|
||||
create policy "External reads assigned submissions" on public.submissions
|
||||
for select using (
|
||||
get_my_role() = 'external' and
|
||||
task_id in (
|
||||
select t.id from public.tasks t
|
||||
join public.project_members pm on pm.project_id = t.project_id
|
||||
where pm.profile_id = auth.uid()
|
||||
)
|
||||
);
|
||||
create policy "External inserts submissions" on public.submissions
|
||||
for insert with check (
|
||||
get_my_role() = 'external' and submitted_by = auth.uid()
|
||||
);
|
||||
|
||||
-- 8. RLS: submission_files
|
||||
create policy "External reads assigned submission_files" on public.submission_files
|
||||
for select using (
|
||||
get_my_role() = 'external' and
|
||||
submission_id in (
|
||||
select s.id from public.submissions s
|
||||
join public.tasks t on t.id = s.task_id
|
||||
join public.project_members pm on pm.project_id = t.project_id
|
||||
where pm.profile_id = auth.uid()
|
||||
)
|
||||
);
|
||||
create policy "External inserts submission_files" on public.submission_files
|
||||
for insert with check (get_my_role() = 'external');
|
||||
|
||||
-- 9. RLS: deliveries (read only)
|
||||
create policy "External reads assigned deliveries" on public.deliveries
|
||||
for select using (
|
||||
get_my_role() = 'external' and
|
||||
submission_id in (
|
||||
select s.id from public.submissions s
|
||||
join public.tasks t on t.id = s.task_id
|
||||
join public.project_members pm on pm.project_id = t.project_id
|
||||
where pm.profile_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
-- 10. RLS: delivery_files (read only)
|
||||
create policy "External reads assigned delivery_files" on public.delivery_files
|
||||
for select using (
|
||||
get_my_role() = 'external' and
|
||||
delivery_id in (
|
||||
select d.id from public.deliveries d
|
||||
join public.submissions s on s.id = d.submission_id
|
||||
join public.tasks t on t.id = s.task_id
|
||||
join public.project_members pm on pm.project_id = t.project_id
|
||||
where pm.profile_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
-- 11. RLS: profiles (external reads own profile only — already covered by existing policy)
|
||||
-- "Own profile select" policy already handles this with: id = auth.uid()
|
||||
-- No additional policy needed.
|
||||
@@ -0,0 +1,17 @@
|
||||
-- ============================================================
|
||||
-- Migration: Add price_type to company_prices (new vs revision)
|
||||
-- Run in Supabase → SQL Editor → Run
|
||||
-- ============================================================
|
||||
|
||||
-- Add price_type column (existing rows default to 'new')
|
||||
alter table public.company_prices
|
||||
add column price_type text not null default 'new'
|
||||
check (price_type in ('new', 'revision'));
|
||||
|
||||
-- Drop old unique constraint and add new one that includes price_type
|
||||
alter table public.company_prices
|
||||
drop constraint company_prices_company_id_service_type_key;
|
||||
|
||||
alter table public.company_prices
|
||||
add constraint company_prices_company_id_service_type_price_type_key
|
||||
unique (company_id, service_type, price_type);
|
||||
@@ -0,0 +1,15 @@
|
||||
-- ============================================================
|
||||
-- Migration: Add revision billing tracking
|
||||
-- Run in Supabase → SQL Editor → Run
|
||||
-- ============================================================
|
||||
|
||||
-- Add revision_type and invoiced to submissions
|
||||
alter table public.submissions
|
||||
add column revision_type text check (revision_type in ('fourge_error', 'client_revision'));
|
||||
|
||||
alter table public.submissions
|
||||
add column invoiced boolean not null default false;
|
||||
|
||||
-- Add submission_id to invoice_items (links a line item to a specific revision)
|
||||
alter table public.invoice_items
|
||||
add column submission_id uuid references public.submissions(id) on delete set null;
|
||||
+308
-15
@@ -9,6 +9,10 @@ create table public.companies (
|
||||
name text not null,
|
||||
phone text default '',
|
||||
address text default '',
|
||||
contact_name text,
|
||||
contact_email text,
|
||||
contact_phone text,
|
||||
client_logo_url text,
|
||||
created_at timestamptz default now() not null
|
||||
);
|
||||
alter table public.companies enable row level security;
|
||||
@@ -18,7 +22,7 @@ create table public.profiles (
|
||||
id uuid references auth.users on delete cascade primary key,
|
||||
name text not null default '',
|
||||
email text default '',
|
||||
role text not null check (role in ('team', 'client')) default 'client',
|
||||
role text not null check (role in ('team', 'client', 'external')) default 'client',
|
||||
company_id uuid references public.companies(id) on delete set null,
|
||||
created_at timestamptz default now() not null
|
||||
);
|
||||
@@ -56,6 +60,8 @@ create table public.submissions (
|
||||
task_id uuid references public.tasks(id) on delete cascade not null,
|
||||
version_number integer not null,
|
||||
type text not null check (type in ('initial', 'revision', 'amendment')) default 'initial',
|
||||
revision_type text check (revision_type in ('fourge_error', 'client_revision')),
|
||||
invoiced boolean not null default false,
|
||||
service_type text default '',
|
||||
deadline date,
|
||||
description text default '',
|
||||
@@ -95,13 +101,24 @@ create table public.delivery_files (
|
||||
);
|
||||
alter table public.delivery_files enable row level security;
|
||||
|
||||
-- Company Prices (per service type, per company)
|
||||
-- Project Members (external users assigned to specific projects)
|
||||
create table public.project_members (
|
||||
id uuid default gen_random_uuid() primary key,
|
||||
project_id uuid references public.projects(id) on delete cascade not null,
|
||||
profile_id uuid references public.profiles(id) on delete cascade not null,
|
||||
created_at timestamptz default now() not null,
|
||||
unique(project_id, profile_id)
|
||||
);
|
||||
alter table public.project_members enable row level security;
|
||||
|
||||
-- Company Prices (per service type, per company, per price type)
|
||||
create table public.company_prices (
|
||||
id uuid default gen_random_uuid() primary key,
|
||||
company_id uuid references public.companies(id) on delete cascade not null,
|
||||
service_type text not null,
|
||||
price numeric(10,2) default 0,
|
||||
unique(company_id, service_type)
|
||||
price_type text not null default 'new' check (price_type in ('new', 'revision')),
|
||||
unique(company_id, service_type, price_type)
|
||||
);
|
||||
alter table public.company_prices enable row level security;
|
||||
|
||||
@@ -115,6 +132,7 @@ create table public.invoices (
|
||||
due_date date,
|
||||
total numeric(10,2) default 0,
|
||||
notes text default '',
|
||||
stripe_fee numeric(10,2),
|
||||
created_by uuid references public.profiles(id) on delete set null,
|
||||
created_at timestamptz default now() not null
|
||||
);
|
||||
@@ -125,6 +143,7 @@ create table public.invoice_items (
|
||||
id uuid default gen_random_uuid() primary key,
|
||||
invoice_id uuid references public.invoices(id) on delete cascade not null,
|
||||
task_id uuid references public.tasks(id) on delete set null,
|
||||
submission_id uuid references public.submissions(id) on delete set null,
|
||||
description text not null,
|
||||
quantity numeric(10,2) default 1,
|
||||
unit_price numeric(10,2) default 0,
|
||||
@@ -132,6 +151,63 @@ create table public.invoice_items (
|
||||
);
|
||||
alter table public.invoice_items enable row level security;
|
||||
|
||||
-- Brand Books (sign survey / brand book records, team only)
|
||||
create table public.brand_books (
|
||||
id uuid default gen_random_uuid() primary key,
|
||||
client_id uuid references public.companies(id) on delete set null,
|
||||
client_name text not null default '',
|
||||
project_name text default '',
|
||||
site_address text default '',
|
||||
book_date date,
|
||||
prepared_by text default '',
|
||||
revision text default '',
|
||||
template text default 'fourge',
|
||||
site_map_path text,
|
||||
inventory_map_path text,
|
||||
signs jsonb default '[]',
|
||||
survey_photo_paths text[] default '{}',
|
||||
updated_at timestamptz default now() not null,
|
||||
-- Cover page fields
|
||||
project_logo_path text,
|
||||
creation_date date,
|
||||
revision_date date,
|
||||
customer_name text,
|
||||
customer_address text,
|
||||
client_logo_url text,
|
||||
client_contact_name text,
|
||||
client_contact_email text,
|
||||
client_contact_phone text,
|
||||
approved_date date,
|
||||
approval_notes text
|
||||
);
|
||||
alter table public.brand_books enable row level security;
|
||||
|
||||
-- Server Status Overrides (team-managed usage inputs for metrics without live APIs)
|
||||
create table public.server_status_overrides (
|
||||
id boolean primary key default true,
|
||||
supabase_egress_bytes bigint,
|
||||
vercel_fast_data_transfer_bytes bigint,
|
||||
vercel_edge_requests bigint,
|
||||
vercel_function_invocations bigint,
|
||||
vercel_active_cpu_hours numeric(10,4),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
alter table public.server_status_overrides enable row level security;
|
||||
|
||||
create table public.fourge_passwords (
|
||||
id uuid default gen_random_uuid() primary key,
|
||||
service_name text not null,
|
||||
service_url text default '',
|
||||
username text not null default '',
|
||||
encrypted_password text not null,
|
||||
password_iv text not null,
|
||||
notes text default '',
|
||||
created_by uuid references public.profiles(id) on delete set null,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
alter table public.fourge_passwords enable row level security;
|
||||
|
||||
-- ============================================================
|
||||
-- Helpers
|
||||
-- ============================================================
|
||||
@@ -141,11 +217,48 @@ returns text as $$
|
||||
select role from public.profiles where id = auth.uid();
|
||||
$$ language sql security definer stable;
|
||||
|
||||
create or replace function public.is_external()
|
||||
returns boolean as $$
|
||||
select get_my_role() = 'external';
|
||||
$$ language sql security definer stable;
|
||||
|
||||
create or replace function public.get_my_company_id()
|
||||
returns uuid as $$
|
||||
select company_id from public.profiles where id = auth.uid();
|
||||
$$ language sql security definer stable;
|
||||
|
||||
create or replace function public.get_database_size_bytes()
|
||||
returns bigint as $$
|
||||
select pg_database_size(current_database());
|
||||
$$ language sql security definer stable;
|
||||
|
||||
-- Prevents clients and externals from modifying protected task fields.
|
||||
-- Fires before UPDATE so WITH CHECK sees the already-corrected new row.
|
||||
create or replace function public.guard_task_update()
|
||||
returns trigger as $$
|
||||
declare
|
||||
caller_role text;
|
||||
begin
|
||||
select role into caller_role from public.profiles where id = auth.uid();
|
||||
|
||||
if caller_role = 'client' then
|
||||
new.project_id := old.project_id;
|
||||
new.assigned_to := old.assigned_to;
|
||||
new.assigned_name := old.assigned_name;
|
||||
new.invoiced := old.invoiced;
|
||||
elsif caller_role = 'external' then
|
||||
new.project_id := old.project_id;
|
||||
new.invoiced := old.invoiced;
|
||||
end if;
|
||||
|
||||
return new;
|
||||
end;
|
||||
$$ language plpgsql security definer;
|
||||
|
||||
create trigger guard_task_update
|
||||
before update on public.tasks
|
||||
for each row execute function public.guard_task_update();
|
||||
|
||||
-- ============================================================
|
||||
-- RLS Policies
|
||||
-- ============================================================
|
||||
@@ -153,17 +266,36 @@ $$ language sql security definer stable;
|
||||
-- Companies
|
||||
create policy "Team all companies" on public.companies for all using (get_my_role() = 'team');
|
||||
create policy "Client reads own company" on public.companies for select using (id = get_my_company_id());
|
||||
create policy "Client updates own company" on public.companies
|
||||
for update using (id = get_my_company_id()) with check (id = get_my_company_id());
|
||||
|
||||
-- Project Members
|
||||
create policy "Team all project_members" on public.project_members for all using (get_my_role() = 'team');
|
||||
create policy "External reads own memberships" on public.project_members for select using (profile_id = auth.uid());
|
||||
|
||||
-- Profiles
|
||||
create policy "Own profile select" on public.profiles for select using (id = auth.uid());
|
||||
create policy "Team reads all profiles" on public.profiles for select using (get_my_role() = 'team');
|
||||
create policy "Own profile update" on public.profiles for update using (id = auth.uid());
|
||||
create policy "Own profile update" on public.profiles
|
||||
for update
|
||||
using (id = auth.uid())
|
||||
with check (
|
||||
id = auth.uid()
|
||||
and role = (select role from public.profiles where id = auth.uid())
|
||||
and company_id is not distinct from (select company_id from public.profiles where id = auth.uid())
|
||||
);
|
||||
create policy "Team update profiles" on public.profiles for update using (get_my_role() = 'team');
|
||||
|
||||
-- Projects
|
||||
create policy "Team all projects" on public.projects for all using (get_my_role() = 'team');
|
||||
create policy "Client reads company projects" on public.projects for select using (company_id = get_my_company_id());
|
||||
create policy "Client inserts company projects" on public.projects for insert with check (company_id = get_my_company_id());
|
||||
create policy "Client updates own company projects" on public.projects
|
||||
for update using (company_id = get_my_company_id()) with check (company_id = get_my_company_id());
|
||||
create policy "External reads assigned projects" on public.projects for select using (
|
||||
get_my_role() = 'external' and
|
||||
id in (select project_id from public.project_members where profile_id = auth.uid())
|
||||
);
|
||||
|
||||
-- Tasks
|
||||
create policy "Team all tasks" on public.tasks for all using (get_my_role() = 'team');
|
||||
@@ -173,9 +305,30 @@ create policy "Client reads company tasks" on public.tasks for select using (
|
||||
create policy "Client insert task" on public.tasks for insert with check (
|
||||
project_id in (select id from public.projects where company_id = get_my_company_id())
|
||||
);
|
||||
create policy "Client updates company tasks" on public.tasks for update using (
|
||||
project_id in (select id from public.projects where company_id = get_my_company_id())
|
||||
create policy "Client updates company tasks" on public.tasks
|
||||
for update
|
||||
using (
|
||||
get_my_role() = 'client'
|
||||
and project_id in (select id from public.projects where company_id = get_my_company_id())
|
||||
)
|
||||
with check (
|
||||
get_my_role() = 'client'
|
||||
and project_id in (select id from public.projects where company_id = get_my_company_id())
|
||||
);
|
||||
create policy "External reads assigned tasks" on public.tasks for select using (
|
||||
get_my_role() = 'external' and
|
||||
project_id in (select project_id from public.project_members where profile_id = auth.uid())
|
||||
);
|
||||
create policy "External updates assigned tasks" on public.tasks
|
||||
for update
|
||||
using (
|
||||
get_my_role() = 'external'
|
||||
and project_id in (select project_id from public.project_members where profile_id = auth.uid())
|
||||
)
|
||||
with check (
|
||||
get_my_role() = 'external'
|
||||
and project_id in (select project_id from public.project_members where profile_id = auth.uid())
|
||||
);
|
||||
|
||||
-- Submissions
|
||||
create policy "Team all submissions" on public.submissions for all using (get_my_role() = 'team');
|
||||
@@ -186,7 +339,32 @@ create policy "Client reads company submissions" on public.submissions for selec
|
||||
where p.company_id = get_my_company_id()
|
||||
)
|
||||
);
|
||||
create policy "Client inserts submissions" on public.submissions for insert with check (submitted_by = auth.uid());
|
||||
create policy "Client inserts submissions" on public.submissions for insert with check (
|
||||
get_my_role() = 'client'
|
||||
and submitted_by = auth.uid()
|
||||
and task_id in (
|
||||
select t.id from public.tasks t
|
||||
join public.projects p on p.id = t.project_id
|
||||
where p.company_id = get_my_company_id()
|
||||
)
|
||||
);
|
||||
create policy "External reads assigned submissions" on public.submissions for select using (
|
||||
get_my_role() = 'external' and
|
||||
task_id in (
|
||||
select t.id from public.tasks t
|
||||
join public.project_members pm on pm.project_id = t.project_id
|
||||
where pm.profile_id = auth.uid()
|
||||
)
|
||||
);
|
||||
create policy "External inserts submissions" on public.submissions for insert with check (
|
||||
get_my_role() = 'external'
|
||||
and submitted_by = auth.uid()
|
||||
and task_id in (
|
||||
select t.id from public.tasks t
|
||||
join public.project_members pm on pm.project_id = t.project_id
|
||||
where pm.profile_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
-- Submission Files
|
||||
create policy "Team all submission_files" on public.submission_files for all using (get_my_role() = 'team');
|
||||
@@ -198,7 +376,35 @@ create policy "Client reads company submission_files" on public.submission_files
|
||||
where p.company_id = get_my_company_id()
|
||||
)
|
||||
);
|
||||
create policy "Client inserts submission_files" on public.submission_files for insert with check (true);
|
||||
create policy "Client inserts submission_files" on public.submission_files for insert with check (
|
||||
get_my_role() = 'client'
|
||||
and submission_id in (
|
||||
select s.id from public.submissions s
|
||||
join public.tasks t on t.id = s.task_id
|
||||
join public.projects p on p.id = t.project_id
|
||||
where p.company_id = get_my_company_id()
|
||||
and s.submitted_by = auth.uid()
|
||||
)
|
||||
);
|
||||
create policy "External reads assigned submission_files" on public.submission_files for select using (
|
||||
get_my_role() = 'external' and
|
||||
submission_id in (
|
||||
select s.id from public.submissions s
|
||||
join public.tasks t on t.id = s.task_id
|
||||
join public.project_members pm on pm.project_id = t.project_id
|
||||
where pm.profile_id = auth.uid()
|
||||
)
|
||||
);
|
||||
create policy "External inserts submission_files" on public.submission_files for insert with check (
|
||||
get_my_role() = 'external'
|
||||
and submission_id in (
|
||||
select s.id from public.submissions s
|
||||
join public.tasks t on t.id = s.task_id
|
||||
join public.project_members pm on pm.project_id = t.project_id
|
||||
where pm.profile_id = auth.uid()
|
||||
and s.submitted_by = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
-- Deliveries
|
||||
create policy "Team all deliveries" on public.deliveries for all using (get_my_role() = 'team');
|
||||
@@ -210,6 +416,15 @@ create policy "Client reads company deliveries" on public.deliveries for select
|
||||
where p.company_id = get_my_company_id()
|
||||
)
|
||||
);
|
||||
create policy "External reads assigned deliveries" on public.deliveries for select using (
|
||||
get_my_role() = 'external' and
|
||||
submission_id in (
|
||||
select s.id from public.submissions s
|
||||
join public.tasks t on t.id = s.task_id
|
||||
join public.project_members pm on pm.project_id = t.project_id
|
||||
where pm.profile_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
-- Delivery Files
|
||||
create policy "Team all delivery_files" on public.delivery_files for all using (get_my_role() = 'team');
|
||||
@@ -222,6 +437,16 @@ create policy "Client reads company delivery_files" on public.delivery_files for
|
||||
where p.company_id = get_my_company_id()
|
||||
)
|
||||
);
|
||||
create policy "External reads assigned delivery_files" on public.delivery_files for select using (
|
||||
get_my_role() = 'external' and
|
||||
delivery_id in (
|
||||
select d.id from public.deliveries d
|
||||
join public.submissions s on s.id = d.submission_id
|
||||
join public.tasks t on t.id = s.task_id
|
||||
join public.project_members pm on pm.project_id = t.project_id
|
||||
where pm.profile_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
-- Company Prices
|
||||
create policy "Team all company_prices" on public.company_prices for all using (get_my_role() = 'team');
|
||||
@@ -237,21 +462,89 @@ create policy "Client reads company invoice_items" on public.invoice_items for s
|
||||
invoice_id in (select id from public.invoices where company_id = get_my_company_id())
|
||||
);
|
||||
|
||||
-- Brand Books
|
||||
create policy "Team all brand_books" on public.brand_books for all using (get_my_role() = 'team');
|
||||
create policy "Team all server_status_overrides" on public.server_status_overrides for all using (get_my_role() = 'team') with check (get_my_role() = 'team');
|
||||
create policy "Team all fourge_passwords" on public.fourge_passwords for all using (get_my_role() = 'team') with check (get_my_role() = 'team');
|
||||
|
||||
-- ============================================================
|
||||
-- Storage Buckets
|
||||
-- ============================================================
|
||||
insert into storage.buckets (id, name, public) values ('submissions', 'submissions', false);
|
||||
insert into storage.buckets (id, name, public) values ('deliveries', 'deliveries', false);
|
||||
insert into storage.buckets (id, name, public) values ('company-logos', 'company-logos', true);
|
||||
insert into storage.buckets (id, name, public) values ('fourge-files', 'fourge-files', false);
|
||||
|
||||
create policy "Auth users upload to submissions" on storage.objects
|
||||
for insert to authenticated with check (bucket_id = 'submissions');
|
||||
create policy "Auth users read submissions" on storage.objects
|
||||
for select to authenticated using (bucket_id = 'submissions');
|
||||
-- Company Logos (public bucket — team manages, public can read for embedded URLs in PDFs)
|
||||
create policy "Team manages company logos" on storage.objects
|
||||
for all to authenticated
|
||||
using (bucket_id = 'company-logos' and get_my_role() = 'team')
|
||||
with check (bucket_id = 'company-logos' and get_my_role() = 'team');
|
||||
create policy "Public can read company logos" on storage.objects
|
||||
for select using (bucket_id = 'company-logos');
|
||||
|
||||
create policy "Team upload deliveries" on storage.objects
|
||||
-- Submissions: SELECT
|
||||
create policy "Team reads submissions storage" on storage.objects
|
||||
for select to authenticated using (bucket_id = 'submissions' and get_my_role() = 'team');
|
||||
create policy "Client reads submissions storage" on storage.objects
|
||||
for select to authenticated using (
|
||||
bucket_id = 'submissions' and get_my_role() = 'client'
|
||||
and split_part(name, '/', 1) in (select t.id::text from public.tasks t join public.projects p on p.id = t.project_id where p.company_id = get_my_company_id())
|
||||
);
|
||||
create policy "External reads submissions storage" on storage.objects
|
||||
for select to authenticated using (
|
||||
bucket_id = 'submissions' and get_my_role() = 'external'
|
||||
and split_part(name, '/', 1) in (select t.id::text from public.tasks t join public.project_members pm on pm.project_id = t.project_id where pm.profile_id = auth.uid())
|
||||
);
|
||||
|
||||
-- Submissions: INSERT
|
||||
create policy "Team inserts submissions storage" on storage.objects
|
||||
for insert to authenticated with check (bucket_id = 'submissions' and get_my_role() = 'team');
|
||||
create policy "Client inserts submissions storage" on storage.objects
|
||||
for insert to authenticated with check (
|
||||
bucket_id = 'submissions' and get_my_role() = 'client'
|
||||
and split_part(name, '/', 1) in (select t.id::text from public.tasks t join public.projects p on p.id = t.project_id where p.company_id = get_my_company_id())
|
||||
);
|
||||
create policy "External inserts submissions storage" on storage.objects
|
||||
for insert to authenticated with check (
|
||||
bucket_id = 'submissions' and get_my_role() = 'external'
|
||||
and split_part(name, '/', 1) in (select t.id::text from public.tasks t join public.project_members pm on pm.project_id = t.project_id where pm.profile_id = auth.uid())
|
||||
);
|
||||
|
||||
-- Submissions: DELETE (team only)
|
||||
create policy "Team deletes submissions storage" on storage.objects
|
||||
for delete to authenticated using (bucket_id = 'submissions' and get_my_role() = 'team');
|
||||
|
||||
-- Deliveries: SELECT
|
||||
create policy "Team reads deliveries storage" on storage.objects
|
||||
for select to authenticated using (bucket_id = 'deliveries' and get_my_role() = 'team');
|
||||
create policy "Client reads deliveries storage" on storage.objects
|
||||
for select to authenticated using (
|
||||
bucket_id = 'deliveries' and get_my_role() = 'client'
|
||||
and split_part(name, '/', 1) in (select t.id::text from public.tasks t join public.projects p on p.id = t.project_id where p.company_id = get_my_company_id())
|
||||
);
|
||||
create policy "External reads deliveries storage" on storage.objects
|
||||
for select to authenticated using (
|
||||
bucket_id = 'deliveries' and get_my_role() = 'external'
|
||||
and split_part(name, '/', 1) in (select t.id::text from public.tasks t join public.project_members pm on pm.project_id = t.project_id where pm.profile_id = auth.uid())
|
||||
);
|
||||
|
||||
-- Deliveries: INSERT + DELETE (team only)
|
||||
create policy "Team inserts deliveries storage" on storage.objects
|
||||
for insert to authenticated with check (bucket_id = 'deliveries' and get_my_role() = 'team');
|
||||
create policy "Auth users read deliveries" on storage.objects
|
||||
for select to authenticated using (bucket_id = 'deliveries');
|
||||
create policy "Team deletes deliveries storage" on storage.objects
|
||||
for delete to authenticated using (bucket_id = 'deliveries' and get_my_role() = 'team');
|
||||
|
||||
-- Fourge Files: internal team-only company documents
|
||||
create policy "Team reads fourge files storage" on storage.objects
|
||||
for select to authenticated using (bucket_id = 'fourge-files' and get_my_role() = 'team');
|
||||
create policy "Team inserts fourge files storage" on storage.objects
|
||||
for insert to authenticated with check (bucket_id = 'fourge-files' and get_my_role() = 'team');
|
||||
create policy "Team updates fourge files storage" on storage.objects
|
||||
for update to authenticated using (bucket_id = 'fourge-files' and get_my_role() = 'team')
|
||||
with check (bucket_id = 'fourge-files' and get_my_role() = 'team');
|
||||
create policy "Team deletes fourge files storage" on storage.objects
|
||||
for delete to authenticated using (bucket_id = 'fourge-files' and get_my_role() = 'team');
|
||||
|
||||
-- ============================================================
|
||||
-- Trigger: auto-create profile on signup
|
||||
|
||||
Reference in New Issue
Block a user