Initial state for GitNexus analysis
This commit is contained in:
448
apps/web/supabase/migrations/20260401000001_cms_foundation.sql
Normal file
448
apps/web/supabase/migrations/20260401000001_cms_foundation.sql
Normal file
@@ -0,0 +1,448 @@
|
||||
/*
|
||||
* -------------------------------------------------------
|
||||
* CMS Foundation Schema
|
||||
* Phase 1: Enums, permissions, account_settings, audit_log,
|
||||
* GDPR register, file metadata, storage buckets.
|
||||
* -------------------------------------------------------
|
||||
*/
|
||||
|
||||
-- =====================================================
|
||||
-- 1. New CMS Enums
|
||||
-- =====================================================
|
||||
|
||||
create type public.cms_field_type as enum(
|
||||
'text',
|
||||
'textarea',
|
||||
'richtext',
|
||||
'checkbox',
|
||||
'radio',
|
||||
'hidden',
|
||||
'select',
|
||||
'password',
|
||||
'file',
|
||||
'date',
|
||||
'time',
|
||||
'decimal',
|
||||
'integer',
|
||||
'email',
|
||||
'phone',
|
||||
'url',
|
||||
'currency',
|
||||
'iban',
|
||||
'color',
|
||||
'computed'
|
||||
);
|
||||
|
||||
create type public.cms_module_status as enum(
|
||||
'active',
|
||||
'inactive',
|
||||
'archived'
|
||||
);
|
||||
|
||||
create type public.cms_record_status as enum(
|
||||
'active',
|
||||
'locked',
|
||||
'deleted',
|
||||
'archived'
|
||||
);
|
||||
|
||||
create type public.gdpr_legal_basis as enum(
|
||||
'consent',
|
||||
'contract',
|
||||
'legal_obligation',
|
||||
'vital_interest',
|
||||
'public_interest',
|
||||
'legitimate_interest'
|
||||
);
|
||||
|
||||
create type public.cms_account_type as enum(
|
||||
'verein',
|
||||
'vhs',
|
||||
'hotel',
|
||||
'kommune',
|
||||
'generic'
|
||||
);
|
||||
|
||||
create type public.audit_action as enum(
|
||||
'insert',
|
||||
'update',
|
||||
'delete',
|
||||
'lock'
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- 2. Extend app_permissions with CMS values
|
||||
-- =====================================================
|
||||
-- ALTER TYPE … ADD VALUE cannot run inside a transaction in
|
||||
-- older Postgres, but Supabase migrations run outside
|
||||
-- explicit transactions by default.
|
||||
|
||||
alter type public.app_permissions add value if not exists 'modules.read';
|
||||
alter type public.app_permissions add value if not exists 'modules.write';
|
||||
alter type public.app_permissions add value if not exists 'modules.delete';
|
||||
alter type public.app_permissions add value if not exists 'modules.insert';
|
||||
alter type public.app_permissions add value if not exists 'modules.lock';
|
||||
alter type public.app_permissions add value if not exists 'modules.import';
|
||||
alter type public.app_permissions add value if not exists 'modules.export';
|
||||
alter type public.app_permissions add value if not exists 'modules.print';
|
||||
alter type public.app_permissions add value if not exists 'modules.manage';
|
||||
alter type public.app_permissions add value if not exists 'members.read';
|
||||
alter type public.app_permissions add value if not exists 'members.write';
|
||||
alter type public.app_permissions add value if not exists 'courses.read';
|
||||
alter type public.app_permissions add value if not exists 'courses.write';
|
||||
alter type public.app_permissions add value if not exists 'bookings.read';
|
||||
alter type public.app_permissions add value if not exists 'bookings.write';
|
||||
alter type public.app_permissions add value if not exists 'finance.read';
|
||||
alter type public.app_permissions add value if not exists 'finance.write';
|
||||
alter type public.app_permissions add value if not exists 'finance.sepa';
|
||||
alter type public.app_permissions add value if not exists 'documents.generate';
|
||||
alter type public.app_permissions add value if not exists 'newsletter.send';
|
||||
|
||||
-- =====================================================
|
||||
-- 3. account_settings — extends accounts per tenant
|
||||
-- =====================================================
|
||||
create table if not exists public.account_settings (
|
||||
account_id uuid primary key references public.accounts(id) on delete cascade,
|
||||
|
||||
-- Organisation type
|
||||
account_type public.cms_account_type not null default 'generic',
|
||||
|
||||
-- Org identity
|
||||
org_name text,
|
||||
org_address text,
|
||||
org_postal_code text,
|
||||
org_city text,
|
||||
org_phone text,
|
||||
org_email text,
|
||||
org_website text,
|
||||
org_chairman text,
|
||||
|
||||
-- Branding
|
||||
logo_url text,
|
||||
primary_color text default '#0f172a',
|
||||
secondary_color text default '#3b82f6',
|
||||
|
||||
-- SEPA banking
|
||||
creditor_id text, -- Gläubiger-ID
|
||||
iban text,
|
||||
bic text,
|
||||
|
||||
-- Email
|
||||
email_sender_name text,
|
||||
email_footer text,
|
||||
|
||||
-- Feature flags per tenant (jsonb for flexibility)
|
||||
features jsonb not null default '{}'::jsonb,
|
||||
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
comment on table public.account_settings is 'CMS-specific settings per team account (organisation, branding, SEPA, features)';
|
||||
|
||||
-- RLS
|
||||
alter table public.account_settings enable row level security;
|
||||
|
||||
-- Revoke + grant
|
||||
revoke all on public.account_settings from authenticated, service_role;
|
||||
grant select, insert, update on public.account_settings to authenticated;
|
||||
grant all on public.account_settings to service_role;
|
||||
|
||||
-- SELECT: any member of the account
|
||||
create policy account_settings_select
|
||||
on public.account_settings for select
|
||||
to authenticated
|
||||
using (public.has_role_on_account(account_id));
|
||||
|
||||
-- INSERT: settings.manage permission
|
||||
create policy account_settings_insert
|
||||
on public.account_settings for insert
|
||||
to authenticated
|
||||
with check (
|
||||
public.has_permission(auth.uid(), account_id, 'settings.manage'::public.app_permissions)
|
||||
);
|
||||
|
||||
-- UPDATE: settings.manage permission
|
||||
create policy account_settings_update
|
||||
on public.account_settings for update
|
||||
to authenticated
|
||||
using (
|
||||
public.has_permission(auth.uid(), account_id, 'settings.manage'::public.app_permissions)
|
||||
);
|
||||
|
||||
-- Auto-update updated_at
|
||||
create or replace function public.update_account_settings_timestamp()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = ''
|
||||
as $$
|
||||
begin
|
||||
new.updated_at = now();
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create trigger trg_account_settings_updated_at
|
||||
before update on public.account_settings
|
||||
for each row
|
||||
execute function public.update_account_settings_timestamp();
|
||||
|
||||
-- =====================================================
|
||||
-- 4. audit_log — immutable action log
|
||||
-- =====================================================
|
||||
create table if not exists public.audit_log (
|
||||
id bigint generated always as identity primary key,
|
||||
account_id uuid not null references public.accounts(id) on delete cascade,
|
||||
user_id uuid references auth.users(id) on delete set null,
|
||||
table_name text not null,
|
||||
record_id text not null,
|
||||
action public.audit_action not null,
|
||||
old_data jsonb,
|
||||
new_data jsonb,
|
||||
ip_address inet,
|
||||
user_agent text,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
comment on table public.audit_log is 'Immutable audit trail for CMS data changes';
|
||||
|
||||
-- Indexes
|
||||
create index ix_audit_log_account on public.audit_log(account_id);
|
||||
create index ix_audit_log_table_record on public.audit_log(table_name, record_id);
|
||||
create index ix_audit_log_created on public.audit_log(created_at desc);
|
||||
create index ix_audit_log_user on public.audit_log(user_id);
|
||||
|
||||
-- RLS
|
||||
alter table public.audit_log enable row level security;
|
||||
|
||||
revoke all on public.audit_log from authenticated, service_role;
|
||||
grant select, insert on public.audit_log to authenticated;
|
||||
grant all on public.audit_log to service_role;
|
||||
|
||||
-- SELECT: members of the account
|
||||
create policy audit_log_select
|
||||
on public.audit_log for select
|
||||
to authenticated
|
||||
using (public.has_role_on_account(account_id));
|
||||
|
||||
-- INSERT: any authenticated member of the account
|
||||
create policy audit_log_insert
|
||||
on public.audit_log for insert
|
||||
to authenticated
|
||||
with check (public.has_role_on_account(account_id));
|
||||
|
||||
-- No UPDATE/DELETE — audit log is append-only
|
||||
|
||||
-- =====================================================
|
||||
-- 5. gdpr_processing_register
|
||||
-- =====================================================
|
||||
create table if not exists public.gdpr_processing_register (
|
||||
id bigint generated always as identity primary key,
|
||||
account_id uuid not null references public.accounts(id) on delete cascade,
|
||||
purpose text not null,
|
||||
legal_basis public.gdpr_legal_basis not null,
|
||||
data_categories text not null,
|
||||
data_subjects text not null,
|
||||
recipients text,
|
||||
retention_period text not null,
|
||||
technical_measures text,
|
||||
responsible_person text,
|
||||
notes text,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
comment on table public.gdpr_processing_register is 'GDPR Art. 30 processing activity register per account';
|
||||
|
||||
-- Index
|
||||
create index ix_gdpr_register_account on public.gdpr_processing_register(account_id);
|
||||
|
||||
-- RLS
|
||||
alter table public.gdpr_processing_register enable row level security;
|
||||
|
||||
revoke all on public.gdpr_processing_register from authenticated, service_role;
|
||||
grant select, insert, update, delete on public.gdpr_processing_register to authenticated;
|
||||
grant all on public.gdpr_processing_register to service_role;
|
||||
|
||||
create policy gdpr_register_select
|
||||
on public.gdpr_processing_register for select
|
||||
to authenticated
|
||||
using (public.has_role_on_account(account_id));
|
||||
|
||||
create policy gdpr_register_insert
|
||||
on public.gdpr_processing_register for insert
|
||||
to authenticated
|
||||
with check (
|
||||
public.has_permission(auth.uid(), account_id, 'settings.manage'::public.app_permissions)
|
||||
);
|
||||
|
||||
create policy gdpr_register_update
|
||||
on public.gdpr_processing_register for update
|
||||
to authenticated
|
||||
using (
|
||||
public.has_permission(auth.uid(), account_id, 'settings.manage'::public.app_permissions)
|
||||
);
|
||||
|
||||
create policy gdpr_register_delete
|
||||
on public.gdpr_processing_register for delete
|
||||
to authenticated
|
||||
using (
|
||||
public.has_permission(auth.uid(), account_id, 'settings.manage'::public.app_permissions)
|
||||
);
|
||||
|
||||
-- Auto-update updated_at
|
||||
create trigger trg_gdpr_register_updated_at
|
||||
before update on public.gdpr_processing_register
|
||||
for each row
|
||||
execute function public.update_account_settings_timestamp();
|
||||
|
||||
-- =====================================================
|
||||
-- 6. cms_files — file metadata with Storage paths
|
||||
-- =====================================================
|
||||
create table if not exists public.cms_files (
|
||||
id bigint generated always as identity primary key,
|
||||
account_id uuid not null references public.accounts(id) on delete cascade,
|
||||
module_name text not null,
|
||||
record_id text not null,
|
||||
field_name text not null,
|
||||
file_name text not null,
|
||||
original_name text not null,
|
||||
mime_type text not null,
|
||||
file_size bigint not null default 0,
|
||||
storage_path text not null,
|
||||
created_at timestamptz not null default now(),
|
||||
created_by uuid references auth.users(id) on delete set null
|
||||
);
|
||||
|
||||
comment on table public.cms_files is 'File metadata linking CMS records to Supabase Storage objects';
|
||||
|
||||
-- Indexes
|
||||
create index ix_cms_files_account on public.cms_files(account_id);
|
||||
create index ix_cms_files_record on public.cms_files(module_name, record_id);
|
||||
create index ix_cms_files_field on public.cms_files(module_name, record_id, field_name);
|
||||
|
||||
-- RLS
|
||||
alter table public.cms_files enable row level security;
|
||||
|
||||
revoke all on public.cms_files from authenticated, service_role;
|
||||
grant select, insert, update, delete on public.cms_files to authenticated;
|
||||
grant all on public.cms_files to service_role;
|
||||
|
||||
create policy cms_files_select
|
||||
on public.cms_files for select
|
||||
to authenticated
|
||||
using (public.has_role_on_account(account_id));
|
||||
|
||||
create policy cms_files_insert
|
||||
on public.cms_files for insert
|
||||
to authenticated
|
||||
with check (public.has_role_on_account(account_id));
|
||||
|
||||
create policy cms_files_update
|
||||
on public.cms_files for update
|
||||
to authenticated
|
||||
using (public.has_role_on_account(account_id));
|
||||
|
||||
create policy cms_files_delete
|
||||
on public.cms_files for delete
|
||||
to authenticated
|
||||
using (public.has_role_on_account(account_id));
|
||||
|
||||
-- =====================================================
|
||||
-- 7. Storage Buckets
|
||||
-- =====================================================
|
||||
|
||||
-- CMS uploads (module file fields)
|
||||
insert into storage.buckets (id, name, public)
|
||||
values ('cms-uploads', 'cms-uploads', false)
|
||||
on conflict (id) do nothing;
|
||||
|
||||
-- CMS documents (generated PDFs, invoices)
|
||||
insert into storage.buckets (id, name, public)
|
||||
values ('cms-documents', 'cms-documents', false)
|
||||
on conflict (id) do nothing;
|
||||
|
||||
-- CMS exports (CSV, Excel exports)
|
||||
insert into storage.buckets (id, name, public)
|
||||
values ('cms-exports', 'cms-exports', false)
|
||||
on conflict (id) do nothing;
|
||||
|
||||
-- Storage RLS: account members can access their own files
|
||||
-- Uses the first path segment as account_id
|
||||
create or replace function kit.get_storage_path_account_id(name text)
|
||||
returns uuid
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = ''
|
||||
as $$
|
||||
begin
|
||||
-- Path format: {account_id}/...
|
||||
return split_part(name, '/', 1)::uuid;
|
||||
exception when others then
|
||||
return null;
|
||||
end;
|
||||
$$;
|
||||
|
||||
grant execute on function kit.get_storage_path_account_id(text)
|
||||
to authenticated, service_role;
|
||||
|
||||
-- RLS for cms-uploads bucket
|
||||
create policy cms_uploads_select
|
||||
on storage.objects for select
|
||||
to authenticated
|
||||
using (
|
||||
bucket_id = 'cms-uploads'
|
||||
and public.has_role_on_account(kit.get_storage_path_account_id(name))
|
||||
);
|
||||
|
||||
create policy cms_uploads_insert
|
||||
on storage.objects for insert
|
||||
to authenticated
|
||||
with check (
|
||||
bucket_id = 'cms-uploads'
|
||||
and public.has_role_on_account(kit.get_storage_path_account_id(name))
|
||||
);
|
||||
|
||||
create policy cms_uploads_delete
|
||||
on storage.objects for delete
|
||||
to authenticated
|
||||
using (
|
||||
bucket_id = 'cms-uploads'
|
||||
and public.has_role_on_account(kit.get_storage_path_account_id(name))
|
||||
);
|
||||
|
||||
-- RLS for cms-documents bucket
|
||||
create policy cms_documents_select
|
||||
on storage.objects for select
|
||||
to authenticated
|
||||
using (
|
||||
bucket_id = 'cms-documents'
|
||||
and public.has_role_on_account(kit.get_storage_path_account_id(name))
|
||||
);
|
||||
|
||||
create policy cms_documents_insert
|
||||
on storage.objects for insert
|
||||
to authenticated
|
||||
with check (
|
||||
bucket_id = 'cms-documents'
|
||||
and public.has_role_on_account(kit.get_storage_path_account_id(name))
|
||||
);
|
||||
|
||||
-- RLS for cms-exports bucket
|
||||
create policy cms_exports_select
|
||||
on storage.objects for select
|
||||
to authenticated
|
||||
using (
|
||||
bucket_id = 'cms-exports'
|
||||
and public.has_role_on_account(kit.get_storage_path_account_id(name))
|
||||
);
|
||||
|
||||
create policy cms_exports_insert
|
||||
on storage.objects for insert
|
||||
to authenticated
|
||||
with check (
|
||||
bucket_id = 'cms-exports'
|
||||
and public.has_role_on_account(kit.get_storage_path_account_id(name))
|
||||
);
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
-- =====================================================
|
||||
-- 8. Seed CMS permissions for existing roles
|
||||
-- =====================================================
|
||||
|
||||
-- Owner gets ALL CMS permissions
|
||||
insert into public.role_permissions (role, permission) values
|
||||
('owner', 'modules.read'),
|
||||
('owner', 'modules.write'),
|
||||
('owner', 'modules.delete'),
|
||||
('owner', 'modules.insert'),
|
||||
('owner', 'modules.lock'),
|
||||
('owner', 'modules.import'),
|
||||
('owner', 'modules.export'),
|
||||
('owner', 'modules.print'),
|
||||
('owner', 'modules.manage'),
|
||||
('owner', 'members.read'),
|
||||
('owner', 'members.write'),
|
||||
('owner', 'courses.read'),
|
||||
('owner', 'courses.write'),
|
||||
('owner', 'bookings.read'),
|
||||
('owner', 'bookings.write'),
|
||||
('owner', 'finance.read'),
|
||||
('owner', 'finance.write'),
|
||||
('owner', 'finance.sepa'),
|
||||
('owner', 'documents.generate'),
|
||||
('owner', 'newsletter.send')
|
||||
on conflict (role, permission) do nothing;
|
||||
|
||||
-- Member gets read + basic write permissions
|
||||
insert into public.role_permissions (role, permission) values
|
||||
('member', 'modules.read'),
|
||||
('member', 'modules.write'),
|
||||
('member', 'modules.insert'),
|
||||
('member', 'modules.export'),
|
||||
('member', 'modules.print'),
|
||||
('member', 'members.read'),
|
||||
('member', 'courses.read'),
|
||||
('member', 'bookings.read'),
|
||||
('member', 'finance.read')
|
||||
on conflict (role, permission) do nothing;
|
||||
482
apps/web/supabase/migrations/20260402000001_module_engine.sql
Normal file
482
apps/web/supabase/migrations/20260402000001_module_engine.sql
Normal file
@@ -0,0 +1,482 @@
|
||||
/*
|
||||
* -------------------------------------------------------
|
||||
* Module Engine Schema
|
||||
* Phase 2: modules, module_fields, module_permissions,
|
||||
* module_relations, module_query() RPC
|
||||
* -------------------------------------------------------
|
||||
*/
|
||||
|
||||
-- =====================================================
|
||||
-- 1. modules — dynamic module definitions
|
||||
-- =====================================================
|
||||
create table if not exists public.modules (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
account_id uuid not null references public.accounts(id) on delete cascade,
|
||||
name text not null,
|
||||
display_name text not null,
|
||||
description text,
|
||||
icon text default 'table',
|
||||
table_name text,
|
||||
primary_key_column text default 'id',
|
||||
status public.cms_module_status not null default 'active',
|
||||
sort_order integer not null default 0,
|
||||
|
||||
-- Defaults
|
||||
default_sort_field text,
|
||||
default_sort_direction text default 'asc' check (default_sort_direction in ('asc', 'desc')),
|
||||
default_page_size integer default 25,
|
||||
|
||||
-- Feature toggles
|
||||
enable_search boolean not null default true,
|
||||
enable_filter boolean not null default true,
|
||||
enable_export boolean not null default true,
|
||||
enable_import boolean not null default false,
|
||||
enable_print boolean not null default true,
|
||||
enable_copy boolean not null default false,
|
||||
enable_bulk_edit boolean not null default false,
|
||||
enable_history boolean not null default true,
|
||||
enable_soft_delete boolean not null default true,
|
||||
enable_lock boolean not null default false,
|
||||
|
||||
-- Extensibility
|
||||
hooks jsonb not null default '{}'::jsonb,
|
||||
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
|
||||
unique(account_id, name)
|
||||
);
|
||||
|
||||
comment on table public.modules is 'Dynamic module definitions — replaces legacy m_module';
|
||||
|
||||
-- Indexes
|
||||
create index ix_modules_account on public.modules(account_id);
|
||||
create index ix_modules_status on public.modules(account_id, status);
|
||||
|
||||
-- RLS
|
||||
alter table public.modules enable row level security;
|
||||
revoke all on public.modules from authenticated, service_role;
|
||||
grant select, insert, update, delete on public.modules to authenticated;
|
||||
grant all on public.modules to service_role;
|
||||
|
||||
create policy modules_select on public.modules for select to authenticated
|
||||
using (public.has_role_on_account(account_id));
|
||||
|
||||
create policy modules_insert on public.modules for insert to authenticated
|
||||
with check (
|
||||
public.has_permission(auth.uid(), account_id, 'modules.manage'::public.app_permissions)
|
||||
);
|
||||
|
||||
create policy modules_update on public.modules for update to authenticated
|
||||
using (
|
||||
public.has_permission(auth.uid(), account_id, 'modules.manage'::public.app_permissions)
|
||||
);
|
||||
|
||||
create policy modules_delete on public.modules for delete to authenticated
|
||||
using (
|
||||
public.has_permission(auth.uid(), account_id, 'modules.manage'::public.app_permissions)
|
||||
);
|
||||
|
||||
-- Auto-update timestamp
|
||||
create trigger trg_modules_updated_at
|
||||
before update on public.modules
|
||||
for each row execute function public.update_account_settings_timestamp();
|
||||
|
||||
-- =====================================================
|
||||
-- 2. module_fields — field definitions per module
|
||||
-- =====================================================
|
||||
create table if not exists public.module_fields (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
module_id uuid not null references public.modules(id) on delete cascade,
|
||||
name text not null,
|
||||
display_name text not null,
|
||||
field_type public.cms_field_type not null default 'text',
|
||||
sql_type text default 'text',
|
||||
|
||||
-- Constraints
|
||||
is_required boolean not null default false,
|
||||
is_unique boolean not null default false,
|
||||
default_value text,
|
||||
min_value numeric,
|
||||
max_value numeric,
|
||||
max_length integer,
|
||||
regex_pattern text,
|
||||
|
||||
-- Layout
|
||||
sort_order integer not null default 0,
|
||||
width text default 'full',
|
||||
section text default 'default',
|
||||
row_index integer default 0,
|
||||
col_index integer default 0,
|
||||
placeholder text,
|
||||
help_text text,
|
||||
|
||||
-- Visibility
|
||||
show_in_table boolean not null default true,
|
||||
show_in_form boolean not null default true,
|
||||
show_in_search boolean not null default false,
|
||||
show_in_filter boolean not null default false,
|
||||
show_in_export boolean not null default true,
|
||||
show_in_print boolean not null default true,
|
||||
|
||||
-- Behavior
|
||||
is_sortable boolean not null default true,
|
||||
is_readonly boolean not null default false,
|
||||
is_encrypted boolean not null default false,
|
||||
is_copyable boolean not null default true,
|
||||
validation_fn text,
|
||||
|
||||
-- Lookup (foreign key to another module)
|
||||
lookup_module_id uuid references public.modules(id) on delete set null,
|
||||
lookup_display_field text,
|
||||
lookup_value_field text,
|
||||
|
||||
-- Select options (for select/radio/checkbox fields)
|
||||
select_options jsonb,
|
||||
|
||||
-- GDPR
|
||||
is_personal_data boolean not null default false,
|
||||
gdpr_purpose text,
|
||||
|
||||
-- File upload config
|
||||
allowed_mime_types text[],
|
||||
max_file_size bigint,
|
||||
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
|
||||
unique(module_id, name)
|
||||
);
|
||||
|
||||
comment on table public.module_fields is 'Field definitions per module — replaces legacy m_modulfeld';
|
||||
|
||||
-- Indexes
|
||||
create index ix_module_fields_module on public.module_fields(module_id);
|
||||
create index ix_module_fields_sort on public.module_fields(module_id, sort_order);
|
||||
create index ix_module_fields_lookup on public.module_fields(lookup_module_id);
|
||||
|
||||
-- RLS — inherits from parent module's account
|
||||
alter table public.module_fields enable row level security;
|
||||
revoke all on public.module_fields from authenticated, service_role;
|
||||
grant select, insert, update, delete on public.module_fields to authenticated;
|
||||
grant all on public.module_fields to service_role;
|
||||
|
||||
create policy module_fields_select on public.module_fields for select to authenticated
|
||||
using (
|
||||
exists (
|
||||
select 1 from public.modules m
|
||||
where m.id = module_fields.module_id
|
||||
and public.has_role_on_account(m.account_id)
|
||||
)
|
||||
);
|
||||
|
||||
create policy module_fields_insert on public.module_fields for insert to authenticated
|
||||
with check (
|
||||
exists (
|
||||
select 1 from public.modules m
|
||||
where m.id = module_fields.module_id
|
||||
and public.has_permission(auth.uid(), m.account_id, 'modules.manage'::public.app_permissions)
|
||||
)
|
||||
);
|
||||
|
||||
create policy module_fields_update on public.module_fields for update to authenticated
|
||||
using (
|
||||
exists (
|
||||
select 1 from public.modules m
|
||||
where m.id = module_fields.module_id
|
||||
and public.has_permission(auth.uid(), m.account_id, 'modules.manage'::public.app_permissions)
|
||||
)
|
||||
);
|
||||
|
||||
create policy module_fields_delete on public.module_fields for delete to authenticated
|
||||
using (
|
||||
exists (
|
||||
select 1 from public.modules m
|
||||
where m.id = module_fields.module_id
|
||||
and public.has_permission(auth.uid(), m.account_id, 'modules.manage'::public.app_permissions)
|
||||
)
|
||||
);
|
||||
|
||||
create trigger trg_module_fields_updated_at
|
||||
before update on public.module_fields
|
||||
for each row execute function public.update_account_settings_timestamp();
|
||||
|
||||
-- =====================================================
|
||||
-- 3. module_permissions — per-module role permissions
|
||||
-- =====================================================
|
||||
create table if not exists public.module_permissions (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
module_id uuid not null references public.modules(id) on delete cascade,
|
||||
role varchar(50) not null references public.roles(name),
|
||||
can_read boolean not null default true,
|
||||
can_insert boolean not null default false,
|
||||
can_update boolean not null default false,
|
||||
can_delete boolean not null default false,
|
||||
can_lock boolean not null default false,
|
||||
can_import boolean not null default false,
|
||||
can_export boolean not null default true,
|
||||
can_print boolean not null default true,
|
||||
can_bulk_edit boolean not null default false,
|
||||
can_manage boolean not null default false,
|
||||
|
||||
unique(module_id, role)
|
||||
);
|
||||
|
||||
comment on table public.module_permissions is 'Per-module, per-role permission overrides';
|
||||
|
||||
create index ix_module_permissions_module on public.module_permissions(module_id);
|
||||
|
||||
alter table public.module_permissions enable row level security;
|
||||
revoke all on public.module_permissions from authenticated, service_role;
|
||||
grant select, insert, update, delete on public.module_permissions to authenticated;
|
||||
grant all on public.module_permissions to service_role;
|
||||
|
||||
create policy module_permissions_select on public.module_permissions for select to authenticated
|
||||
using (
|
||||
exists (
|
||||
select 1 from public.modules m
|
||||
where m.id = module_permissions.module_id
|
||||
and public.has_role_on_account(m.account_id)
|
||||
)
|
||||
);
|
||||
|
||||
create policy module_permissions_mutate on public.module_permissions for all to authenticated
|
||||
using (
|
||||
exists (
|
||||
select 1 from public.modules m
|
||||
where m.id = module_permissions.module_id
|
||||
and public.has_permission(auth.uid(), m.account_id, 'modules.manage'::public.app_permissions)
|
||||
)
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- 4. module_relations — inter-module relationships
|
||||
-- =====================================================
|
||||
create table if not exists public.module_relations (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
source_module_id uuid not null references public.modules(id) on delete cascade,
|
||||
source_field_id uuid not null references public.module_fields(id) on delete cascade,
|
||||
target_module_id uuid not null references public.modules(id) on delete cascade,
|
||||
target_field_id uuid references public.module_fields(id) on delete set null,
|
||||
relation_type text not null check (relation_type in ('lookup', 'parent', 'child', 'many_to_many')),
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
comment on table public.module_relations is 'Relationships between modules (lookup, parent-child, M:N)';
|
||||
|
||||
create index ix_module_relations_source on public.module_relations(source_module_id);
|
||||
create index ix_module_relations_target on public.module_relations(target_module_id);
|
||||
|
||||
alter table public.module_relations enable row level security;
|
||||
revoke all on public.module_relations from authenticated, service_role;
|
||||
grant select, insert, update, delete on public.module_relations to authenticated;
|
||||
grant all on public.module_relations to service_role;
|
||||
|
||||
create policy module_relations_select on public.module_relations for select to authenticated
|
||||
using (
|
||||
exists (
|
||||
select 1 from public.modules m
|
||||
where m.id = module_relations.source_module_id
|
||||
and public.has_role_on_account(m.account_id)
|
||||
)
|
||||
);
|
||||
|
||||
create policy module_relations_mutate on public.module_relations for all to authenticated
|
||||
using (
|
||||
exists (
|
||||
select 1 from public.modules m
|
||||
where m.id = module_relations.source_module_id
|
||||
and public.has_permission(auth.uid(), m.account_id, 'modules.manage'::public.app_permissions)
|
||||
)
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- 5. module_records — generic record storage
|
||||
-- =====================================================
|
||||
create table if not exists public.module_records (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
module_id uuid not null references public.modules(id) on delete cascade,
|
||||
account_id uuid not null references public.accounts(id) on delete cascade,
|
||||
data jsonb not null default '{}'::jsonb,
|
||||
status public.cms_record_status not null default 'active',
|
||||
locked_by uuid references auth.users(id) on delete set null,
|
||||
locked_at timestamptz,
|
||||
created_by uuid references auth.users(id) on delete set null,
|
||||
updated_by uuid references auth.users(id) on delete set null,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
comment on table public.module_records is 'Generic record storage for all dynamic modules';
|
||||
|
||||
create index ix_module_records_module on public.module_records(module_id);
|
||||
create index ix_module_records_account on public.module_records(account_id);
|
||||
create index ix_module_records_status on public.module_records(module_id, status);
|
||||
create index ix_module_records_data on public.module_records using gin(data);
|
||||
create index ix_module_records_created on public.module_records(created_at desc);
|
||||
|
||||
alter table public.module_records enable row level security;
|
||||
revoke all on public.module_records from authenticated, service_role;
|
||||
grant select, insert, update, delete on public.module_records to authenticated;
|
||||
grant all on public.module_records to service_role;
|
||||
|
||||
create policy module_records_select on public.module_records for select to authenticated
|
||||
using (public.has_role_on_account(account_id));
|
||||
|
||||
create policy module_records_insert on public.module_records for insert to authenticated
|
||||
with check (
|
||||
public.has_permission(auth.uid(), account_id, 'modules.insert'::public.app_permissions)
|
||||
);
|
||||
|
||||
create policy module_records_update on public.module_records for update to authenticated
|
||||
using (
|
||||
public.has_permission(auth.uid(), account_id, 'modules.write'::public.app_permissions)
|
||||
);
|
||||
|
||||
create policy module_records_delete on public.module_records for delete to authenticated
|
||||
using (
|
||||
public.has_permission(auth.uid(), account_id, 'modules.delete'::public.app_permissions)
|
||||
);
|
||||
|
||||
create trigger trg_module_records_updated_at
|
||||
before update on public.module_records
|
||||
for each row execute function public.update_account_settings_timestamp();
|
||||
|
||||
-- =====================================================
|
||||
-- 6. module_query() — dynamic query RPC
|
||||
-- =====================================================
|
||||
create or replace function public.module_query(
|
||||
p_module_id uuid,
|
||||
p_filters jsonb default '[]'::jsonb,
|
||||
p_sort_field text default null,
|
||||
p_sort_direction text default 'asc',
|
||||
p_page integer default 1,
|
||||
p_page_size integer default 25,
|
||||
p_search text default null
|
||||
)
|
||||
returns jsonb
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = ''
|
||||
as $$
|
||||
declare
|
||||
v_account_id uuid;
|
||||
v_total bigint;
|
||||
v_offset integer;
|
||||
v_records jsonb;
|
||||
v_query text;
|
||||
v_where text := '';
|
||||
v_filter jsonb;
|
||||
v_field text;
|
||||
v_operator text;
|
||||
v_value text;
|
||||
begin
|
||||
-- Get the module's account_id and verify access
|
||||
select m.account_id into v_account_id
|
||||
from public.modules m
|
||||
where m.id = p_module_id and m.status = 'active';
|
||||
|
||||
if v_account_id is null then
|
||||
raise exception 'Module not found or inactive';
|
||||
end if;
|
||||
|
||||
if not public.has_role_on_account(v_account_id) then
|
||||
raise exception 'Access denied';
|
||||
end if;
|
||||
|
||||
-- Build filter WHERE clause from JSON filters
|
||||
-- Each filter: {"field": "name", "operator": "eq", "value": "test"}
|
||||
if p_filters is not null and jsonb_array_length(p_filters) > 0 then
|
||||
for v_filter in select * from jsonb_array_elements(p_filters)
|
||||
loop
|
||||
v_field := v_filter->>'field';
|
||||
v_operator := v_filter->>'operator';
|
||||
v_value := v_filter->>'value';
|
||||
|
||||
-- Sanitize field name (only alphanumeric + underscore)
|
||||
if v_field !~ '^[a-zA-Z_][a-zA-Z0-9_]*$' then
|
||||
continue;
|
||||
end if;
|
||||
|
||||
case v_operator
|
||||
when 'eq' then
|
||||
v_where := v_where || format(' AND data->>%L = %L', v_field, v_value);
|
||||
when 'neq' then
|
||||
v_where := v_where || format(' AND data->>%L != %L', v_field, v_value);
|
||||
when 'gt' then
|
||||
v_where := v_where || format(' AND (data->>%L)::numeric > %L::numeric', v_field, v_value);
|
||||
when 'gte' then
|
||||
v_where := v_where || format(' AND (data->>%L)::numeric >= %L::numeric', v_field, v_value);
|
||||
when 'lt' then
|
||||
v_where := v_where || format(' AND (data->>%L)::numeric < %L::numeric', v_field, v_value);
|
||||
when 'lte' then
|
||||
v_where := v_where || format(' AND (data->>%L)::numeric <= %L::numeric', v_field, v_value);
|
||||
when 'like' then
|
||||
v_where := v_where || format(' AND data->>%L ILIKE %L', v_field, '%' || v_value || '%');
|
||||
when 'is_null' then
|
||||
v_where := v_where || format(' AND (data->>%L IS NULL OR data->>%L = '''')', v_field, v_field);
|
||||
when 'not_null' then
|
||||
v_where := v_where || format(' AND data->>%L IS NOT NULL AND data->>%L != ''''', v_field, v_field);
|
||||
else
|
||||
null; -- skip unknown operators
|
||||
end case;
|
||||
end loop;
|
||||
end if;
|
||||
|
||||
-- Text search across all data fields
|
||||
if p_search is not null and p_search != '' then
|
||||
v_where := v_where || format(' AND data::text ILIKE %L', '%' || p_search || '%');
|
||||
end if;
|
||||
|
||||
-- Count total
|
||||
execute format(
|
||||
'SELECT count(*) FROM public.module_records WHERE module_id = %L AND status != ''deleted'' %s',
|
||||
p_module_id, v_where
|
||||
) into v_total;
|
||||
|
||||
-- Calculate offset
|
||||
v_offset := (p_page - 1) * p_page_size;
|
||||
|
||||
-- Build sort
|
||||
if p_sort_field is not null and p_sort_field ~ '^[a-zA-Z_][a-zA-Z0-9_]*$' then
|
||||
v_query := format(
|
||||
'SELECT jsonb_agg(row_to_json(r)) FROM (
|
||||
SELECT id, data, status, locked_by, locked_at, created_by, updated_by, created_at, updated_at
|
||||
FROM public.module_records
|
||||
WHERE module_id = %L AND status != ''deleted'' %s
|
||||
ORDER BY data->>%L %s NULLS LAST
|
||||
LIMIT %L OFFSET %L
|
||||
) r',
|
||||
p_module_id, v_where, p_sort_field,
|
||||
case when p_sort_direction = 'desc' then 'DESC' else 'ASC' end,
|
||||
p_page_size, v_offset
|
||||
);
|
||||
else
|
||||
v_query := format(
|
||||
'SELECT jsonb_agg(row_to_json(r)) FROM (
|
||||
SELECT id, data, status, locked_by, locked_at, created_by, updated_by, created_at, updated_at
|
||||
FROM public.module_records
|
||||
WHERE module_id = %L AND status != ''deleted'' %s
|
||||
ORDER BY created_at DESC
|
||||
LIMIT %L OFFSET %L
|
||||
) r',
|
||||
p_module_id, v_where, p_page_size, v_offset
|
||||
);
|
||||
end if;
|
||||
|
||||
execute v_query into v_records;
|
||||
|
||||
return jsonb_build_object(
|
||||
'data', coalesce(v_records, '[]'::jsonb),
|
||||
'pagination', jsonb_build_object(
|
||||
'page', p_page,
|
||||
'pageSize', p_page_size,
|
||||
'total', v_total,
|
||||
'totalPages', ceil(v_total::numeric / p_page_size::numeric)::integer
|
||||
)
|
||||
);
|
||||
end;
|
||||
$$;
|
||||
|
||||
grant execute on function public.module_query(uuid, jsonb, text, text, integer, integer, text)
|
||||
to authenticated, service_role;
|
||||
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* -------------------------------------------------------
|
||||
* Member Management Schema
|
||||
* Phase 4: members, membership_applications, dues_categories, member_cards
|
||||
* -------------------------------------------------------
|
||||
*/
|
||||
|
||||
-- =====================================================
|
||||
-- 1. Enums
|
||||
-- =====================================================
|
||||
create type public.membership_status as enum(
|
||||
'active', 'inactive', 'pending', 'resigned', 'excluded', 'deceased'
|
||||
);
|
||||
|
||||
create type public.sepa_mandate_status as enum(
|
||||
'active', 'pending', 'revoked', 'expired'
|
||||
);
|
||||
|
||||
create type public.application_status as enum(
|
||||
'submitted', 'review', 'approved', 'rejected'
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- 2. members
|
||||
-- =====================================================
|
||||
create table if not exists public.members (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
account_id uuid not null references public.accounts(id) on delete cascade,
|
||||
member_number text,
|
||||
|
||||
-- Personal
|
||||
first_name text not null,
|
||||
last_name text not null,
|
||||
date_of_birth date,
|
||||
gender text check (gender in ('male', 'female', 'diverse', null)),
|
||||
title text, -- Dr., Prof., etc.
|
||||
|
||||
-- Contact
|
||||
email text,
|
||||
phone text,
|
||||
mobile text,
|
||||
|
||||
-- Address
|
||||
street text,
|
||||
house_number text,
|
||||
postal_code text,
|
||||
city text,
|
||||
country text default 'DE',
|
||||
|
||||
-- Membership
|
||||
status public.membership_status not null default 'active',
|
||||
entry_date date not null default current_date,
|
||||
exit_date date,
|
||||
exit_reason text,
|
||||
dues_category_id uuid,
|
||||
|
||||
-- SEPA
|
||||
sepa_mandate_id text,
|
||||
sepa_mandate_date date,
|
||||
sepa_mandate_status public.sepa_mandate_status default 'pending',
|
||||
iban text,
|
||||
bic text,
|
||||
account_holder text,
|
||||
|
||||
-- GDPR
|
||||
gdpr_consent boolean not null default false,
|
||||
gdpr_consent_date timestamptz,
|
||||
gdpr_data_source text,
|
||||
|
||||
-- Meta
|
||||
notes text,
|
||||
custom_data jsonb not null default '{}'::jsonb,
|
||||
created_by uuid references auth.users(id) on delete set null,
|
||||
updated_by uuid references auth.users(id) on delete set null,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
|
||||
unique(account_id, member_number)
|
||||
);
|
||||
|
||||
comment on table public.members is 'Club/association members — replaces legacy ve_mitglieder';
|
||||
|
||||
create index ix_members_account on public.members(account_id);
|
||||
create index ix_members_status on public.members(account_id, status);
|
||||
create index ix_members_name on public.members(account_id, last_name, first_name);
|
||||
create index ix_members_email on public.members(account_id, email);
|
||||
|
||||
alter table public.members enable row level security;
|
||||
revoke all on public.members from authenticated, service_role;
|
||||
grant select, insert, update, delete on public.members to authenticated;
|
||||
grant all on public.members to service_role;
|
||||
|
||||
create policy members_select on public.members for select to authenticated
|
||||
using (public.has_role_on_account(account_id));
|
||||
|
||||
create policy members_insert on public.members for insert to authenticated
|
||||
with check (public.has_permission(auth.uid(), account_id, 'members.write'::public.app_permissions));
|
||||
|
||||
create policy members_update on public.members for update to authenticated
|
||||
using (public.has_permission(auth.uid(), account_id, 'members.write'::public.app_permissions));
|
||||
|
||||
create policy members_delete on public.members for delete to authenticated
|
||||
using (public.has_permission(auth.uid(), account_id, 'members.write'::public.app_permissions));
|
||||
|
||||
create trigger trg_members_updated_at
|
||||
before update on public.members
|
||||
for each row execute function public.update_account_settings_timestamp();
|
||||
|
||||
-- =====================================================
|
||||
-- 3. dues_categories — tiered pricing
|
||||
-- =====================================================
|
||||
create table if not exists public.dues_categories (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
account_id uuid not null references public.accounts(id) on delete cascade,
|
||||
name text not null,
|
||||
description text,
|
||||
amount numeric(10,2) not null default 0,
|
||||
interval text not null default 'yearly' check (interval in ('monthly', 'quarterly', 'half_yearly', 'yearly')),
|
||||
is_default boolean not null default false,
|
||||
sort_order integer not null default 0,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
comment on table public.dues_categories is 'Membership dues/fee categories';
|
||||
|
||||
create index ix_dues_categories_account on public.dues_categories(account_id);
|
||||
|
||||
alter table public.dues_categories enable row level security;
|
||||
revoke all on public.dues_categories from authenticated, service_role;
|
||||
grant select, insert, update, delete on public.dues_categories to authenticated;
|
||||
grant all on public.dues_categories to service_role;
|
||||
|
||||
create policy dues_categories_select on public.dues_categories for select to authenticated
|
||||
using (public.has_role_on_account(account_id));
|
||||
|
||||
create policy dues_categories_mutate on public.dues_categories for all to authenticated
|
||||
using (public.has_permission(auth.uid(), account_id, 'members.write'::public.app_permissions));
|
||||
|
||||
-- Add FK from members to dues_categories
|
||||
alter table public.members
|
||||
add constraint fk_members_dues_category
|
||||
foreign key (dues_category_id) references public.dues_categories(id) on delete set null;
|
||||
|
||||
-- =====================================================
|
||||
-- 4. membership_applications — workflow
|
||||
-- =====================================================
|
||||
create table if not exists public.membership_applications (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
account_id uuid not null references public.accounts(id) on delete cascade,
|
||||
first_name text not null,
|
||||
last_name text not null,
|
||||
email text,
|
||||
phone text,
|
||||
street text,
|
||||
postal_code text,
|
||||
city text,
|
||||
date_of_birth date,
|
||||
message text,
|
||||
status public.application_status not null default 'submitted',
|
||||
reviewed_by uuid references auth.users(id) on delete set null,
|
||||
reviewed_at timestamptz,
|
||||
review_notes text,
|
||||
member_id uuid references public.members(id) on delete set null,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
comment on table public.membership_applications is 'Online membership applications with approval workflow';
|
||||
|
||||
create index ix_applications_account on public.membership_applications(account_id);
|
||||
create index ix_applications_status on public.membership_applications(account_id, status);
|
||||
|
||||
alter table public.membership_applications enable row level security;
|
||||
revoke all on public.membership_applications from authenticated, service_role;
|
||||
grant select, insert, update, delete on public.membership_applications to authenticated;
|
||||
grant all on public.membership_applications to service_role;
|
||||
|
||||
create policy applications_select on public.membership_applications for select to authenticated
|
||||
using (public.has_role_on_account(account_id));
|
||||
|
||||
create policy applications_mutate on public.membership_applications for all to authenticated
|
||||
using (public.has_permission(auth.uid(), account_id, 'members.write'::public.app_permissions));
|
||||
|
||||
-- =====================================================
|
||||
-- 5. member_cards — ID cards
|
||||
-- =====================================================
|
||||
create table if not exists public.member_cards (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
member_id uuid not null references public.members(id) on delete cascade,
|
||||
account_id uuid not null references public.accounts(id) on delete cascade,
|
||||
card_number text,
|
||||
valid_from date not null default current_date,
|
||||
valid_until date,
|
||||
pdf_storage_path text,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
comment on table public.member_cards is 'Member ID cards with generated PDFs';
|
||||
|
||||
create index ix_member_cards_member on public.member_cards(member_id);
|
||||
|
||||
alter table public.member_cards enable row level security;
|
||||
revoke all on public.member_cards from authenticated, service_role;
|
||||
grant select, insert, update, delete on public.member_cards to authenticated;
|
||||
grant all on public.member_cards to service_role;
|
||||
|
||||
create policy member_cards_select on public.member_cards for select to authenticated
|
||||
using (public.has_role_on_account(account_id));
|
||||
|
||||
create policy member_cards_mutate on public.member_cards for all to authenticated
|
||||
using (public.has_permission(auth.uid(), account_id, 'members.write'::public.app_permissions));
|
||||
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* -------------------------------------------------------
|
||||
* Course Management Schema
|
||||
* Phase 5: courses, sessions, categories, participants,
|
||||
* instructors, locations, attendance
|
||||
* -------------------------------------------------------
|
||||
*/
|
||||
|
||||
create type public.enrollment_status as enum(
|
||||
'enrolled', 'waitlisted', 'cancelled', 'completed'
|
||||
);
|
||||
|
||||
-- Course categories (hierarchical)
|
||||
create table if not exists public.course_categories (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
account_id uuid not null references public.accounts(id) on delete cascade,
|
||||
parent_id uuid references public.course_categories(id) on delete set null,
|
||||
name text not null,
|
||||
description text,
|
||||
sort_order integer not null default 0,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
create index ix_course_categories_account on public.course_categories(account_id);
|
||||
alter table public.course_categories enable row level security;
|
||||
revoke all on public.course_categories from authenticated, service_role;
|
||||
grant select, insert, update, delete on public.course_categories to authenticated;
|
||||
grant all on public.course_categories to service_role;
|
||||
create policy course_categories_select on public.course_categories for select to authenticated using (public.has_role_on_account(account_id));
|
||||
create policy course_categories_mutate on public.course_categories for all to authenticated using (public.has_permission(auth.uid(), account_id, 'courses.write'::public.app_permissions));
|
||||
|
||||
-- Locations
|
||||
create table if not exists public.course_locations (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
account_id uuid not null references public.accounts(id) on delete cascade,
|
||||
name text not null,
|
||||
address text,
|
||||
room text,
|
||||
capacity integer,
|
||||
notes text,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
create index ix_course_locations_account on public.course_locations(account_id);
|
||||
alter table public.course_locations enable row level security;
|
||||
revoke all on public.course_locations from authenticated, service_role;
|
||||
grant select, insert, update, delete on public.course_locations to authenticated;
|
||||
grant all on public.course_locations to service_role;
|
||||
create policy course_locations_select on public.course_locations for select to authenticated using (public.has_role_on_account(account_id));
|
||||
create policy course_locations_mutate on public.course_locations for all to authenticated using (public.has_permission(auth.uid(), account_id, 'courses.write'::public.app_permissions));
|
||||
|
||||
-- Instructors
|
||||
create table if not exists public.course_instructors (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
account_id uuid not null references public.accounts(id) on delete cascade,
|
||||
first_name text not null,
|
||||
last_name text not null,
|
||||
email text,
|
||||
phone text,
|
||||
qualifications text,
|
||||
hourly_rate numeric(10,2),
|
||||
notes text,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
create index ix_course_instructors_account on public.course_instructors(account_id);
|
||||
alter table public.course_instructors enable row level security;
|
||||
revoke all on public.course_instructors from authenticated, service_role;
|
||||
grant select, insert, update, delete on public.course_instructors to authenticated;
|
||||
grant all on public.course_instructors to service_role;
|
||||
create policy course_instructors_select on public.course_instructors for select to authenticated using (public.has_role_on_account(account_id));
|
||||
create policy course_instructors_mutate on public.course_instructors for all to authenticated using (public.has_permission(auth.uid(), account_id, 'courses.write'::public.app_permissions));
|
||||
|
||||
-- Courses
|
||||
create table if not exists public.courses (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
account_id uuid not null references public.accounts(id) on delete cascade,
|
||||
course_number text,
|
||||
name text not null,
|
||||
description text,
|
||||
category_id uuid references public.course_categories(id) on delete set null,
|
||||
instructor_id uuid references public.course_instructors(id) on delete set null,
|
||||
location_id uuid references public.course_locations(id) on delete set null,
|
||||
start_date date,
|
||||
end_date date,
|
||||
fee numeric(10,2) not null default 0,
|
||||
reduced_fee numeric(10,2),
|
||||
capacity integer not null default 20,
|
||||
min_participants integer default 5,
|
||||
status text not null default 'planned' check (status in ('planned', 'open', 'running', 'completed', 'cancelled')),
|
||||
registration_deadline date,
|
||||
notes text,
|
||||
custom_data jsonb not null default '{}'::jsonb,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
create index ix_courses_account on public.courses(account_id);
|
||||
create index ix_courses_status on public.courses(account_id, status);
|
||||
create index ix_courses_dates on public.courses(account_id, start_date, end_date);
|
||||
alter table public.courses enable row level security;
|
||||
revoke all on public.courses from authenticated, service_role;
|
||||
grant select, insert, update, delete on public.courses to authenticated;
|
||||
grant all on public.courses to service_role;
|
||||
create policy courses_select on public.courses for select to authenticated using (public.has_role_on_account(account_id));
|
||||
create policy courses_insert on public.courses for insert to authenticated with check (public.has_permission(auth.uid(), account_id, 'courses.write'::public.app_permissions));
|
||||
create policy courses_update on public.courses for update to authenticated using (public.has_permission(auth.uid(), account_id, 'courses.write'::public.app_permissions));
|
||||
create policy courses_delete on public.courses for delete to authenticated using (public.has_permission(auth.uid(), account_id, 'courses.write'::public.app_permissions));
|
||||
create trigger trg_courses_updated_at before update on public.courses for each row execute function public.update_account_settings_timestamp();
|
||||
|
||||
-- Course sessions (individual dates/times)
|
||||
create table if not exists public.course_sessions (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
course_id uuid not null references public.courses(id) on delete cascade,
|
||||
session_date date not null,
|
||||
start_time time not null,
|
||||
end_time time not null,
|
||||
location_id uuid references public.course_locations(id) on delete set null,
|
||||
notes text,
|
||||
is_cancelled boolean not null default false,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
create index ix_course_sessions_course on public.course_sessions(course_id);
|
||||
create index ix_course_sessions_date on public.course_sessions(session_date);
|
||||
alter table public.course_sessions enable row level security;
|
||||
revoke all on public.course_sessions from authenticated, service_role;
|
||||
grant select, insert, update, delete on public.course_sessions to authenticated;
|
||||
grant all on public.course_sessions to service_role;
|
||||
create policy course_sessions_select on public.course_sessions for select to authenticated using (exists (select 1 from public.courses c where c.id = course_sessions.course_id and public.has_role_on_account(c.account_id)));
|
||||
create policy course_sessions_mutate on public.course_sessions for all to authenticated using (exists (select 1 from public.courses c where c.id = course_sessions.course_id and public.has_permission(auth.uid(), c.account_id, 'courses.write'::public.app_permissions)));
|
||||
|
||||
-- Course participants (enrollments)
|
||||
create table if not exists public.course_participants (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
course_id uuid not null references public.courses(id) on delete cascade,
|
||||
member_id uuid references public.members(id) on delete set null,
|
||||
first_name text not null,
|
||||
last_name text not null,
|
||||
email text,
|
||||
phone text,
|
||||
status public.enrollment_status not null default 'enrolled',
|
||||
enrolled_at timestamptz not null default now(),
|
||||
cancelled_at timestamptz,
|
||||
fee_paid numeric(10,2) default 0,
|
||||
notes text,
|
||||
unique(course_id, member_id)
|
||||
);
|
||||
create index ix_course_participants_course on public.course_participants(course_id);
|
||||
create index ix_course_participants_member on public.course_participants(member_id);
|
||||
alter table public.course_participants enable row level security;
|
||||
revoke all on public.course_participants from authenticated, service_role;
|
||||
grant select, insert, update, delete on public.course_participants to authenticated;
|
||||
grant all on public.course_participants to service_role;
|
||||
create policy course_participants_select on public.course_participants for select to authenticated using (exists (select 1 from public.courses c where c.id = course_participants.course_id and public.has_role_on_account(c.account_id)));
|
||||
create policy course_participants_mutate on public.course_participants for all to authenticated using (exists (select 1 from public.courses c where c.id = course_participants.course_id and public.has_permission(auth.uid(), c.account_id, 'courses.write'::public.app_permissions)));
|
||||
|
||||
-- Attendance
|
||||
create table if not exists public.course_attendance (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
session_id uuid not null references public.course_sessions(id) on delete cascade,
|
||||
participant_id uuid not null references public.course_participants(id) on delete cascade,
|
||||
present boolean not null default false,
|
||||
notes text,
|
||||
unique(session_id, participant_id)
|
||||
);
|
||||
create index ix_course_attendance_session on public.course_attendance(session_id);
|
||||
alter table public.course_attendance enable row level security;
|
||||
revoke all on public.course_attendance from authenticated, service_role;
|
||||
grant select, insert, update, delete on public.course_attendance to authenticated;
|
||||
grant all on public.course_attendance to service_role;
|
||||
create policy course_attendance_select on public.course_attendance for select to authenticated using (exists (select 1 from public.course_sessions s join public.courses c on c.id = s.course_id where s.id = course_attendance.session_id and public.has_role_on_account(c.account_id)));
|
||||
create policy course_attendance_mutate on public.course_attendance for all to authenticated using (exists (select 1 from public.course_sessions s join public.courses c on c.id = s.course_id where s.id = course_attendance.session_id and public.has_permission(auth.uid(), c.account_id, 'courses.write'::public.app_permissions)));
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* -------------------------------------------------------
|
||||
* Hotel/Booking Management Schema
|
||||
* Phase 6: rooms, amenities, bookings, guests
|
||||
* -------------------------------------------------------
|
||||
*/
|
||||
|
||||
create table if not exists public.rooms (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
account_id uuid not null references public.accounts(id) on delete cascade,
|
||||
room_number text not null,
|
||||
name text,
|
||||
room_type text not null default 'standard',
|
||||
capacity integer not null default 2,
|
||||
floor integer,
|
||||
price_per_night numeric(10,2) not null default 0,
|
||||
description text,
|
||||
is_active boolean not null default true,
|
||||
amenities jsonb not null default '[]'::jsonb,
|
||||
created_at timestamptz not null default now(),
|
||||
unique(account_id, room_number)
|
||||
);
|
||||
create index ix_rooms_account on public.rooms(account_id);
|
||||
alter table public.rooms enable row level security;
|
||||
revoke all on public.rooms from authenticated, service_role;
|
||||
grant select, insert, update, delete on public.rooms to authenticated;
|
||||
grant all on public.rooms to service_role;
|
||||
create policy rooms_select on public.rooms for select to authenticated using (public.has_role_on_account(account_id));
|
||||
create policy rooms_mutate on public.rooms for all to authenticated using (public.has_permission(auth.uid(), account_id, 'bookings.write'::public.app_permissions));
|
||||
|
||||
create table if not exists public.guests (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
account_id uuid not null references public.accounts(id) on delete cascade,
|
||||
first_name text not null,
|
||||
last_name text not null,
|
||||
email text,
|
||||
phone text,
|
||||
street text,
|
||||
postal_code text,
|
||||
city text,
|
||||
country text default 'DE',
|
||||
date_of_birth date,
|
||||
id_number text,
|
||||
notes text,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
create index ix_guests_account on public.guests(account_id);
|
||||
create index ix_guests_name on public.guests(account_id, last_name, first_name);
|
||||
alter table public.guests enable row level security;
|
||||
revoke all on public.guests from authenticated, service_role;
|
||||
grant select, insert, update, delete on public.guests to authenticated;
|
||||
grant all on public.guests to service_role;
|
||||
create policy guests_select on public.guests for select to authenticated using (public.has_role_on_account(account_id));
|
||||
create policy guests_mutate on public.guests for all to authenticated using (public.has_permission(auth.uid(), account_id, 'bookings.write'::public.app_permissions));
|
||||
|
||||
create table if not exists public.bookings (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
account_id uuid not null references public.accounts(id) on delete cascade,
|
||||
room_id uuid not null references public.rooms(id) on delete cascade,
|
||||
guest_id uuid references public.guests(id) on delete set null,
|
||||
check_in date not null,
|
||||
check_out date not null,
|
||||
adults integer not null default 1,
|
||||
children integer not null default 0,
|
||||
status text not null default 'confirmed' check (status in ('pending', 'confirmed', 'checked_in', 'checked_out', 'cancelled', 'no_show')),
|
||||
total_price numeric(10,2) not null default 0,
|
||||
notes text,
|
||||
extras jsonb not null default '[]'::jsonb,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
check (check_out > check_in)
|
||||
);
|
||||
create index ix_bookings_account on public.bookings(account_id);
|
||||
create index ix_bookings_room on public.bookings(room_id);
|
||||
create index ix_bookings_dates on public.bookings(room_id, check_in, check_out);
|
||||
create index ix_bookings_guest on public.bookings(guest_id);
|
||||
alter table public.bookings enable row level security;
|
||||
revoke all on public.bookings from authenticated, service_role;
|
||||
grant select, insert, update, delete on public.bookings to authenticated;
|
||||
grant all on public.bookings to service_role;
|
||||
create policy bookings_select on public.bookings for select to authenticated using (public.has_role_on_account(account_id));
|
||||
create policy bookings_insert on public.bookings for insert to authenticated with check (public.has_permission(auth.uid(), account_id, 'bookings.write'::public.app_permissions));
|
||||
create policy bookings_update on public.bookings for update to authenticated using (public.has_permission(auth.uid(), account_id, 'bookings.write'::public.app_permissions));
|
||||
create policy bookings_delete on public.bookings for delete to authenticated using (public.has_permission(auth.uid(), account_id, 'bookings.write'::public.app_permissions));
|
||||
create trigger trg_bookings_updated_at before update on public.bookings for each row execute function public.update_account_settings_timestamp();
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* -------------------------------------------------------
|
||||
* Municipality/Events Schema (Ferienpass)
|
||||
* Phase 7: events, registrations, holiday passes
|
||||
* -------------------------------------------------------
|
||||
*/
|
||||
|
||||
create table if not exists public.events (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
account_id uuid not null references public.accounts(id) on delete cascade,
|
||||
name text not null,
|
||||
description text,
|
||||
event_date date not null,
|
||||
event_time time,
|
||||
end_date date,
|
||||
location text,
|
||||
capacity integer,
|
||||
min_age integer,
|
||||
max_age integer,
|
||||
fee numeric(10,2) default 0,
|
||||
status text not null default 'planned' check (status in ('planned', 'open', 'full', 'running', 'completed', 'cancelled')),
|
||||
registration_deadline date,
|
||||
contact_name text,
|
||||
contact_email text,
|
||||
contact_phone text,
|
||||
custom_data jsonb not null default '{}'::jsonb,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
create index ix_events_account on public.events(account_id);
|
||||
create index ix_events_date on public.events(account_id, event_date);
|
||||
alter table public.events enable row level security;
|
||||
revoke all on public.events from authenticated, service_role;
|
||||
grant select, insert, update, delete on public.events to authenticated;
|
||||
grant all on public.events to service_role;
|
||||
create policy events_select on public.events for select to authenticated using (public.has_role_on_account(account_id));
|
||||
create policy events_mutate on public.events for all to authenticated using (public.has_permission(auth.uid(), account_id, 'modules.write'::public.app_permissions));
|
||||
create trigger trg_events_updated_at before update on public.events for each row execute function public.update_account_settings_timestamp();
|
||||
|
||||
create table if not exists public.event_registrations (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
event_id uuid not null references public.events(id) on delete cascade,
|
||||
first_name text not null,
|
||||
last_name text not null,
|
||||
email text,
|
||||
phone text,
|
||||
date_of_birth date,
|
||||
parent_name text,
|
||||
parent_phone text,
|
||||
status text not null default 'confirmed' check (status in ('pending', 'confirmed', 'waitlisted', 'cancelled')),
|
||||
notes text,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
create index ix_event_registrations_event on public.event_registrations(event_id);
|
||||
alter table public.event_registrations enable row level security;
|
||||
revoke all on public.event_registrations from authenticated, service_role;
|
||||
grant select, insert, update, delete on public.event_registrations to authenticated;
|
||||
grant all on public.event_registrations to service_role;
|
||||
create policy event_registrations_select on public.event_registrations for select to authenticated using (exists (select 1 from public.events e where e.id = event_registrations.event_id and public.has_role_on_account(e.account_id)));
|
||||
create policy event_registrations_mutate on public.event_registrations for all to authenticated using (exists (select 1 from public.events e where e.id = event_registrations.event_id and public.has_permission(auth.uid(), e.account_id, 'modules.write'::public.app_permissions)));
|
||||
|
||||
-- Holiday passes (Ferienpass)
|
||||
create table if not exists public.holiday_passes (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
account_id uuid not null references public.accounts(id) on delete cascade,
|
||||
name text not null,
|
||||
year integer not null,
|
||||
description text,
|
||||
price numeric(10,2) default 0,
|
||||
valid_from date,
|
||||
valid_until date,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
create index ix_holiday_passes_account on public.holiday_passes(account_id);
|
||||
alter table public.holiday_passes enable row level security;
|
||||
revoke all on public.holiday_passes from authenticated, service_role;
|
||||
grant select, insert, update, delete on public.holiday_passes to authenticated;
|
||||
grant all on public.holiday_passes to service_role;
|
||||
create policy holiday_passes_select on public.holiday_passes for select to authenticated using (public.has_role_on_account(account_id));
|
||||
create policy holiday_passes_mutate on public.holiday_passes for all to authenticated using (public.has_permission(auth.uid(), account_id, 'modules.write'::public.app_permissions));
|
||||
|
||||
create table if not exists public.holiday_pass_activities (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
pass_id uuid not null references public.holiday_passes(id) on delete cascade,
|
||||
event_id uuid references public.events(id) on delete set null,
|
||||
name text not null,
|
||||
description text,
|
||||
activity_date date,
|
||||
capacity integer,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
create index ix_holiday_pass_activities_pass on public.holiday_pass_activities(pass_id);
|
||||
alter table public.holiday_pass_activities enable row level security;
|
||||
revoke all on public.holiday_pass_activities from authenticated, service_role;
|
||||
grant select, insert, update, delete on public.holiday_pass_activities to authenticated;
|
||||
grant all on public.holiday_pass_activities to service_role;
|
||||
create policy holiday_pass_activities_select on public.holiday_pass_activities for select to authenticated using (exists (select 1 from public.holiday_passes hp where hp.id = holiday_pass_activities.pass_id and public.has_role_on_account(hp.account_id)));
|
||||
create policy holiday_pass_activities_mutate on public.holiday_pass_activities for all to authenticated using (exists (select 1 from public.holiday_passes hp where hp.id = holiday_pass_activities.pass_id and public.has_permission(auth.uid(), hp.account_id, 'modules.write'::public.app_permissions)));
|
||||
118
apps/web/supabase/migrations/20260407000001_finance_sepa.sql
Normal file
118
apps/web/supabase/migrations/20260407000001_finance_sepa.sql
Normal file
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* -------------------------------------------------------
|
||||
* Finance / SEPA Schema
|
||||
* Phase 8: sepa_batches, sepa_items, invoices, invoice_items
|
||||
* -------------------------------------------------------
|
||||
*/
|
||||
|
||||
create type public.sepa_batch_type as enum('direct_debit', 'credit_transfer');
|
||||
create type public.sepa_batch_status as enum('draft', 'ready', 'submitted', 'executed', 'failed', 'cancelled');
|
||||
create type public.sepa_item_status as enum('pending', 'success', 'failed', 'rejected');
|
||||
create type public.invoice_status as enum('draft', 'sent', 'paid', 'overdue', 'cancelled', 'credited');
|
||||
|
||||
-- SEPA batches
|
||||
create table if not exists public.sepa_batches (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
account_id uuid not null references public.accounts(id) on delete cascade,
|
||||
batch_type public.sepa_batch_type not null,
|
||||
status public.sepa_batch_status not null default 'draft',
|
||||
description text,
|
||||
execution_date date not null,
|
||||
total_amount numeric(12,2) not null default 0,
|
||||
item_count integer not null default 0,
|
||||
xml_storage_path text,
|
||||
pain_format text not null default 'pain.008.003.02',
|
||||
created_by uuid references auth.users(id) on delete set null,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
create index ix_sepa_batches_account on public.sepa_batches(account_id);
|
||||
alter table public.sepa_batches enable row level security;
|
||||
revoke all on public.sepa_batches from authenticated, service_role;
|
||||
grant select, insert, update, delete on public.sepa_batches to authenticated;
|
||||
grant all on public.sepa_batches to service_role;
|
||||
create policy sepa_batches_select on public.sepa_batches for select to authenticated using (public.has_role_on_account(account_id));
|
||||
create policy sepa_batches_insert on public.sepa_batches for insert to authenticated with check (public.has_permission(auth.uid(), account_id, 'finance.sepa'::public.app_permissions));
|
||||
create policy sepa_batches_update on public.sepa_batches for update to authenticated using (public.has_permission(auth.uid(), account_id, 'finance.sepa'::public.app_permissions));
|
||||
create policy sepa_batches_delete on public.sepa_batches for delete to authenticated using (public.has_permission(auth.uid(), account_id, 'finance.sepa'::public.app_permissions));
|
||||
|
||||
-- SEPA items (individual transactions)
|
||||
create table if not exists public.sepa_items (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
batch_id uuid not null references public.sepa_batches(id) on delete cascade,
|
||||
member_id uuid references public.members(id) on delete set null,
|
||||
debtor_name text not null,
|
||||
debtor_iban text not null,
|
||||
debtor_bic text,
|
||||
amount numeric(10,2) not null,
|
||||
mandate_id text,
|
||||
mandate_date date,
|
||||
remittance_info text,
|
||||
status public.sepa_item_status not null default 'pending',
|
||||
error_message text,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
create index ix_sepa_items_batch on public.sepa_items(batch_id);
|
||||
create index ix_sepa_items_member on public.sepa_items(member_id);
|
||||
alter table public.sepa_items enable row level security;
|
||||
revoke all on public.sepa_items from authenticated, service_role;
|
||||
grant select, insert, update, delete on public.sepa_items to authenticated;
|
||||
grant all on public.sepa_items to service_role;
|
||||
create policy sepa_items_select on public.sepa_items for select to authenticated using (exists (select 1 from public.sepa_batches b where b.id = sepa_items.batch_id and public.has_role_on_account(b.account_id)));
|
||||
create policy sepa_items_mutate on public.sepa_items for all to authenticated using (exists (select 1 from public.sepa_batches b where b.id = sepa_items.batch_id and public.has_permission(auth.uid(), b.account_id, 'finance.sepa'::public.app_permissions)));
|
||||
|
||||
-- Invoices
|
||||
create table if not exists public.invoices (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
account_id uuid not null references public.accounts(id) on delete cascade,
|
||||
invoice_number text not null,
|
||||
member_id uuid references public.members(id) on delete set null,
|
||||
recipient_name text not null,
|
||||
recipient_address text,
|
||||
issue_date date not null default current_date,
|
||||
due_date date not null,
|
||||
status public.invoice_status not null default 'draft',
|
||||
subtotal numeric(10,2) not null default 0,
|
||||
tax_rate numeric(5,2) not null default 0,
|
||||
tax_amount numeric(10,2) not null default 0,
|
||||
total_amount numeric(10,2) not null default 0,
|
||||
paid_amount numeric(10,2) not null default 0,
|
||||
paid_at timestamptz,
|
||||
notes text,
|
||||
pdf_storage_path text,
|
||||
created_by uuid references auth.users(id) on delete set null,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
unique(account_id, invoice_number)
|
||||
);
|
||||
create index ix_invoices_account on public.invoices(account_id);
|
||||
create index ix_invoices_member on public.invoices(member_id);
|
||||
create index ix_invoices_status on public.invoices(account_id, status);
|
||||
alter table public.invoices enable row level security;
|
||||
revoke all on public.invoices from authenticated, service_role;
|
||||
grant select, insert, update, delete on public.invoices to authenticated;
|
||||
grant all on public.invoices to service_role;
|
||||
create policy invoices_select on public.invoices for select to authenticated using (public.has_role_on_account(account_id));
|
||||
create policy invoices_insert on public.invoices for insert to authenticated with check (public.has_permission(auth.uid(), account_id, 'finance.write'::public.app_permissions));
|
||||
create policy invoices_update on public.invoices for update to authenticated using (public.has_permission(auth.uid(), account_id, 'finance.write'::public.app_permissions));
|
||||
create policy invoices_delete on public.invoices for delete to authenticated using (public.has_permission(auth.uid(), account_id, 'finance.write'::public.app_permissions));
|
||||
create trigger trg_invoices_updated_at before update on public.invoices for each row execute function public.update_account_settings_timestamp();
|
||||
|
||||
-- Invoice line items
|
||||
create table if not exists public.invoice_items (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
invoice_id uuid not null references public.invoices(id) on delete cascade,
|
||||
description text not null,
|
||||
quantity numeric(10,2) not null default 1,
|
||||
unit_price numeric(10,2) not null,
|
||||
total_price numeric(10,2) not null,
|
||||
sort_order integer not null default 0,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
create index ix_invoice_items_invoice on public.invoice_items(invoice_id);
|
||||
alter table public.invoice_items enable row level security;
|
||||
revoke all on public.invoice_items from authenticated, service_role;
|
||||
grant select, insert, update, delete on public.invoice_items to authenticated;
|
||||
grant all on public.invoice_items to service_role;
|
||||
create policy invoice_items_select on public.invoice_items for select to authenticated using (exists (select 1 from public.invoices i where i.id = invoice_items.invoice_id and public.has_role_on_account(i.account_id)));
|
||||
create policy invoice_items_mutate on public.invoice_items for all to authenticated using (exists (select 1 from public.invoices i where i.id = invoice_items.invoice_id and public.has_permission(auth.uid(), i.account_id, 'finance.write'::public.app_permissions)));
|
||||
70
apps/web/supabase/migrations/20260408000001_newsletter.sql
Normal file
70
apps/web/supabase/migrations/20260408000001_newsletter.sql
Normal file
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* -------------------------------------------------------
|
||||
* Newsletter Schema
|
||||
* Phase 10: newsletters, recipients, templates, messages
|
||||
* -------------------------------------------------------
|
||||
*/
|
||||
|
||||
create type public.newsletter_status as enum('draft', 'scheduled', 'sending', 'sent', 'failed');
|
||||
|
||||
create table if not exists public.newsletter_templates (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
account_id uuid not null references public.accounts(id) on delete cascade,
|
||||
name text not null,
|
||||
subject text not null,
|
||||
body_html text not null,
|
||||
body_text text,
|
||||
variables jsonb not null default '[]'::jsonb,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
create index ix_newsletter_templates_account on public.newsletter_templates(account_id);
|
||||
alter table public.newsletter_templates enable row level security;
|
||||
revoke all on public.newsletter_templates from authenticated, service_role;
|
||||
grant select, insert, update, delete on public.newsletter_templates to authenticated;
|
||||
grant all on public.newsletter_templates to service_role;
|
||||
create policy newsletter_templates_select on public.newsletter_templates for select to authenticated using (public.has_role_on_account(account_id));
|
||||
create policy newsletter_templates_mutate on public.newsletter_templates for all to authenticated using (public.has_permission(auth.uid(), account_id, 'newsletter.send'::public.app_permissions));
|
||||
|
||||
create table if not exists public.newsletters (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
account_id uuid not null references public.accounts(id) on delete cascade,
|
||||
template_id uuid references public.newsletter_templates(id) on delete set null,
|
||||
subject text not null,
|
||||
body_html text not null,
|
||||
body_text text,
|
||||
status public.newsletter_status not null default 'draft',
|
||||
scheduled_at timestamptz,
|
||||
sent_at timestamptz,
|
||||
total_recipients integer not null default 0,
|
||||
sent_count integer not null default 0,
|
||||
failed_count integer not null default 0,
|
||||
created_by uuid references auth.users(id) on delete set null,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
create index ix_newsletters_account on public.newsletters(account_id);
|
||||
alter table public.newsletters enable row level security;
|
||||
revoke all on public.newsletters from authenticated, service_role;
|
||||
grant select, insert, update, delete on public.newsletters to authenticated;
|
||||
grant all on public.newsletters to service_role;
|
||||
create policy newsletters_select on public.newsletters for select to authenticated using (public.has_role_on_account(account_id));
|
||||
create policy newsletters_mutate on public.newsletters for all to authenticated using (public.has_permission(auth.uid(), account_id, 'newsletter.send'::public.app_permissions));
|
||||
|
||||
create table if not exists public.newsletter_recipients (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
newsletter_id uuid not null references public.newsletters(id) on delete cascade,
|
||||
member_id uuid references public.members(id) on delete set null,
|
||||
email text not null,
|
||||
name text,
|
||||
status text not null default 'pending' check (status in ('pending', 'sent', 'failed', 'bounced')),
|
||||
sent_at timestamptz,
|
||||
error_message text
|
||||
);
|
||||
create index ix_newsletter_recipients_newsletter on public.newsletter_recipients(newsletter_id);
|
||||
alter table public.newsletter_recipients enable row level security;
|
||||
revoke all on public.newsletter_recipients from authenticated, service_role;
|
||||
grant select, insert, update, delete on public.newsletter_recipients to authenticated;
|
||||
grant all on public.newsletter_recipients to service_role;
|
||||
create policy newsletter_recipients_select on public.newsletter_recipients for select to authenticated using (exists (select 1 from public.newsletters n where n.id = newsletter_recipients.newsletter_id and public.has_role_on_account(n.account_id)));
|
||||
create policy newsletter_recipients_mutate on public.newsletter_recipients for all to authenticated using (exists (select 1 from public.newsletters n where n.id = newsletter_recipients.newsletter_id and public.has_permission(auth.uid(), n.account_id, 'newsletter.send'::public.app_permissions)));
|
||||
Reference in New Issue
Block a user