Per hive/agents/god/OWNERSHIP-residency.md (Julian): "Clan kleine Chaoten"
is the wife's own Zucht. Classify each animal:
(a) RESIDENT if zuchtCanon contains 'kleinechaote' (separator/declension-
insensitive: "v.d. Kleinen Chaoten", "Clan-Kleine-Chaoten", …); OR
(b) PARENT EXCEPTION — a parent of a Clan offspring is resident even if its
own Zuchtname is foreign (you can only breed a Clan litter if the parents
were in your care). Runs AFTER the parentRefs→litter linking.
Otherwise EXTERNAL (pedigree ancestor, not living stock — kept, not deleted).
Stored as a dedicated Gerbil.IsResident bool (distinct from Status/Abgabe —
origin/identity, not current location), defaulting true (own stock unless
marked external; DB HasDefaultValue(true)). Additive migration
AddGerbilResidency; has-pending-model-changes clean. Round-trips in
GerbilDto/GerbilInput and is Gridify-filterable (isResident==true) so Kevin
can build a "nur Bestand" filter. ImportService computes it at (re-)import
and refreshes existing rows on the idempotent sweep. ResidencySummary added
to the import report.
Projected on real data: 306 loadable → 227 resident (188 rule a, +39 via
parent exception), 79 external ancestors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
97 lines
4.5 KiB
C#
97 lines
4.5 KiB
C#
using System.ComponentModel.DataAnnotations;
|
||
|
||
namespace GerbilManagerWebAPI.Models
|
||
{
|
||
/// <summary>
|
||
/// A gerbil (Rennmaus). Pedigree is traversed via Litter → Father/Mother (no redundant
|
||
/// parent FKs here). Genotype is one compact frozen-contract string (e.g.
|
||
/// "Aa CC Dd EE GG Pp Spsp rere", "?" wildcards allowed); never SQL-filtered.
|
||
/// </summary>
|
||
public class Gerbil
|
||
{
|
||
[Key]
|
||
public Guid Id { get; set; }
|
||
public required string Name { get; set; }
|
||
public Gender Gender { get; set; }
|
||
public GerbilStatus Status { get; set; } = GerbilStatus.Active;
|
||
|
||
// Birth litter (the litter this gerbil was born in).
|
||
public Guid? LitterId { get; set; }
|
||
public Litter? Litter { get; set; }
|
||
|
||
// Contacts: Herkunft (origin) and Abnehmer (receiver, when given away).
|
||
public Guid? OriginContactId { get; set; }
|
||
public Contact? OriginContact { get; set; }
|
||
public Guid? ReceiverContactId { get; set; }
|
||
public Contact? ReceiverContact { get; set; }
|
||
|
||
// Current home.
|
||
public Guid? EnclosureId { get; set; }
|
||
public Enclosure? Enclosure { get; set; }
|
||
|
||
// Farbschlag.
|
||
public Guid? ColorVarietyId { get; set; }
|
||
public ColorVariety? ColorVariety { get; set; }
|
||
|
||
public DateOnly? DateOfBirth { get; set; }
|
||
public DateOnly? DateOfDeath { get; set; }
|
||
public string? CauseOfDeath { get; set; }
|
||
public DateOnly? GoHomeDate { get; set; }
|
||
|
||
/// <summary>Compact genotype string (frozen GEN-1 contract). Null = not genotyped.</summary>
|
||
public string? Genotype { get; set; }
|
||
|
||
public string? Notes { get; set; }
|
||
|
||
// Provenance (for the FEAT-8 spreadsheet import).
|
||
public string? ImportSource { get; set; }
|
||
public string? ExternalRef { get; set; }
|
||
|
||
/// <summary>Raw import payload preserved verbatim (rawGenotype + unmappedTokens like
|
||
/// the Uw locus / WFNZ markers) so nothing from the spreadsheets is lost. JSON text.</summary>
|
||
public string? RawImportData { get; set; }
|
||
|
||
/// <summary>Breeder/Herkunft (cattery/Zucht) as free text — set by the FEAT-8 import from
|
||
/// the source Zucht (imported animals have no OriginContact). Gridify-filterable; the
|
||
/// distinct values back the Tiere "Herkunft" dropdown (GET /gerbils/breeders).</summary>
|
||
public string? OriginBreeder { get; set; }
|
||
|
||
/// <summary>Separator-insensitive search key: Name lowercased with whitespace/.-_ stripped.
|
||
/// Kept in sync automatically on save (see ApplicationContext.SaveChanges). Gridify-filterable
|
||
/// so "clan kleine chaoten" matches "Clan-Kleine-Chaoten" (client strips separators too).</summary>
|
||
public string? NameSearch { get; set; }
|
||
|
||
/// <summary>FEAT-14: Charakterbogen trait labels (opaque to the backend — the
|
||
/// {key,label} vocabulary lives frontend-side). Stored as a JSON text column.</summary>
|
||
public List<string> CharacterTraits { get; set; } = new();
|
||
|
||
/// <summary>FEAT-14: free-text character note; feeds the AI Verkaufstext.</summary>
|
||
public string? CharacterNote { get; set; }
|
||
|
||
/// <summary>GEN-3b: hearing/deaf phenotype flag (NOT a genotype locus — it's the
|
||
/// downstream effect of high white load / Sp×Sls). null = not stated, true = deaf
|
||
/// (dea/taub), false = hearing (Dea/hörend). Set by the FEAT-8 import from the
|
||
/// after-spsp deafness annotation; see hive/agents/god/GENETIK-notation.md.</summary>
|
||
public bool? IsDeaf { get; set; }
|
||
|
||
/// <summary>Residency/ownership (ORIGIN, distinct from Abgabe location): true = part of
|
||
/// the Clan-kleine-Chaoten Bestand, false = external pedigree ancestor (bred elsewhere).
|
||
/// Rule (a) Zuchtname matches the Clan kennel, OR (b) it's a parent of a Clan offspring.
|
||
/// Defaults true (manually-added animals are own stock); the FEAT-8 import classifies
|
||
/// imported animals. See hive/agents/god/OWNERSHIP-residency.md. Gridify-filterable.</summary>
|
||
public bool IsResident { get; set; } = true;
|
||
}
|
||
|
||
/// <summary>Shared normalisation for the separator-insensitive name search.</summary>
|
||
public static class GerbilSearch
|
||
{
|
||
public static string Normalize(string? name)
|
||
{
|
||
if (string.IsNullOrEmpty(name)) return "";
|
||
var chars = name.ToLowerInvariant()
|
||
.Where(c => !char.IsWhiteSpace(c) && c != '-' && c != '.' && c != '_');
|
||
return new string(chars.ToArray());
|
||
}
|
||
}
|
||
}
|