fixing web-app

This commit is contained in:
stefan
2025-09-24 14:21:57 +02:00
parent cd2b0796a6
commit 1c4184809a
156 changed files with 440 additions and 1708 deletions
@@ -0,0 +1,30 @@
package at.mocode.masterdata.service
import at.mocode.core.utils.config.AppConfig
import at.mocode.core.utils.database.DatabaseFactory
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
/**
* Main application class for the Masterdata Service.
*
* This service provides APIs for managing master data such as countries, regions, and other reference data.
*/
@SpringBootApplication
class MasterdataServiceApplication
fun main(args: Array<String>) {
// 1. Lade die Konfiguration explizit, genau einmal beim Start.
val appConfig = AppConfig.load()
println("Konfiguration für Umgebung '${appConfig.environment}' geladen.")
// 2. Initialisiere die Datenbank mit der geladenen Konfiguration.
// Flyway-Migrationen werden hier automatisch ausgeführt.
DatabaseFactory.init(appConfig.database)
println("Datenbank initialisiert und migriert.")
// 3. Starte die Spring Boot / Ktor Anwendung.
// Der appConfig-Wert kann hier an die Anwendung übergeben werden,
// um ihn später per Dependency Injection zu nutzen.
runApplication<MasterdataServiceApplication>(*args)
}
@@ -0,0 +1,154 @@
package at.mocode.masterdata.service.config
import at.mocode.masterdata.application.usecase.*
import at.mocode.masterdata.domain.repository.*
import at.mocode.masterdata.infrastructure.persistence.*
import at.mocode.masterdata.api.rest.*
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Profile
/**
* Spring Boot configuration for the Masterdata Service.
*
* This configuration class sets up all the necessary beans for dependency injection
* following the clean architecture pattern with proper separation of concerns.
*/
@Configuration
class MasterdataConfiguration {
// Repository Implementations
@Bean
fun landRepository(): LandRepository {
return LandRepositoryImpl()
}
@Bean
fun bundeslandRepository(): BundeslandRepository {
return BundeslandRepositoryImpl()
}
@Bean
fun altersklasseRepository(): AltersklasseRepository {
return AltersklasseRepositoryImpl()
}
@Bean
fun platzRepository(): PlatzRepository {
return PlatzRepositoryImpl()
}
// Use Cases - Country/Land
@Bean
fun getCountryUseCase(landRepository: LandRepository): GetCountryUseCase {
return GetCountryUseCase(landRepository)
}
@Bean
fun createCountryUseCase(landRepository: LandRepository): CreateCountryUseCase {
return CreateCountryUseCase(landRepository)
}
// Use Cases - Federal State/Bundesland
@Bean
fun getBundeslandUseCase(bundeslandRepository: BundeslandRepository): GetBundeslandUseCase {
return GetBundeslandUseCase(bundeslandRepository)
}
@Bean
fun createBundeslandUseCase(bundeslandRepository: BundeslandRepository): CreateBundeslandUseCase {
return CreateBundeslandUseCase(bundeslandRepository)
}
// Use Cases - Age Class/Altersklasse
@Bean
fun getAltersklasseUseCase(altersklasseRepository: AltersklasseRepository): GetAltersklasseUseCase {
return GetAltersklasseUseCase(altersklasseRepository)
}
@Bean
fun createAltersklasseUseCase(altersklasseRepository: AltersklasseRepository): CreateAltersklasseUseCase {
return CreateAltersklasseUseCase(altersklasseRepository)
}
// Use Cases - Venue/Platz
@Bean
fun getPlatzUseCase(platzRepository: PlatzRepository): GetPlatzUseCase {
return GetPlatzUseCase(platzRepository)
}
@Bean
fun createPlatzUseCase(platzRepository: PlatzRepository): CreatePlatzUseCase {
return CreatePlatzUseCase(platzRepository)
}
// API Controllers
@Bean
fun countryController(
getCountryUseCase: GetCountryUseCase,
createCountryUseCase: CreateCountryUseCase
): CountryController {
return CountryController(getCountryUseCase, createCountryUseCase)
}
@Bean
fun bundeslandController(
getBundeslandUseCase: GetBundeslandUseCase,
createBundeslandUseCase: CreateBundeslandUseCase
): BundeslandController {
return BundeslandController(getBundeslandUseCase, createBundeslandUseCase)
}
@Bean
fun altersklasseController(
getAltersklasseUseCase: GetAltersklasseUseCase,
createAltersklasseUseCase: CreateAltersklasseUseCase
): AltersklasseController {
return AltersklasseController(getAltersklasseUseCase, createAltersklasseUseCase)
}
@Bean
fun platzController(
getPlatzUseCase: GetPlatzUseCase,
createPlatzUseCase: CreatePlatzUseCase
): PlatzController {
return PlatzController(getPlatzUseCase, createPlatzUseCase)
}
}
/**
* Database configuration for different environments.
*/
@Configuration
class DatabaseConfiguration {
/**
* Development database configuration.
*/
@Configuration
@Profile("dev", "development")
class DevelopmentDatabaseConfig {
// Development-specific database configuration
// This would typically include H2 or local PostgreSQL setup
}
/**
* Production database configuration.
*/
@Configuration
@Profile("prod", "production")
class ProductionDatabaseConfig {
// Production-specific database configuration
// This would include production PostgreSQL setup with connection pooling
}
/**
* Test database configuration.
*/
@Configuration
@Profile("test")
class TestDatabaseConfig {
// Test-specific database configuration
// This would typically include in-memory H2 database
}
}
@@ -0,0 +1,117 @@
package at.mocode.masterdata.service.config
import at.mocode.core.utils.database.DatabaseConfig
import at.mocode.core.utils.database.DatabaseFactory
import at.mocode.masterdata.infrastructure.persistence.LandTable
import at.mocode.masterdata.infrastructure.persistence.BundeslandTable
import at.mocode.masterdata.infrastructure.persistence.AltersklasseTable
import at.mocode.masterdata.infrastructure.persistence.PlatzTable
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Profile
import jakarta.annotation.PostConstruct
import jakarta.annotation.PreDestroy
import org.slf4j.LoggerFactory
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.transaction
/**
* Database configuration for the Masterdata Service.
*
* This configuration ensures that Database.connect() is called properly
* before any Exposed operations are performed.
*/
@Configuration
@Profile("!test")
class MasterdataDatabaseConfiguration {
private val log = LoggerFactory.getLogger(MasterdataDatabaseConfiguration::class.java)
@PostConstruct
fun initializeDatabase() {
log.info("Initializing database schema for Masterdata Service...")
try {
// Database connection is already initialized by the gateway
// Only initialize the schema for this service
transaction {
SchemaUtils.createMissingTablesAndColumns(
LandTable,
BundeslandTable,
AltersklasseTable,
PlatzTable
)
log.info("Masterdata database schema initialized successfully")
}
} catch (e: Exception) {
log.error("Failed to initialize database schema", e)
throw e
}
}
@PreDestroy
fun closeDatabase() {
log.info("Closing database connection for Masterdata Service...")
try {
DatabaseFactory.close()
log.info("Database connection closed successfully")
} catch (e: Exception) {
log.error("Error closing database connection", e)
}
}
}
/**
* Test-specific database configuration.
*/
@Configuration
@Profile("test")
class MasterdataTestDatabaseConfiguration {
private val log = LoggerFactory.getLogger(MasterdataTestDatabaseConfiguration::class.java)
@PostConstruct
fun initializeTestDatabase() {
log.info("Initializing test database connection for Masterdata Service...")
try {
// Use H2 in-memory database for tests
val testConfig = DatabaseConfig(
jdbcUrl = "jdbc:h2:mem:masterdata_test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE",
username = "sa",
password = "",
driverClassName = "org.h2.Driver",
maxPoolSize = 5,
minPoolSize = 1,
autoMigrate = true
)
DatabaseFactory.init(testConfig)
log.info("Test database connection initialized successfully")
// Initialize database schema for tests
transaction {
SchemaUtils.createMissingTablesAndColumns(
LandTable,
BundeslandTable,
AltersklasseTable,
PlatzTable
)
log.info("Test masterdata database schema initialized successfully")
}
} catch (e: Exception) {
log.error("Failed to initialize test database connection", e)
throw e
}
}
@PreDestroy
fun closeTestDatabase() {
log.info("Closing test database connection for Masterdata Service...")
try {
DatabaseFactory.close()
log.info("Test database connection closed successfully")
} catch (e: Exception) {
log.error("Error closing test database connection", e)
}
}
}
@@ -0,0 +1,62 @@
-- Migration V001: Create Land (Country) table
-- This migration creates the base table for country master data
CREATE TABLE IF NOT EXISTS land (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
iso_alpha2_code VARCHAR(2) NOT NULL,
iso_alpha3_code VARCHAR(3) NOT NULL,
iso_numerischer_code VARCHAR(3),
name_deutsch VARCHAR(100) NOT NULL,
name_englisch VARCHAR(100),
wappen_url VARCHAR(500),
ist_eu_mitglied BOOLEAN,
ist_ewr_mitglied BOOLEAN,
ist_aktiv BOOLEAN NOT NULL DEFAULT true,
sortier_reihenfolge INTEGER,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- Create unique indexes for ISO codes
CREATE UNIQUE INDEX IF NOT EXISTS uk_land_iso_alpha2 ON land(iso_alpha2_code);
CREATE UNIQUE INDEX IF NOT EXISTS uk_land_iso_alpha3 ON land(iso_alpha3_code);
-- Create indexes for performance
CREATE INDEX IF NOT EXISTS idx_land_aktiv ON land(ist_aktiv);
CREATE INDEX IF NOT EXISTS idx_land_sortierung ON land(sortier_reihenfolge);
CREATE INDEX IF NOT EXISTS idx_land_eu_mitglied ON land(ist_eu_mitglied);
CREATE INDEX IF NOT EXISTS idx_land_ewr_mitglied ON land(ist_ewr_mitglied);
-- Create index for name searches
CREATE INDEX IF NOT EXISTS idx_land_name_deutsch ON land(name_deutsch);
CREATE INDEX IF NOT EXISTS idx_land_name_englisch ON land(name_englisch);
-- Add comments for documentation
COMMENT ON TABLE land IS 'Master data table for countries/nations with ISO codes and EU/EWR membership information';
COMMENT ON COLUMN land.id IS 'Unique internal identifier (UUID)';
COMMENT ON COLUMN land.iso_alpha2_code IS '2-letter ISO 3166-1 Alpha-2 code (e.g., AT, DE)';
COMMENT ON COLUMN land.iso_alpha3_code IS '3-letter ISO 3166-1 Alpha-3 code (e.g., AUT, DEU)';
COMMENT ON COLUMN land.iso_numerischer_code IS '3-digit ISO 3166-1 numeric code (e.g., 040 for Austria)';
COMMENT ON COLUMN land.name_deutsch IS 'Official German name of the country';
COMMENT ON COLUMN land.name_englisch IS 'Official English name of the country';
COMMENT ON COLUMN land.wappen_url IS 'Optional URL path to country coat of arms or flag image';
COMMENT ON COLUMN land.ist_eu_mitglied IS 'Indicates if the country is a member of the European Union';
COMMENT ON COLUMN land.ist_ewr_mitglied IS 'Indicates if the country is a member of the European Economic Area';
COMMENT ON COLUMN land.ist_aktiv IS 'Indicates if this country is currently active/selectable in the system';
COMMENT ON COLUMN land.sortier_reihenfolge IS 'Optional number for controlling sort order in selection lists';
COMMENT ON COLUMN land.created_at IS 'Timestamp when this record was created';
COMMENT ON COLUMN land.updated_at IS 'Timestamp when this record was last updated';
-- Insert some initial data for common countries
INSERT INTO land (iso_alpha2_code, iso_alpha3_code, iso_numerischer_code, name_deutsch, name_englisch, ist_eu_mitglied, ist_ewr_mitglied, sortier_reihenfolge) VALUES
('AT', 'AUT', '040', 'Österreich', 'Austria', true, true, 1),
('DE', 'DEU', '276', 'Deutschland', 'Germany', true, true, 2),
('CH', 'CHE', '756', 'Schweiz', 'Switzerland', false, false, 3),
('IT', 'ITA', '380', 'Italien', 'Italy', true, true, 4),
('FR', 'FRA', '250', 'Frankreich', 'France', true, true, 5),
('CZ', 'CZE', '203', 'Tschechien', 'Czech Republic', true, true, 6),
('SK', 'SVK', '703', 'Slowakei', 'Slovakia', true, true, 7),
('SI', 'SVN', '705', 'Slowenien', 'Slovenia', true, true, 8),
('HU', 'HUN', '348', 'Ungarn', 'Hungary', true, true, 9),
('PL', 'POL', '616', 'Polen', 'Poland', true, true, 10)
ON CONFLICT (iso_alpha2_code) DO NOTHING;
@@ -0,0 +1,132 @@
-- Migration V002: Create Bundesland (Federal State) table
-- This migration creates the table for federal states/regions with OEPS and ISO codes
CREATE TABLE IF NOT EXISTS bundesland (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
land_id UUID NOT NULL REFERENCES land(id) ON DELETE CASCADE,
oeps_code VARCHAR(10),
iso_3166_2_code VARCHAR(10),
name VARCHAR(100) NOT NULL,
kuerzel VARCHAR(10),
wappen_url VARCHAR(500),
ist_aktiv BOOLEAN NOT NULL DEFAULT true,
sortier_reihenfolge INTEGER,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- Create unique constraints
CREATE UNIQUE INDEX IF NOT EXISTS uk_bundesland_oeps_land ON bundesland(oeps_code, land_id) WHERE oeps_code IS NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS uk_bundesland_iso3166_2 ON bundesland(iso_3166_2_code) WHERE iso_3166_2_code IS NOT NULL;
-- Create indexes for performance
CREATE INDEX IF NOT EXISTS idx_bundesland_land_id ON bundesland(land_id);
CREATE INDEX IF NOT EXISTS idx_bundesland_aktiv ON bundesland(ist_aktiv);
CREATE INDEX IF NOT EXISTS idx_bundesland_sortierung ON bundesland(sortier_reihenfolge);
CREATE INDEX IF NOT EXISTS idx_bundesland_name ON bundesland(name);
CREATE INDEX IF NOT EXISTS idx_bundesland_land_aktiv ON bundesland(land_id, ist_aktiv);
-- Add comments for documentation
COMMENT ON TABLE bundesland IS 'Master data table for federal states/regions with OEPS and ISO 3166-2 codes';
COMMENT ON COLUMN bundesland.id IS 'Unique internal identifier (UUID)';
COMMENT ON COLUMN bundesland.land_id IS 'Foreign key reference to the country this federal state belongs to';
COMMENT ON COLUMN bundesland.oeps_code IS '2-digit OEPS code for Austrian federal states (e.g., 01 for Vienna, 02 for Lower Austria)';
COMMENT ON COLUMN bundesland.iso_3166_2_code IS 'Official ISO 3166-2 code for the federal state (e.g., AT-1 for Burgenland, DE-BY for Bavaria)';
COMMENT ON COLUMN bundesland.name IS 'Official name of the federal state';
COMMENT ON COLUMN bundesland.kuerzel IS 'Common abbreviation for the federal state (e.g., NÖ, W, STMK)';
COMMENT ON COLUMN bundesland.wappen_url IS 'Optional URL path to federal state coat of arms image';
COMMENT ON COLUMN bundesland.ist_aktiv IS 'Indicates if this federal state is currently active/selectable in the system';
COMMENT ON COLUMN bundesland.sortier_reihenfolge IS 'Optional number for controlling sort order in selection lists';
COMMENT ON COLUMN bundesland.created_at IS 'Timestamp when this record was created';
COMMENT ON COLUMN bundesland.updated_at IS 'Timestamp when this record was last updated';
-- Insert Austrian federal states (Bundesländer)
-- First, get the Austria country ID
DO $$
DECLARE
austria_id UUID;
BEGIN
SELECT id INTO austria_id FROM land WHERE iso_alpha2_code = 'AT';
IF austria_id IS NOT NULL THEN
INSERT INTO bundesland (land_id, oeps_code, iso_3166_2_code, name, kuerzel, sortier_reihenfolge) VALUES
(austria_id, '01', 'AT-1', 'Burgenland', 'BGLD', 1),
(austria_id, '02', 'AT-2', 'Kärnten', 'KTN', 2),
(austria_id, '03', 'AT-3', 'Niederösterreich', '', 3),
(austria_id, '04', 'AT-4', 'Oberösterreich', '', 4),
(austria_id, '05', 'AT-5', 'Salzburg', 'SBG', 5),
(austria_id, '06', 'AT-6', 'Steiermark', 'STMK', 6),
(austria_id, '07', 'AT-7', 'Tirol', 'T', 7),
(austria_id, '08', 'AT-8', 'Vorarlberg', 'VBG', 8),
(austria_id, '09', 'AT-9', 'Wien', 'W', 9)
ON CONFLICT DO NOTHING;
END IF;
END $$;
-- Insert German federal states (Bundesländer)
DO $$
DECLARE
germany_id UUID;
BEGIN
SELECT id INTO germany_id FROM land WHERE iso_alpha2_code = 'DE';
IF germany_id IS NOT NULL THEN
INSERT INTO bundesland (land_id, iso_3166_2_code, name, kuerzel, sortier_reihenfolge) VALUES
(germany_id, 'DE-BW', 'Baden-Württemberg', 'BW', 1),
(germany_id, 'DE-BY', 'Bayern', 'BY', 2),
(germany_id, 'DE-BE', 'Berlin', 'BE', 3),
(germany_id, 'DE-BB', 'Brandenburg', 'BB', 4),
(germany_id, 'DE-HB', 'Bremen', 'HB', 5),
(germany_id, 'DE-HH', 'Hamburg', 'HH', 6),
(germany_id, 'DE-HE', 'Hessen', 'HE', 7),
(germany_id, 'DE-MV', 'Mecklenburg-Vorpommern', 'MV', 8),
(germany_id, 'DE-NI', 'Niedersachsen', 'NI', 9),
(germany_id, 'DE-NW', 'Nordrhein-Westfalen', 'NW', 10),
(germany_id, 'DE-RP', 'Rheinland-Pfalz', 'RP', 11),
(germany_id, 'DE-SL', 'Saarland', 'SL', 12),
(germany_id, 'DE-SN', 'Sachsen', 'SN', 13),
(germany_id, 'DE-ST', 'Sachsen-Anhalt', 'ST', 14),
(germany_id, 'DE-SH', 'Schleswig-Holstein', 'SH', 15),
(germany_id, 'DE-TH', 'Thüringen', 'TH', 16)
ON CONFLICT DO NOTHING;
END IF;
END $$;
-- Insert Swiss cantons
DO $$
DECLARE
switzerland_id UUID;
BEGIN
SELECT id INTO switzerland_id FROM land WHERE iso_alpha2_code = 'CH';
IF switzerland_id IS NOT NULL THEN
INSERT INTO bundesland (land_id, iso_3166_2_code, name, kuerzel, sortier_reihenfolge) VALUES
(switzerland_id, 'CH-AG', 'Aargau', 'AG', 1),
(switzerland_id, 'CH-AI', 'Appenzell Innerrhoden', 'AI', 2),
(switzerland_id, 'CH-AR', 'Appenzell Ausserrhoden', 'AR', 3),
(switzerland_id, 'CH-BE', 'Bern', 'BE', 4),
(switzerland_id, 'CH-BL', 'Basel-Landschaft', 'BL', 5),
(switzerland_id, 'CH-BS', 'Basel-Stadt', 'BS', 6),
(switzerland_id, 'CH-FR', 'Freiburg', 'FR', 7),
(switzerland_id, 'CH-GE', 'Genf', 'GE', 8),
(switzerland_id, 'CH-GL', 'Glarus', 'GL', 9),
(switzerland_id, 'CH-GR', 'Graubünden', 'GR', 10),
(switzerland_id, 'CH-JU', 'Jura', 'JU', 11),
(switzerland_id, 'CH-LU', 'Luzern', 'LU', 12),
(switzerland_id, 'CH-NE', 'Neuenburg', 'NE', 13),
(switzerland_id, 'CH-NW', 'Nidwalden', 'NW', 14),
(switzerland_id, 'CH-OW', 'Obwalden', 'OW', 15),
(switzerland_id, 'CH-SG', 'St. Gallen', 'SG', 16),
(switzerland_id, 'CH-SH', 'Schaffhausen', 'SH', 17),
(switzerland_id, 'CH-SO', 'Solothurn', 'SO', 18),
(switzerland_id, 'CH-SZ', 'Schwyz', 'SZ', 19),
(switzerland_id, 'CH-TG', 'Thurgau', 'TG', 20),
(switzerland_id, 'CH-TI', 'Tessin', 'TI', 21),
(switzerland_id, 'CH-UR', 'Uri', 'UR', 22),
(switzerland_id, 'CH-VD', 'Waadt', 'VD', 23),
(switzerland_id, 'CH-VS', 'Wallis', 'VS', 24),
(switzerland_id, 'CH-ZG', 'Zug', 'ZG', 25),
(switzerland_id, 'CH-ZH', 'Zürich', 'ZH', 26)
ON CONFLICT DO NOTHING;
END IF;
END $$;
@@ -0,0 +1,105 @@
-- Migration V003: Create Altersklasse (Age Class) table
-- This migration creates the table for age class definitions with sport and gender filters
CREATE TABLE IF NOT EXISTS altersklasse (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
altersklasse_code VARCHAR(50) NOT NULL UNIQUE,
bezeichnung VARCHAR(200) NOT NULL,
min_alter INTEGER,
max_alter INTEGER,
stichtag_regel_text VARCHAR(500),
sparte_filter VARCHAR(50),
geschlecht_filter CHAR(1),
oeto_regel_referenz_id UUID,
ist_aktiv BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- Constraints
CONSTRAINT chk_altersklasse_geschlecht CHECK (geschlecht_filter IN ('M', 'W') OR geschlecht_filter IS NULL),
CONSTRAINT chk_altersklasse_alter_range CHECK (min_alter IS NULL OR max_alter IS NULL OR min_alter <= max_alter),
CONSTRAINT chk_altersklasse_min_alter CHECK (min_alter IS NULL OR min_alter >= 0),
CONSTRAINT chk_altersklasse_max_alter CHECK (max_alter IS NULL OR max_alter >= 0)
);
-- Create indexes for performance
CREATE INDEX IF NOT EXISTS idx_altersklasse_aktiv ON altersklasse(ist_aktiv);
CREATE INDEX IF NOT EXISTS idx_altersklasse_sparte ON altersklasse(sparte_filter);
CREATE INDEX IF NOT EXISTS idx_altersklasse_geschlecht ON altersklasse(geschlecht_filter);
CREATE INDEX IF NOT EXISTS idx_altersklasse_alter ON altersklasse(min_alter, max_alter);
CREATE INDEX IF NOT EXISTS idx_altersklasse_bezeichnung ON altersklasse(bezeichnung);
CREATE INDEX IF NOT EXISTS idx_altersklasse_code ON altersklasse(altersklasse_code);
-- Add comments for documentation
COMMENT ON TABLE altersklasse IS 'Master data table for age class definitions with eligibility rules for participants';
COMMENT ON COLUMN altersklasse.id IS 'Unique internal identifier (UUID)';
COMMENT ON COLUMN altersklasse.altersklasse_code IS 'Unique code for the age class (e.g., JGD_U16, JUN_U18, YR_U21, AK)';
COMMENT ON COLUMN altersklasse.bezeichnung IS 'Official or commonly understood designation of the age class';
COMMENT ON COLUMN altersklasse.min_alter IS 'Minimum age (years, inclusive) for this age class. NULL if no lower limit';
COMMENT ON COLUMN altersklasse.max_alter IS 'Maximum age (years, inclusive) for this age class. NULL if no upper limit';
COMMENT ON COLUMN altersklasse.stichtag_regel_text IS 'Description of the rule for the reference date for age calculation';
COMMENT ON COLUMN altersklasse.sparte_filter IS 'Optional specification if this age class definition only applies to a specific sport';
COMMENT ON COLUMN altersklasse.geschlecht_filter IS 'Optional filter for gender (M, W) if the age class is gender-specific. NULL means valid for all genders';
COMMENT ON COLUMN altersklasse.oeto_regel_referenz_id IS 'Optional link to a specific rule in the OETO rule reference table';
COMMENT ON COLUMN altersklasse.ist_aktiv IS 'Indicates if this age class definition can currently be used in the system';
COMMENT ON COLUMN altersklasse.created_at IS 'Timestamp when this record was created';
COMMENT ON COLUMN altersklasse.updated_at IS 'Timestamp when this record was last updated';
-- Insert common age class definitions for equestrian sports
INSERT INTO altersklasse (altersklasse_code, bezeichnung, min_alter, max_alter, stichtag_regel_text, sparte_filter, geschlecht_filter) VALUES
-- General age classes (all sports)
('PONY_U10', 'Pony Führzügel U10', NULL, 9, '31.12. des laufenden Kalenderjahres', NULL, NULL),
('PONY_U12', 'Pony U12', NULL, 11, '31.12. des laufenden Kalenderjahres', NULL, NULL),
('PONY_U14', 'Pony U14', NULL, 13, '31.12. des laufenden Kalenderjahres', NULL, NULL),
('PONY_U16', 'Pony U16', NULL, 15, '31.12. des laufenden Kalenderjahres', NULL, NULL),
('JGD_U16', 'Jugend U16', NULL, 15, '31.12. des laufenden Kalenderjahres', NULL, NULL),
('JGD_U18', 'Jugend U18', NULL, 17, '31.12. des laufenden Kalenderjahres', NULL, NULL),
('JUN_U21', 'Junioren U21', NULL, 20, '31.12. des laufenden Kalenderjahres', NULL, NULL),
('YR_U25', 'Junge Reiter U25', NULL, 24, '31.12. des laufenden Kalenderjahres', NULL, NULL),
('AK', 'Allgemeine Klasse', 18, NULL, '31.12. des laufenden Kalenderjahres', NULL, NULL),
('SEN_40', 'Senioren Ü40', 40, NULL, '31.12. des laufenden Kalenderjahres', NULL, NULL),
('SEN_50', 'Senioren Ü50', 50, NULL, '31.12. des laufenden Kalenderjahres', NULL, NULL),
('SEN_60', 'Senioren Ü60', 60, NULL, '31.12. des laufenden Kalenderjahres', NULL, NULL),
-- Dressage-specific age classes
('DR_PONY_U12', 'Dressur Pony U12', NULL, 11, '31.12. des laufenden Kalenderjahres', 'DRESSUR', NULL),
('DR_PONY_U14', 'Dressur Pony U14', NULL, 13, '31.12. des laufenden Kalenderjahres', 'DRESSUR', NULL),
('DR_PONY_U16', 'Dressur Pony U16', NULL, 15, '31.12. des laufenden Kalenderjahres', 'DRESSUR', NULL),
('DR_JGD_U18', 'Dressur Jugend U18', NULL, 17, '31.12. des laufenden Kalenderjahres', 'DRESSUR', NULL),
('DR_JUN_U21', 'Dressur Junioren U21', NULL, 20, '31.12. des laufenden Kalenderjahres', 'DRESSUR', NULL),
('DR_YR_U25', 'Dressur Junge Reiter U25', NULL, 24, '31.12. des laufenden Kalenderjahres', 'DRESSUR', NULL),
-- Jumping-specific age classes
('SP_PONY_U12', 'Springen Pony U12', NULL, 11, '31.12. des laufenden Kalenderjahres', 'SPRINGEN', NULL),
('SP_PONY_U14', 'Springen Pony U14', NULL, 13, '31.12. des laufenden Kalenderjahres', 'SPRINGEN', NULL),
('SP_PONY_U16', 'Springen Pony U16', NULL, 15, '31.12. des laufenden Kalenderjahres', 'SPRINGEN', NULL),
('SP_JGD_U18', 'Springen Jugend U18', NULL, 17, '31.12. des laufenden Kalenderjahres', 'SPRINGEN', NULL),
('SP_JUN_U21', 'Springen Junioren U21', NULL, 20, '31.12. des laufenden Kalenderjahres', 'SPRINGEN', NULL),
('SP_YR_U25', 'Springen Junge Reiter U25', NULL, 24, '31.12. des laufenden Kalenderjahres', 'SPRINGEN', NULL),
-- Eventing-specific age classes
('VK_PONY_U14', 'Vielseitigkeit Pony U14', NULL, 13, '31.12. des laufenden Kalenderjahres', 'VIELSEITIGKEIT', NULL),
('VK_PONY_U16', 'Vielseitigkeit Pony U16', NULL, 15, '31.12. des laufenden Kalenderjahres', 'VIELSEITIGKEIT', NULL),
('VK_JGD_U18', 'Vielseitigkeit Jugend U18', NULL, 17, '31.12. des laufenden Kalenderjahres', 'VIELSEITIGKEIT', NULL),
('VK_JUN_U21', 'Vielseitigkeit Junioren U21', NULL, 20, '31.12. des laufenden Kalenderjahres', 'VIELSEITIGKEIT', NULL),
('VK_YR_U25', 'Vielseitigkeit Junge Reiter U25', NULL, 24, '31.12. des laufenden Kalenderjahres', 'VIELSEITIGKEIT', NULL),
-- Driving-specific age classes
('FA_PONY_U16', 'Fahren Pony U16', NULL, 15, '31.12. des laufenden Kalenderjahres', 'FAHREN', NULL),
('FA_JGD_U18', 'Fahren Jugend U18', NULL, 17, '31.12. des laufenden Kalenderjahres', 'FAHREN', NULL),
('FA_JUN_U21', 'Fahren Junioren U21', NULL, 20, '31.12. des laufenden Kalenderjahres', 'FAHREN', NULL),
('FA_YR_U25', 'Fahren Junge Reiter U25', NULL, 24, '31.12. des laufenden Kalenderjahres', 'FAHREN', NULL),
-- Vaulting-specific age classes
('VT_U10', 'Voltigieren U10', NULL, 9, '31.12. des laufenden Kalenderjahres', 'VOLTIGIEREN', NULL),
('VT_U12', 'Voltigieren U12', NULL, 11, '31.12. des laufenden Kalenderjahres', 'VOLTIGIEREN', NULL),
('VT_U14', 'Voltigieren U14', NULL, 13, '31.12. des laufenden Kalenderjahres', 'VOLTIGIEREN', NULL),
('VT_U16', 'Voltigieren U16', NULL, 15, '31.12. des laufenden Kalenderjahres', 'VOLTIGIEREN', NULL),
('VT_U18', 'Voltigieren U18', NULL, 17, '31.12. des laufenden Kalenderjahres', 'VOLTIGIEREN', NULL),
('VT_JUN_U21', 'Voltigieren Junioren U21', NULL, 20, '31.12. des laufenden Kalenderjahres', 'VOLTIGIEREN', NULL),
-- Gender-specific examples (if needed)
('DR_DAMEN', 'Dressur Damen', 18, NULL, '31.12. des laufenden Kalenderjahres', 'DRESSUR', 'W'),
('DR_HERREN', 'Dressur Herren', 18, NULL, '31.12. des laufenden Kalenderjahres', 'DRESSUR', 'M')
ON CONFLICT (altersklasse_code) DO NOTHING;
@@ -0,0 +1,137 @@
-- Migration V004: Create Platz (Venue/Arena) table
-- This migration creates the table for tournament venues and arenas
CREATE TABLE IF NOT EXISTS platz (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
turnier_id UUID NOT NULL, -- Foreign key to tournament (not enforced as tournament might be in different module)
name VARCHAR(200) NOT NULL,
dimension VARCHAR(50),
boden VARCHAR(100),
typ VARCHAR(50) NOT NULL,
ist_aktiv BOOLEAN NOT NULL DEFAULT true,
sortier_reihenfolge INTEGER,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- Constraints
CONSTRAINT chk_platz_sortier_reihenfolge CHECK (sortier_reihenfolge IS NULL OR sortier_reihenfolge >= 0)
);
-- Create unique constraint for name per tournament
CREATE UNIQUE INDEX IF NOT EXISTS uk_platz_name_turnier ON platz(name, turnier_id);
-- Create indexes for performance
CREATE INDEX IF NOT EXISTS idx_platz_turnier ON platz(turnier_id);
CREATE INDEX IF NOT EXISTS idx_platz_aktiv ON platz(ist_aktiv);
CREATE INDEX IF NOT EXISTS idx_platz_typ ON platz(typ);
CREATE INDEX IF NOT EXISTS idx_platz_turnier_aktiv ON platz(turnier_id, ist_aktiv);
CREATE INDEX IF NOT EXISTS idx_platz_name ON platz(name);
CREATE INDEX IF NOT EXISTS idx_platz_dimension ON platz(dimension);
CREATE INDEX IF NOT EXISTS idx_platz_boden ON platz(boden);
CREATE INDEX IF NOT EXISTS idx_platz_sortierung ON platz(sortier_reihenfolge);
-- Add comments for documentation
COMMENT ON TABLE platz IS 'Master data table for tournament venues and arenas with type and dimension specifications';
COMMENT ON COLUMN platz.id IS 'Unique internal identifier (UUID)';
COMMENT ON COLUMN platz.turnier_id IS 'Foreign key reference to the tournament this venue belongs to';
COMMENT ON COLUMN platz.name IS 'Name or designation of the venue (e.g., "Hauptplatz", "Dressurplatz A")';
COMMENT ON COLUMN platz.dimension IS 'Dimensions of the venue (e.g., "20x60m", "20x40m")';
COMMENT ON COLUMN platz.boden IS 'Type of ground surface (e.g., "Sand", "Gras", "Kunststoff")';
COMMENT ON COLUMN platz.typ IS 'Type of venue (see PlatzTypE enum)';
COMMENT ON COLUMN platz.ist_aktiv IS 'Indicates if this venue can currently be used';
COMMENT ON COLUMN platz.sortier_reihenfolge IS 'Optional number for controlling sort order';
COMMENT ON COLUMN platz.created_at IS 'Timestamp when this record was created';
COMMENT ON COLUMN platz.updated_at IS 'Timestamp when this record was last updated';
-- Insert some example venue types and common configurations
-- Note: These are examples and would typically be created per tournament
-- Using a dummy tournament ID for demonstration purposes
-- Create a function to generate example venues for a tournament
CREATE OR REPLACE FUNCTION create_example_venues(tournament_id UUID) RETURNS VOID AS $$
BEGIN
INSERT INTO platz (turnier_id, name, dimension, boden, typ, sortier_reihenfolge) VALUES
-- Dressage arenas
(tournament_id, 'Dressurplatz A', '20x60m', 'Sand', 'DRESSURPLATZ', 10),
(tournament_id, 'Dressurplatz B', '20x40m', 'Sand', 'DRESSURPLATZ', 20),
(tournament_id, 'Abreiteplatz Dressur', '20x40m', 'Sand', 'ABREITEPLATZ', 30),
-- Jumping arenas
(tournament_id, 'Springplatz Hauptring', '40x80m', 'Sand', 'SPRINGPLATZ', 40),
(tournament_id, 'Springplatz Ring 2', '35x70m', 'Sand', 'SPRINGPLATZ', 50),
(tournament_id, 'Abreiteplatz Springen', '30x60m', 'Sand', 'ABREITEPLATZ', 60),
-- Cross-country and eventing
(tournament_id, 'Geländestrecke', 'variabel', 'Gras', 'GELAENDESTRECKE', 70),
(tournament_id, 'Vielseitigkeitsplatz', '25x65m', 'Sand', 'VIELSEITIGKEITSPLATZ', 80),
-- Driving arenas
(tournament_id, 'Fahrplatz', '40x100m', 'Sand', 'FAHRPLATZ', 90),
(tournament_id, 'Hindernisfahren', '40x80m', 'Sand', 'FAHRPLATZ', 100),
-- Vaulting
(tournament_id, 'Voltigierplatz', '20m Durchmesser', 'Sand', 'VOLTIGIERPLATZ', 110),
-- Training and warm-up areas
(tournament_id, 'Führanlage', '20m Durchmesser', 'Sand', 'FUEHRANLAGE', 120),
(tournament_id, 'Longierplatz', '20m Durchmesser', 'Sand', 'LONGIERPLATZ', 130),
(tournament_id, 'Trainingsplatz 1', '20x40m', 'Sand', 'TRAININGSPLATZ', 140),
(tournament_id, 'Trainingsplatz 2', '20x40m', 'Gras', 'TRAININGSPLATZ', 150),
-- Indoor arenas
(tournament_id, 'Reithalle A', '20x60m', 'Sand', 'REITHALLE', 160),
(tournament_id, 'Reithalle B', '20x40m', 'Sand', 'REITHALLE', 170),
-- Outdoor areas
(tournament_id, 'Außenplatz 1', '25x50m', 'Gras', 'AUSSENPLATZ', 180),
(tournament_id, 'Außenplatz 2', '20x40m', 'Sand', 'AUSSENPLATZ', 190),
-- Special purpose areas
(tournament_id, 'Siegerehrungsplatz', '15x25m', 'Gras', 'SONDERPLATZ', 200),
(tournament_id, 'Vorführplatz', '20x30m', 'Sand', 'SONDERPLATZ', 210)
ON CONFLICT (name, turnier_id) DO NOTHING;
END;
$$ LANGUAGE plpgsql;
-- Add some venue type validation comments
COMMENT ON FUNCTION create_example_venues(UUID) IS 'Helper function to create example venues for a tournament. Call with tournament UUID.';
-- Create a view for venue statistics
CREATE OR REPLACE VIEW platz_statistics AS
SELECT
typ,
COUNT(*) as total_count,
COUNT(CASE WHEN ist_aktiv THEN 1 END) as active_count,
COUNT(CASE WHEN NOT ist_aktiv THEN 1 END) as inactive_count,
COUNT(DISTINCT turnier_id) as tournament_count,
COUNT(DISTINCT dimension) as dimension_variants,
COUNT(DISTINCT boden) as ground_type_variants
FROM platz
GROUP BY typ
ORDER BY typ;
COMMENT ON VIEW platz_statistics IS 'Statistical overview of venues by type, showing counts and variants';
-- Create a view for tournament venue overview
CREATE OR REPLACE VIEW tournament_venue_overview AS
SELECT
turnier_id,
COUNT(*) as total_venues,
COUNT(CASE WHEN ist_aktiv THEN 1 END) as active_venues,
COUNT(DISTINCT typ) as venue_types,
COUNT(DISTINCT dimension) as dimension_variants,
COUNT(DISTINCT boden) as ground_types,
STRING_AGG(DISTINCT typ, ', ' ORDER BY typ) as available_types
FROM platz
GROUP BY turnier_id
ORDER BY turnier_id;
COMMENT ON VIEW tournament_venue_overview IS 'Overview of venues per tournament with summary statistics';
-- Example of how to use the function (commented out as it requires actual tournament IDs)
-- SELECT create_example_venues('550e8400-e29b-41d4-a716-446655440000'::UUID);
-- Add some helpful indexes for the views
CREATE INDEX IF NOT EXISTS idx_platz_typ_aktiv ON platz(typ, ist_aktiv);
CREATE INDEX IF NOT EXISTS idx_platz_turnier_typ ON platz(turnier_id, typ);
@@ -0,0 +1,33 @@
-- File: V1__Create_Initial_Tables.sql
-- Tabelle zur Verwaltung der Vereine (Mandanten)
CREATE TABLE IF NOT EXISTS dom_verein (
verein_id UUID PRIMARY KEY,
oeps_vereins_nr VARCHAR(4) UNIQUE,
name VARCHAR(100) NOT NULL,
kuerzel VARCHAR(20),
bundesland_code VARCHAR(2),
daten_quelle VARCHAR(50) NOT NULL,
ist_aktiv BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- Tabelle zur Verwaltung der Personen (Sportler, Funktionäre)
CREATE TABLE IF NOT EXISTS dom_person (
person_id UUID PRIMARY KEY,
oeps_satz_nr VARCHAR(6) UNIQUE,
nachname VARCHAR(100) NOT NULL,
vorname VARCHAR(100) NOT NULL,
geburtsdatum DATE,
geschlecht VARCHAR(10),
nationalitaet_code VARCHAR(3),
stamm_verein_id UUID REFERENCES dom_verein(verein_id),
ist_gesperrt BOOLEAN NOT NULL DEFAULT false,
daten_quelle VARCHAR(50) NOT NULL,
ist_aktiv BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- Weitere Tabellen können hier hinzugefügt werden...
@@ -0,0 +1,10 @@
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE" />
</root>
</configuration>