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:
+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