Refactor to generic ERP base
Remove influencer-specific features (campaigns, TikTok, gifts, shadow ledger, contracts, briefings) and consolidate into a clean ERP foundation with core bookkeeping, invoicing, receipts, tax reporting, and calendar functionality. Reorganize database migrations into a clean numbered sequence. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
a25d10a528
commit
0aecf5b41c
@@ -0,0 +1,403 @@
|
||||
-- Migration 1: Core Schema
|
||||
-- Profiles, company_settings, bank_connections, customers, invoices,
|
||||
-- invoice_items, transactions, salary_payments, tax_rates
|
||||
|
||||
-- =============================================================================
|
||||
-- Extensions
|
||||
-- =============================================================================
|
||||
create extension if not exists "uuid-ossp" with schema extensions;
|
||||
|
||||
-- =============================================================================
|
||||
-- Trigger function: update_updated_at_column (reused across all migrations)
|
||||
-- =============================================================================
|
||||
create or replace function public.update_updated_at_column()
|
||||
returns trigger as $$
|
||||
begin
|
||||
new.updated_at = now();
|
||||
return new;
|
||||
end;
|
||||
$$ language plpgsql;
|
||||
|
||||
-- =============================================================================
|
||||
-- 1. profiles
|
||||
-- =============================================================================
|
||||
create table public.profiles (
|
||||
id uuid primary key references auth.users on delete cascade,
|
||||
email text,
|
||||
full_name text,
|
||||
avatar_url text,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
alter table public.profiles enable row level security;
|
||||
|
||||
create policy "profiles_select" on public.profiles
|
||||
for select using (auth.uid() = id);
|
||||
create policy "profiles_insert" on public.profiles
|
||||
for insert with check (auth.uid() = id);
|
||||
create policy "profiles_update" on public.profiles
|
||||
for update using (auth.uid() = id);
|
||||
|
||||
create trigger profiles_updated_at
|
||||
before update on public.profiles
|
||||
for each row execute function public.update_updated_at_column();
|
||||
|
||||
-- Auto-create profile on new user signup
|
||||
create or replace function public.handle_new_user()
|
||||
returns trigger as $$
|
||||
begin
|
||||
insert into public.profiles (id, email, full_name)
|
||||
values (new.id, new.email, new.raw_user_meta_data->>'full_name');
|
||||
return new;
|
||||
end;
|
||||
$$ language plpgsql security definer;
|
||||
|
||||
create trigger on_auth_user_created
|
||||
after insert on auth.users
|
||||
for each row execute function public.handle_new_user();
|
||||
|
||||
-- =============================================================================
|
||||
-- 2. company_settings
|
||||
-- =============================================================================
|
||||
create table public.company_settings (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
user_id uuid references auth.users on delete cascade unique not null,
|
||||
entity_type text check (entity_type in ('enskild_firma', 'aktiebolag')),
|
||||
company_name text,
|
||||
org_number text,
|
||||
vat_number text,
|
||||
address_line1 text,
|
||||
address_line2 text,
|
||||
postal_code text,
|
||||
city text,
|
||||
country text default 'SE',
|
||||
phone text,
|
||||
email text,
|
||||
website text,
|
||||
bankgiro text,
|
||||
plusgiro text,
|
||||
iban text,
|
||||
bic text,
|
||||
f_skatt boolean default true,
|
||||
vat_registered boolean default false,
|
||||
moms_period text check (moms_period in ('monthly', 'quarterly', 'yearly')),
|
||||
fiscal_year_start_month integer default 1,
|
||||
invoice_prefix text,
|
||||
next_invoice_number integer default 1,
|
||||
invoice_default_days integer default 30,
|
||||
logo_url text,
|
||||
onboarding_complete boolean default false,
|
||||
schablon_mileage_rate numeric default 2.50,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
alter table public.company_settings enable row level security;
|
||||
|
||||
create policy "company_settings_select" on public.company_settings
|
||||
for select using (auth.uid() = user_id);
|
||||
create policy "company_settings_insert" on public.company_settings
|
||||
for insert with check (auth.uid() = user_id);
|
||||
create policy "company_settings_update" on public.company_settings
|
||||
for update using (auth.uid() = user_id);
|
||||
create policy "company_settings_delete" on public.company_settings
|
||||
for delete using (auth.uid() = user_id);
|
||||
|
||||
create index idx_company_settings_user_id on public.company_settings (user_id);
|
||||
|
||||
create trigger company_settings_updated_at
|
||||
before update on public.company_settings
|
||||
for each row execute function public.update_updated_at_column();
|
||||
|
||||
-- =============================================================================
|
||||
-- 3. bank_connections
|
||||
-- =============================================================================
|
||||
create table public.bank_connections (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
user_id uuid references auth.users on delete cascade not null,
|
||||
provider text not null default 'enablebanking',
|
||||
bank_name text,
|
||||
session_id text,
|
||||
status text default 'pending'
|
||||
check (status in ('pending', 'active', 'expired', 'error', 'revoked')),
|
||||
consent_expires timestamptz,
|
||||
accounts_data jsonb,
|
||||
error_message text,
|
||||
last_synced_at timestamptz,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
alter table public.bank_connections enable row level security;
|
||||
|
||||
create policy "bank_connections_select" on public.bank_connections
|
||||
for select using (auth.uid() = user_id);
|
||||
create policy "bank_connections_insert" on public.bank_connections
|
||||
for insert with check (auth.uid() = user_id);
|
||||
create policy "bank_connections_update" on public.bank_connections
|
||||
for update using (auth.uid() = user_id);
|
||||
create policy "bank_connections_delete" on public.bank_connections
|
||||
for delete using (auth.uid() = user_id);
|
||||
|
||||
create index idx_bank_connections_user_id on public.bank_connections (user_id);
|
||||
|
||||
create trigger bank_connections_updated_at
|
||||
before update on public.bank_connections
|
||||
for each row execute function public.update_updated_at_column();
|
||||
|
||||
-- =============================================================================
|
||||
-- 4. customers
|
||||
-- =============================================================================
|
||||
create table public.customers (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
user_id uuid references auth.users on delete cascade not null,
|
||||
name text not null,
|
||||
org_number text,
|
||||
vat_number text,
|
||||
email text,
|
||||
phone text,
|
||||
address_line1 text,
|
||||
postal_code text,
|
||||
city text,
|
||||
country text default 'SE',
|
||||
is_international boolean default false,
|
||||
notes text,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
alter table public.customers enable row level security;
|
||||
|
||||
create policy "customers_select" on public.customers
|
||||
for select using (auth.uid() = user_id);
|
||||
create policy "customers_insert" on public.customers
|
||||
for insert with check (auth.uid() = user_id);
|
||||
create policy "customers_update" on public.customers
|
||||
for update using (auth.uid() = user_id);
|
||||
create policy "customers_delete" on public.customers
|
||||
for delete using (auth.uid() = user_id);
|
||||
|
||||
create index idx_customers_user_id on public.customers (user_id);
|
||||
|
||||
create trigger customers_updated_at
|
||||
before update on public.customers
|
||||
for each row execute function public.update_updated_at_column();
|
||||
|
||||
-- =============================================================================
|
||||
-- 5. invoices
|
||||
-- =============================================================================
|
||||
create table public.invoices (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
user_id uuid references auth.users on delete cascade not null,
|
||||
customer_id uuid references public.customers (id) on delete set null,
|
||||
invoice_number text not null,
|
||||
invoice_date date not null,
|
||||
due_date date not null,
|
||||
status text default 'draft'
|
||||
check (status in ('draft', 'sent', 'paid', 'overdue', 'cancelled', 'credited')),
|
||||
currency text default 'SEK',
|
||||
exchange_rate numeric,
|
||||
exchange_rate_date date,
|
||||
subtotal numeric default 0,
|
||||
subtotal_sek numeric default 0,
|
||||
vat_amount numeric default 0,
|
||||
vat_amount_sek numeric default 0,
|
||||
total numeric default 0,
|
||||
total_sek numeric default 0,
|
||||
vat_treatment text default 'standard_25',
|
||||
vat_rate numeric default 25,
|
||||
moms_ruta text,
|
||||
your_reference text,
|
||||
our_reference text,
|
||||
notes text,
|
||||
reverse_charge_text text,
|
||||
credited_invoice_id uuid references public.invoices (id) on delete set null,
|
||||
paid_at timestamptz,
|
||||
paid_amount numeric,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
|
||||
unique (user_id, invoice_number)
|
||||
);
|
||||
|
||||
alter table public.invoices enable row level security;
|
||||
|
||||
create policy "invoices_select" on public.invoices
|
||||
for select using (auth.uid() = user_id);
|
||||
create policy "invoices_insert" on public.invoices
|
||||
for insert with check (auth.uid() = user_id);
|
||||
create policy "invoices_update" on public.invoices
|
||||
for update using (auth.uid() = user_id);
|
||||
create policy "invoices_delete" on public.invoices
|
||||
for delete using (auth.uid() = user_id);
|
||||
|
||||
create index idx_invoices_user_id on public.invoices (user_id);
|
||||
create index idx_invoices_status on public.invoices (status);
|
||||
create index idx_invoices_due_date on public.invoices (due_date);
|
||||
|
||||
create trigger invoices_updated_at
|
||||
before update on public.invoices
|
||||
for each row execute function public.update_updated_at_column();
|
||||
|
||||
-- =============================================================================
|
||||
-- 6. invoice_items
|
||||
-- =============================================================================
|
||||
create table public.invoice_items (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
invoice_id uuid references public.invoices (id) on delete cascade not null,
|
||||
sort_order integer default 0,
|
||||
description text not null,
|
||||
quantity numeric default 1,
|
||||
unit text default 'st',
|
||||
unit_price numeric default 0,
|
||||
line_total numeric default 0,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
alter table public.invoice_items enable row level security;
|
||||
|
||||
create policy "invoice_items_select" on public.invoice_items
|
||||
for select using (
|
||||
exists (
|
||||
select 1 from public.invoices
|
||||
where invoices.id = invoice_items.invoice_id
|
||||
and invoices.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
create policy "invoice_items_insert" on public.invoice_items
|
||||
for insert with check (
|
||||
exists (
|
||||
select 1 from public.invoices
|
||||
where invoices.id = invoice_items.invoice_id
|
||||
and invoices.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
create policy "invoice_items_update" on public.invoice_items
|
||||
for update using (
|
||||
exists (
|
||||
select 1 from public.invoices
|
||||
where invoices.id = invoice_items.invoice_id
|
||||
and invoices.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
create policy "invoice_items_delete" on public.invoice_items
|
||||
for delete using (
|
||||
exists (
|
||||
select 1 from public.invoices
|
||||
where invoices.id = invoice_items.invoice_id
|
||||
and invoices.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
create index idx_invoice_items_invoice_id on public.invoice_items (invoice_id);
|
||||
|
||||
-- =============================================================================
|
||||
-- 7. transactions
|
||||
-- =============================================================================
|
||||
create table public.transactions (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
user_id uuid references auth.users on delete cascade not null,
|
||||
bank_connection_id uuid references public.bank_connections (id) on delete set null,
|
||||
external_id text,
|
||||
date date not null,
|
||||
description text not null,
|
||||
amount numeric not null,
|
||||
currency text default 'SEK',
|
||||
amount_sek numeric,
|
||||
exchange_rate numeric,
|
||||
exchange_rate_date date,
|
||||
category text default 'uncategorized',
|
||||
is_business boolean,
|
||||
invoice_id uuid references public.invoices (id) on delete set null,
|
||||
potential_invoice_id uuid references public.invoices (id) on delete set null,
|
||||
journal_entry_id uuid,
|
||||
mcc_code integer,
|
||||
merchant_name text,
|
||||
receipt_id uuid,
|
||||
notes text,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
|
||||
unique (user_id, external_id)
|
||||
);
|
||||
|
||||
alter table public.transactions enable row level security;
|
||||
|
||||
create policy "transactions_select" on public.transactions
|
||||
for select using (auth.uid() = user_id);
|
||||
create policy "transactions_insert" on public.transactions
|
||||
for insert with check (auth.uid() = user_id);
|
||||
create policy "transactions_update" on public.transactions
|
||||
for update using (auth.uid() = user_id);
|
||||
create policy "transactions_delete" on public.transactions
|
||||
for delete using (auth.uid() = user_id);
|
||||
|
||||
create index idx_transactions_user_id on public.transactions (user_id);
|
||||
create index idx_transactions_date on public.transactions (date);
|
||||
create index idx_transactions_external_id on public.transactions (external_id);
|
||||
create index idx_transactions_category on public.transactions (category);
|
||||
|
||||
create trigger transactions_updated_at
|
||||
before update on public.transactions
|
||||
for each row execute function public.update_updated_at_column();
|
||||
|
||||
-- =============================================================================
|
||||
-- 8. salary_payments
|
||||
-- =============================================================================
|
||||
create table public.salary_payments (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
user_id uuid references auth.users on delete cascade not null,
|
||||
gross_amount numeric not null,
|
||||
net_amount numeric not null,
|
||||
employer_tax numeric not null,
|
||||
preliminary_tax numeric not null,
|
||||
payment_date date not null,
|
||||
period_start date not null,
|
||||
period_end date not null,
|
||||
status text default 'planned'
|
||||
check (status in ('planned', 'paid', 'reported')),
|
||||
notes text,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
alter table public.salary_payments enable row level security;
|
||||
|
||||
create policy "salary_payments_select" on public.salary_payments
|
||||
for select using (auth.uid() = user_id);
|
||||
create policy "salary_payments_insert" on public.salary_payments
|
||||
for insert with check (auth.uid() = user_id);
|
||||
create policy "salary_payments_update" on public.salary_payments
|
||||
for update using (auth.uid() = user_id);
|
||||
create policy "salary_payments_delete" on public.salary_payments
|
||||
for delete using (auth.uid() = user_id);
|
||||
|
||||
create index idx_salary_payments_user_id on public.salary_payments (user_id);
|
||||
|
||||
create trigger salary_payments_updated_at
|
||||
before update on public.salary_payments
|
||||
for each row execute function public.update_updated_at_column();
|
||||
|
||||
-- =============================================================================
|
||||
-- 9. tax_rates (read-only reference table)
|
||||
-- =============================================================================
|
||||
create table public.tax_rates (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
rate_type text not null,
|
||||
rate numeric not null,
|
||||
valid_from date not null,
|
||||
valid_to date,
|
||||
description text
|
||||
);
|
||||
|
||||
alter table public.tax_rates enable row level security;
|
||||
|
||||
create policy "tax_rates_select" on public.tax_rates
|
||||
for select to authenticated using (true);
|
||||
|
||||
-- Seed current Swedish tax rates
|
||||
insert into public.tax_rates (rate_type, rate, valid_from, description) values
|
||||
('egenavgifter', 28.97, '2024-01-01', 'Egenavgifter for enskild firma'),
|
||||
('bolagsskatt', 20.6, '2019-01-01', 'Bolagsskatt (corporate tax)'),
|
||||
('arbetsgivaravgifter', 31.42, '2024-01-01', 'Arbetsgivaravgifter (employer social contributions)');
|
||||
@@ -0,0 +1,285 @@
|
||||
-- Migration 2: Bookkeeping Schema
|
||||
-- BAS Kontoplan, fiscal periods, journal entries, mapping rules, account balances
|
||||
|
||||
-- =============================================================================
|
||||
-- 1. chart_of_accounts (BAS Kontoplan)
|
||||
-- =============================================================================
|
||||
create table public.chart_of_accounts (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
user_id uuid references auth.users on delete cascade not null,
|
||||
account_number text not null,
|
||||
account_name text not null,
|
||||
account_class integer not null,
|
||||
account_group text,
|
||||
account_type text not null
|
||||
check (account_type in ('asset', 'equity', 'liability', 'revenue', 'expense')),
|
||||
normal_balance text not null
|
||||
check (normal_balance in ('debit', 'credit')),
|
||||
plan_type text default 'k1'
|
||||
check (plan_type in ('k1', 'full_bas')),
|
||||
is_active boolean default true,
|
||||
is_system_account boolean default false,
|
||||
default_vat_code text,
|
||||
description text,
|
||||
sort_order integer default 0,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
|
||||
unique (user_id, account_number)
|
||||
);
|
||||
|
||||
alter table public.chart_of_accounts enable row level security;
|
||||
|
||||
create policy "chart_of_accounts_select" on public.chart_of_accounts
|
||||
for select using (auth.uid() = user_id);
|
||||
create policy "chart_of_accounts_insert" on public.chart_of_accounts
|
||||
for insert with check (auth.uid() = user_id);
|
||||
create policy "chart_of_accounts_update" on public.chart_of_accounts
|
||||
for update using (auth.uid() = user_id);
|
||||
create policy "chart_of_accounts_delete" on public.chart_of_accounts
|
||||
for delete using (auth.uid() = user_id);
|
||||
|
||||
create index idx_chart_of_accounts_user_id on public.chart_of_accounts (user_id);
|
||||
create index idx_chart_of_accounts_account_number on public.chart_of_accounts (account_number);
|
||||
create index idx_chart_of_accounts_account_type on public.chart_of_accounts (account_type);
|
||||
|
||||
create trigger chart_of_accounts_updated_at
|
||||
before update on public.chart_of_accounts
|
||||
for each row execute function public.update_updated_at_column();
|
||||
|
||||
-- =============================================================================
|
||||
-- 2. fiscal_periods
|
||||
-- =============================================================================
|
||||
create table public.fiscal_periods (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
user_id uuid references auth.users on delete cascade not null,
|
||||
name text not null,
|
||||
period_start date not null,
|
||||
period_end date not null,
|
||||
is_closed boolean default false,
|
||||
closed_at timestamptz,
|
||||
opening_balances_set boolean default false,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
|
||||
unique (user_id, period_start, period_end)
|
||||
);
|
||||
|
||||
alter table public.fiscal_periods enable row level security;
|
||||
|
||||
create policy "fiscal_periods_select" on public.fiscal_periods
|
||||
for select using (auth.uid() = user_id);
|
||||
create policy "fiscal_periods_insert" on public.fiscal_periods
|
||||
for insert with check (auth.uid() = user_id);
|
||||
create policy "fiscal_periods_update" on public.fiscal_periods
|
||||
for update using (auth.uid() = user_id);
|
||||
create policy "fiscal_periods_delete" on public.fiscal_periods
|
||||
for delete using (auth.uid() = user_id);
|
||||
|
||||
create index idx_fiscal_periods_user_id on public.fiscal_periods (user_id);
|
||||
create index idx_fiscal_periods_dates on public.fiscal_periods (period_start, period_end);
|
||||
|
||||
create trigger fiscal_periods_updated_at
|
||||
before update on public.fiscal_periods
|
||||
for each row execute function public.update_updated_at_column();
|
||||
|
||||
-- =============================================================================
|
||||
-- 3. journal_entries (Verifikationer)
|
||||
-- =============================================================================
|
||||
create table public.journal_entries (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
user_id uuid references auth.users on delete cascade not null,
|
||||
fiscal_period_id uuid references public.fiscal_periods (id) on delete restrict not null,
|
||||
voucher_number integer not null,
|
||||
voucher_series text default 'A',
|
||||
entry_date date not null,
|
||||
description text not null,
|
||||
source_type text not null
|
||||
check (source_type in (
|
||||
'manual', 'bank_transaction', 'invoice_created',
|
||||
'invoice_paid', 'credit_note', 'salary_payment',
|
||||
'opening_balance', 'year_end'
|
||||
)),
|
||||
source_id uuid,
|
||||
status text default 'draft'
|
||||
check (status in ('draft', 'posted', 'reversed')),
|
||||
attachment_urls text[],
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
alter table public.journal_entries enable row level security;
|
||||
|
||||
create policy "journal_entries_select" on public.journal_entries
|
||||
for select using (auth.uid() = user_id);
|
||||
create policy "journal_entries_insert" on public.journal_entries
|
||||
for insert with check (auth.uid() = user_id);
|
||||
create policy "journal_entries_update" on public.journal_entries
|
||||
for update using (auth.uid() = user_id);
|
||||
create policy "journal_entries_delete" on public.journal_entries
|
||||
for delete using (auth.uid() = user_id);
|
||||
|
||||
create index idx_journal_entries_user_id on public.journal_entries (user_id);
|
||||
create index idx_journal_entries_fiscal_period_id on public.journal_entries (fiscal_period_id);
|
||||
create index idx_journal_entries_entry_date on public.journal_entries (entry_date);
|
||||
create index idx_journal_entries_source on public.journal_entries (source_type, source_id);
|
||||
create index idx_journal_entries_status on public.journal_entries (status);
|
||||
|
||||
create trigger journal_entries_updated_at
|
||||
before update on public.journal_entries
|
||||
for each row execute function public.update_updated_at_column();
|
||||
|
||||
-- =============================================================================
|
||||
-- 4. journal_entry_lines
|
||||
-- =============================================================================
|
||||
create table public.journal_entry_lines (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
journal_entry_id uuid references public.journal_entries (id) on delete cascade not null,
|
||||
account_number text not null,
|
||||
account_id uuid references public.chart_of_accounts (id) on delete set null,
|
||||
debit_amount numeric default 0,
|
||||
credit_amount numeric default 0,
|
||||
currency text default 'SEK',
|
||||
amount_in_currency numeric,
|
||||
exchange_rate numeric,
|
||||
line_description text,
|
||||
sort_order integer default 0,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
alter table public.journal_entry_lines enable row level security;
|
||||
|
||||
create policy "journal_entry_lines_select" on public.journal_entry_lines
|
||||
for select using (
|
||||
exists (
|
||||
select 1 from public.journal_entries
|
||||
where journal_entries.id = journal_entry_lines.journal_entry_id
|
||||
and journal_entries.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
create policy "journal_entry_lines_insert" on public.journal_entry_lines
|
||||
for insert with check (
|
||||
exists (
|
||||
select 1 from public.journal_entries
|
||||
where journal_entries.id = journal_entry_lines.journal_entry_id
|
||||
and journal_entries.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
create policy "journal_entry_lines_update" on public.journal_entry_lines
|
||||
for update using (
|
||||
exists (
|
||||
select 1 from public.journal_entries
|
||||
where journal_entries.id = journal_entry_lines.journal_entry_id
|
||||
and journal_entries.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
create policy "journal_entry_lines_delete" on public.journal_entry_lines
|
||||
for delete using (
|
||||
exists (
|
||||
select 1 from public.journal_entries
|
||||
where journal_entries.id = journal_entry_lines.journal_entry_id
|
||||
and journal_entries.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
create index idx_journal_entry_lines_entry_id on public.journal_entry_lines (journal_entry_id);
|
||||
create index idx_journal_entry_lines_account on public.journal_entry_lines (account_number);
|
||||
|
||||
-- =============================================================================
|
||||
-- 5. mapping_rules
|
||||
-- =============================================================================
|
||||
create table public.mapping_rules (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
user_id uuid references auth.users on delete cascade,
|
||||
rule_name text not null,
|
||||
rule_type text not null
|
||||
check (rule_type in ('mcc_code', 'merchant_name', 'description_pattern', 'amount_threshold', 'combined')),
|
||||
priority integer default 100,
|
||||
mcc_codes integer[],
|
||||
merchant_pattern text,
|
||||
description_pattern text,
|
||||
amount_min numeric,
|
||||
amount_max numeric,
|
||||
debit_account text,
|
||||
credit_account text,
|
||||
vat_treatment text,
|
||||
vat_debit_account text,
|
||||
vat_credit_account text,
|
||||
risk_level text default 'LOW',
|
||||
default_private boolean default false,
|
||||
requires_review boolean default false,
|
||||
confidence_score numeric default 0.5,
|
||||
capitalization_threshold numeric,
|
||||
capitalized_debit_account text,
|
||||
is_active boolean default true,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
alter table public.mapping_rules enable row level security;
|
||||
|
||||
create policy "mapping_rules_select" on public.mapping_rules
|
||||
for select using (auth.uid() = user_id or user_id is null);
|
||||
create policy "mapping_rules_insert" on public.mapping_rules
|
||||
for insert with check (auth.uid() = user_id);
|
||||
create policy "mapping_rules_update" on public.mapping_rules
|
||||
for update using (auth.uid() = user_id);
|
||||
create policy "mapping_rules_delete" on public.mapping_rules
|
||||
for delete using (auth.uid() = user_id);
|
||||
|
||||
create index idx_mapping_rules_user_id on public.mapping_rules (user_id);
|
||||
create index idx_mapping_rules_rule_type on public.mapping_rules (rule_type);
|
||||
create index idx_mapping_rules_priority on public.mapping_rules (priority);
|
||||
|
||||
create trigger mapping_rules_updated_at
|
||||
before update on public.mapping_rules
|
||||
for each row execute function public.update_updated_at_column();
|
||||
|
||||
-- =============================================================================
|
||||
-- 6. account_balances (cached balances)
|
||||
-- =============================================================================
|
||||
create table public.account_balances (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
user_id uuid references auth.users on delete cascade not null,
|
||||
fiscal_period_id uuid references public.fiscal_periods (id) on delete cascade not null,
|
||||
account_number text not null,
|
||||
account_id uuid references public.chart_of_accounts (id) on delete set null,
|
||||
opening_debit numeric default 0,
|
||||
opening_credit numeric default 0,
|
||||
period_debit numeric default 0,
|
||||
period_credit numeric default 0,
|
||||
closing_debit numeric default 0,
|
||||
closing_credit numeric default 0,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
|
||||
unique (user_id, fiscal_period_id, account_number)
|
||||
);
|
||||
|
||||
alter table public.account_balances enable row level security;
|
||||
|
||||
create policy "account_balances_select" on public.account_balances
|
||||
for select using (auth.uid() = user_id);
|
||||
create policy "account_balances_insert" on public.account_balances
|
||||
for insert with check (auth.uid() = user_id);
|
||||
create policy "account_balances_update" on public.account_balances
|
||||
for update using (auth.uid() = user_id);
|
||||
create policy "account_balances_delete" on public.account_balances
|
||||
for delete using (auth.uid() = user_id);
|
||||
|
||||
create index idx_account_balances_user_id on public.account_balances (user_id);
|
||||
create index idx_account_balances_fiscal_period on public.account_balances (fiscal_period_id);
|
||||
create index idx_account_balances_account on public.account_balances (account_number);
|
||||
|
||||
create trigger account_balances_updated_at
|
||||
before update on public.account_balances
|
||||
for each row execute function public.update_updated_at_column();
|
||||
|
||||
-- =============================================================================
|
||||
-- Add FK from transactions.journal_entry_id -> journal_entries
|
||||
-- =============================================================================
|
||||
alter table public.transactions
|
||||
add constraint fk_transactions_journal_entry
|
||||
foreign key (journal_entry_id) references public.journal_entries (id) on delete set null;
|
||||
|
||||
create index idx_transactions_journal_entry_id on public.transactions (journal_entry_id);
|
||||
@@ -0,0 +1,88 @@
|
||||
-- Migration 3: Deadlines & Calendar
|
||||
-- Deadline tracking and calendar feed subscriptions
|
||||
|
||||
-- =============================================================================
|
||||
-- 1. deadlines
|
||||
-- =============================================================================
|
||||
create table public.deadlines (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
user_id uuid references auth.users on delete cascade not null,
|
||||
title text not null,
|
||||
due_date date not null,
|
||||
due_time time,
|
||||
deadline_type text default 'other'
|
||||
check (deadline_type in ('delivery', 'invoicing', 'report', 'tax', 'other')),
|
||||
priority text default 'normal'
|
||||
check (priority in ('critical', 'important', 'normal')),
|
||||
is_completed boolean default false,
|
||||
completed_at timestamptz,
|
||||
customer_id uuid references public.customers (id) on delete set null,
|
||||
is_auto_generated boolean default false,
|
||||
notes text,
|
||||
tax_deadline_type text,
|
||||
tax_period text,
|
||||
source text default 'user'
|
||||
check (source in ('system', 'user')),
|
||||
reminder_offsets integer[] default '{14,7,1,0}',
|
||||
status text default 'upcoming'
|
||||
check (status in ('upcoming', 'action_needed', 'in_progress', 'submitted', 'confirmed', 'overdue')),
|
||||
status_changed_at timestamptz default now(),
|
||||
linked_report_type text,
|
||||
linked_report_period jsonb,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
alter table public.deadlines enable row level security;
|
||||
|
||||
create policy "deadlines_select" on public.deadlines
|
||||
for select using (auth.uid() = user_id);
|
||||
create policy "deadlines_insert" on public.deadlines
|
||||
for insert with check (auth.uid() = user_id);
|
||||
create policy "deadlines_update" on public.deadlines
|
||||
for update using (auth.uid() = user_id);
|
||||
create policy "deadlines_delete" on public.deadlines
|
||||
for delete using (auth.uid() = user_id);
|
||||
|
||||
create index idx_deadlines_user_id on public.deadlines (user_id);
|
||||
create index idx_deadlines_due_date on public.deadlines (due_date);
|
||||
create index idx_deadlines_deadline_type on public.deadlines (deadline_type);
|
||||
create index idx_deadlines_user_completed on public.deadlines (user_id, is_completed);
|
||||
|
||||
create trigger deadlines_updated_at
|
||||
before update on public.deadlines
|
||||
for each row execute function public.update_updated_at_column();
|
||||
|
||||
-- =============================================================================
|
||||
-- 2. calendar_feeds
|
||||
-- =============================================================================
|
||||
create table public.calendar_feeds (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
user_id uuid references auth.users on delete cascade unique not null,
|
||||
feed_token text unique not null default gen_random_uuid()::text,
|
||||
is_active boolean default true,
|
||||
include_tax_deadlines boolean default true,
|
||||
include_invoices boolean default true,
|
||||
last_accessed_at timestamptz,
|
||||
access_count integer default 0,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
alter table public.calendar_feeds enable row level security;
|
||||
|
||||
create policy "calendar_feeds_select" on public.calendar_feeds
|
||||
for select using (auth.uid() = user_id);
|
||||
create policy "calendar_feeds_insert" on public.calendar_feeds
|
||||
for insert with check (auth.uid() = user_id);
|
||||
create policy "calendar_feeds_update" on public.calendar_feeds
|
||||
for update using (auth.uid() = user_id);
|
||||
create policy "calendar_feeds_delete" on public.calendar_feeds
|
||||
for delete using (auth.uid() = user_id);
|
||||
|
||||
create index idx_calendar_feeds_user_id on public.calendar_feeds (user_id);
|
||||
create index idx_calendar_feeds_feed_token on public.calendar_feeds (feed_token);
|
||||
|
||||
create trigger calendar_feeds_updated_at
|
||||
before update on public.calendar_feeds
|
||||
for each row execute function public.update_updated_at_column();
|
||||
@@ -0,0 +1,118 @@
|
||||
-- Migration 4: Receipts
|
||||
-- Receipt scanning with OCR extraction and transaction matching
|
||||
|
||||
-- =============================================================================
|
||||
-- 1. receipts
|
||||
-- =============================================================================
|
||||
create table public.receipts (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
user_id uuid references auth.users on delete cascade not null,
|
||||
image_url text not null,
|
||||
image_thumbnail_url text,
|
||||
status text default 'pending' not null
|
||||
check (status in ('pending', 'processing', 'extracted', 'confirmed', 'error')),
|
||||
extraction_confidence numeric,
|
||||
merchant_name text,
|
||||
merchant_org_number text,
|
||||
merchant_vat_number text,
|
||||
receipt_date date,
|
||||
receipt_time time,
|
||||
total_amount numeric,
|
||||
currency text default 'SEK',
|
||||
vat_amount numeric,
|
||||
is_restaurant boolean default false,
|
||||
is_systembolaget boolean default false,
|
||||
is_foreign_merchant boolean default false,
|
||||
representation_persons integer,
|
||||
representation_purpose text,
|
||||
matched_transaction_id uuid references public.transactions (id) on delete set null,
|
||||
match_confidence numeric,
|
||||
raw_extraction jsonb,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
alter table public.receipts enable row level security;
|
||||
|
||||
create policy "receipts_select" on public.receipts
|
||||
for select using (auth.uid() = user_id);
|
||||
create policy "receipts_insert" on public.receipts
|
||||
for insert with check (auth.uid() = user_id);
|
||||
create policy "receipts_update" on public.receipts
|
||||
for update using (auth.uid() = user_id);
|
||||
create policy "receipts_delete" on public.receipts
|
||||
for delete using (auth.uid() = user_id);
|
||||
|
||||
create index idx_receipts_user_status on public.receipts (user_id, status);
|
||||
create index idx_receipts_matched_transaction on public.receipts (matched_transaction_id);
|
||||
|
||||
create trigger receipts_updated_at
|
||||
before update on public.receipts
|
||||
for each row execute function public.update_updated_at_column();
|
||||
|
||||
-- =============================================================================
|
||||
-- 2. receipt_line_items
|
||||
-- =============================================================================
|
||||
create table public.receipt_line_items (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
receipt_id uuid references public.receipts (id) on delete cascade not null,
|
||||
description text not null,
|
||||
quantity numeric default 1,
|
||||
unit_price numeric,
|
||||
line_total numeric not null,
|
||||
vat_rate numeric,
|
||||
vat_amount numeric,
|
||||
is_business boolean,
|
||||
category text,
|
||||
bas_account text,
|
||||
extraction_confidence numeric,
|
||||
suggested_category text,
|
||||
sort_order integer default 0,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
alter table public.receipt_line_items enable row level security;
|
||||
|
||||
create policy "receipt_line_items_select" on public.receipt_line_items
|
||||
for select using (
|
||||
exists (
|
||||
select 1 from public.receipts
|
||||
where receipts.id = receipt_line_items.receipt_id
|
||||
and receipts.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
create policy "receipt_line_items_insert" on public.receipt_line_items
|
||||
for insert with check (
|
||||
exists (
|
||||
select 1 from public.receipts
|
||||
where receipts.id = receipt_line_items.receipt_id
|
||||
and receipts.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
create policy "receipt_line_items_update" on public.receipt_line_items
|
||||
for update using (
|
||||
exists (
|
||||
select 1 from public.receipts
|
||||
where receipts.id = receipt_line_items.receipt_id
|
||||
and receipts.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
create policy "receipt_line_items_delete" on public.receipt_line_items
|
||||
for delete using (
|
||||
exists (
|
||||
select 1 from public.receipts
|
||||
where receipts.id = receipt_line_items.receipt_id
|
||||
and receipts.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
create index idx_receipt_line_items_receipt on public.receipt_line_items (receipt_id);
|
||||
|
||||
-- =============================================================================
|
||||
-- Add FK from transactions.receipt_id -> receipts
|
||||
-- =============================================================================
|
||||
alter table public.transactions
|
||||
add constraint fk_transactions_receipt
|
||||
foreign key (receipt_id) references public.receipts (id) on delete set null;
|
||||
|
||||
create index idx_transactions_receipt_id on public.transactions (receipt_id);
|
||||
@@ -0,0 +1,33 @@
|
||||
-- Migration 5: Invoice Reminders
|
||||
-- Automated reminder tracking with self-service action tokens
|
||||
|
||||
-- =============================================================================
|
||||
-- invoice_reminders
|
||||
-- =============================================================================
|
||||
create table public.invoice_reminders (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
invoice_id uuid references public.invoices (id) on delete cascade not null,
|
||||
user_id uuid references auth.users on delete cascade not null,
|
||||
reminder_level integer not null check (reminder_level in (1, 2, 3)),
|
||||
sent_at timestamptz default now(),
|
||||
email_to text not null,
|
||||
response_type text check (response_type in ('marked_paid', 'disputed')),
|
||||
response_at timestamptz,
|
||||
action_token text unique not null default encode(gen_random_bytes(32), 'hex'),
|
||||
action_token_used boolean default false,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
alter table public.invoice_reminders enable row level security;
|
||||
|
||||
create policy "invoice_reminders_select" on public.invoice_reminders
|
||||
for select using (auth.uid() = user_id);
|
||||
create policy "invoice_reminders_insert" on public.invoice_reminders
|
||||
for insert with check (auth.uid() = user_id);
|
||||
create policy "invoice_reminders_update" on public.invoice_reminders
|
||||
for update using (auth.uid() = user_id);
|
||||
create policy "invoice_reminders_delete" on public.invoice_reminders
|
||||
for delete using (auth.uid() = user_id);
|
||||
|
||||
create index idx_invoice_reminders_action_token on public.invoice_reminders (action_token);
|
||||
create index idx_invoice_reminders_invoice on public.invoice_reminders (invoice_id);
|
||||
@@ -0,0 +1,36 @@
|
||||
-- Migration 6: Schablonavdrag - Mileage Entries
|
||||
-- Mileage deduction tracking (milersattning / korjournal)
|
||||
|
||||
-- =============================================================================
|
||||
-- mileage_entries
|
||||
-- =============================================================================
|
||||
create table public.mileage_entries (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
user_id uuid references auth.users on delete cascade not null,
|
||||
date date not null,
|
||||
distance_km numeric not null,
|
||||
purpose text not null,
|
||||
from_location text,
|
||||
to_location text,
|
||||
rate_per_km numeric default 2.50,
|
||||
total_deduction numeric not null,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
alter table public.mileage_entries enable row level security;
|
||||
|
||||
create policy "mileage_entries_select" on public.mileage_entries
|
||||
for select using (auth.uid() = user_id);
|
||||
create policy "mileage_entries_insert" on public.mileage_entries
|
||||
for insert with check (auth.uid() = user_id);
|
||||
create policy "mileage_entries_update" on public.mileage_entries
|
||||
for update using (auth.uid() = user_id);
|
||||
create policy "mileage_entries_delete" on public.mileage_entries
|
||||
for delete using (auth.uid() = user_id);
|
||||
|
||||
create index idx_mileage_entries_user_date on public.mileage_entries (user_id, date);
|
||||
|
||||
create trigger mileage_entries_updated_at
|
||||
before update on public.mileage_entries
|
||||
for each row execute function public.update_updated_at_column();
|
||||
@@ -0,0 +1,82 @@
|
||||
-- Migration 7: SIE Imports
|
||||
-- SIE (Standard Import Export) file import tracking and account mapping
|
||||
|
||||
-- =============================================================================
|
||||
-- 1. sie_imports
|
||||
-- =============================================================================
|
||||
create table public.sie_imports (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
user_id uuid references auth.users on delete cascade not null,
|
||||
filename text not null,
|
||||
file_hash text not null,
|
||||
org_number text,
|
||||
company_name text,
|
||||
sie_type integer not null,
|
||||
fiscal_year_start date,
|
||||
fiscal_year_end date,
|
||||
accounts_count integer default 0,
|
||||
transactions_count integer default 0,
|
||||
opening_balance_total numeric,
|
||||
status text default 'pending'
|
||||
check (status in ('pending', 'mapped', 'completed', 'failed')),
|
||||
error_message text,
|
||||
fiscal_period_id uuid references public.fiscal_periods (id) on delete set null,
|
||||
opening_balance_entry_id uuid references public.journal_entries (id) on delete set null,
|
||||
imported_at timestamptz,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
|
||||
unique (user_id, file_hash)
|
||||
);
|
||||
|
||||
alter table public.sie_imports enable row level security;
|
||||
|
||||
create policy "sie_imports_select" on public.sie_imports
|
||||
for select using (auth.uid() = user_id);
|
||||
create policy "sie_imports_insert" on public.sie_imports
|
||||
for insert with check (auth.uid() = user_id);
|
||||
create policy "sie_imports_update" on public.sie_imports
|
||||
for update using (auth.uid() = user_id);
|
||||
create policy "sie_imports_delete" on public.sie_imports
|
||||
for delete using (auth.uid() = user_id);
|
||||
|
||||
create index idx_sie_imports_user_status on public.sie_imports (user_id, status);
|
||||
|
||||
create trigger sie_imports_updated_at
|
||||
before update on public.sie_imports
|
||||
for each row execute function public.update_updated_at_column();
|
||||
|
||||
-- =============================================================================
|
||||
-- 2. sie_account_mappings
|
||||
-- =============================================================================
|
||||
create table public.sie_account_mappings (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
user_id uuid references auth.users on delete cascade not null,
|
||||
source_account text not null,
|
||||
source_name text,
|
||||
target_account text not null,
|
||||
confidence numeric default 1.0,
|
||||
match_type text default 'exact'
|
||||
check (match_type in ('exact', 'name', 'class', 'manual')),
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
|
||||
unique (user_id, source_account)
|
||||
);
|
||||
|
||||
alter table public.sie_account_mappings enable row level security;
|
||||
|
||||
create policy "sie_account_mappings_select" on public.sie_account_mappings
|
||||
for select using (auth.uid() = user_id);
|
||||
create policy "sie_account_mappings_insert" on public.sie_account_mappings
|
||||
for insert with check (auth.uid() = user_id);
|
||||
create policy "sie_account_mappings_update" on public.sie_account_mappings
|
||||
for update using (auth.uid() = user_id);
|
||||
create policy "sie_account_mappings_delete" on public.sie_account_mappings
|
||||
for delete using (auth.uid() = user_id);
|
||||
|
||||
create index idx_sie_account_mappings_user on public.sie_account_mappings (user_id);
|
||||
|
||||
create trigger sie_account_mappings_updated_at
|
||||
before update on public.sie_account_mappings
|
||||
for each row execute function public.update_updated_at_column();
|
||||
@@ -0,0 +1,89 @@
|
||||
-- Migration 8: Push Notifications
|
||||
-- Web push subscriptions, per-user notification preferences, delivery logging
|
||||
|
||||
-- =============================================================================
|
||||
-- 1. push_subscriptions
|
||||
-- =============================================================================
|
||||
create table public.push_subscriptions (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
user_id uuid references auth.users on delete cascade not null,
|
||||
endpoint text unique not null,
|
||||
p256dh text not null,
|
||||
auth text not null,
|
||||
user_agent text,
|
||||
is_active boolean default true,
|
||||
last_used_at timestamptz,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
alter table public.push_subscriptions enable row level security;
|
||||
|
||||
create policy "push_subscriptions_select" on public.push_subscriptions
|
||||
for select using (auth.uid() = user_id);
|
||||
create policy "push_subscriptions_insert" on public.push_subscriptions
|
||||
for insert with check (auth.uid() = user_id);
|
||||
create policy "push_subscriptions_update" on public.push_subscriptions
|
||||
for update using (auth.uid() = user_id);
|
||||
create policy "push_subscriptions_delete" on public.push_subscriptions
|
||||
for delete using (auth.uid() = user_id);
|
||||
|
||||
create index idx_push_subscriptions_user_active on public.push_subscriptions (user_id, is_active);
|
||||
|
||||
-- =============================================================================
|
||||
-- 2. notification_settings
|
||||
-- =============================================================================
|
||||
create table public.notification_settings (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
user_id uuid references auth.users on delete cascade unique not null,
|
||||
tax_deadlines_enabled boolean default true,
|
||||
invoice_reminders_enabled boolean default true,
|
||||
push_enabled boolean default true,
|
||||
email_enabled boolean default true,
|
||||
quiet_start text default '21:00',
|
||||
quiet_end text default '08:00',
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
alter table public.notification_settings enable row level security;
|
||||
|
||||
create policy "notification_settings_select" on public.notification_settings
|
||||
for select using (auth.uid() = user_id);
|
||||
create policy "notification_settings_insert" on public.notification_settings
|
||||
for insert with check (auth.uid() = user_id);
|
||||
create policy "notification_settings_update" on public.notification_settings
|
||||
for update using (auth.uid() = user_id);
|
||||
create policy "notification_settings_delete" on public.notification_settings
|
||||
for delete using (auth.uid() = user_id);
|
||||
|
||||
create trigger notification_settings_updated_at
|
||||
before update on public.notification_settings
|
||||
for each row execute function public.update_updated_at_column();
|
||||
|
||||
-- =============================================================================
|
||||
-- 3. notification_log
|
||||
-- =============================================================================
|
||||
create table public.notification_log (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
user_id uuid references auth.users on delete cascade not null,
|
||||
notification_type text not null
|
||||
check (notification_type in ('tax_deadline', 'invoice_due', 'invoice_overdue')),
|
||||
reference_id uuid not null,
|
||||
days_before integer not null,
|
||||
sent_at timestamptz default now(),
|
||||
delivery_status text default 'sent'
|
||||
check (delivery_status in ('sent', 'delivered', 'failed'))
|
||||
);
|
||||
|
||||
alter table public.notification_log enable row level security;
|
||||
|
||||
create policy "notification_log_select" on public.notification_log
|
||||
for select using (auth.uid() = user_id);
|
||||
create policy "notification_log_insert" on public.notification_log
|
||||
for insert with check (auth.uid() = user_id);
|
||||
create policy "notification_log_update" on public.notification_log
|
||||
for update using (auth.uid() = user_id);
|
||||
create policy "notification_log_delete" on public.notification_log
|
||||
for delete using (auth.uid() = user_id);
|
||||
|
||||
create index idx_notification_log_user_type_ref on public.notification_log (user_id, notification_type, reference_id);
|
||||
@@ -0,0 +1,171 @@
|
||||
-- Migration 9: Functions & Triggers
|
||||
-- seed_chart_of_accounts (BAS Kontoplan K1) and next_voucher_number
|
||||
|
||||
-- =============================================================================
|
||||
-- Function: seed_chart_of_accounts
|
||||
-- Seeds the standard Swedish BAS Kontoplan (K1) for a new user.
|
||||
-- Handles both enskild_firma (EF) and aktiebolag (AB) entity types.
|
||||
-- =============================================================================
|
||||
create or replace function public.seed_chart_of_accounts(p_user_id uuid, p_entity_type text)
|
||||
returns void
|
||||
language plpgsql
|
||||
security definer
|
||||
as $$
|
||||
declare
|
||||
v_account_count integer;
|
||||
begin
|
||||
-- Only seed if user has no existing accounts
|
||||
select count(*) into v_account_count
|
||||
from public.chart_of_accounts
|
||||
where user_id = p_user_id;
|
||||
|
||||
if v_account_count > 0 then
|
||||
return;
|
||||
end if;
|
||||
|
||||
-- =========================================================================
|
||||
-- Assets (1xxx) - Common to both EF and AB
|
||||
-- =========================================================================
|
||||
insert into public.chart_of_accounts (user_id, account_number, account_name, account_class, account_group, account_type, normal_balance, plan_type, is_system_account)
|
||||
values
|
||||
(p_user_id, '1510', 'Kundfordringar', 1, '15', 'asset', 'debit', 'k1', true),
|
||||
(p_user_id, '1910', 'Kassa', 1, '19', 'asset', 'debit', 'k1', true),
|
||||
(p_user_id, '1930', 'Foretagskonto / checkkonto', 1, '19', 'asset', 'debit', 'k1', true),
|
||||
(p_user_id, '1940', 'Ovriga bankkonton', 1, '19', 'asset', 'debit', 'k1', true);
|
||||
|
||||
-- =========================================================================
|
||||
-- Equity (2xxx) - Entity-type dependent
|
||||
-- =========================================================================
|
||||
if p_entity_type = 'enskild_firma' then
|
||||
insert into public.chart_of_accounts (user_id, account_number, account_name, account_class, account_group, account_type, normal_balance, plan_type, is_system_account)
|
||||
values
|
||||
(p_user_id, '2010', 'Eget kapital', 2, '20', 'equity', 'credit', 'k1', true),
|
||||
(p_user_id, '2013', 'Ovriga egna uttag', 2, '20', 'equity', 'credit', 'k1', true),
|
||||
(p_user_id, '2018', 'Ovriga egna insattningar', 2, '20', 'equity', 'credit', 'k1', true);
|
||||
end if;
|
||||
|
||||
if p_entity_type = 'aktiebolag' then
|
||||
insert into public.chart_of_accounts (user_id, account_number, account_name, account_class, account_group, account_type, normal_balance, plan_type, is_system_account)
|
||||
values
|
||||
(p_user_id, '2081', 'Aktiekapital', 2, '20', 'equity', 'credit', 'k1', true),
|
||||
(p_user_id, '2091', 'Balanserat resultat', 2, '20', 'equity', 'credit', 'k1', true),
|
||||
(p_user_id, '2099', 'Arets resultat', 2, '20', 'equity', 'credit', 'k1', true);
|
||||
end if;
|
||||
|
||||
-- =========================================================================
|
||||
-- Liabilities (2xxx) - Common
|
||||
-- =========================================================================
|
||||
insert into public.chart_of_accounts (user_id, account_number, account_name, account_class, account_group, account_type, normal_balance, plan_type, is_system_account)
|
||||
values
|
||||
(p_user_id, '2440', 'Leverantorsskulder', 2, '24', 'liability', 'credit', 'k1', true),
|
||||
(p_user_id, '2610', 'Utgaende moms 25%', 2, '26', 'liability', 'credit', 'k1', true),
|
||||
(p_user_id, '2611', 'Utgaende moms 12%', 2, '26', 'liability', 'credit', 'k1', true),
|
||||
(p_user_id, '2612', 'Utgaende moms 6%', 2, '26', 'liability', 'credit', 'k1', true),
|
||||
(p_user_id, '2641', 'Debiterad ingaende moms', 2, '26', 'liability', 'credit', 'k1', true),
|
||||
(p_user_id, '2650', 'Redovisningskonto for moms', 2, '26', 'liability', 'credit', 'k1', true),
|
||||
(p_user_id, '2710', 'Personalskatt', 2, '27', 'liability', 'credit', 'k1', true),
|
||||
(p_user_id, '2731', 'Avrakning socialavgifter', 2, '27', 'liability', 'credit', 'k1', true);
|
||||
|
||||
-- AB-only liability
|
||||
if p_entity_type = 'aktiebolag' then
|
||||
insert into public.chart_of_accounts (user_id, account_number, account_name, account_class, account_group, account_type, normal_balance, plan_type, is_system_account)
|
||||
values
|
||||
(p_user_id, '2893', 'Skuld till aktieagare', 2, '28', 'liability', 'credit', 'k1', true);
|
||||
end if;
|
||||
|
||||
-- =========================================================================
|
||||
-- Revenue (3xxx) - Common
|
||||
-- =========================================================================
|
||||
insert into public.chart_of_accounts (user_id, account_number, account_name, account_class, account_group, account_type, normal_balance, plan_type, is_system_account)
|
||||
values
|
||||
(p_user_id, '3001', 'Forsaljning tjanster 25%', 3, '30', 'revenue', 'credit', 'k1', true),
|
||||
(p_user_id, '3002', 'Forsaljning varor 25%', 3, '30', 'revenue', 'credit', 'k1', true),
|
||||
(p_user_id, '3100', 'Momsfri forsaljning', 3, '31', 'revenue', 'credit', 'k1', true),
|
||||
(p_user_id, '3900', 'Ovriga rorelseintakter', 3, '39', 'revenue', 'credit', 'k1', true),
|
||||
(p_user_id, '3960', 'Valutakursvinster', 3, '39', 'revenue', 'credit', 'k1', true);
|
||||
|
||||
-- =========================================================================
|
||||
-- COGS (4xxx) - Common
|
||||
-- =========================================================================
|
||||
insert into public.chart_of_accounts (user_id, account_number, account_name, account_class, account_group, account_type, normal_balance, plan_type, is_system_account)
|
||||
values
|
||||
(p_user_id, '4000', 'Varuinkop', 4, '40', 'expense', 'debit', 'k1', true);
|
||||
|
||||
-- =========================================================================
|
||||
-- External expenses (5xxx-6xxx) - Common
|
||||
-- =========================================================================
|
||||
insert into public.chart_of_accounts (user_id, account_number, account_name, account_class, account_group, account_type, normal_balance, plan_type, is_system_account)
|
||||
values
|
||||
(p_user_id, '5010', 'Lokalhyra', 5, '50', 'expense', 'debit', 'k1', true),
|
||||
(p_user_id, '5410', 'Forbrukningsinventarier', 5, '54', 'expense', 'debit', 'k1', true),
|
||||
(p_user_id, '5420', 'Programvaror', 5, '54', 'expense', 'debit', 'k1', true),
|
||||
(p_user_id, '5460', 'Forbrukningsmaterial', 5, '54', 'expense', 'debit', 'k1', true),
|
||||
(p_user_id, '5800', 'Resekostnader', 5, '58', 'expense', 'debit', 'k1', true),
|
||||
(p_user_id, '5910', 'Annonsering', 5, '59', 'expense', 'debit', 'k1', true),
|
||||
(p_user_id, '6071', 'Representation avdragsgill', 6, '60', 'expense', 'debit', 'k1', true),
|
||||
(p_user_id, '6110', 'Kontorsmateriel', 6, '61', 'expense', 'debit', 'k1', true),
|
||||
(p_user_id, '6212', 'Mobiltelefon', 6, '62', 'expense', 'debit', 'k1', true),
|
||||
(p_user_id, '6230', 'Datakommunikation', 6, '62', 'expense', 'debit', 'k1', true),
|
||||
(p_user_id, '6530', 'Redovisningstjanster', 6, '65', 'expense', 'debit', 'k1', true),
|
||||
(p_user_id, '6570', 'Bankavgifter', 6, '65', 'expense', 'debit', 'k1', true),
|
||||
(p_user_id, '6991', 'Ovriga avdragsgilla kostnader', 6, '69', 'expense', 'debit', 'k1', true);
|
||||
|
||||
-- =========================================================================
|
||||
-- Personnel (7xxx) - AB-only salary accounts + common currency losses
|
||||
-- =========================================================================
|
||||
if p_entity_type = 'aktiebolag' then
|
||||
insert into public.chart_of_accounts (user_id, account_number, account_name, account_class, account_group, account_type, normal_balance, plan_type, is_system_account)
|
||||
values
|
||||
(p_user_id, '7010', 'Loner', 7, '70', 'expense', 'debit', 'k1', true),
|
||||
(p_user_id, '7210', 'Semesterloner', 7, '72', 'expense', 'debit', 'k1', true),
|
||||
(p_user_id, '7510', 'Arbetsgivaravgifter', 7, '75', 'expense', 'debit', 'k1', true);
|
||||
end if;
|
||||
|
||||
-- Currency losses - Common
|
||||
insert into public.chart_of_accounts (user_id, account_number, account_name, account_class, account_group, account_type, normal_balance, plan_type, is_system_account)
|
||||
values
|
||||
(p_user_id, '7960', 'Valutakursforluster', 7, '79', 'expense', 'debit', 'k1', true);
|
||||
|
||||
-- =========================================================================
|
||||
-- Financial (8xxx) - Common
|
||||
-- =========================================================================
|
||||
insert into public.chart_of_accounts (user_id, account_number, account_name, account_class, account_group, account_type, normal_balance, plan_type, is_system_account)
|
||||
values
|
||||
(p_user_id, '8310', 'Ranteintakter', 8, '83', 'revenue', 'credit', 'k1', true),
|
||||
(p_user_id, '8410', 'Rantekostnader', 8, '84', 'expense', 'debit', 'k1', true);
|
||||
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- =============================================================================
|
||||
-- Function: next_voucher_number
|
||||
-- Returns the next sequential voucher number for a given fiscal period + series
|
||||
-- =============================================================================
|
||||
create or replace function public.next_voucher_number(
|
||||
p_user_id uuid,
|
||||
p_fiscal_period_id uuid,
|
||||
p_series text default 'A'
|
||||
)
|
||||
returns integer
|
||||
language plpgsql
|
||||
security definer
|
||||
as $$
|
||||
declare
|
||||
v_next integer;
|
||||
begin
|
||||
select coalesce(max(voucher_number), 0) + 1
|
||||
into v_next
|
||||
from public.journal_entries
|
||||
where user_id = p_user_id
|
||||
and fiscal_period_id = p_fiscal_period_id
|
||||
and voucher_series = p_series;
|
||||
|
||||
return v_next;
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- =============================================================================
|
||||
-- Grants
|
||||
-- =============================================================================
|
||||
grant execute on function public.seed_chart_of_accounts(uuid, text) to authenticated;
|
||||
grant execute on function public.next_voucher_number(uuid, uuid, text) to authenticated;
|
||||
@@ -1,14 +0,0 @@
|
||||
-- Add authorization_id column to bank_connections table
|
||||
-- This column stores the Enable Banking authorization_id before a session is created
|
||||
|
||||
ALTER TABLE bank_connections
|
||||
ADD COLUMN IF NOT EXISTS authorization_id TEXT;
|
||||
|
||||
-- Create index for faster lookups
|
||||
CREATE INDEX IF NOT EXISTS idx_bank_connections_authorization_id
|
||||
ON bank_connections(authorization_id)
|
||||
WHERE authorization_id IS NOT NULL;
|
||||
|
||||
-- Drop old requisition_id column if it exists (no longer used)
|
||||
ALTER TABLE bank_connections
|
||||
DROP COLUMN IF EXISTS requisition_id;
|
||||
@@ -1,13 +0,0 @@
|
||||
-- Add potential_invoice_id column to transactions table
|
||||
-- This column stores a suggested invoice match that awaits user confirmation
|
||||
|
||||
ALTER TABLE transactions
|
||||
ADD COLUMN IF NOT EXISTS potential_invoice_id UUID REFERENCES invoices(id) ON DELETE SET NULL;
|
||||
|
||||
-- Create index for faster lookups of transactions with potential matches
|
||||
CREATE INDEX IF NOT EXISTS idx_transactions_potential_invoice_id
|
||||
ON transactions(potential_invoice_id)
|
||||
WHERE potential_invoice_id IS NOT NULL;
|
||||
|
||||
-- Add comment explaining the column
|
||||
COMMENT ON COLUMN transactions.potential_invoice_id IS 'Suggested invoice match - requires user confirmation before linking';
|
||||
@@ -1,60 +0,0 @@
|
||||
-- Add schablonavdrag settings to company_settings
|
||||
-- hemmakontor_housing_type: 'villa' = 2000 kr/year, 'apartment' = 4000 kr/year
|
||||
ALTER TABLE company_settings ADD COLUMN IF NOT EXISTS
|
||||
schablonavdrag_settings jsonb DEFAULT '{"hemmakontor_enabled": false, "hemmakontor_housing_type": "apartment", "bil_enabled": false}';
|
||||
|
||||
-- Create mileage entries table for bil schablonavdrag
|
||||
-- Rate: 25 kr/mil = 2.50 kr/km
|
||||
CREATE TABLE IF NOT EXISTS mileage_entries (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id uuid REFERENCES auth.users NOT NULL,
|
||||
date date NOT NULL,
|
||||
distance_km decimal(10,2) NOT NULL,
|
||||
purpose text NOT NULL,
|
||||
from_location text,
|
||||
to_location text,
|
||||
rate_per_km decimal(5,2) DEFAULT 2.50,
|
||||
total_deduction decimal(10,2) GENERATED ALWAYS AS (distance_km * rate_per_km) STORED,
|
||||
created_at timestamptz DEFAULT now(),
|
||||
updated_at timestamptz DEFAULT now()
|
||||
);
|
||||
|
||||
-- Add indexes
|
||||
CREATE INDEX IF NOT EXISTS idx_mileage_entries_user_id ON mileage_entries(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_mileage_entries_date ON mileage_entries(date);
|
||||
CREATE INDEX IF NOT EXISTS idx_mileage_entries_user_date ON mileage_entries(user_id, date);
|
||||
|
||||
-- Enable RLS
|
||||
ALTER TABLE mileage_entries ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- RLS policies for mileage_entries
|
||||
CREATE POLICY "Users can view their own mileage entries"
|
||||
ON mileage_entries FOR SELECT
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can insert their own mileage entries"
|
||||
ON mileage_entries FOR INSERT
|
||||
WITH CHECK (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can update their own mileage entries"
|
||||
ON mileage_entries FOR UPDATE
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can delete their own mileage entries"
|
||||
ON mileage_entries FOR DELETE
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
-- Add trigger for updated_at
|
||||
CREATE OR REPLACE FUNCTION update_mileage_entries_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = now();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
DROP TRIGGER IF EXISTS update_mileage_entries_updated_at ON mileage_entries;
|
||||
CREATE TRIGGER update_mileage_entries_updated_at
|
||||
BEFORE UPDATE ON mileage_entries
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE update_mileage_entries_updated_at();
|
||||
@@ -1,69 +0,0 @@
|
||||
-- Create gifts table for benefits/gifts module (Förmånshantering)
|
||||
-- Tracks PR products, gifted collabs, and equipment that may be taxable income
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gifts (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id uuid REFERENCES auth.users NOT NULL,
|
||||
date date NOT NULL,
|
||||
brand_name text NOT NULL,
|
||||
description text NOT NULL,
|
||||
estimated_value decimal(12,2) NOT NULL,
|
||||
|
||||
-- Decision tree inputs
|
||||
has_motprestation boolean NOT NULL DEFAULT false, -- Required post/video/mention?
|
||||
used_in_business boolean NOT NULL DEFAULT false, -- Used as props/equipment?
|
||||
used_privately boolean NOT NULL DEFAULT false, -- Personal use?
|
||||
is_simple_promo boolean DEFAULT false, -- Pen, mug, basic merch?
|
||||
|
||||
-- Classification result (computed by application)
|
||||
classification jsonb NOT NULL DEFAULT '{}',
|
||||
-- Structure: { taxable, deductibleAsExpense, bookingType, reasoning }
|
||||
|
||||
-- Bookkeeping link
|
||||
journal_entry_id uuid REFERENCES journal_entries(id),
|
||||
|
||||
-- Timestamps
|
||||
created_at timestamptz DEFAULT now(),
|
||||
updated_at timestamptz DEFAULT now()
|
||||
);
|
||||
|
||||
-- Add indexes for common queries
|
||||
CREATE INDEX IF NOT EXISTS idx_gifts_user_id ON gifts(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_gifts_date ON gifts(date);
|
||||
CREATE INDEX IF NOT EXISTS idx_gifts_user_date ON gifts(user_id, date);
|
||||
CREATE INDEX IF NOT EXISTS idx_gifts_user_year ON gifts(user_id, EXTRACT(YEAR FROM date));
|
||||
|
||||
-- Enable RLS
|
||||
ALTER TABLE gifts ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- RLS policies for gifts
|
||||
CREATE POLICY "Users can view their own gifts"
|
||||
ON gifts FOR SELECT
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can insert their own gifts"
|
||||
ON gifts FOR INSERT
|
||||
WITH CHECK (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can update their own gifts"
|
||||
ON gifts FOR UPDATE
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can delete their own gifts"
|
||||
ON gifts FOR DELETE
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
-- Add trigger for updated_at
|
||||
CREATE OR REPLACE FUNCTION update_gifts_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = now();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
DROP TRIGGER IF EXISTS update_gifts_updated_at ON gifts;
|
||||
CREATE TRIGGER update_gifts_updated_at
|
||||
BEFORE UPDATE ON gifts
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE update_gifts_updated_at();
|
||||
@@ -1,122 +0,0 @@
|
||||
-- SIE Import Tracking
|
||||
-- Stores metadata about SIE file imports for audit trail and duplicate detection
|
||||
|
||||
CREATE TABLE sie_imports (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
||||
|
||||
-- File info
|
||||
filename TEXT NOT NULL,
|
||||
file_hash TEXT NOT NULL, -- SHA-256 hash for duplicate detection
|
||||
|
||||
-- Company info from SIE file
|
||||
org_number TEXT,
|
||||
company_name TEXT,
|
||||
|
||||
-- SIE metadata
|
||||
sie_type INTEGER NOT NULL, -- 1, 2, 3, or 4
|
||||
fiscal_year_start DATE,
|
||||
fiscal_year_end DATE,
|
||||
|
||||
-- Import statistics
|
||||
accounts_count INTEGER DEFAULT 0,
|
||||
transactions_count INTEGER DEFAULT 0,
|
||||
opening_balance_total DECIMAL(15,2),
|
||||
|
||||
-- Status tracking
|
||||
status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'mapped', 'completed', 'failed')),
|
||||
error_message TEXT,
|
||||
|
||||
-- Import result references
|
||||
fiscal_period_id UUID REFERENCES fiscal_periods(id),
|
||||
opening_balance_entry_id UUID REFERENCES journal_entries(id),
|
||||
|
||||
-- Timestamps
|
||||
imported_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Index for duplicate detection
|
||||
CREATE INDEX idx_sie_imports_user_hash ON sie_imports(user_id, file_hash);
|
||||
CREATE INDEX idx_sie_imports_user_status ON sie_imports(user_id, status);
|
||||
|
||||
-- Row Level Security
|
||||
ALTER TABLE sie_imports ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY "Users can view own imports"
|
||||
ON sie_imports FOR SELECT
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can insert own imports"
|
||||
ON sie_imports FOR INSERT
|
||||
WITH CHECK (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can update own imports"
|
||||
ON sie_imports FOR UPDATE
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can delete own imports"
|
||||
ON sie_imports FOR DELETE
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
-- Trigger for updated_at
|
||||
CREATE TRIGGER update_sie_imports_updated_at
|
||||
BEFORE UPDATE ON sie_imports
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
|
||||
-- Account Mappings
|
||||
-- Stores user overrides for account mapping during SIE import
|
||||
|
||||
CREATE TABLE sie_account_mappings (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
||||
|
||||
-- Source account (from SIE file)
|
||||
source_account TEXT NOT NULL,
|
||||
source_name TEXT,
|
||||
|
||||
-- Target account (in our system)
|
||||
target_account TEXT NOT NULL,
|
||||
|
||||
-- Mapping quality
|
||||
confidence DECIMAL(3,2) DEFAULT 1.0 CHECK (confidence >= 0 AND confidence <= 1),
|
||||
match_type TEXT DEFAULT 'manual' CHECK (match_type IN ('exact', 'name', 'class', 'manual')),
|
||||
|
||||
-- Timestamps
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
|
||||
-- One mapping per source account per user
|
||||
UNIQUE(user_id, source_account)
|
||||
);
|
||||
|
||||
-- Index for lookups
|
||||
CREATE INDEX idx_sie_account_mappings_user ON sie_account_mappings(user_id);
|
||||
|
||||
-- Row Level Security
|
||||
ALTER TABLE sie_account_mappings ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY "Users can view own mappings"
|
||||
ON sie_account_mappings FOR SELECT
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can insert own mappings"
|
||||
ON sie_account_mappings FOR INSERT
|
||||
WITH CHECK (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can update own mappings"
|
||||
ON sie_account_mappings FOR UPDATE
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can delete own mappings"
|
||||
ON sie_account_mappings FOR DELETE
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
-- Trigger for updated_at
|
||||
CREATE TRIGGER update_sie_account_mappings_updated_at
|
||||
BEFORE UPDATE ON sie_account_mappings
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
@@ -1,548 +0,0 @@
|
||||
-- Create campaigns and related tables for influencer contract/campaign management
|
||||
-- This migration creates: campaigns, deliverables, exclusivities, contracts
|
||||
-- and extends: customers, invoices, deadlines
|
||||
|
||||
-- ============================================================
|
||||
-- ENUMS (using CHECK constraints for compatibility)
|
||||
-- ============================================================
|
||||
|
||||
-- Campaign status enum values:
|
||||
-- negotiation: Under negotiation/discussion
|
||||
-- contracted: Contract signed, not yet started
|
||||
-- active: Currently running
|
||||
-- delivered: All deliverables completed
|
||||
-- invoiced: Invoice sent
|
||||
-- completed: Fully paid and closed
|
||||
-- cancelled: Cancelled/terminated
|
||||
|
||||
-- Campaign type enum values:
|
||||
-- influencer: Standard influencer campaign
|
||||
-- ugc: User-generated content only
|
||||
-- ambassador: Long-term ambassador deal
|
||||
|
||||
-- Deliverable type enum values:
|
||||
-- video: Video content (YouTube, TikTok video)
|
||||
-- image: Static image/photo
|
||||
-- story: Instagram/TikTok story
|
||||
-- reel: Instagram/TikTok reel
|
||||
-- post: Feed post
|
||||
-- raw_material: Raw content files for brand use
|
||||
|
||||
-- Deliverable status enum values:
|
||||
-- pending: Not started
|
||||
-- in_progress: Being worked on
|
||||
-- submitted: Submitted for review
|
||||
-- revision: Needs revisions
|
||||
-- approved: Approved by brand
|
||||
-- published: Published/live
|
||||
|
||||
-- Platform type enum values:
|
||||
-- instagram, tiktok, youtube, blog, podcast, other
|
||||
|
||||
-- ============================================================
|
||||
-- NEW TABLE: campaigns
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS campaigns (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id uuid REFERENCES auth.users NOT NULL,
|
||||
|
||||
-- Customer relationships
|
||||
customer_id uuid REFERENCES customers(id) ON DELETE SET NULL,
|
||||
end_customer_id uuid REFERENCES customers(id) ON DELETE SET NULL, -- For agency-brand relationships
|
||||
|
||||
-- Basic info
|
||||
name text NOT NULL,
|
||||
description text NULL,
|
||||
|
||||
-- Status and type
|
||||
status text NOT NULL DEFAULT 'negotiation' CHECK (status IN (
|
||||
'negotiation', 'contracted', 'active', 'delivered', 'invoiced', 'completed', 'cancelled'
|
||||
)),
|
||||
campaign_type text NOT NULL DEFAULT 'influencer' CHECK (campaign_type IN (
|
||||
'influencer', 'ugc', 'ambassador'
|
||||
)),
|
||||
|
||||
-- Financial
|
||||
total_value decimal(12,2) NULL,
|
||||
currency text NOT NULL DEFAULT 'SEK',
|
||||
vat_included boolean NOT NULL DEFAULT false,
|
||||
payment_terms integer NULL, -- Days
|
||||
billing_frequency text NULL CHECK (billing_frequency IS NULL OR billing_frequency IN (
|
||||
'upfront', 'on_delivery', 'monthly', 'split'
|
||||
)),
|
||||
|
||||
-- Dates
|
||||
start_date date NULL,
|
||||
end_date date NULL,
|
||||
contract_signed_at timestamptz NULL,
|
||||
|
||||
-- Notes
|
||||
notes text NULL,
|
||||
|
||||
-- Timestamps
|
||||
created_at timestamptz DEFAULT now(),
|
||||
updated_at timestamptz DEFAULT now()
|
||||
);
|
||||
|
||||
-- Indexes for campaigns
|
||||
CREATE INDEX IF NOT EXISTS idx_campaigns_user_id ON campaigns(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_campaigns_customer_id ON campaigns(customer_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_campaigns_status ON campaigns(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_campaigns_dates ON campaigns(start_date, end_date);
|
||||
CREATE INDEX IF NOT EXISTS idx_campaigns_user_status ON campaigns(user_id, status);
|
||||
|
||||
-- Enable RLS for campaigns
|
||||
ALTER TABLE campaigns ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- RLS policies for campaigns
|
||||
CREATE POLICY "Users can view their own campaigns"
|
||||
ON campaigns FOR SELECT
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can insert their own campaigns"
|
||||
ON campaigns FOR INSERT
|
||||
WITH CHECK (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can update their own campaigns"
|
||||
ON campaigns FOR UPDATE
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can delete their own campaigns"
|
||||
ON campaigns FOR DELETE
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
-- ============================================================
|
||||
-- NEW TABLE: deliverables
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS deliverables (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
campaign_id uuid REFERENCES campaigns(id) ON DELETE CASCADE NOT NULL,
|
||||
user_id uuid REFERENCES auth.users NOT NULL,
|
||||
|
||||
-- Content details
|
||||
title text NOT NULL,
|
||||
deliverable_type text NOT NULL CHECK (deliverable_type IN (
|
||||
'video', 'image', 'story', 'reel', 'post', 'raw_material'
|
||||
)),
|
||||
platform text NOT NULL CHECK (platform IN (
|
||||
'instagram', 'tiktok', 'youtube', 'blog', 'podcast', 'other'
|
||||
)),
|
||||
account_handle text NULL, -- @handle or channel name
|
||||
|
||||
-- Quantity and description
|
||||
quantity integer NOT NULL DEFAULT 1,
|
||||
description text NULL,
|
||||
specifications jsonb NULL DEFAULT '{}', -- Custom specs like length, format, etc.
|
||||
|
||||
-- Dates
|
||||
due_date date NULL,
|
||||
|
||||
-- Status tracking
|
||||
status text NOT NULL DEFAULT 'pending' CHECK (status IN (
|
||||
'pending', 'in_progress', 'submitted', 'revision', 'approved', 'published'
|
||||
)),
|
||||
submitted_at timestamptz NULL,
|
||||
approved_at timestamptz NULL,
|
||||
published_at timestamptz NULL,
|
||||
|
||||
-- Notes
|
||||
notes text NULL,
|
||||
|
||||
-- Timestamps
|
||||
created_at timestamptz DEFAULT now(),
|
||||
updated_at timestamptz DEFAULT now()
|
||||
);
|
||||
|
||||
-- Indexes for deliverables
|
||||
CREATE INDEX IF NOT EXISTS idx_deliverables_user_id ON deliverables(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_deliverables_campaign_id ON deliverables(campaign_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_deliverables_status ON deliverables(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_deliverables_due_date ON deliverables(due_date);
|
||||
CREATE INDEX IF NOT EXISTS idx_deliverables_user_due_date ON deliverables(user_id, due_date);
|
||||
|
||||
-- Enable RLS for deliverables
|
||||
ALTER TABLE deliverables ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- RLS policies for deliverables
|
||||
CREATE POLICY "Users can view their own deliverables"
|
||||
ON deliverables FOR SELECT
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can insert their own deliverables"
|
||||
ON deliverables FOR INSERT
|
||||
WITH CHECK (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can update their own deliverables"
|
||||
ON deliverables FOR UPDATE
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can delete their own deliverables"
|
||||
ON deliverables FOR DELETE
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
-- ============================================================
|
||||
-- NEW TABLE: exclusivities
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS exclusivities (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
campaign_id uuid REFERENCES campaigns(id) ON DELETE CASCADE NOT NULL,
|
||||
user_id uuid REFERENCES auth.users NOT NULL,
|
||||
|
||||
-- What is excluded
|
||||
categories text[] NOT NULL DEFAULT '{}', -- Product/industry categories
|
||||
excluded_brands text[] NULL DEFAULT '{}', -- Specific brand names
|
||||
|
||||
-- Time period - absolute dates
|
||||
start_date date NOT NULL,
|
||||
end_date date NOT NULL,
|
||||
|
||||
-- Date calculation metadata (for future relative date support)
|
||||
start_calculation_type text NOT NULL DEFAULT 'absolute' CHECK (start_calculation_type IN (
|
||||
'absolute', 'relative'
|
||||
)),
|
||||
end_calculation_type text NOT NULL DEFAULT 'absolute' CHECK (end_calculation_type IN (
|
||||
'absolute', 'relative'
|
||||
)),
|
||||
start_reference text NULL CHECK (start_reference IS NULL OR start_reference IN (
|
||||
'publication', 'delivery', 'approval', 'contract'
|
||||
)),
|
||||
end_reference text NULL CHECK (end_reference IS NULL OR end_reference IN (
|
||||
'publication', 'delivery', 'approval', 'contract'
|
||||
)),
|
||||
start_offset_days integer NULL,
|
||||
end_offset_days integer NULL,
|
||||
|
||||
-- Notes
|
||||
notes text NULL,
|
||||
|
||||
-- Timestamps
|
||||
created_at timestamptz DEFAULT now(),
|
||||
updated_at timestamptz DEFAULT now()
|
||||
);
|
||||
|
||||
-- Indexes for exclusivities
|
||||
CREATE INDEX IF NOT EXISTS idx_exclusivities_user_id ON exclusivities(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_exclusivities_campaign_id ON exclusivities(campaign_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_exclusivities_dates ON exclusivities(start_date, end_date);
|
||||
CREATE INDEX IF NOT EXISTS idx_exclusivities_user_dates ON exclusivities(user_id, start_date, end_date);
|
||||
|
||||
-- Enable RLS for exclusivities
|
||||
ALTER TABLE exclusivities ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- RLS policies for exclusivities
|
||||
CREATE POLICY "Users can view their own exclusivities"
|
||||
ON exclusivities FOR SELECT
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can insert their own exclusivities"
|
||||
ON exclusivities FOR INSERT
|
||||
WITH CHECK (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can update their own exclusivities"
|
||||
ON exclusivities FOR UPDATE
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can delete their own exclusivities"
|
||||
ON exclusivities FOR DELETE
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
-- ============================================================
|
||||
-- NEW TABLE: contracts
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS contracts (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
campaign_id uuid REFERENCES campaigns(id) ON DELETE CASCADE NULL, -- Nullable for import workflow
|
||||
user_id uuid REFERENCES auth.users NOT NULL,
|
||||
|
||||
-- File info
|
||||
filename text NOT NULL,
|
||||
file_path text NOT NULL, -- Supabase Storage path
|
||||
file_size integer NULL, -- bytes
|
||||
mime_type text NULL,
|
||||
|
||||
-- AI extraction (for future use)
|
||||
extracted_data jsonb NULL DEFAULT '{}',
|
||||
extraction_status text NOT NULL DEFAULT 'pending' CHECK (extraction_status IN (
|
||||
'pending', 'processing', 'completed', 'failed', 'skipped'
|
||||
)),
|
||||
|
||||
-- Contract details
|
||||
signing_date date NULL,
|
||||
is_primary boolean NOT NULL DEFAULT false, -- Main contract vs amendments
|
||||
|
||||
-- Notes
|
||||
notes text NULL,
|
||||
|
||||
-- Timestamps
|
||||
uploaded_at timestamptz DEFAULT now(),
|
||||
created_at timestamptz DEFAULT now(),
|
||||
updated_at timestamptz DEFAULT now()
|
||||
);
|
||||
|
||||
-- Indexes for contracts
|
||||
CREATE INDEX IF NOT EXISTS idx_contracts_user_id ON contracts(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_contracts_campaign_id ON contracts(campaign_id);
|
||||
|
||||
-- Enable RLS for contracts
|
||||
ALTER TABLE contracts ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- RLS policies for contracts
|
||||
CREATE POLICY "Users can view their own contracts"
|
||||
ON contracts FOR SELECT
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can insert their own contracts"
|
||||
ON contracts FOR INSERT
|
||||
WITH CHECK (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can update their own contracts"
|
||||
ON contracts FOR UPDATE
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can delete their own contracts"
|
||||
ON contracts FOR DELETE
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
-- ============================================================
|
||||
-- EXTEND TABLE: customers
|
||||
-- ============================================================
|
||||
|
||||
-- Add customer_category column
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'customers' AND column_name = 'customer_category'
|
||||
) THEN
|
||||
ALTER TABLE customers ADD COLUMN customer_category text NULL CHECK (
|
||||
customer_category IS NULL OR customer_category IN ('brand', 'agency', 'platform')
|
||||
);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Add average_payment_days column
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'customers' AND column_name = 'average_payment_days'
|
||||
) THEN
|
||||
ALTER TABLE customers ADD COLUMN average_payment_days integer NULL;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- ============================================================
|
||||
-- EXTEND TABLE: invoices
|
||||
-- ============================================================
|
||||
|
||||
-- Add campaign_id column
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'invoices' AND column_name = 'campaign_id'
|
||||
) THEN
|
||||
ALTER TABLE invoices ADD COLUMN campaign_id uuid REFERENCES campaigns(id) ON DELETE SET NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_invoices_campaign_id ON invoices(campaign_id);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Add payment_status column
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'invoices' AND column_name = 'payment_status'
|
||||
) THEN
|
||||
ALTER TABLE invoices ADD COLUMN payment_status text NULL CHECK (
|
||||
payment_status IS NULL OR payment_status IN ('pending', 'paid', 'overdue', 'partial')
|
||||
);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Add expected_payment_date column
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'invoices' AND column_name = 'expected_payment_date'
|
||||
) THEN
|
||||
ALTER TABLE invoices ADD COLUMN expected_payment_date date NULL;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Add actual_payment_date column
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'invoices' AND column_name = 'actual_payment_date'
|
||||
) THEN
|
||||
ALTER TABLE invoices ADD COLUMN actual_payment_date date NULL;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- ============================================================
|
||||
-- EXTEND TABLE: deadlines
|
||||
-- ============================================================
|
||||
|
||||
-- Add campaign_id column
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'deadlines' AND column_name = 'campaign_id'
|
||||
) THEN
|
||||
ALTER TABLE deadlines ADD COLUMN campaign_id uuid REFERENCES campaigns(id) ON DELETE SET NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_deadlines_campaign_id ON deadlines(campaign_id);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Add deliverable_id column
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'deadlines' AND column_name = 'deliverable_id'
|
||||
) THEN
|
||||
ALTER TABLE deadlines ADD COLUMN deliverable_id uuid REFERENCES deliverables(id) ON DELETE SET NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_deadlines_deliverable_id ON deadlines(deliverable_id);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Add is_auto_generated column
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'deadlines' AND column_name = 'is_auto_generated'
|
||||
) THEN
|
||||
ALTER TABLE deadlines ADD COLUMN is_auto_generated boolean NOT NULL DEFAULT false;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Add date_calculation_type column
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'deadlines' AND column_name = 'date_calculation_type'
|
||||
) THEN
|
||||
ALTER TABLE deadlines ADD COLUMN date_calculation_type text NULL CHECK (
|
||||
date_calculation_type IS NULL OR date_calculation_type IN ('absolute', 'relative')
|
||||
);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Add reference_event column
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'deadlines' AND column_name = 'reference_event'
|
||||
) THEN
|
||||
ALTER TABLE deadlines ADD COLUMN reference_event text NULL CHECK (
|
||||
reference_event IS NULL OR reference_event IN ('publication', 'delivery', 'approval', 'contract')
|
||||
);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Add offset_days column
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'deadlines' AND column_name = 'offset_days'
|
||||
) THEN
|
||||
ALTER TABLE deadlines ADD COLUMN offset_days integer NULL;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Update deadline_type CHECK constraint to include new types
|
||||
-- First, drop the old constraint and add new one
|
||||
DO $$
|
||||
BEGIN
|
||||
-- Drop old constraint if exists (ignore error if not exists)
|
||||
BEGIN
|
||||
ALTER TABLE deadlines DROP CONSTRAINT IF EXISTS deadlines_deadline_type_check;
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
-- Constraint might not exist or have different name
|
||||
NULL;
|
||||
END;
|
||||
|
||||
-- Add new constraint with all types
|
||||
ALTER TABLE deadlines ADD CONSTRAINT deadlines_deadline_type_check CHECK (deadline_type IN (
|
||||
'delivery', 'approval', 'invoicing', 'report', 'revision', 'assets', 'spark_ad', 'statistics', 'other'
|
||||
));
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
-- If constraint already exists with correct values, ignore
|
||||
NULL;
|
||||
END $$;
|
||||
|
||||
-- ============================================================
|
||||
-- TRIGGERS for updated_at
|
||||
-- ============================================================
|
||||
|
||||
-- Campaigns updated_at trigger
|
||||
CREATE OR REPLACE FUNCTION update_campaigns_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = now();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
DROP TRIGGER IF EXISTS update_campaigns_updated_at ON campaigns;
|
||||
CREATE TRIGGER update_campaigns_updated_at
|
||||
BEFORE UPDATE ON campaigns
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE update_campaigns_updated_at();
|
||||
|
||||
-- Deliverables updated_at trigger
|
||||
CREATE OR REPLACE FUNCTION update_deliverables_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = now();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
DROP TRIGGER IF EXISTS update_deliverables_updated_at ON deliverables;
|
||||
CREATE TRIGGER update_deliverables_updated_at
|
||||
BEFORE UPDATE ON deliverables
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE update_deliverables_updated_at();
|
||||
|
||||
-- Exclusivities updated_at trigger
|
||||
CREATE OR REPLACE FUNCTION update_exclusivities_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = now();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
DROP TRIGGER IF EXISTS update_exclusivities_updated_at ON exclusivities;
|
||||
CREATE TRIGGER update_exclusivities_updated_at
|
||||
BEFORE UPDATE ON exclusivities
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE update_exclusivities_updated_at();
|
||||
|
||||
-- Contracts updated_at trigger
|
||||
CREATE OR REPLACE FUNCTION update_contracts_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = now();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
DROP TRIGGER IF EXISTS update_contracts_updated_at ON contracts;
|
||||
CREATE TRIGGER update_contracts_updated_at
|
||||
BEFORE UPDATE ON contracts
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE update_contracts_updated_at();
|
||||
@@ -1,76 +0,0 @@
|
||||
-- Create deadlines table for calendar/planner module
|
||||
-- Tracks deliveries, invoicing deadlines, reports, and other important dates
|
||||
|
||||
CREATE TABLE IF NOT EXISTS deadlines (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id uuid REFERENCES auth.users NOT NULL,
|
||||
|
||||
-- Core fields
|
||||
title text NOT NULL,
|
||||
due_date date NOT NULL,
|
||||
due_time time NULL,
|
||||
|
||||
-- Type and priority
|
||||
deadline_type text NOT NULL CHECK (deadline_type IN (
|
||||
'delivery', 'invoicing', 'report', 'approval', 'other'
|
||||
)),
|
||||
priority text NOT NULL DEFAULT 'normal' CHECK (priority IN (
|
||||
'critical', 'important', 'normal'
|
||||
)),
|
||||
|
||||
-- Completion status
|
||||
is_completed boolean NOT NULL DEFAULT false,
|
||||
completed_at timestamptz NULL,
|
||||
|
||||
-- Optional customer link
|
||||
customer_id uuid REFERENCES customers(id) ON DELETE SET NULL,
|
||||
|
||||
-- Notes
|
||||
notes text NULL,
|
||||
|
||||
-- Timestamps
|
||||
created_at timestamptz DEFAULT now(),
|
||||
updated_at timestamptz DEFAULT now()
|
||||
);
|
||||
|
||||
-- Add indexes for common queries
|
||||
CREATE INDEX IF NOT EXISTS idx_deadlines_user_id ON deadlines(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_deadlines_due_date ON deadlines(due_date);
|
||||
CREATE INDEX IF NOT EXISTS idx_deadlines_user_due_date ON deadlines(user_id, due_date);
|
||||
CREATE INDEX IF NOT EXISTS idx_deadlines_user_completed ON deadlines(user_id, is_completed);
|
||||
CREATE INDEX IF NOT EXISTS idx_deadlines_customer_id ON deadlines(customer_id);
|
||||
|
||||
-- Enable RLS
|
||||
ALTER TABLE deadlines ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- RLS policies for deadlines
|
||||
CREATE POLICY "Users can view their own deadlines"
|
||||
ON deadlines FOR SELECT
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can insert their own deadlines"
|
||||
ON deadlines FOR INSERT
|
||||
WITH CHECK (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can update their own deadlines"
|
||||
ON deadlines FOR UPDATE
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can delete their own deadlines"
|
||||
ON deadlines FOR DELETE
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
-- Add trigger for updated_at
|
||||
CREATE OR REPLACE FUNCTION update_deadlines_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = now();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
DROP TRIGGER IF EXISTS update_deadlines_updated_at ON deadlines;
|
||||
CREATE TRIGGER update_deadlines_updated_at
|
||||
BEFORE UPDATE ON deadlines
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE update_deadlines_updated_at();
|
||||
@@ -1,315 +0,0 @@
|
||||
-- Create TikTok integration tables for influencer analytics and ROI tracking
|
||||
-- This migration creates: tiktok_accounts, tiktok_daily_stats, tiktok_videos, tiktok_sync_logs
|
||||
|
||||
-- ============================================================
|
||||
-- TABLE: tiktok_accounts
|
||||
-- Stores connected TikTok accounts with encrypted tokens
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tiktok_accounts (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id uuid REFERENCES auth.users NOT NULL,
|
||||
|
||||
-- TikTok user info
|
||||
tiktok_user_id text NOT NULL UNIQUE,
|
||||
username text NOT NULL,
|
||||
display_name text NULL,
|
||||
avatar_url text NULL,
|
||||
|
||||
-- Encrypted tokens (AES-256-GCM, format: iv:authTag:encrypted in base64)
|
||||
access_token_encrypted text NOT NULL,
|
||||
refresh_token_encrypted text NOT NULL,
|
||||
|
||||
-- Token expiration
|
||||
token_expires_at timestamptz NOT NULL,
|
||||
refresh_token_expires_at timestamptz NOT NULL,
|
||||
|
||||
-- Account status
|
||||
status text NOT NULL DEFAULT 'active' CHECK (status IN (
|
||||
'active', 'expired', 'revoked', 'error'
|
||||
)),
|
||||
|
||||
-- Sync tracking
|
||||
last_synced_at timestamptz NULL,
|
||||
last_stats_sync_at timestamptz NULL,
|
||||
last_video_sync_at timestamptz NULL,
|
||||
|
||||
-- Error tracking
|
||||
last_error text NULL,
|
||||
error_count integer NOT NULL DEFAULT 0,
|
||||
|
||||
-- Timestamps
|
||||
created_at timestamptz DEFAULT now(),
|
||||
updated_at timestamptz DEFAULT now()
|
||||
);
|
||||
|
||||
-- Indexes for tiktok_accounts
|
||||
CREATE INDEX IF NOT EXISTS idx_tiktok_accounts_user_id ON tiktok_accounts(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tiktok_accounts_status ON tiktok_accounts(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_tiktok_accounts_tiktok_user_id ON tiktok_accounts(tiktok_user_id);
|
||||
|
||||
-- Enable RLS for tiktok_accounts
|
||||
ALTER TABLE tiktok_accounts ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- RLS policies for tiktok_accounts
|
||||
CREATE POLICY "Users can view their own TikTok accounts"
|
||||
ON tiktok_accounts FOR SELECT
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can insert their own TikTok accounts"
|
||||
ON tiktok_accounts FOR INSERT
|
||||
WITH CHECK (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can update their own TikTok accounts"
|
||||
ON tiktok_accounts FOR UPDATE
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can delete their own TikTok accounts"
|
||||
ON tiktok_accounts FOR DELETE
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
-- ============================================================
|
||||
-- TABLE: tiktok_daily_stats
|
||||
-- Daily snapshots of account statistics for growth tracking
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tiktok_daily_stats (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tiktok_account_id uuid REFERENCES tiktok_accounts(id) ON DELETE CASCADE NOT NULL,
|
||||
user_id uuid REFERENCES auth.users NOT NULL,
|
||||
|
||||
-- Date of snapshot
|
||||
stats_date date NOT NULL,
|
||||
|
||||
-- Follower metrics
|
||||
follower_count bigint NOT NULL DEFAULT 0,
|
||||
following_count bigint NOT NULL DEFAULT 0,
|
||||
|
||||
-- Content metrics
|
||||
likes_count bigint NOT NULL DEFAULT 0,
|
||||
video_count integer NOT NULL DEFAULT 0,
|
||||
|
||||
-- Calculated change from previous day
|
||||
follower_change integer NULL,
|
||||
|
||||
-- Timestamps
|
||||
created_at timestamptz DEFAULT now(),
|
||||
|
||||
-- Unique constraint to prevent duplicate entries
|
||||
UNIQUE(tiktok_account_id, stats_date)
|
||||
);
|
||||
|
||||
-- Indexes for tiktok_daily_stats
|
||||
CREATE INDEX IF NOT EXISTS idx_tiktok_daily_stats_user_id ON tiktok_daily_stats(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tiktok_daily_stats_account_id ON tiktok_daily_stats(tiktok_account_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tiktok_daily_stats_date ON tiktok_daily_stats(stats_date);
|
||||
CREATE INDEX IF NOT EXISTS idx_tiktok_daily_stats_account_date ON tiktok_daily_stats(tiktok_account_id, stats_date DESC);
|
||||
|
||||
-- Enable RLS for tiktok_daily_stats
|
||||
ALTER TABLE tiktok_daily_stats ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- RLS policies for tiktok_daily_stats
|
||||
CREATE POLICY "Users can view their own TikTok stats"
|
||||
ON tiktok_daily_stats FOR SELECT
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can insert their own TikTok stats"
|
||||
ON tiktok_daily_stats FOR INSERT
|
||||
WITH CHECK (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can update their own TikTok stats"
|
||||
ON tiktok_daily_stats FOR UPDATE
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can delete their own TikTok stats"
|
||||
ON tiktok_daily_stats FOR DELETE
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
-- ============================================================
|
||||
-- TABLE: tiktok_videos
|
||||
-- Video metadata and performance metrics
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tiktok_videos (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tiktok_account_id uuid REFERENCES tiktok_accounts(id) ON DELETE CASCADE NOT NULL,
|
||||
user_id uuid REFERENCES auth.users NOT NULL,
|
||||
|
||||
-- TikTok video identification
|
||||
tiktok_video_id text NOT NULL,
|
||||
|
||||
-- Video metadata
|
||||
title text NULL,
|
||||
share_url text NULL,
|
||||
cover_image_url text NULL,
|
||||
|
||||
-- Performance metrics
|
||||
view_count bigint NOT NULL DEFAULT 0,
|
||||
like_count bigint NOT NULL DEFAULT 0,
|
||||
comment_count bigint NOT NULL DEFAULT 0,
|
||||
share_count bigint NOT NULL DEFAULT 0,
|
||||
|
||||
-- Video duration in seconds
|
||||
duration integer NULL,
|
||||
|
||||
-- Publication date
|
||||
published_at timestamptz NULL,
|
||||
|
||||
-- Campaign/deliverable linking for ROI tracking
|
||||
campaign_id uuid REFERENCES campaigns(id) ON DELETE SET NULL,
|
||||
deliverable_id uuid REFERENCES deliverables(id) ON DELETE SET NULL,
|
||||
|
||||
-- Timestamps
|
||||
created_at timestamptz DEFAULT now(),
|
||||
updated_at timestamptz DEFAULT now(),
|
||||
|
||||
-- Unique constraint
|
||||
UNIQUE(tiktok_account_id, tiktok_video_id)
|
||||
);
|
||||
|
||||
-- Indexes for tiktok_videos
|
||||
CREATE INDEX IF NOT EXISTS idx_tiktok_videos_user_id ON tiktok_videos(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tiktok_videos_account_id ON tiktok_videos(tiktok_account_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tiktok_videos_campaign_id ON tiktok_videos(campaign_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tiktok_videos_deliverable_id ON tiktok_videos(deliverable_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tiktok_videos_published_at ON tiktok_videos(published_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_tiktok_videos_tiktok_video_id ON tiktok_videos(tiktok_video_id);
|
||||
|
||||
-- Enable RLS for tiktok_videos
|
||||
ALTER TABLE tiktok_videos ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- RLS policies for tiktok_videos
|
||||
CREATE POLICY "Users can view their own TikTok videos"
|
||||
ON tiktok_videos FOR SELECT
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can insert their own TikTok videos"
|
||||
ON tiktok_videos FOR INSERT
|
||||
WITH CHECK (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can update their own TikTok videos"
|
||||
ON tiktok_videos FOR UPDATE
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can delete their own TikTok videos"
|
||||
ON tiktok_videos FOR DELETE
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
-- ============================================================
|
||||
-- TABLE: tiktok_sync_logs
|
||||
-- Sync history for debugging and monitoring
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tiktok_sync_logs (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tiktok_account_id uuid REFERENCES tiktok_accounts(id) ON DELETE CASCADE NOT NULL,
|
||||
user_id uuid REFERENCES auth.users NOT NULL,
|
||||
|
||||
-- Sync type
|
||||
sync_type text NOT NULL CHECK (sync_type IN (
|
||||
'stats', 'videos', 'full', 'token_refresh'
|
||||
)),
|
||||
|
||||
-- Sync result
|
||||
status text NOT NULL CHECK (status IN (
|
||||
'started', 'success', 'partial', 'failed'
|
||||
)),
|
||||
|
||||
-- Details
|
||||
stats_synced boolean NOT NULL DEFAULT false,
|
||||
videos_synced integer NOT NULL DEFAULT 0,
|
||||
new_videos integer NOT NULL DEFAULT 0,
|
||||
|
||||
-- Error info
|
||||
error_message text NULL,
|
||||
error_code text NULL,
|
||||
|
||||
-- Duration
|
||||
started_at timestamptz NOT NULL DEFAULT now(),
|
||||
completed_at timestamptz NULL,
|
||||
|
||||
-- Timestamps
|
||||
created_at timestamptz DEFAULT now()
|
||||
);
|
||||
|
||||
-- Indexes for tiktok_sync_logs
|
||||
CREATE INDEX IF NOT EXISTS idx_tiktok_sync_logs_user_id ON tiktok_sync_logs(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tiktok_sync_logs_account_id ON tiktok_sync_logs(tiktok_account_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tiktok_sync_logs_created_at ON tiktok_sync_logs(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_tiktok_sync_logs_status ON tiktok_sync_logs(status);
|
||||
|
||||
-- Enable RLS for tiktok_sync_logs
|
||||
ALTER TABLE tiktok_sync_logs ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- RLS policies for tiktok_sync_logs
|
||||
CREATE POLICY "Users can view their own TikTok sync logs"
|
||||
ON tiktok_sync_logs FOR SELECT
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can insert their own TikTok sync logs"
|
||||
ON tiktok_sync_logs FOR INSERT
|
||||
WITH CHECK (auth.uid() = user_id);
|
||||
|
||||
-- ============================================================
|
||||
-- TRIGGERS for updated_at
|
||||
-- ============================================================
|
||||
|
||||
-- tiktok_accounts updated_at trigger
|
||||
CREATE OR REPLACE FUNCTION update_tiktok_accounts_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = now();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
DROP TRIGGER IF EXISTS update_tiktok_accounts_updated_at ON tiktok_accounts;
|
||||
CREATE TRIGGER update_tiktok_accounts_updated_at
|
||||
BEFORE UPDATE ON tiktok_accounts
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE update_tiktok_accounts_updated_at();
|
||||
|
||||
-- tiktok_videos updated_at trigger
|
||||
CREATE OR REPLACE FUNCTION update_tiktok_videos_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = now();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
DROP TRIGGER IF EXISTS update_tiktok_videos_updated_at ON tiktok_videos;
|
||||
CREATE TRIGGER update_tiktok_videos_updated_at
|
||||
BEFORE UPDATE ON tiktok_videos
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE update_tiktok_videos_updated_at();
|
||||
|
||||
-- ============================================================
|
||||
-- FUNCTION: Calculate follower change on insert
|
||||
-- ============================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION calculate_follower_change()
|
||||
RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
prev_count bigint;
|
||||
BEGIN
|
||||
-- Get the previous day's follower count
|
||||
SELECT follower_count INTO prev_count
|
||||
FROM tiktok_daily_stats
|
||||
WHERE tiktok_account_id = NEW.tiktok_account_id
|
||||
AND stats_date = NEW.stats_date - INTERVAL '1 day';
|
||||
|
||||
-- Calculate change if previous record exists
|
||||
IF prev_count IS NOT NULL THEN
|
||||
NEW.follower_change = NEW.follower_count - prev_count;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
DROP TRIGGER IF EXISTS calculate_follower_change_trigger ON tiktok_daily_stats;
|
||||
CREATE TRIGGER calculate_follower_change_trigger
|
||||
BEFORE INSERT ON tiktok_daily_stats
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE calculate_follower_change();
|
||||
@@ -1,83 +0,0 @@
|
||||
-- Create briefings table for storing campaign briefing materials
|
||||
-- Supports three types: PDF files, links (Canva, Google Docs, etc.), and raw text
|
||||
|
||||
-- ============================================================
|
||||
-- NEW TABLE: briefings
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS briefings (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
campaign_id uuid REFERENCES campaigns(id) ON DELETE CASCADE NOT NULL,
|
||||
user_id uuid REFERENCES auth.users NOT NULL,
|
||||
|
||||
-- Type of briefing material
|
||||
briefing_type text NOT NULL CHECK (briefing_type IN ('pdf', 'link', 'text')),
|
||||
|
||||
-- Title/name
|
||||
title text NOT NULL,
|
||||
|
||||
-- Content based on type:
|
||||
-- For 'pdf': file path in Supabase Storage
|
||||
-- For 'link': URL to external resource
|
||||
-- For 'text': NULL (use text_content instead)
|
||||
content text NULL,
|
||||
|
||||
-- For 'text' type: the actual text content
|
||||
text_content text NULL,
|
||||
|
||||
-- PDF metadata (only for 'pdf' type)
|
||||
filename text NULL,
|
||||
file_size integer NULL, -- bytes
|
||||
mime_type text NULL,
|
||||
|
||||
-- Optional notes
|
||||
notes text NULL,
|
||||
|
||||
-- Timestamps
|
||||
created_at timestamptz DEFAULT now(),
|
||||
updated_at timestamptz DEFAULT now()
|
||||
);
|
||||
|
||||
-- Indexes for briefings
|
||||
CREATE INDEX IF NOT EXISTS idx_briefings_user_id ON briefings(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_briefings_campaign_id ON briefings(campaign_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_briefings_type ON briefings(briefing_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_briefings_user_campaign ON briefings(user_id, campaign_id);
|
||||
|
||||
-- Enable RLS for briefings
|
||||
ALTER TABLE briefings ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- RLS policies for briefings
|
||||
CREATE POLICY "Users can view their own briefings"
|
||||
ON briefings FOR SELECT
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can insert their own briefings"
|
||||
ON briefings FOR INSERT
|
||||
WITH CHECK (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can update their own briefings"
|
||||
ON briefings FOR UPDATE
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can delete their own briefings"
|
||||
ON briefings FOR DELETE
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
-- ============================================================
|
||||
-- TRIGGER for updated_at
|
||||
-- ============================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION update_briefings_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = now();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
DROP TRIGGER IF EXISTS update_briefings_updated_at ON briefings;
|
||||
CREATE TRIGGER update_briefings_updated_at
|
||||
BEFORE UPDATE ON briefings
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE update_briefings_updated_at();
|
||||
@@ -1,179 +0,0 @@
|
||||
-- ============================================================
|
||||
-- Receipt Photo Capture System
|
||||
-- ============================================================
|
||||
|
||||
-- Receipts table: stores uploaded receipt images and extraction data
|
||||
CREATE TABLE IF NOT EXISTS receipts (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
||||
|
||||
-- Image storage
|
||||
image_url TEXT NOT NULL,
|
||||
image_thumbnail_url TEXT,
|
||||
|
||||
-- Extraction status
|
||||
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'extracted', 'confirmed', 'error')),
|
||||
extraction_confidence DECIMAL(3,2) CHECK (extraction_confidence >= 0 AND extraction_confidence <= 1),
|
||||
|
||||
-- Extracted header data
|
||||
merchant_name TEXT,
|
||||
merchant_org_number TEXT,
|
||||
merchant_vat_number TEXT,
|
||||
receipt_date DATE,
|
||||
receipt_time TIME,
|
||||
total_amount DECIMAL(12,2),
|
||||
currency TEXT DEFAULT 'SEK',
|
||||
vat_amount DECIMAL(12,2),
|
||||
|
||||
-- Special flags
|
||||
is_restaurant BOOLEAN DEFAULT FALSE,
|
||||
is_systembolaget BOOLEAN DEFAULT FALSE,
|
||||
is_foreign_merchant BOOLEAN DEFAULT FALSE,
|
||||
|
||||
-- Restaurant representation data
|
||||
representation_persons INTEGER,
|
||||
representation_purpose TEXT,
|
||||
|
||||
-- Transaction matching
|
||||
matched_transaction_id UUID REFERENCES transactions(id) ON DELETE SET NULL,
|
||||
match_confidence DECIMAL(3,2) CHECK (match_confidence >= 0 AND match_confidence <= 1),
|
||||
|
||||
-- Raw extraction data (for debugging)
|
||||
raw_extraction JSONB,
|
||||
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Receipt line items table
|
||||
CREATE TABLE IF NOT EXISTS receipt_line_items (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
receipt_id UUID NOT NULL REFERENCES receipts(id) ON DELETE CASCADE,
|
||||
|
||||
-- Extracted data
|
||||
description TEXT NOT NULL,
|
||||
quantity DECIMAL(10,3) DEFAULT 1,
|
||||
unit_price DECIMAL(12,2),
|
||||
line_total DECIMAL(12,2) NOT NULL,
|
||||
vat_rate DECIMAL(5,2),
|
||||
vat_amount DECIMAL(12,2),
|
||||
|
||||
-- Classification
|
||||
is_business BOOLEAN, -- null = unclassified
|
||||
category TEXT, -- expense category if business
|
||||
bas_account TEXT,
|
||||
|
||||
-- Confidence
|
||||
extraction_confidence DECIMAL(3,2) CHECK (extraction_confidence >= 0 AND extraction_confidence <= 1),
|
||||
suggested_category TEXT, -- AI suggestion
|
||||
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Indexes for performance
|
||||
CREATE INDEX IF NOT EXISTS idx_receipts_user_id ON receipts(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_receipts_status ON receipts(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_receipts_matched_transaction_id ON receipts(matched_transaction_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_receipts_receipt_date ON receipts(receipt_date);
|
||||
CREATE INDEX IF NOT EXISTS idx_receipt_line_items_receipt_id ON receipt_line_items(receipt_id);
|
||||
|
||||
-- Add receipt_id column to transactions for reverse lookup
|
||||
ALTER TABLE transactions ADD COLUMN IF NOT EXISTS receipt_id UUID REFERENCES receipts(id) ON DELETE SET NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_transactions_receipt_id ON transactions(receipt_id);
|
||||
|
||||
-- Enable RLS
|
||||
ALTER TABLE receipts ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE receipt_line_items ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- RLS policies for receipts
|
||||
DROP POLICY IF EXISTS "Users can view their own receipts" ON receipts;
|
||||
CREATE POLICY "Users can view their own receipts"
|
||||
ON receipts FOR SELECT
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
DROP POLICY IF EXISTS "Users can insert their own receipts" ON receipts;
|
||||
CREATE POLICY "Users can insert their own receipts"
|
||||
ON receipts FOR INSERT
|
||||
WITH CHECK (auth.uid() = user_id);
|
||||
|
||||
DROP POLICY IF EXISTS "Users can update their own receipts" ON receipts;
|
||||
CREATE POLICY "Users can update their own receipts"
|
||||
ON receipts FOR UPDATE
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
DROP POLICY IF EXISTS "Users can delete their own receipts" ON receipts;
|
||||
CREATE POLICY "Users can delete their own receipts"
|
||||
ON receipts FOR DELETE
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
-- RLS policies for receipt_line_items (based on receipt ownership)
|
||||
DROP POLICY IF EXISTS "Users can view their receipt line items" ON receipt_line_items;
|
||||
CREATE POLICY "Users can view their receipt line items"
|
||||
ON receipt_line_items FOR SELECT
|
||||
USING (EXISTS (
|
||||
SELECT 1 FROM receipts WHERE receipts.id = receipt_line_items.receipt_id AND receipts.user_id = auth.uid()
|
||||
));
|
||||
|
||||
DROP POLICY IF EXISTS "Users can insert their receipt line items" ON receipt_line_items;
|
||||
CREATE POLICY "Users can insert their receipt line items"
|
||||
ON receipt_line_items FOR INSERT
|
||||
WITH CHECK (EXISTS (
|
||||
SELECT 1 FROM receipts WHERE receipts.id = receipt_line_items.receipt_id AND receipts.user_id = auth.uid()
|
||||
));
|
||||
|
||||
DROP POLICY IF EXISTS "Users can update their receipt line items" ON receipt_line_items;
|
||||
CREATE POLICY "Users can update their receipt line items"
|
||||
ON receipt_line_items FOR UPDATE
|
||||
USING (EXISTS (
|
||||
SELECT 1 FROM receipts WHERE receipts.id = receipt_line_items.receipt_id AND receipts.user_id = auth.uid()
|
||||
));
|
||||
|
||||
DROP POLICY IF EXISTS "Users can delete their receipt line items" ON receipt_line_items;
|
||||
CREATE POLICY "Users can delete their receipt line items"
|
||||
ON receipt_line_items FOR DELETE
|
||||
USING (EXISTS (
|
||||
SELECT 1 FROM receipts WHERE receipts.id = receipt_line_items.receipt_id AND receipts.user_id = auth.uid()
|
||||
));
|
||||
|
||||
-- Trigger for updated_at (if the function exists)
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_proc WHERE proname = 'update_updated_at_column') THEN
|
||||
DROP TRIGGER IF EXISTS update_receipts_updated_at ON receipts;
|
||||
CREATE TRIGGER update_receipts_updated_at
|
||||
BEFORE UPDATE ON receipts
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Create storage bucket for receipt images (if not exists)
|
||||
INSERT INTO storage.buckets (id, name, public)
|
||||
VALUES ('receipts', 'receipts', false)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- Storage policies for receipts bucket
|
||||
DROP POLICY IF EXISTS "Users can upload receipt images" ON storage.objects;
|
||||
CREATE POLICY "Users can upload receipt images"
|
||||
ON storage.objects FOR INSERT
|
||||
WITH CHECK (
|
||||
bucket_id = 'receipts' AND
|
||||
auth.uid()::text = (storage.foldername(name))[1]
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS "Users can view their receipt images" ON storage.objects;
|
||||
CREATE POLICY "Users can view their receipt images"
|
||||
ON storage.objects FOR SELECT
|
||||
USING (
|
||||
bucket_id = 'receipts' AND
|
||||
auth.uid()::text = (storage.foldername(name))[1]
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS "Users can delete their receipt images" ON storage.objects;
|
||||
CREATE POLICY "Users can delete their receipt images"
|
||||
ON storage.objects FOR DELETE
|
||||
USING (
|
||||
bucket_id = 'receipts' AND
|
||||
auth.uid()::text = (storage.foldername(name))[1]
|
||||
);
|
||||
@@ -1,75 +0,0 @@
|
||||
-- Create invoice_reminders table for tracking payment reminders
|
||||
-- Part of the email invoicing and automated payment reminders feature
|
||||
|
||||
CREATE TABLE IF NOT EXISTS invoice_reminders (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
invoice_id uuid REFERENCES invoices(id) ON DELETE CASCADE NOT NULL,
|
||||
user_id uuid REFERENCES auth.users NOT NULL,
|
||||
|
||||
-- Reminder level (1 = 15 days, 2 = 30 days, 3 = 45 days)
|
||||
reminder_level integer NOT NULL CHECK (reminder_level IN (1, 2, 3)),
|
||||
|
||||
-- When and where sent
|
||||
sent_at timestamptz NOT NULL DEFAULT now(),
|
||||
email_to text NOT NULL,
|
||||
|
||||
-- Customer response tracking
|
||||
response_type text NULL CHECK (response_type IS NULL OR response_type IN ('marked_paid', 'disputed')),
|
||||
response_at timestamptz NULL,
|
||||
|
||||
-- Action token for customer links (allows marking as paid without auth)
|
||||
action_token uuid UNIQUE DEFAULT gen_random_uuid(),
|
||||
action_token_used boolean DEFAULT false,
|
||||
|
||||
-- Timestamps
|
||||
created_at timestamptz DEFAULT now()
|
||||
);
|
||||
|
||||
-- Indexes for efficient queries
|
||||
CREATE INDEX IF NOT EXISTS idx_invoice_reminders_invoice_id ON invoice_reminders(invoice_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_invoice_reminders_user_id ON invoice_reminders(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_invoice_reminders_action_token ON invoice_reminders(action_token);
|
||||
CREATE INDEX IF NOT EXISTS idx_invoice_reminders_user_sent ON invoice_reminders(user_id, sent_at DESC);
|
||||
|
||||
-- Enable RLS
|
||||
ALTER TABLE invoice_reminders ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- RLS policies
|
||||
CREATE POLICY "Users can view own reminders"
|
||||
ON invoice_reminders FOR SELECT
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can insert own reminders"
|
||||
ON invoice_reminders FOR INSERT
|
||||
WITH CHECK (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can update own reminders"
|
||||
ON invoice_reminders FOR UPDATE
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
-- Allow public access for action token validation (customer response)
|
||||
CREATE POLICY "Anyone can view reminder by action token"
|
||||
ON invoice_reminders FOR SELECT
|
||||
USING (action_token IS NOT NULL);
|
||||
|
||||
-- Add sent_at column to invoices table for tracking when invoice was emailed
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'invoices' AND column_name = 'sent_at'
|
||||
) THEN
|
||||
ALTER TABLE invoices ADD COLUMN sent_at timestamptz NULL;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Add email column to company_settings for reply-to address
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'company_settings' AND column_name = 'email'
|
||||
) THEN
|
||||
ALTER TABLE company_settings ADD COLUMN email text NULL;
|
||||
END IF;
|
||||
END $$;
|
||||
@@ -1,229 +0,0 @@
|
||||
-- Calendar enhancements: Tax deadlines, status tracking, push notifications, calendar feeds
|
||||
-- Migration: 20250206_calendar_enhancements.sql
|
||||
|
||||
-- 0. Extend company_settings for arbetsgivardeklaration
|
||||
ALTER TABLE company_settings ADD COLUMN IF NOT EXISTS pays_salaries boolean DEFAULT false;
|
||||
|
||||
-- 1. Extend deadlines table with tax-specific fields
|
||||
|
||||
-- Tax deadline type
|
||||
ALTER TABLE deadlines ADD COLUMN IF NOT EXISTS tax_deadline_type text NULL CHECK (
|
||||
tax_deadline_type IS NULL OR tax_deadline_type IN (
|
||||
'moms_monthly', 'moms_quarterly', 'moms_yearly',
|
||||
'f_skatt', 'arbetsgivardeklaration',
|
||||
'inkomstdeklaration_ef', 'inkomstdeklaration_ab',
|
||||
'arsredovisning', 'periodisk_sammanstallning', 'bokslut'
|
||||
)
|
||||
);
|
||||
|
||||
-- Tax period (e.g., "2025-Q1", "2025-01", "2025")
|
||||
ALTER TABLE deadlines ADD COLUMN IF NOT EXISTS tax_period text NULL;
|
||||
|
||||
-- Source: system-generated or user-created
|
||||
ALTER TABLE deadlines ADD COLUMN IF NOT EXISTS source text NOT NULL DEFAULT 'user' CHECK (source IN ('system', 'user'));
|
||||
|
||||
-- Reminder offsets in days before deadline (e.g., {14, 7, 1, 0})
|
||||
ALTER TABLE deadlines ADD COLUMN IF NOT EXISTS reminder_offsets integer[] DEFAULT '{14,7,1,0}';
|
||||
|
||||
-- Status workflow: upcoming → action_needed → in_progress → submitted → confirmed/overdue
|
||||
ALTER TABLE deadlines ADD COLUMN IF NOT EXISTS status text NOT NULL DEFAULT 'upcoming' CHECK (
|
||||
status IN ('upcoming', 'action_needed', 'in_progress', 'submitted', 'confirmed', 'overdue')
|
||||
);
|
||||
|
||||
-- Track when status was last changed
|
||||
ALTER TABLE deadlines ADD COLUMN IF NOT EXISTS status_changed_at timestamptz DEFAULT now();
|
||||
|
||||
-- Link to report type for quick navigation
|
||||
ALTER TABLE deadlines ADD COLUMN IF NOT EXISTS linked_report_type text NULL;
|
||||
|
||||
-- Link to specific report period (e.g., {"year": 2025, "quarter": 1})
|
||||
ALTER TABLE deadlines ADD COLUMN IF NOT EXISTS linked_report_period jsonb NULL;
|
||||
|
||||
-- Update deadline_type CHECK constraint to include new types
|
||||
ALTER TABLE deadlines DROP CONSTRAINT IF EXISTS deadlines_deadline_type_check;
|
||||
ALTER TABLE deadlines ADD CONSTRAINT deadlines_deadline_type_check CHECK (deadline_type IN (
|
||||
'delivery', 'invoicing', 'report', 'approval', 'other',
|
||||
'revision', 'assets', 'spark_ad', 'statistics', 'tax'
|
||||
));
|
||||
|
||||
-- Create indexes for tax deadlines
|
||||
CREATE INDEX IF NOT EXISTS idx_deadlines_tax_type ON deadlines(user_id, tax_deadline_type) WHERE tax_deadline_type IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_deadlines_status ON deadlines(user_id, status);
|
||||
CREATE INDEX IF NOT EXISTS idx_deadlines_source ON deadlines(user_id, source);
|
||||
CREATE INDEX IF NOT EXISTS idx_deadlines_status_date ON deadlines(user_id, status, due_date);
|
||||
|
||||
-- Link to campaign and deliverable if not already present
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'deadlines' AND column_name = 'campaign_id') THEN
|
||||
ALTER TABLE deadlines ADD COLUMN campaign_id uuid REFERENCES campaigns(id) ON DELETE SET NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_deadlines_campaign_id ON deadlines(campaign_id);
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'deadlines' AND column_name = 'deliverable_id') THEN
|
||||
ALTER TABLE deadlines ADD COLUMN deliverable_id uuid REFERENCES deliverables(id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'deadlines' AND column_name = 'is_auto_generated') THEN
|
||||
ALTER TABLE deadlines ADD COLUMN is_auto_generated boolean DEFAULT false;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'deadlines' AND column_name = 'date_calculation_type') THEN
|
||||
ALTER TABLE deadlines ADD COLUMN date_calculation_type text NULL CHECK (date_calculation_type IN ('absolute', 'relative'));
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'deadlines' AND column_name = 'reference_event') THEN
|
||||
ALTER TABLE deadlines ADD COLUMN reference_event text NULL CHECK (reference_event IN ('publication', 'delivery', 'approval', 'contract'));
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'deadlines' AND column_name = 'offset_days') THEN
|
||||
ALTER TABLE deadlines ADD COLUMN offset_days integer NULL;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 2. Push subscriptions for Web Push notifications
|
||||
CREATE TABLE IF NOT EXISTS push_subscriptions (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id uuid NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
||||
endpoint text NOT NULL,
|
||||
p256dh text NOT NULL,
|
||||
auth text NOT NULL,
|
||||
user_agent text NULL,
|
||||
is_active boolean DEFAULT true,
|
||||
last_used_at timestamptz NULL,
|
||||
created_at timestamptz DEFAULT now(),
|
||||
UNIQUE(user_id, endpoint)
|
||||
);
|
||||
|
||||
-- Index for finding active subscriptions
|
||||
CREATE INDEX IF NOT EXISTS idx_push_subscriptions_user_active ON push_subscriptions(user_id, is_active) WHERE is_active = true;
|
||||
|
||||
-- Enable RLS
|
||||
ALTER TABLE push_subscriptions ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- RLS policies for push_subscriptions
|
||||
DROP POLICY IF EXISTS "Users manage own push subscriptions" ON push_subscriptions;
|
||||
CREATE POLICY "Users manage own push subscriptions" ON push_subscriptions FOR ALL USING (auth.uid() = user_id);
|
||||
|
||||
-- 3. Notification settings per user
|
||||
CREATE TABLE IF NOT EXISTS notification_settings (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id uuid NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
||||
|
||||
-- Category toggles
|
||||
tax_deadlines_enabled boolean DEFAULT true,
|
||||
invoice_reminders_enabled boolean DEFAULT true,
|
||||
campaign_deadlines_enabled boolean DEFAULT true,
|
||||
|
||||
-- Quiet hours (no notifications during this time)
|
||||
quiet_start time DEFAULT '21:00',
|
||||
quiet_end time DEFAULT '08:00',
|
||||
|
||||
-- Notification preferences
|
||||
email_enabled boolean DEFAULT true,
|
||||
push_enabled boolean DEFAULT true,
|
||||
|
||||
created_at timestamptz DEFAULT now(),
|
||||
updated_at timestamptz DEFAULT now(),
|
||||
|
||||
UNIQUE(user_id)
|
||||
);
|
||||
|
||||
-- Enable RLS
|
||||
ALTER TABLE notification_settings ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- RLS policies for notification_settings
|
||||
DROP POLICY IF EXISTS "Users manage own notification settings" ON notification_settings;
|
||||
CREATE POLICY "Users manage own notification settings" ON notification_settings FOR ALL USING (auth.uid() = user_id);
|
||||
|
||||
-- Add trigger for updated_at
|
||||
CREATE OR REPLACE FUNCTION update_notification_settings_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = now();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
DROP TRIGGER IF EXISTS update_notification_settings_updated_at ON notification_settings;
|
||||
CREATE TRIGGER update_notification_settings_updated_at
|
||||
BEFORE UPDATE ON notification_settings
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE update_notification_settings_updated_at();
|
||||
|
||||
-- 4. Calendar feeds for Apple Calendar / Google Calendar sync
|
||||
CREATE TABLE IF NOT EXISTS calendar_feeds (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id uuid NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
||||
|
||||
-- Unique token for feed URL (acts as authentication)
|
||||
feed_token uuid DEFAULT gen_random_uuid() UNIQUE,
|
||||
|
||||
-- Feed status
|
||||
is_active boolean DEFAULT true,
|
||||
|
||||
-- What to include in the feed
|
||||
include_tax_deadlines boolean DEFAULT true,
|
||||
include_invoices boolean DEFAULT true,
|
||||
include_campaigns boolean DEFAULT true,
|
||||
include_exclusivity boolean DEFAULT false, -- Avstängt som standard
|
||||
|
||||
-- Tracking
|
||||
last_accessed_at timestamptz,
|
||||
access_count integer DEFAULT 0,
|
||||
|
||||
created_at timestamptz DEFAULT now(),
|
||||
updated_at timestamptz DEFAULT now(),
|
||||
|
||||
UNIQUE(user_id)
|
||||
);
|
||||
|
||||
-- Index for token lookup (no RLS, token is the auth mechanism)
|
||||
CREATE INDEX IF NOT EXISTS idx_calendar_feeds_token ON calendar_feeds(feed_token) WHERE is_active = true;
|
||||
|
||||
-- NO RLS on calendar_feeds - the feed_token acts as authentication
|
||||
-- This allows unauthenticated access via the token
|
||||
|
||||
-- Add trigger for updated_at
|
||||
CREATE OR REPLACE FUNCTION update_calendar_feeds_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = now();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
DROP TRIGGER IF EXISTS update_calendar_feeds_updated_at ON calendar_feeds;
|
||||
CREATE TRIGGER update_calendar_feeds_updated_at
|
||||
BEFORE UPDATE ON calendar_feeds
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE update_calendar_feeds_updated_at();
|
||||
|
||||
-- 5. Notification log for tracking sent notifications (prevents duplicates)
|
||||
CREATE TABLE IF NOT EXISTS notification_log (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id uuid NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
||||
|
||||
-- What triggered the notification
|
||||
notification_type text NOT NULL CHECK (notification_type IN (
|
||||
'tax_deadline', 'invoice_due', 'invoice_overdue', 'campaign_deadline'
|
||||
)),
|
||||
|
||||
-- Reference to the source (deadline_id, invoice_id, etc.)
|
||||
reference_id uuid NOT NULL,
|
||||
|
||||
-- Days before deadline when sent (for deduplication)
|
||||
days_before integer NOT NULL,
|
||||
|
||||
-- Status
|
||||
sent_at timestamptz DEFAULT now(),
|
||||
delivery_status text DEFAULT 'sent' CHECK (delivery_status IN ('sent', 'delivered', 'failed')),
|
||||
|
||||
-- Prevent duplicate notifications
|
||||
UNIQUE(user_id, notification_type, reference_id, days_before)
|
||||
);
|
||||
|
||||
-- Index for checking recent notifications
|
||||
CREATE INDEX IF NOT EXISTS idx_notification_log_user_type ON notification_log(user_id, notification_type, sent_at);
|
||||
|
||||
-- Enable RLS
|
||||
ALTER TABLE notification_log ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- RLS policy
|
||||
DROP POLICY IF EXISTS "Users can view own notification log" ON notification_log;
|
||||
CREATE POLICY "Users can view own notification log" ON notification_log FOR SELECT USING (auth.uid() = user_id);
|
||||
Reference in New Issue
Block a user