Initial state for GitNexus analysis

This commit is contained in:
Zaid Marzguioui
2026-03-29 19:44:57 +02:00
parent 9d7c7f8030
commit 61ff48cb73
155 changed files with 23483 additions and 1722 deletions

View 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))
);

View File

@@ -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;

View 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;

View File

@@ -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));

View File

@@ -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)));

View File

@@ -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();

View File

@@ -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)));

View 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)));

View 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)));