diff --git a/GerbilManager.Tests/ApiFactory.cs b/GerbilManager.Tests/ApiFactory.cs
new file mode 100644
index 0000000..98c1167
--- /dev/null
+++ b/GerbilManager.Tests/ApiFactory.cs
@@ -0,0 +1,61 @@
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.Data.Sqlite;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+namespace GerbilManager.Tests;
+
+///
+/// FEAT-13: In-Memory-Test-Host für Endpoint-Round-Trips. Ersetzt die
+/// Aspire/Npgsql-Registrierung durch SQLite in-memory (eine offene Verbindung
+/// hält die DB am Leben), Umgebung "Testing" überspringt Database.Migrate()
+/// (Npgsql-Migrationen laufen nicht auf SQLite) — Schema via EnsureCreated,
+/// inklusive der HasData-Seeds (73 Farbschläge + Zuchtprofil-Singleton).
+/// Vertrags-Dateien landen in einem Temp-Ordner, der mit dem Host stirbt.
+///
+public sealed class ApiFactory : WebApplicationFactory
+{
+ private readonly SqliteConnection _connection = new("DataSource=:memory:");
+
+ public string ContractRoot { get; } =
+ Path.Combine(Path.GetTempPath(), $"gerbil-contract-tests-{Guid.NewGuid():N}");
+
+ protected override void ConfigureWebHost(IWebHostBuilder builder)
+ {
+ builder.UseEnvironment("Testing");
+ // Dummy, damit Aspires AddNpgsqlDbContext beim Host-Aufbau zufrieden ist;
+ // die eigentliche Registrierung wird unten durch SQLite ersetzt.
+ builder.UseSetting("ConnectionStrings:gerbilmanager",
+ "Host=localhost;Database=test;Username=test;Password=test");
+ builder.UseSetting("Contracts:RootPath", ContractRoot);
+
+ builder.ConfigureServices(services =>
+ {
+ // EF 9+: AddDbContext registriert die Provider-Konfiguration als
+ // IDbContextOptionsConfiguration — ohne deren Entfernung blieben
+ // Npgsql UND SQLite registriert (ein Provider pro ServiceProvider).
+ services.RemoveAll>();
+ services.RemoveAll>();
+ services.RemoveAll();
+
+ _connection.Open();
+ services.AddDbContext(o => o.UseSqlite(_connection));
+
+ using var provider = services.BuildServiceProvider();
+ using var scope = provider.CreateScope();
+ scope.ServiceProvider.GetRequiredService().Database.EnsureCreated();
+ });
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ if (disposing)
+ {
+ _connection.Dispose();
+ if (Directory.Exists(ContractRoot)) Directory.Delete(ContractRoot, recursive: true);
+ }
+ }
+}
diff --git a/GerbilManager.Tests/ContractEndpointTests.cs b/GerbilManager.Tests/ContractEndpointTests.cs
new file mode 100644
index 0000000..b6ed59a
--- /dev/null
+++ b/GerbilManager.Tests/ContractEndpointTests.cs
@@ -0,0 +1,198 @@
+using System.IO.Compression;
+using System.Net;
+using System.Net.Http.Json;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Text.RegularExpressions;
+
+namespace GerbilManager.Tests;
+
+///
+/// FEAT-13 phase B: Endpoint-Round-Trips für /contracts + /settings/breeder-profile
+/// gegen den In-Memory-Host (ApiFactory). Deckt den Abgabe-Abschluss ab:
+/// Vertrag erzeugen -> Tiere stehen auf Abgegeben -> .docx ist herunterladbar.
+///
+public class ContractEndpointTests : IClassFixture
+{
+ private static readonly JsonSerializerOptions Json = CreateJsonOptions();
+
+ private static JsonSerializerOptions CreateJsonOptions()
+ {
+ var o = new JsonSerializerOptions(JsonSerializerDefaults.Web);
+ o.Converters.Add(new JsonStringEnumConverter());
+ return o;
+ }
+
+ private readonly ApiFactory _factory;
+
+ public ContractEndpointTests(ApiFactory factory) => _factory = factory;
+
+ private sealed record IdDto(Guid Id);
+
+ private sealed record GerbilView(Guid Id, string Status, Guid? ReceiverContactId, DateOnly? GoHomeDate);
+
+ private sealed record ContractView(
+ Guid Id, Guid ContactId, decimal Price, DateOnly HandoverDate, DateOnly ContractDate,
+ string FileName, List GerbilIds, string Url);
+
+ private sealed record Paged(List Items, int TotalCount, int Page, int PageSize);
+
+ private async Task CreateContactAsync(HttpClient client, string name = "Herr Max Beispiel")
+ {
+ var response = await client.PostAsJsonAsync("/contacts", new
+ {
+ name,
+ email = "max@example.com",
+ phone = "0987 654321",
+ address = "Beispielallee 7, 54321 Beispielstadt",
+ }, Json);
+ Assert.Equal(HttpStatusCode.Created, response.StatusCode);
+ return (await response.Content.ReadFromJsonAsync(Json))!.Id;
+ }
+
+ private async Task CreateGerbilAsync(HttpClient client, string name)
+ {
+ var response = await client.PostAsJsonAsync("/gerbils", new
+ {
+ name,
+ gender = "female",
+ dateOfBirth = "2025-03-09",
+ }, Json);
+ Assert.Equal(HttpStatusCode.Created, response.StatusCode);
+ return (await response.Content.ReadFromJsonAsync(Json))!.Id;
+ }
+
+ [Fact]
+ public async Task ZuchtprofilRoundTrip_PutDannGet()
+ {
+ var client = _factory.CreateClient();
+
+ var put = await client.PutAsJsonAsync("/settings/breeder-profile", new
+ {
+ zuchtName = "Zucht Testhausen",
+ name = "Frau Erika Muster",
+ address = "Musterweg 1, 12345 Testhausen",
+ phone = "0123 456789",
+ email = "zucht@example.org",
+ homepage = "https://zucht.example.org/",
+ city = "Testhausen",
+ }, Json);
+ Assert.Equal(HttpStatusCode.NoContent, put.StatusCode);
+
+ var profile = await client.GetFromJsonAsync>("/settings/breeder-profile", Json);
+ Assert.Equal("Zucht Testhausen", profile!["zuchtName"]);
+ Assert.Equal("Testhausen", profile["city"]);
+ }
+
+ [Fact]
+ public async Task VertragErzeugen_SchließtDieAbgabeAb()
+ {
+ var client = _factory.CreateClient();
+
+ // Zuchtprofil füllen, damit der Verkäufer-Block im Dokument landet.
+ await client.PutAsJsonAsync("/settings/breeder-profile", new
+ {
+ zuchtName = "Zucht Testhausen",
+ name = "Frau Erika Muster",
+ address = "Musterweg 1, 12345 Testhausen",
+ phone = "0123 456789",
+ email = "zucht@example.org",
+ homepage = "https://zucht.example.org/",
+ city = "Testhausen",
+ }, Json);
+
+ var contactId = await CreateContactAsync(client);
+ var krümel = await CreateGerbilAsync(client, "Krümel");
+ var luna = await CreateGerbilAsync(client, "Luna");
+
+ // POST /contracts — der Abschluss.
+ var post = await client.PostAsJsonAsync("/contracts", new
+ {
+ contactId,
+ gerbilIds = new[] { krümel, luna },
+ price = 72.0m,
+ handoverDate = "2026-06-05",
+ }, Json);
+ Assert.Equal(HttpStatusCode.Created, post.StatusCode);
+ var contract = (await post.Content.ReadFromJsonAsync(Json))!;
+
+ Assert.Equal(contactId, contract.ContactId);
+ Assert.Equal(2, contract.GerbilIds.Count);
+ Assert.Equal(new DateOnly(2026, 6, 5), contract.ContractDate); // Standard = Übergabedatum
+ Assert.Equal($"/contracts/{contract.Id}/file", contract.Url);
+
+ // Tiere stehen jetzt auf Abgegeben — mit Abnehmer und Abgabedatum.
+ foreach (var id in new[] { krümel, luna })
+ {
+ var gerbil = await client.GetFromJsonAsync($"/gerbils/{id}", Json);
+ Assert.Equal("GivenAway", gerbil!.Status);
+ Assert.Equal(contactId, gerbil.ReceiverContactId);
+ Assert.Equal(new DateOnly(2026, 6, 5), gerbil.GoHomeDate);
+ }
+
+ // Download: echtes .docx mit beiden Tieren, Käufer, de-DE-Preis.
+ var file = await client.GetAsync(contract.Url);
+ Assert.Equal(HttpStatusCode.OK, file.StatusCode);
+ Assert.Equal(
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ file.Content.Headers.ContentType!.MediaType);
+ Assert.Contains("Abgabevertrag_2026-06-05_Herr-Max-Beispiel.docx",
+ file.Content.Headers.ContentDisposition!.FileNameStar ?? file.Content.Headers.ContentDisposition.FileName);
+
+ var text = DocumentText(await file.Content.ReadAsByteArrayAsync());
+ Assert.Contains("Krümel", text);
+ Assert.Contains("Luna", text);
+ Assert.Contains("Herr Max Beispiel", text);
+ Assert.Contains("Kaufpreis von 72,00 €", text);
+ Assert.Contains("Testhausen, den 05.06.2026", text);
+ Assert.DoesNotContain("{{", text);
+
+ // Liste: nach Abnehmer filterbar (Gridify).
+ var list = await client.GetFromJsonAsync>(
+ $"/contracts?filter=contactId=={contactId}", Json);
+ Assert.Equal(1, list!.TotalCount);
+ Assert.Equal(contract.Id, list.Items[0].Id);
+
+ // DELETE räumt Zeile und Datei ab; Tier-Status bleibt unangetastet.
+ var delete = await client.DeleteAsync($"/contracts/{contract.Id}");
+ Assert.Equal(HttpStatusCode.NoContent, delete.StatusCode);
+ Assert.Equal(HttpStatusCode.NotFound, (await client.GetAsync(contract.Url)).StatusCode);
+ var still = await client.GetFromJsonAsync($"/gerbils/{krümel}", Json);
+ Assert.Equal("GivenAway", still!.Status);
+ }
+
+ [Fact]
+ public async Task VertragOhneTiereOderMitUnbekanntemTier_Validierungsfehler()
+ {
+ var client = _factory.CreateClient();
+ var contactId = await CreateContactAsync(client, "Frau Lisa Test");
+
+ var empty = await client.PostAsJsonAsync("/contracts", new
+ {
+ contactId,
+ gerbilIds = Array.Empty(),
+ price = 10m,
+ handoverDate = "2026-06-05",
+ }, Json);
+ Assert.Equal(HttpStatusCode.BadRequest, empty.StatusCode);
+
+ var unknown = await client.PostAsJsonAsync("/contracts", new
+ {
+ contactId,
+ gerbilIds = new[] { Guid.NewGuid() },
+ price = 10m,
+ handoverDate = "2026-06-05",
+ }, Json);
+ Assert.Equal(HttpStatusCode.BadRequest, unknown.StatusCode);
+ }
+
+ /// Sichtbarer Text aus word/document.xml (Tags entfernt).
+ private static string DocumentText(byte[] docx)
+ {
+ using var zip = new ZipArchive(new MemoryStream(docx), ZipArchiveMode.Read);
+ var entry = Assert.Single(zip.Entries, e => e.FullName == "word/document.xml");
+ using var reader = new StreamReader(entry.Open(), Encoding.UTF8);
+ return Regex.Replace(reader.ReadToEnd(), "<[^>]+>", "");
+ }
+}
diff --git a/GerbilManager.Tests/GerbilManager.Tests.csproj b/GerbilManager.Tests/GerbilManager.Tests.csproj
index d59000b..4feafc8 100644
--- a/GerbilManager.Tests/GerbilManager.Tests.csproj
+++ b/GerbilManager.Tests/GerbilManager.Tests.csproj
@@ -9,6 +9,8 @@
+
+
diff --git a/GerbilManagerWebAPI/ApplicationContext.cs b/GerbilManagerWebAPI/ApplicationContext.cs
index aae01c7..fad81b3 100644
--- a/GerbilManagerWebAPI/ApplicationContext.cs
+++ b/GerbilManagerWebAPI/ApplicationContext.cs
@@ -15,6 +15,8 @@ public class ApplicationContext : DbContext
public DbSet GerbilPhotos => Set();
public DbSet HealthRecords => Set();
public DbSet WeightRecords => Set();
+ public DbSet SaleContracts => Set();
+ public DbSet BreederSettings => Set();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
@@ -59,6 +61,31 @@ public class ApplicationContext : DbContext
e.HasOne().WithMany()
.HasForeignKey(p => p.GerbilId).OnDelete(DeleteBehavior.Cascade));
+ // FEAT-13: Abgabeverträge + Zuchtprofil.
+ modelBuilder.Entity(e =>
+ {
+ e.Property(c => c.Price).HasPrecision(10, 2);
+ // Restrict: ein Kontakt mit Verträgen ist ein Dokumentenbestand,
+ // kein versehentlich löschbarer Datensatz.
+ e.HasOne(c => c.Contact).WithMany()
+ .HasForeignKey(c => c.ContactId).OnDelete(DeleteBehavior.Restrict);
+ });
+
+ modelBuilder.Entity(e =>
+ {
+ e.HasKey(a => new { a.SaleContractId, a.GerbilId });
+ e.HasOne().WithMany(c => c.Animals)
+ .HasForeignKey(a => a.SaleContractId).OnDelete(DeleteBehavior.Cascade);
+ // Cascade: wird ein Tier gelöscht, verschwindet nur die Verknüpfung —
+ // der Vertrag (und seine .docx als Beleg) bleibt bestehen.
+ e.HasOne(a => a.Gerbil).WithMany()
+ .HasForeignKey(a => a.GerbilId).OnDelete(DeleteBehavior.Cascade);
+ });
+
+ // Zuchtprofil: genau eine (leere) Zeile mit fixer Id.
+ modelBuilder.Entity()
+ .HasData(new BreederSettings { Id = GerbilManagerWebAPI.Models.BreederSettings.SingletonId });
+
SeedColorVarieties(modelBuilder);
}
diff --git a/GerbilManagerWebAPI/Dtos/ContractDtos.cs b/GerbilManagerWebAPI/Dtos/ContractDtos.cs
new file mode 100644
index 0000000..0d24787
--- /dev/null
+++ b/GerbilManagerWebAPI/Dtos/ContractDtos.cs
@@ -0,0 +1,33 @@
+namespace GerbilManagerWebAPI.Dtos
+{
+ // FEAT-13: Abgabeverträge + Zuchtprofil (eigene Datei, hält ApiDtos.cs konfliktfrei).
+
+ public record SaleContractDto(
+ Guid Id,
+ Guid ContactId,
+ decimal Price,
+ DateOnly HandoverDate,
+ DateOnly ContractDate,
+ string FileName,
+ DateTimeOffset CreatedAt,
+ IReadOnlyList GerbilIds,
+ // Download-URL der .docx ("/contracts/{id}/file").
+ string Url);
+
+ public record SaleContractInput(
+ Guid ContactId,
+ List GerbilIds,
+ decimal Price,
+ DateOnly HandoverDate,
+ DateOnly? ContractDate);
+
+ /// Zuchtprofil — Antwort UND Request-Body von /settings/breeder-profile.
+ public record BreederProfileDto(
+ string ZuchtName,
+ string Name,
+ string Address,
+ string Phone,
+ string Email,
+ string Homepage,
+ string City);
+}
diff --git a/GerbilManagerWebAPI/Endpoints/ContractEndpoints.cs b/GerbilManagerWebAPI/Endpoints/ContractEndpoints.cs
new file mode 100644
index 0000000..8f7be57
--- /dev/null
+++ b/GerbilManagerWebAPI/Endpoints/ContractEndpoints.cs
@@ -0,0 +1,201 @@
+using GerbilManagerWebAPI.Common;
+using GerbilManagerWebAPI.Contracts;
+using GerbilManagerWebAPI.Dtos;
+using GerbilManagerWebAPI.Models;
+using Microsoft.AspNetCore.Http.HttpResults;
+using Microsoft.EntityFrameworkCore;
+
+namespace GerbilManagerWebAPI.Endpoints
+{
+ ///
+ /// FEAT-13: Abgabeverträge.
+ /// POST /contracts -> erzeugt .docx (phase-A-Generator), speichert sie im
+ /// Vertrags-Dateiroot, legt die Vertragszeile an und stellt
+ /// die Tiere in DERSELBEN Transaktion auf Abgegeben
+ /// (ReceiverContactId, GoHomeDate, Status).
+ /// GET /contracts -> Gridify-paged (z. B. filter=contactId==…)
+ /// GET /contracts/{id} -> Metadaten
+ /// GET /contracts/{id}/file -> .docx-Download (deutscher Dateiname)
+ /// DELETE /contracts/{id} -> entfernt Zeile + Datei (Tier-Status bleibt unberührt)
+ ///
+ public static class ContractEndpoints
+ {
+ private const string DocxContentType =
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
+
+ public static IEndpointRouteBuilder MapContractEndpoints(this IEndpointRouteBuilder app)
+ {
+ var group = app.MapGroup("/contracts").WithTags("Contracts");
+
+ // GET /contracts (Gridify; GerbilIds via Include mitgeladen)
+ group.MapGet("/", async ([AsParameters] GridifyParams query, ApplicationContext db) =>
+ TypedResults.Ok(await db.SaleContracts.AsNoTracking()
+ .Include(c => c.Animals)
+ .ToPagedResultAsync(query, ToDto)));
+
+ // GET /contracts/{id}
+ group.MapGet("/{id:guid}", async Task, NotFound>> (Guid id, ApplicationContext db) =>
+ {
+ var c = await db.SaleContracts.AsNoTracking()
+ .Include(x => x.Animals)
+ .FirstOrDefaultAsync(x => x.Id == id);
+ return c is null ? TypedResults.NotFound() : TypedResults.Ok(ToDto(c));
+ });
+
+ // POST /contracts — der Abgabe-Abschluss.
+ group.MapPost("/", async Task, ValidationProblem>> (
+ SaleContractInput input, ApplicationContext db, IConfiguration config, IWebHostEnvironment env) =>
+ {
+ var errors = new Dictionary();
+
+ var gerbilIds = (input.GerbilIds ?? []).Distinct().ToList();
+ if (gerbilIds.Count == 0)
+ errors["gerbilIds"] = ["Mindestens ein Tier auswählen."];
+ if (input.Price < 0)
+ errors["price"] = ["Der Kaufpreis darf nicht negativ sein."];
+
+ var contact = await db.Contacts.AsNoTracking()
+ .FirstOrDefaultAsync(c => c.Id == input.ContactId);
+ if (contact is null)
+ errors["contactId"] = ["Der Abnehmer wurde nicht gefunden."];
+
+ var gerbils = await db.Gerbils
+ .Include(g => g.ColorVariety)
+ .Where(g => gerbilIds.Contains(g.Id))
+ .ToListAsync();
+ if (gerbils.Count != gerbilIds.Count)
+ errors["gerbilIds"] = ["Mindestens ein ausgewähltes Tier wurde nicht gefunden."];
+
+ if (errors.Count > 0) return TypedResults.ValidationProblem(errors);
+
+ var settings = await db.BreederSettings.AsNoTracking()
+ .FirstOrDefaultAsync(s => s.Id == BreederSettings.SingletonId)
+ ?? new BreederSettings();
+
+ var contractDate = input.ContractDate ?? input.HandoverDate;
+ var data = new ContractData(
+ Seller: ToSeller(settings),
+ Buyer: new ContractBuyer(contact!.Name, contact.Address ?? "", contact.Phone, contact.Email),
+ Animals: gerbils
+ .Select(g => new ContractAnimal(g.Name, GeschlechtText(g.Gender), g.DateOfBirth, g.ColorVariety?.Name))
+ .ToList(),
+ Price: input.Price,
+ HandoverDate: input.HandoverDate,
+ ContractDate: contractDate);
+
+ var bytes = ContractGenerator.Generate(data);
+
+ var fileName = $"{Guid.NewGuid():N}.docx";
+ var root = ContractRoot(config, env);
+ Directory.CreateDirectory(root);
+ await File.WriteAllBytesAsync(Path.Combine(root, fileName), bytes);
+
+ var entity = new SaleContract
+ {
+ Id = Guid.NewGuid(),
+ ContactId = contact.Id,
+ Price = input.Price,
+ HandoverDate = input.HandoverDate,
+ ContractDate = contractDate,
+ FileName = fileName,
+ CreatedAt = DateTimeOffset.UtcNow,
+ Animals = gerbilIds.Select(id => new SaleContractAnimal { GerbilId = id }).ToList(),
+ };
+ db.SaleContracts.Add(entity);
+
+ // Abgabe-Abschluss-Semantik: in derselben SaveChanges-Transaktion.
+ foreach (var g in gerbils)
+ {
+ g.ReceiverContactId = contact.Id;
+ g.GoHomeDate = input.HandoverDate;
+ g.Status = GerbilStatus.GivenAway;
+ }
+
+ try
+ {
+ await db.SaveChangesAsync();
+ }
+ catch
+ {
+ // DB fehlgeschlagen -> verwaiste Datei wieder aufräumen.
+ var orphan = Path.Combine(root, fileName);
+ if (File.Exists(orphan)) File.Delete(orphan);
+ throw;
+ }
+
+ return TypedResults.Created($"/contracts/{entity.Id}", ToDto(entity));
+ });
+
+ // GET /contracts/{id}/file — Download mit sprechendem deutschen Dateinamen.
+ group.MapGet("/{id:guid}/file", async Task> (
+ Guid id, ApplicationContext db, IConfiguration config, IWebHostEnvironment env) =>
+ {
+ var c = await db.SaleContracts.AsNoTracking()
+ .Include(x => x.Contact)
+ .FirstOrDefaultAsync(x => x.Id == id);
+ if (c is null) return TypedResults.NotFound();
+
+ var path = Path.Combine(ContractRoot(config, env), c.FileName);
+ if (!File.Exists(path)) return TypedResults.NotFound();
+
+ var download = $"Abgabevertrag_{c.ContractDate:yyyy-MM-dd}_{Sanitize(c.Contact?.Name)}.docx";
+ return TypedResults.PhysicalFile(path, DocxContentType, download);
+ });
+
+ // DELETE /contracts/{id} — Zeile + Datei; Tier-Status wird NICHT zurückgedreht
+ // (das wäre Magie — Status korrigiert man am Tier selbst).
+ group.MapDelete("/{id:guid}", async Task> (
+ Guid id, ApplicationContext db, IConfiguration config, IWebHostEnvironment env) =>
+ {
+ var c = await db.SaleContracts.FirstOrDefaultAsync(x => x.Id == id);
+ if (c is null) return TypedResults.NotFound();
+
+ var path = Path.Combine(ContractRoot(config, env), c.FileName);
+ if (File.Exists(path)) File.Delete(path);
+
+ db.SaleContracts.Remove(c); // Joins kaskadieren
+ await db.SaveChangesAsync();
+ return TypedResults.NoContent();
+ });
+
+ return app;
+ }
+
+ private static string ContractRoot(IConfiguration config, IWebHostEnvironment env) =>
+ config["Contracts:RootPath"] ?? Path.Combine(env.ContentRootPath, "contract-storage");
+
+ private static BreederProfile ToSeller(BreederSettings s) => new()
+ {
+ ZuchtName = s.ZuchtName,
+ Name = s.Name,
+ Address = s.Address,
+ Phone = s.Phone,
+ Email = s.Email,
+ Homepage = s.Homepage,
+ City = s.City,
+ };
+
+ /// Deutscher Anzeigetext fürs Geschlecht (Vertragsdokument).
+ internal static string GeschlechtText(Gender gender) => gender switch
+ {
+ Gender.male => "Männlich",
+ Gender.female => "Weiblich",
+ _ => "Unbekannt",
+ };
+
+ /// Kontaktname → Dateinamens-tauglich (Umlaute bleiben, Trenner -> '-').
+ private static string Sanitize(string? name)
+ {
+ if (string.IsNullOrWhiteSpace(name)) return "Abnehmer";
+ var cleaned = new string(name.Trim()
+ .Select(ch => char.IsLetterOrDigit(ch) ? ch : '-')
+ .ToArray());
+ return cleaned.Trim('-');
+ }
+
+ internal static SaleContractDto ToDto(SaleContract c) => new(
+ c.Id, c.ContactId, c.Price, c.HandoverDate, c.ContractDate, c.FileName, c.CreatedAt,
+ c.Animals.Select(a => a.GerbilId).ToList(),
+ $"/contracts/{c.Id}/file");
+ }
+}
diff --git a/GerbilManagerWebAPI/Endpoints/SettingsEndpoints.cs b/GerbilManagerWebAPI/Endpoints/SettingsEndpoints.cs
new file mode 100644
index 0000000..cf032a4
--- /dev/null
+++ b/GerbilManagerWebAPI/Endpoints/SettingsEndpoints.cs
@@ -0,0 +1,60 @@
+using GerbilManagerWebAPI.Dtos;
+using GerbilManagerWebAPI.Models;
+using Microsoft.AspNetCore.Http.HttpResults;
+using Microsoft.EntityFrameworkCore;
+
+namespace GerbilManagerWebAPI.Endpoints
+{
+ ///
+ /// FEAT-13: Zuchtprofil (Verkäufer-Block der Abgabeverträge) — Einzelzeile,
+ /// per Migration leer geseedet, von der Züchterin unter /einstellungen gepflegt.
+ /// GET /settings/breeder-profile -> BreederProfileDto
+ /// PUT /settings/breeder-profile -> 204
+ ///
+ public static class SettingsEndpoints
+ {
+ public static IEndpointRouteBuilder MapSettingsEndpoints(this IEndpointRouteBuilder app)
+ {
+ var group = app.MapGroup("/settings").WithTags("Settings");
+
+ group.MapGet("/breeder-profile", async Task> (ApplicationContext db) =>
+ {
+ var s = await LoadAsync(db, track: false);
+ return TypedResults.Ok(ToDto(s));
+ });
+
+ group.MapPut("/breeder-profile", async Task (BreederProfileDto input, ApplicationContext db) =>
+ {
+ var s = await LoadAsync(db, track: true);
+ s.ZuchtName = input.ZuchtName ?? "";
+ s.Name = input.Name ?? "";
+ s.Address = input.Address ?? "";
+ s.Phone = input.Phone ?? "";
+ s.Email = input.Email ?? "";
+ s.Homepage = input.Homepage ?? "";
+ s.City = input.City ?? "";
+ await db.SaveChangesAsync();
+ return TypedResults.NoContent();
+ });
+
+ return app;
+ }
+
+ /// Die Singleton-Zeile; defensiv neu anlegen, falls sie fehlt.
+ private static async Task LoadAsync(ApplicationContext db, bool track)
+ {
+ var query = track ? db.BreederSettings : db.BreederSettings.AsNoTracking();
+ var s = await query.FirstOrDefaultAsync(x => x.Id == BreederSettings.SingletonId);
+ if (s is null)
+ {
+ s = new BreederSettings { Id = BreederSettings.SingletonId };
+ db.BreederSettings.Add(s);
+ await db.SaveChangesAsync();
+ }
+ return s;
+ }
+
+ internal static BreederProfileDto ToDto(BreederSettings s) =>
+ new(s.ZuchtName, s.Name, s.Address, s.Phone, s.Email, s.Homepage, s.City);
+ }
+}
diff --git a/GerbilManagerWebAPI/Migrations/20260606051954_SaleContractsAndBreederSettings.Designer.cs b/GerbilManagerWebAPI/Migrations/20260606051954_SaleContractsAndBreederSettings.Designer.cs
new file mode 100644
index 0000000..c7fd2d3
--- /dev/null
+++ b/GerbilManagerWebAPI/Migrations/20260606051954_SaleContractsAndBreederSettings.Designer.cs
@@ -0,0 +1,1024 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace GerbilManagerWebAPI.Migrations
+{
+ [DbContext(typeof(ApplicationContext))]
+ [Migration("20260606051954_SaleContractsAndBreederSettings")]
+ partial class SaleContractsAndBreederSettings
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.8")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.BreederSettings", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Address")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("City")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Homepage")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Phone")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("ZuchtName")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("BreederSettings");
+
+ b.HasData(
+ new
+ {
+ Id = new Guid("11111111-1111-1111-1111-000000000001"),
+ Address = "",
+ City = "",
+ Email = "",
+ Homepage = "",
+ Name = "",
+ Phone = "",
+ ZuchtName = ""
+ });
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.ColorVariety", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CanonicalGenotype")
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("SortOrder")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.ToTable("ColorVarieties");
+
+ b.HasData(
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000001"),
+ CanonicalGenotype = "AA chch DD EE GG pp spsp rere",
+ Name = "Pink Eyed White (PEW)",
+ SortOrder = 0
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000002"),
+ CanonicalGenotype = "aa chch DD EE GG PP spsp rere",
+ Name = "Hermelin",
+ SortOrder = 1
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000003"),
+ CanonicalGenotype = "AA chch DD EE GG PP spsp rere",
+ Name = "Himalaya",
+ SortOrder = 2
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000004"),
+ CanonicalGenotype = "aa cchmcchm DD EE gg PP spsp rere",
+ Name = "Zobel",
+ SortOrder = 3
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000005"),
+ CanonicalGenotype = "AA CC DD efef GG PP spsp rere",
+ Name = "Schwarzschimmel",
+ SortOrder = 4
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000006"),
+ CanonicalGenotype = "AA CC DD efef GG pp spsp rere",
+ Name = "Rotaugenschimmel",
+ SortOrder = 5
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000007"),
+ CanonicalGenotype = "AA CC DD EE GG PP spsp rere",
+ Name = "Agouti",
+ SortOrder = 6
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000008"),
+ CanonicalGenotype = "aa CC DD EE GG PP spsp rere",
+ Name = "Schwarz",
+ SortOrder = 7
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000009"),
+ CanonicalGenotype = "AA CC DD EE gg PP spsp rere",
+ Name = "Silberagouti",
+ SortOrder = 8
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000010"),
+ CanonicalGenotype = "aa CC DD EE gg PP spsp rere",
+ Name = "Anthrazit",
+ SortOrder = 9
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000011"),
+ CanonicalGenotype = "AA CC DD ee GG PP spsp rere",
+ Name = "Algierfuchs",
+ SortOrder = 10
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000012"),
+ CanonicalGenotype = "aa CC dd EE GG PP spsp rere",
+ Name = "Blau",
+ SortOrder = 11
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000013"),
+ CanonicalGenotype = "AA CC DD EE GG pp spsp rere",
+ Name = "Gold",
+ SortOrder = 12
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000014"),
+ CanonicalGenotype = "aa CC DD EE GG pp spsp rere",
+ Name = "Platin",
+ SortOrder = 13
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000015"),
+ CanonicalGenotype = "AA CC DD ee GG pp spsp rere",
+ Name = "Goldfuchs",
+ SortOrder = 14
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000016"),
+ CanonicalGenotype = "aa CC DD ee GG pp spsp rere",
+ Name = "Rotfuchs",
+ SortOrder = 15
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000017"),
+ CanonicalGenotype = "AA CC dd EE GG pp spsp rere",
+ Name = "dd Gold",
+ SortOrder = 16
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000018"),
+ CanonicalGenotype = "aa CC dd EE GG pp spsp rere",
+ Name = "dd Platin",
+ SortOrder = 17
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000019"),
+ CanonicalGenotype = "aa CC DD EE gg pp spsp rere",
+ Name = "Altweiss (REW)",
+ SortOrder = 18
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000020"),
+ CanonicalGenotype = "AA CC DD ee gg pp spsp rere",
+ Name = "Apricot (Blassfuchs)",
+ SortOrder = 19
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000021"),
+ CanonicalGenotype = "aa CC DD ee gg PP spsp rere",
+ Name = "Blaufuchs",
+ SortOrder = 20
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000022"),
+ CanonicalGenotype = "aa CC DD ee gg pp spsp rere",
+ Name = "C-Separator",
+ SortOrder = 21
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000023"),
+ CanonicalGenotype = "AA CC DD EE gg pp spsp rere",
+ Name = "Elfenbein",
+ SortOrder = 22
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000024"),
+ CanonicalGenotype = "aa CC DD ee GG PP spsp rere",
+ Name = "Kohlfuchs",
+ SortOrder = 23
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000025"),
+ CanonicalGenotype = "aa cchmcchm DD EE GG PP spsp rere",
+ Name = "Marder",
+ SortOrder = 24
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000026"),
+ CanonicalGenotype = "aa cchmcchm DD EE GG PP spsp rere",
+ Name = "Siam (Marder-Hell)",
+ SortOrder = 25
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000027"),
+ CanonicalGenotype = "AA CC DD ee gg PP spsp rere",
+ Name = "Polarfuchs",
+ SortOrder = 26
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000028"),
+ CanonicalGenotype = "aa CC DD EE GG pp spsp rere",
+ Name = "Saphir",
+ SortOrder = 27
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000029"),
+ CanonicalGenotype = "AA CC DD efef GG PP spsp rere",
+ Name = "Schimmel (Orangeschimmel)",
+ SortOrder = 28
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000030"),
+ CanonicalGenotype = "AA CC DD EE GG pp spsp rere",
+ Name = "Topas",
+ SortOrder = 29
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000031"),
+ CanonicalGenotype = "aa CC DD EE GG pp spsp rere",
+ Name = "Platin-Hell",
+ SortOrder = 30
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000032"),
+ CanonicalGenotype = "AA CC dd EE GG PP spsp rere",
+ Name = "Agouti dd",
+ SortOrder = 31
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000033"),
+ CanonicalGenotype = "AA CC dd EE gg PP spsp rere",
+ Name = "Silberagouti dd",
+ SortOrder = 32
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000034"),
+ CanonicalGenotype = "aa CC dd ee GG PP spsp rere",
+ Name = "Kohlfuchs dd",
+ SortOrder = 33
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000035"),
+ CanonicalGenotype = "aa CC dd EE gg PP spsp rere",
+ Name = "Anthrazit dd",
+ SortOrder = 34
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000036"),
+ CanonicalGenotype = "AA cchmcchm DD EE GG PP spsp rere",
+ Name = "Agouti CP-Hell",
+ SortOrder = 35
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000037"),
+ CanonicalGenotype = "aa cchmcchm DD ee gg PP spsp rere",
+ Name = "Blaufuchs CP",
+ SortOrder = 36
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000038"),
+ CanonicalGenotype = "AA CC DD efef gg PP spsp rere",
+ Name = "Polarfuchsschimmel",
+ SortOrder = 37
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000039"),
+ CanonicalGenotype = "AA CC DD efef gg PP spsp rere",
+ Name = "Silberschimmel",
+ SortOrder = 38
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000040"),
+ CanonicalGenotype = "AA CC DD efef GG PP spsp rere",
+ Name = "Algierfuchsschimmel",
+ SortOrder = 39
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000041"),
+ CanonicalGenotype = "AA cchmcchm DD ee gg PP spsp rere",
+ Name = "Polarfuchs-Hell CP",
+ SortOrder = 40
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000042"),
+ CanonicalGenotype = "aa CC DD efef GG PP spsp rere",
+ Name = "Kohlfuchsschimmel",
+ SortOrder = 41
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000043"),
+ CanonicalGenotype = "aa CC DD efef gg PP spsp rere",
+ Name = "Blaufuchsschimmel",
+ SortOrder = 42
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000044"),
+ CanonicalGenotype = "aa CC DD ee GG PP spsp rere",
+ Name = "Kohlfuchs, hell",
+ SortOrder = 43
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000045"),
+ CanonicalGenotype = "AA CC DD ee GG pp spsp rere",
+ Name = "Goldfuchs, hell",
+ SortOrder = 44
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000046"),
+ CanonicalGenotype = "AA CC DD efef GG pp spsp rere",
+ Name = "Goldfuchsschimmel",
+ SortOrder = 45
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000047"),
+ CanonicalGenotype = "AA CC DD EE GG pp spsp rere",
+ Name = "Gold-Hell",
+ SortOrder = 46
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000048"),
+ CanonicalGenotype = "aa cchmcchm dd EE GG PP spsp rere",
+ Name = "Siam (Marder-Hell) dd",
+ SortOrder = 47
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000049"),
+ CanonicalGenotype = "aa cchmcchm dd EE GG PP spsp rere",
+ Name = "Marder dd",
+ SortOrder = 48
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000050"),
+ CanonicalGenotype = "aa cchmcchm DD EE gg PP spsp rere",
+ Name = "Zobel-Hell",
+ SortOrder = 49
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000051"),
+ CanonicalGenotype = "AA cchmcchm dd EE gg PP spsp rere",
+ Name = "Silberagouti dd CP",
+ SortOrder = 50
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000052"),
+ CanonicalGenotype = "AA cchmcchm dd EE gg PP spsp rere",
+ Name = "Silberagouti-Hell dd CP",
+ SortOrder = 51
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000053"),
+ CanonicalGenotype = "AA cchmcchm dd EE GG PP spsp rere",
+ Name = "Agouti dd CP",
+ SortOrder = 52
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000054"),
+ CanonicalGenotype = "AA cchmcchm dd EE GG PP spsp rere",
+ Name = "Agouti-Hell dd CP",
+ SortOrder = 53
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000055"),
+ CanonicalGenotype = "aa CC DD ee gg PP spsp rere",
+ Name = "Blaufuchs, hell",
+ SortOrder = 54
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000056"),
+ CanonicalGenotype = "aa CC DD efef GG pp spsp rere",
+ Name = "Rotfuchsschimmel",
+ SortOrder = 55
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000057"),
+ CanonicalGenotype = "AA CC DD ee gg PP spsp rere",
+ Name = "Polarfuchs, hell",
+ SortOrder = 56
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000058"),
+ CanonicalGenotype = "aa CC DD efef GG PP spsp rere",
+ Name = "Kohlfuchsschimmel, hell",
+ SortOrder = 57
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000059"),
+ CanonicalGenotype = "aa CC DD ee GG pp spsp rere",
+ Name = "Rotfuchs, hell",
+ SortOrder = 58
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000060"),
+ CanonicalGenotype = "aa cchmcchm dd EE gg PP spsp rere",
+ Name = "Zobel dd",
+ SortOrder = 59
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000061"),
+ CanonicalGenotype = "aa CC DD ee GG PP spsp rere",
+ Name = "Kohlfuchs-Hell",
+ SortOrder = 60
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000062"),
+ CanonicalGenotype = "aa cchmcchm DD ee GG PP spsp rere",
+ Name = "Kohlfuchs CP",
+ SortOrder = 61
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000063"),
+ CanonicalGenotype = "AA cchmcchm DD ee GG PP spsp rere",
+ Name = "Algierfuchs CP",
+ SortOrder = 62
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000064"),
+ CanonicalGenotype = "AA cchmcchm DD EE gg PP spsp rere",
+ Name = "Silberagouti CP",
+ SortOrder = 63
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000065"),
+ CanonicalGenotype = "AA cchmcchm DD EE GG PP spsp rere",
+ Name = "Agouti CP",
+ SortOrder = 64
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000066"),
+ CanonicalGenotype = "AA cchmcchm DD ee GG PP spsp rere",
+ Name = "Algierfuchs-Hell CP",
+ SortOrder = 65
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000067"),
+ CanonicalGenotype = "aa cchmcchm DD ee GG PP spsp rere",
+ Name = "Kohlfuchs,hell CP",
+ SortOrder = 66
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000068"),
+ CanonicalGenotype = "AA cchmcchm DD ee gg PP spsp rere",
+ Name = "Polarfuchs CP",
+ SortOrder = 67
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000069"),
+ CanonicalGenotype = "AA CC DD ee GG PP spsp rere",
+ Name = "Algierfuchs, hell",
+ SortOrder = 68
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000070"),
+ CanonicalGenotype = "AA CC dd EE GG pp spsp rere",
+ Name = "Topas dd",
+ SortOrder = 69
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000071"),
+ CanonicalGenotype = "aa cchmcchm dd EE gg PP spsp rere",
+ Name = "Zobel-Hell dd",
+ SortOrder = 70
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000072"),
+ CanonicalGenotype = "aa cchmcchm DD efef GG PP spsp rere",
+ Name = "Kohlfuchsschimmel CP",
+ SortOrder = 71
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000073"),
+ CanonicalGenotype = "aa CC dd ee gg PP spsp rere",
+ Name = "Blaufuchs dd",
+ SortOrder = 72
+ });
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.Contact", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Address")
+ .HasColumnType("text");
+
+ b.Property("Email")
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Notes")
+ .HasColumnType("text");
+
+ b.Property("Phone")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("Contacts");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.Enclosure", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Notes")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("Enclosures");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.Gerbil", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CauseOfDeath")
+ .HasColumnType("text");
+
+ b.Property("ColorVarietyId")
+ .HasColumnType("uuid");
+
+ b.Property("DateOfBirth")
+ .HasColumnType("date");
+
+ b.Property("DateOfDeath")
+ .HasColumnType("date");
+
+ b.Property("EnclosureId")
+ .HasColumnType("uuid");
+
+ b.Property("ExternalRef")
+ .HasColumnType("text");
+
+ b.Property("Gender")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Genotype")
+ .HasColumnType("text");
+
+ b.Property("GoHomeDate")
+ .HasColumnType("date");
+
+ b.Property("ImportSource")
+ .HasColumnType("text");
+
+ b.Property("LitterId")
+ .HasColumnType("uuid");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Notes")
+ .HasColumnType("text");
+
+ b.Property("OriginContactId")
+ .HasColumnType("uuid");
+
+ b.Property("ReceiverContactId")
+ .HasColumnType("uuid");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ColorVarietyId");
+
+ b.HasIndex("EnclosureId");
+
+ b.HasIndex("LitterId");
+
+ b.HasIndex("OriginContactId");
+
+ b.HasIndex("ReceiverContactId");
+
+ b.ToTable("Gerbils");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.GerbilPhoto", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Caption")
+ .HasColumnType("text");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("FileName")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("GerbilId")
+ .HasColumnType("uuid");
+
+ b.Property("SortOrder")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("GerbilId");
+
+ b.ToTable("GerbilPhotos");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.HealthRecord", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Date")
+ .HasColumnType("date");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("GerbilId")
+ .HasColumnType("uuid");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Veterinarian")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("GerbilId");
+
+ b.ToTable("HealthRecords");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.Litter", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Date")
+ .HasColumnType("date");
+
+ b.Property("ExpectedGoHomeDate")
+ .HasColumnType("date");
+
+ b.Property("FatherId")
+ .HasColumnType("uuid");
+
+ b.Property("MotherId")
+ .HasColumnType("uuid");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Notes")
+ .HasColumnType("text");
+
+ b.Property("TotalBorn")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("FatherId");
+
+ b.HasIndex("MotherId");
+
+ b.ToTable("Litters");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("ContactId")
+ .HasColumnType("uuid");
+
+ b.Property("ContractDate")
+ .HasColumnType("date");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("FileName")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("HandoverDate")
+ .HasColumnType("date");
+
+ b.Property("Price")
+ .HasPrecision(10, 2)
+ .HasColumnType("numeric(10,2)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ContactId");
+
+ b.ToTable("SaleContracts");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContractAnimal", b =>
+ {
+ b.Property("SaleContractId")
+ .HasColumnType("uuid");
+
+ b.Property("GerbilId")
+ .HasColumnType("uuid");
+
+ b.HasKey("SaleContractId", "GerbilId");
+
+ b.HasIndex("GerbilId");
+
+ b.ToTable("SaleContractAnimal");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.WeightRecord", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Date")
+ .HasColumnType("date");
+
+ b.Property("GerbilId")
+ .HasColumnType("uuid");
+
+ b.Property("Notes")
+ .HasColumnType("text");
+
+ b.Property("WeightGrams")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("GerbilId");
+
+ b.ToTable("WeightRecords");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.Gerbil", b =>
+ {
+ b.HasOne("GerbilManagerWebAPI.Models.ColorVariety", "ColorVariety")
+ .WithMany()
+ .HasForeignKey("ColorVarietyId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.HasOne("GerbilManagerWebAPI.Models.Enclosure", "Enclosure")
+ .WithMany("Gerbils")
+ .HasForeignKey("EnclosureId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.HasOne("GerbilManagerWebAPI.Models.Litter", "Litter")
+ .WithMany()
+ .HasForeignKey("LitterId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.HasOne("GerbilManagerWebAPI.Models.Contact", "OriginContact")
+ .WithMany()
+ .HasForeignKey("OriginContactId")
+ .OnDelete(DeleteBehavior.Restrict);
+
+ b.HasOne("GerbilManagerWebAPI.Models.Contact", "ReceiverContact")
+ .WithMany()
+ .HasForeignKey("ReceiverContactId")
+ .OnDelete(DeleteBehavior.Restrict);
+
+ b.Navigation("ColorVariety");
+
+ b.Navigation("Enclosure");
+
+ b.Navigation("Litter");
+
+ b.Navigation("OriginContact");
+
+ b.Navigation("ReceiverContact");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.GerbilPhoto", b =>
+ {
+ b.HasOne("GerbilManagerWebAPI.Models.Gerbil", null)
+ .WithMany()
+ .HasForeignKey("GerbilId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.HealthRecord", b =>
+ {
+ b.HasOne("GerbilManagerWebAPI.Models.Gerbil", null)
+ .WithMany()
+ .HasForeignKey("GerbilId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.Litter", b =>
+ {
+ b.HasOne("GerbilManagerWebAPI.Models.Gerbil", "Father")
+ .WithMany()
+ .HasForeignKey("FatherId")
+ .OnDelete(DeleteBehavior.Restrict);
+
+ b.HasOne("GerbilManagerWebAPI.Models.Gerbil", "Mother")
+ .WithMany()
+ .HasForeignKey("MotherId")
+ .OnDelete(DeleteBehavior.Restrict);
+
+ b.Navigation("Father");
+
+ b.Navigation("Mother");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b =>
+ {
+ b.HasOne("GerbilManagerWebAPI.Models.Contact", "Contact")
+ .WithMany()
+ .HasForeignKey("ContactId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Contact");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContractAnimal", b =>
+ {
+ b.HasOne("GerbilManagerWebAPI.Models.Gerbil", "Gerbil")
+ .WithMany()
+ .HasForeignKey("GerbilId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("GerbilManagerWebAPI.Models.SaleContract", null)
+ .WithMany("Animals")
+ .HasForeignKey("SaleContractId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Gerbil");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.WeightRecord", b =>
+ {
+ b.HasOne("GerbilManagerWebAPI.Models.Gerbil", null)
+ .WithMany()
+ .HasForeignKey("GerbilId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.Enclosure", b =>
+ {
+ b.Navigation("Gerbils");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b =>
+ {
+ b.Navigation("Animals");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/GerbilManagerWebAPI/Migrations/20260606051954_SaleContractsAndBreederSettings.cs b/GerbilManagerWebAPI/Migrations/20260606051954_SaleContractsAndBreederSettings.cs
new file mode 100644
index 0000000..2467b32
--- /dev/null
+++ b/GerbilManagerWebAPI/Migrations/20260606051954_SaleContractsAndBreederSettings.cs
@@ -0,0 +1,108 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace GerbilManagerWebAPI.Migrations
+{
+ ///
+ public partial class SaleContractsAndBreederSettings : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "BreederSettings",
+ columns: table => new
+ {
+ Id = table.Column(type: "uuid", nullable: false),
+ ZuchtName = table.Column(type: "text", nullable: false),
+ Name = table.Column(type: "text", nullable: false),
+ Address = table.Column(type: "text", nullable: false),
+ Phone = table.Column(type: "text", nullable: false),
+ Email = table.Column(type: "text", nullable: false),
+ Homepage = table.Column(type: "text", nullable: false),
+ City = table.Column(type: "text", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_BreederSettings", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "SaleContracts",
+ columns: table => new
+ {
+ Id = table.Column(type: "uuid", nullable: false),
+ ContactId = table.Column(type: "uuid", nullable: false),
+ Price = table.Column(type: "numeric(10,2)", precision: 10, scale: 2, nullable: false),
+ HandoverDate = table.Column(type: "date", nullable: false),
+ ContractDate = table.Column(type: "date", nullable: false),
+ FileName = table.Column(type: "text", nullable: false),
+ CreatedAt = table.Column(type: "timestamp with time zone", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_SaleContracts", x => x.Id);
+ table.ForeignKey(
+ name: "FK_SaleContracts_Contacts_ContactId",
+ column: x => x.ContactId,
+ principalTable: "Contacts",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Restrict);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "SaleContractAnimal",
+ columns: table => new
+ {
+ SaleContractId = table.Column(type: "uuid", nullable: false),
+ GerbilId = table.Column(type: "uuid", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_SaleContractAnimal", x => new { x.SaleContractId, x.GerbilId });
+ table.ForeignKey(
+ name: "FK_SaleContractAnimal_Gerbils_GerbilId",
+ column: x => x.GerbilId,
+ principalTable: "Gerbils",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_SaleContractAnimal_SaleContracts_SaleContractId",
+ column: x => x.SaleContractId,
+ principalTable: "SaleContracts",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.InsertData(
+ table: "BreederSettings",
+ columns: new[] { "Id", "Address", "City", "Email", "Homepage", "Name", "Phone", "ZuchtName" },
+ values: new object[] { new Guid("11111111-1111-1111-1111-000000000001"), "", "", "", "", "", "", "" });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_SaleContractAnimal_GerbilId",
+ table: "SaleContractAnimal",
+ column: "GerbilId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_SaleContracts_ContactId",
+ table: "SaleContracts",
+ column: "ContactId");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "BreederSettings");
+
+ migrationBuilder.DropTable(
+ name: "SaleContractAnimal");
+
+ migrationBuilder.DropTable(
+ name: "SaleContracts");
+ }
+ }
+}
diff --git a/GerbilManagerWebAPI/Migrations/ApplicationContextModelSnapshot.cs b/GerbilManagerWebAPI/Migrations/ApplicationContextModelSnapshot.cs
index 2116917..4a23362 100644
--- a/GerbilManagerWebAPI/Migrations/ApplicationContextModelSnapshot.cs
+++ b/GerbilManagerWebAPI/Migrations/ApplicationContextModelSnapshot.cs
@@ -21,6 +21,58 @@ namespace GerbilManagerWebAPI.Migrations
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.BreederSettings", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Address")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("City")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Homepage")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Phone")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("ZuchtName")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("BreederSettings");
+
+ b.HasData(
+ new
+ {
+ Id = new Guid("11111111-1111-1111-1111-000000000001"),
+ Address = "",
+ City = "",
+ Email = "",
+ Homepage = "",
+ Name = "",
+ Phone = "",
+ ZuchtName = ""
+ });
+ });
+
modelBuilder.Entity("GerbilManagerWebAPI.Models.ColorVariety", b =>
{
b.Property("Id")
@@ -771,6 +823,54 @@ namespace GerbilManagerWebAPI.Migrations
b.ToTable("Litters");
});
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("ContactId")
+ .HasColumnType("uuid");
+
+ b.Property("ContractDate")
+ .HasColumnType("date");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("FileName")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("HandoverDate")
+ .HasColumnType("date");
+
+ b.Property("Price")
+ .HasPrecision(10, 2)
+ .HasColumnType("numeric(10,2)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ContactId");
+
+ b.ToTable("SaleContracts");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContractAnimal", b =>
+ {
+ b.Property("SaleContractId")
+ .HasColumnType("uuid");
+
+ b.Property("GerbilId")
+ .HasColumnType("uuid");
+
+ b.HasKey("SaleContractId", "GerbilId");
+
+ b.HasIndex("GerbilId");
+
+ b.ToTable("SaleContractAnimal");
+ });
+
modelBuilder.Entity("GerbilManagerWebAPI.Models.WeightRecord", b =>
{
b.Property("Id")
@@ -869,6 +969,34 @@ namespace GerbilManagerWebAPI.Migrations
b.Navigation("Mother");
});
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b =>
+ {
+ b.HasOne("GerbilManagerWebAPI.Models.Contact", "Contact")
+ .WithMany()
+ .HasForeignKey("ContactId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Contact");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContractAnimal", b =>
+ {
+ b.HasOne("GerbilManagerWebAPI.Models.Gerbil", "Gerbil")
+ .WithMany()
+ .HasForeignKey("GerbilId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("GerbilManagerWebAPI.Models.SaleContract", null)
+ .WithMany("Animals")
+ .HasForeignKey("SaleContractId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Gerbil");
+ });
+
modelBuilder.Entity("GerbilManagerWebAPI.Models.WeightRecord", b =>
{
b.HasOne("GerbilManagerWebAPI.Models.Gerbil", null)
@@ -882,6 +1010,11 @@ namespace GerbilManagerWebAPI.Migrations
{
b.Navigation("Gerbils");
});
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b =>
+ {
+ b.Navigation("Animals");
+ });
#pragma warning restore 612, 618
}
}
diff --git a/GerbilManagerWebAPI/Models/BreederSettings.cs b/GerbilManagerWebAPI/Models/BreederSettings.cs
new file mode 100644
index 0000000..ab56ac9
--- /dev/null
+++ b/GerbilManagerWebAPI/Models/BreederSettings.cs
@@ -0,0 +1,36 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace GerbilManagerWebAPI.Models
+{
+ ///
+ /// Zuchtprofil (Verkäufer-Block der Abgabeverträge) — Einzelzeilen-Entity
+ /// (Singleton, fixe Id, leer geseedet). Die Züchterin pflegt ihre Daten in
+ /// der App unter /einstellungen statt in einer JSON-Datei; FEAT-13 phase B
+ /// ersetzt damit die appsettings-Variante aus phase A.
+ ///
+ public class BreederSettings
+ {
+ /// Fixe Id der einzigen Zeile (per HasData geseedet).
+ public static readonly Guid SingletonId = new("11111111-1111-1111-1111-000000000001");
+
+ [Key]
+ public Guid Id { get; set; }
+
+ public string ZuchtName { get; set; } = "";
+
+ /// Vor- und Nachname inkl. Anrede, z. B. „Frau Erika Muster“.
+ public string Name { get; set; } = "";
+
+ /// Anschrift einzeilig: „Straße Nr, PLZ Ort“.
+ public string Address { get; set; } = "";
+
+ public string Phone { get; set; } = "";
+
+ public string Email { get; set; } = "";
+
+ public string Homepage { get; set; } = "";
+
+ /// Ort für die Unterschriftszeile („{Ort}, den {Datum}“).
+ public string City { get; set; } = "";
+ }
+}
diff --git a/GerbilManagerWebAPI/Models/SaleContract.cs b/GerbilManagerWebAPI/Models/SaleContract.cs
new file mode 100644
index 0000000..497179c
--- /dev/null
+++ b/GerbilManagerWebAPI/Models/SaleContract.cs
@@ -0,0 +1,45 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace GerbilManagerWebAPI.Models
+{
+ ///
+ /// Ein erzeugter Abgabevertrag (FEAT-13). Die .docx liegt wie die Fotos im
+ /// Datei-Root (Contracts:RootPath bzw. contract-storage/), in der DB steht
+ /// nur der Dateiname. Tiere hängen über
+ /// (Join-Entity statt uuid[]-Spalte: referenzielle Integrität, „Verträge
+ /// eines Tieres“ bleibt abfragbar, und pro Tier-Zeile ist später Platz für
+ /// Zusatzdaten wie den Einzelpreis).
+ ///
+ public class SaleContract
+ {
+ [Key]
+ public Guid Id { get; set; }
+
+ /// Abnehmer (Käufer-Block des Vertrags).
+ public Guid ContactId { get; set; }
+ public Contact? Contact { get; set; }
+
+ /// Kaufpreis in Euro (gesamt).
+ public decimal Price { get; set; }
+
+ public DateOnly HandoverDate { get; set; }
+
+ /// Datum der Unterschriftszeile (Standard: Übergabedatum).
+ public DateOnly ContractDate { get; set; }
+
+ /// Dateiname der erzeugten .docx im Vertrags-Dateiroot.
+ public required string FileName { get; set; }
+
+ public DateTimeOffset CreatedAt { get; set; }
+
+ public List Animals { get; set; } = [];
+ }
+
+ /// Join-Zeile Vertrag ↔ Tier (zusammengesetzter Schlüssel).
+ public class SaleContractAnimal
+ {
+ public Guid SaleContractId { get; set; }
+ public Guid GerbilId { get; set; }
+ public Gerbil? Gerbil { get; set; }
+ }
+}
diff --git a/GerbilManagerWebAPI/Program.cs b/GerbilManagerWebAPI/Program.cs
index c4bfc3e..897d765 100644
--- a/GerbilManagerWebAPI/Program.cs
+++ b/GerbilManagerWebAPI/Program.cs
@@ -62,5 +62,10 @@ app.MapHealthRecordEndpoints();
app.MapWeightRecordEndpoints();
app.MapInbreedingEndpoints();
app.MapPhotoEndpoints();
+app.MapContractEndpoints();
+app.MapSettingsEndpoints();
app.Run();
+
+// Sichtbarer Programmtyp für WebApplicationFactory (Endpoint-Tests).
+public partial class Program { }