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:
Jakob Wennberg
2026-02-13 15:39:13 +01:00
co-authored by Claude Opus 4.6
parent a25d10a528
commit 0aecf5b41c
184 changed files with 1418 additions and 24763 deletions
@@ -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);