test: add full-stack Playwright integration test infrastructure

Sprint 12 Phase 2: Real integration tests with seed DB
- R__seed_test_data.sql (Flyway repeatable, 7 members, strains, batches, docs, board, events)
- TestResetController (profile-gated per-test DB reset)
- docker-compose.test.yml (self-contained, tmpfs Postgres)
- Dockerfile.playwright (v1.60.0, pre-installed deps)
- 13 integration spec files, 70+ test cases (@smoke + @full)
- seed-constants.ts, selectors.ts, api-client.ts test helpers
This commit is contained in:
Patrick Plate
2026-06-18 14:43:16 +02:00
parent 6e25914074
commit 776149e7d3
25 changed files with 2127 additions and 39 deletions
@@ -0,0 +1,93 @@
package de.cannamanage.api.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.ResponseEntity;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
/**
* Test-only controller for resetting the database to a known seed state.
* Only active when cannamanage.test.endpoints.enabled=true (test profile).
* NEVER activate this in production.
*/
@Slf4j
@RestController
@RequestMapping("/api/v1/test")
@RequiredArgsConstructor
@ConditionalOnProperty(name = "cannamanage.test.endpoints.enabled", havingValue = "true")
public class TestResetController {
private final DataSource dataSource;
/**
* Truncates all application tables and re-seeds with test data.
* The Flyway schema_history table is preserved.
*/
@PostMapping("/reset-db")
public ResponseEntity<Void> resetDatabase() {
log.info("Test DB reset requested — truncating all tables and re-seeding");
try (Connection conn = dataSource.getConnection()) {
truncateAllTables(conn);
reseed();
log.info("Test DB reset complete — seed data re-applied");
return ResponseEntity.ok().build();
} catch (SQLException e) {
log.error("Failed to reset test database", e);
return ResponseEntity.internalServerError().build();
}
}
private void truncateAllTables(Connection conn) throws SQLException {
List<String> tables = getApplicationTables(conn);
try (Statement stmt = conn.createStatement()) {
// Disable FK constraints for truncation
stmt.execute("SET session_replication_role = 'replica'");
for (String table : tables) {
stmt.execute("TRUNCATE TABLE " + table + " CASCADE");
log.debug("Truncated table: {}", table);
}
// Re-enable FK constraints
stmt.execute("SET session_replication_role = 'origin'");
}
}
private List<String> getApplicationTables(Connection conn) throws SQLException {
List<String> tables = new ArrayList<>();
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(
"SELECT tablename FROM pg_tables " +
"WHERE schemaname = 'public' " +
"AND tablename != 'flyway_schema_history'")) {
while (rs.next()) {
tables.add(rs.getString("tablename"));
}
}
return tables;
}
private void reseed() {
ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
populator.addScript(new ClassPathResource("db/testdata/R__seed_test_data.sql"));
populator.setSeparator(";");
populator.execute(dataSource);
}
}
@@ -0,0 +1,31 @@
# =============================================
# application-test.properties
# Profile: test — for integration test environment
# Activate with: -Dspring.profiles.active=test
# =============================================
# Database: use docker-compose.test.yml PostgreSQL
spring.datasource.url=jdbc:postgresql://localhost:5433/cannamanage_test
spring.datasource.username=cannamanage_test
spring.datasource.password=test_password
spring.jpa.hibernate.ddl-auto=validate
# Flyway: include test seed data
spring.flyway.enabled=true
spring.flyway.locations=classpath:db/migration,classpath:db/testdata
# Enable test-only endpoints (TestResetController)
cannamanage.test.endpoints.enabled=true
# Disable schedulers during test runs
cannamanage.schedulers.enabled=false
# JWT: deterministic test secret (base64-encoded 256-bit key)
cannamanage.security.jwt.secret=dGVzdC1zZWNyZXQta2V5LWZvci1pbnRlZ3JhdGlvbi10ZXN0cy1vbmx5LTMyYg==
cannamanage.security.jwt.access-token-expiry=3600
cannamanage.security.jwt.refresh-token-expiry=86400
# Logging
logging.level.de.cannamanage=DEBUG
logging.level.org.flywaydb=INFO
logging.level.org.springframework.security=DEBUG
@@ -0,0 +1,265 @@
-- R__seed_test_data.sql — Repeatable Flyway migration for integration test data
-- This file is idempotent: uses ON CONFLICT DO NOTHING for all inserts.
-- Activated only when spring.flyway.locations includes classpath:db/testdata
-- ============================================================
-- 1. CLUB
-- ============================================================
INSERT INTO clubs (id, tenant_id, name, address, license_number, max_members, status, created_at)
VALUES (
'a0000000-0000-0000-0000-000000000001',
'a0000000-0000-0000-0000-000000000001',
'Grüner Daumen e.V.',
'Hanfstraße 42, 10115 Berlin',
'LIC-2024-GD-001',
500,
'ACTIVE',
'2024-01-01T00:00:00Z'
) ON CONFLICT (id) DO NOTHING;
-- ============================================================
-- 2. MEMBERS (7)
-- ============================================================
INSERT INTO members (id, tenant_id, club_id, first_name, last_name, email, date_of_birth, membership_date, membership_number, status, is_under_21, prevention_officer, created_at)
VALUES
('c1000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001',
'Max', 'Mustermann', 'max@gruener-daumen.de', '1990-05-20', '2024-01-15', 'GD-001', 'ACTIVE', FALSE, FALSE, '2024-01-15T10:00:00Z'),
('c1000000-0000-0000-0000-000000000002', 'a0000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001',
'Anna', 'Schmidt', 'anna@gruener-daumen.de', '1985-11-03', '2024-02-01', 'GD-002', 'ACTIVE', FALSE, FALSE, '2024-02-01T10:00:00Z'),
('c1000000-0000-0000-0000-000000000003', 'a0000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001',
'Jonas', 'Weber', 'jonas@gruener-daumen.de', '2006-03-15', '2024-03-10', 'GD-003', 'ACTIVE', TRUE, FALSE, '2024-03-10T10:00:00Z'),
('c1000000-0000-0000-0000-000000000004', 'a0000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001',
'Maria', 'Müller', 'maria@gruener-daumen.de', '1978-08-22', '2023-06-01', 'GD-004', 'SUSPENDED', FALSE, FALSE, '2023-06-01T10:00:00Z'),
('c1000000-0000-0000-0000-000000000005', 'a0000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001',
'Thomas', 'Müller', 'thomas@gruener-daumen.de', '1992-12-01', '2024-01-20', 'GD-005', 'ACTIVE', FALSE, FALSE, '2024-01-20T10:00:00Z'),
('c1000000-0000-0000-0000-000000000006', 'a0000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001',
'Lisa', 'Bauer', 'lisa@gruener-daumen.de', '1995-07-14', '2024-04-01', 'GD-006', 'ACTIVE', FALSE, FALSE, '2024-04-01T10:00:00Z'),
('c1000000-0000-0000-0000-000000000007', 'a0000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001',
'Karl', 'Fischer', 'karl@gruener-daumen.de', '1980-02-28', '2023-01-01', 'GD-007', 'EXPELLED', FALSE, FALSE, '2023-01-01T10:00:00Z')
ON CONFLICT (id) DO NOTHING;
-- ============================================================
-- 3. USERS (admin staff account)
-- ============================================================
INSERT INTO users (id, tenant_id, member_id, email, password_hash, role, active, created_at)
VALUES (
'b1000000-0000-0000-0000-000000000001',
'a0000000-0000-0000-0000-000000000001',
'c1000000-0000-0000-0000-000000000001',
'admin@gruener-daumen.de',
'$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy',
'ROLE_ADMIN',
TRUE,
'2024-01-15T10:00:00Z'
) ON CONFLICT (id) DO NOTHING;
-- Additional user accounts for members who need to author forum/info-board posts
INSERT INTO users (id, tenant_id, member_id, email, password_hash, role, active, created_at)
VALUES
('b1000000-0000-0000-0000-000000000002', 'a0000000-0000-0000-0000-000000000001', 'c1000000-0000-0000-0000-000000000002',
'anna.user@gruener-daumen.de', '$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy', 'ROLE_MEMBER', TRUE, '2024-02-01T10:00:00Z'),
('b1000000-0000-0000-0000-000000000003', 'a0000000-0000-0000-0000-000000000001', 'c1000000-0000-0000-0000-000000000003',
'jonas.user@gruener-daumen.de', '$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy', 'ROLE_MEMBER', TRUE, '2024-03-10T10:00:00Z')
ON CONFLICT (id) DO NOTHING;
-- ============================================================
-- 4. STRAINS (3)
-- ============================================================
INSERT INTO strains (id, tenant_id, name, thc_percentage, cbd_percentage, description, created_at)
VALUES
('d1000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001',
'Northern Lights', 18.50, 0.50, 'Klassische Indica, entspannend und schmerzlindernd', '2024-04-01T10:00:00Z'),
('d1000000-0000-0000-0000-000000000002', 'a0000000-0000-0000-0000-000000000001',
'CBD Critical Mass', 5.00, 12.00, 'CBD-dominante Sorte für medizinische Anwendungen', '2024-04-01T10:00:00Z'),
('d1000000-0000-0000-0000-000000000003', 'a0000000-0000-0000-0000-000000000001',
'Amnesia Haze', 22.00, 0.10, 'Starke Sativa mit hohem THC-Gehalt', '2024-04-01T10:00:00Z')
ON CONFLICT (id) DO NOTHING;
-- ============================================================
-- 5. BATCHES (3)
-- ============================================================
INSERT INTO batches (id, tenant_id, strain_id, quantity_grams, harvest_date, batch_code, status, contamination_flag, created_at)
VALUES
('e1000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001',
'd1000000-0000-0000-0000-000000000001', 500.00, '2024-04-25', 'NL-2024-001', 'AVAILABLE', FALSE, '2024-05-01T10:00:00Z'),
('e1000000-0000-0000-0000-000000000002', 'a0000000-0000-0000-0000-000000000001',
'd1000000-0000-0000-0000-000000000002', 300.00, '2024-05-10', 'CM-2024-001', 'AVAILABLE', FALSE, '2024-05-15T10:00:00Z'),
('e1000000-0000-0000-0000-000000000003', 'a0000000-0000-0000-0000-000000000001',
'd1000000-0000-0000-0000-000000000003', 200.00, '2024-03-20', 'AH-2024-001', 'RECALLED', TRUE, '2024-04-01T10:00:00Z')
ON CONFLICT (id) DO NOTHING;
-- ============================================================
-- 6. DISTRIBUTIONS (3 recent)
-- ============================================================
INSERT INTO distributions (id, tenant_id, member_id, batch_id, quantity_grams, distributed_at, recorded_by, notes, thc_percentage, cbd_percentage, strain_name, created_at)
VALUES
('dd000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001',
'c1000000-0000-0000-0000-000000000001', 'e1000000-0000-0000-0000-000000000001',
5.00, NOW() - INTERVAL '2 days', 'c1000000-0000-0000-0000-000000000001', 'Reguläre Abgabe',
18.50, 0.50, 'Northern Lights', NOW() - INTERVAL '2 days'),
('dd000000-0000-0000-0000-000000000002', 'a0000000-0000-0000-0000-000000000001',
'c1000000-0000-0000-0000-000000000002', 'e1000000-0000-0000-0000-000000000002',
3.00, NOW() - INTERVAL '1 day', 'c1000000-0000-0000-0000-000000000001', 'CBD-Abgabe',
5.00, 12.00, 'CBD Critical Mass', NOW() - INTERVAL '1 day'),
('dd000000-0000-0000-0000-000000000003', 'a0000000-0000-0000-0000-000000000001',
'c1000000-0000-0000-0000-000000000005', 'e1000000-0000-0000-0000-000000000002',
23.00, NOW(), 'c1000000-0000-0000-0000-000000000001', 'Nahe am Monatslimit (25g)',
5.00, 12.00, 'CBD Critical Mass', NOW())
ON CONFLICT (id) DO NOTHING;
-- ============================================================
-- 7. MONTHLY QUOTAS (Thomas near-quota)
-- ============================================================
INSERT INTO monthly_quotas (id, tenant_id, member_id, year, month, total_distributed, max_allowed, version, created_at)
VALUES
('mq000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001',
'c1000000-0000-0000-0000-000000000005',
EXTRACT(YEAR FROM NOW())::INT, EXTRACT(MONTH FROM NOW())::INT,
23.00, 25.00, 1, NOW())
ON CONFLICT (member_id, year, month) DO NOTHING;
-- ============================================================
-- 8. DOCUMENTS (4)
-- ============================================================
INSERT INTO documents (id, tenant_id, club_id, title, category, filename, content_type, file_size, storage_path, access_level, description, uploaded_by, created_at)
VALUES
('f1000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001',
'Vereinssatzung 2024', 'SATZUNG', 'satzung-2024.pdf', 'application/pdf', 245000,
'/documents/a0000000/satzung-2024.pdf', 'ALL_MEMBERS', 'Aktuelle Vereinssatzung gemäß §18 KCanG',
'b1000000-0000-0000-0000-000000000001', '2024-01-15T10:00:00Z'),
('f1000000-0000-0000-0000-000000000002', 'a0000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001',
'Protokoll MV März 2024', 'PROTOKOLL', 'protokoll-mv-2024-03.pdf', 'application/pdf', 128000,
'/documents/a0000000/protokoll-mv-2024-03.pdf', 'ALL_MEMBERS', 'Protokoll der Mitgliederversammlung vom 15.03.2024',
'b1000000-0000-0000-0000-000000000001', '2024-03-16T10:00:00Z'),
('f1000000-0000-0000-0000-000000000003', 'a0000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001',
'KCanG-Genehmigung', 'GENEHMIGUNG', 'kcang-genehmigung.pdf', 'application/pdf', 340000,
'/documents/a0000000/kcang-genehmigung.pdf', 'BOARD_ONLY', 'Genehmigungsbescheid nach §11 KCanG',
'b1000000-0000-0000-0000-000000000001', '2024-01-10T10:00:00Z'),
('f1000000-0000-0000-0000-000000000004', 'a0000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001',
'Mietvertrag', 'VERTRAG', 'mietvertrag-vereinsheim.pdf', 'application/pdf', 520000,
'/documents/a0000000/mietvertrag-vereinsheim.pdf', 'BOARD_ONLY', 'Mietvertrag für Vereinsräume',
'b1000000-0000-0000-0000-000000000001', '2023-12-01T10:00:00Z')
ON CONFLICT (id) DO NOTHING;
-- ============================================================
-- 9. BOARD POSITIONS (3)
-- ============================================================
INSERT INTO board_positions (id, tenant_id, club_id, title, description, sort_order, is_active, created_at)
VALUES
('g1000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001',
'Vorsitzende/r', 'Erste/r Vorsitzende/r des Vereins', 1, TRUE, '2024-01-15T10:00:00Z'),
('g1000000-0000-0000-0000-000000000002', 'a0000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001',
'Kassenführung', 'Schatzmeister/in — Kassenführung und Finanzen', 2, TRUE, '2024-01-15T10:00:00Z'),
('g1000000-0000-0000-0000-000000000003', 'a0000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001',
'Schriftführung', 'Protokollführung und Korrespondenz', 3, TRUE, '2024-01-15T10:00:00Z')
ON CONFLICT (id) DO NOTHING;
-- Board members (Max = Vorsitzender, Anna = Kassenführung, Schriftführung = vacant)
INSERT INTO board_members (id, tenant_id, club_id, position_id, member_id, elected_at, term_start, is_current, created_at)
VALUES
('gm000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001',
'g1000000-0000-0000-0000-000000000001', 'c1000000-0000-0000-0000-000000000001',
'2024-01-15', '2024-01-15', TRUE, '2024-01-15T10:00:00Z'),
('gm000000-0000-0000-0000-000000000002', 'a0000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001',
'g1000000-0000-0000-0000-000000000002', 'c1000000-0000-0000-0000-000000000002',
'2024-01-15', '2024-01-15', TRUE, '2024-01-15T10:00:00Z')
ON CONFLICT (id) DO NOTHING;
-- ============================================================
-- 10. EVENTS (2)
-- ============================================================
INSERT INTO club_events (id, club_id, title, description, event_type, start_at, end_at, location, created_by, tenant_id, created_at, updated_at)
VALUES
('ev000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001',
'Mitgliederversammlung Q3', 'Ordentliche Mitgliederversammlung mit Vorstandswahl',
'ASSEMBLY', NOW() + INTERVAL '14 days', NOW() + INTERVAL '14 days' + INTERVAL '2 hours',
'Vereinsheim, Hanfstraße 42', 'b1000000-0000-0000-0000-000000000001',
'a0000000-0000-0000-0000-000000000001', NOW() - INTERVAL '7 days', NOW() - INTERVAL '7 days'),
('ev000000-0000-0000-0000-000000000002', 'a0000000-0000-0000-0000-000000000001',
'Gartentag Mai', 'Gemeinsamer Gartentag — Pflege der Anbauflächen',
'SOCIAL', NOW() - INTERVAL '30 days', NOW() - INTERVAL '30 days' + INTERVAL '4 hours',
'Vereinsgarten', 'b1000000-0000-0000-0000-000000000001',
'a0000000-0000-0000-0000-000000000001', NOW() - INTERVAL '45 days', NOW() - INTERVAL '45 days')
ON CONFLICT (id) DO NOTHING;
-- ============================================================
-- 11. FORUM TOPICS (2) + REPLIES
-- ============================================================
INSERT INTO forum_topics (id, club_id, tenant_id, title, content, author_id, reply_count, last_reply_at, created_at)
VALUES
('ft000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001',
'Neue Sorten für Sommer', 'Welche Sorten sollen wir diesen Sommer anbauen? Ich schlage vor, mehr CBD-lastige Sorten zu probieren.',
'b1000000-0000-0000-0000-000000000001', 3, NOW() - INTERVAL '2 days', NOW() - INTERVAL '10 days'),
('ft000000-0000-0000-0000-000000000002', 'a0000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001',
'Bewässerungssystem', 'Hat jemand Erfahrung mit automatischen Bewässerungssystemen für den Indoor-Bereich?',
'b1000000-0000-0000-0000-000000000002', 1, NOW() - INTERVAL '5 days', NOW() - INTERVAL '7 days')
ON CONFLICT (id) DO NOTHING;
-- Forum replies
INSERT INTO forum_replies (id, topic_id, club_id, tenant_id, content, author_id, created_at)
VALUES
('fr000000-0000-0000-0000-000000000001', 'ft000000-0000-0000-0000-000000000001',
'a0000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001',
'CBD Critical Mass hat sich bei uns bewährt — guter Ertrag und medizinisch wertvoll!',
'b1000000-0000-0000-0000-000000000002', NOW() - INTERVAL '9 days'),
('fr000000-0000-0000-0000-000000000002', 'ft000000-0000-0000-0000-000000000001',
'a0000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001',
'Finde ich gut! Vielleicht auch Charlotte''s Web als weitere CBD-Option?',
'b1000000-0000-0000-0000-000000000003', NOW() - INTERVAL '7 days'),
('fr000000-0000-0000-0000-000000000003', 'ft000000-0000-0000-0000-000000000001',
'a0000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001',
'Stimme zu — lasst uns in der MV darüber abstimmen.',
'b1000000-0000-0000-0000-000000000001', NOW() - INTERVAL '2 days'),
('fr000000-0000-0000-0000-000000000004', 'ft000000-0000-0000-0000-000000000002',
'a0000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001',
'Wir nutzen BlueMat-Tropfer — funktioniert super für Erde und Kokos.',
'b1000000-0000-0000-0000-000000000001', NOW() - INTERVAL '5 days')
ON CONFLICT (id) DO NOTHING;
-- ============================================================
-- 12. INFO BOARD POSTS (2)
-- ============================================================
INSERT INTO info_board_posts (id, club_id, title, content, category, is_pinned, is_archived, author_id, tenant_id, created_at, updated_at)
VALUES
('ib000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001',
'Willkommen neue Mitglieder', 'Herzlich willkommen bei Grüner Daumen e.V.! Bitte lest die Vereinssatzung und meldet euch bei Fragen beim Vorstand.',
'GENERAL', TRUE, FALSE, 'b1000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001',
NOW() - INTERVAL '30 days', NOW() - INTERVAL '30 days'),
('ib000000-0000-0000-0000-000000000002', 'a0000000-0000-0000-0000-000000000001',
'Öffnungszeiten Sommer', 'Ab Juni gelten erweiterte Öffnungszeiten: Mo-Fr 10-20 Uhr, Sa 10-16 Uhr.',
'MAINTENANCE', FALSE, FALSE, 'b1000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001',
NOW() - INTERVAL '14 days', NOW() - INTERVAL '14 days')
ON CONFLICT (id) DO NOTHING;
-- ============================================================
-- 13. GROW ENTRIES (2)
-- ============================================================
INSERT INTO grow_entries (id, name, strain_id, status, started_at, expected_harvest_at, notes, tenant_id, created_at, updated_at)
VALUES
('ge000000-0000-0000-0000-000000000001',
'Northern Lights Batch #2', 'd1000000-0000-0000-0000-000000000001', 'VEGETATIVE',
NOW() - INTERVAL '21 days', NOW() + INTERVAL '49 days',
'Zweiter Indoor-Batch NL, 6 Pflanzen',
'a0000000-0000-0000-0000-000000000001', NOW() - INTERVAL '21 days', NOW() - INTERVAL '1 day'),
('ge000000-0000-0000-0000-000000000002',
'CBD Outdoor', 'd1000000-0000-0000-0000-000000000002', 'SEEDLING',
NOW() - INTERVAL '7 days', NOW() + INTERVAL '90 days',
'Outdoor-Test mit CBD Critical Mass, 4 Pflanzen',
'a0000000-0000-0000-0000-000000000001', NOW() - INTERVAL '7 days', NOW() - INTERVAL '1 day')
ON CONFLICT (id) DO NOTHING;
-- ============================================================
-- 14. COMPLIANCE DEADLINES (3)
-- ============================================================
INSERT INTO compliance_deadlines (id, tenant_id, club_id, area, title, description, due_date, is_recurring, created_at)
VALUES
('cd000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001',
'KCANG', 'Jahresbericht', 'Jährlicher Tätigkeitsbericht an die zuständige Behörde gemäß §22 KCanG',
(CURRENT_DATE + INTERVAL '60 days')::DATE, TRUE, NOW() - INTERVAL '30 days'),
('cd000000-0000-0000-0000-000000000002', 'a0000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001',
'FINANCE', 'EÜR Abgabe', 'Einnahmen-Überschuss-Rechnung an das Finanzamt',
(CURRENT_DATE - INTERVAL '5 days')::DATE, FALSE, NOW() - INTERVAL '60 days'),
('cd000000-0000-0000-0000-000000000003', 'a0000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001',
'VEREIN', 'Mitgliederversammlung', 'Ordentliche Mitgliederversammlung (mindestens 1x jährlich)',
(CURRENT_DATE + INTERVAL '14 days')::DATE, TRUE, NOW() - INTERVAL '14 days')
ON CONFLICT (id) DO NOTHING;