Repository: granit-fx/granit-dotnet
Author: jfmeyers
## Parent epic
#1220
## Goal
Ship the new `Granit.Contacts` module: `Contact` aggregate + persistence + admin endpoints + GDPR handlers. **No migration of downstream modules in this feature** — this lands the module in isolation so it can be reviewed standalone and consumed by Features 2–5 in subsequent PRs.
## Dual-use design (see Epic for context)
The `Contact` aggregate is `IMultiTenant`:
- `TenantId == null` ⇒ **host-scoped customer** (the SaaS host's B2B customers — i.e. its tenants viewed as billable entities)
- `TenantId == <tenant>` ⇒ **tenant-scoped customer** (an e-commerce app running on a tenant stores its own end-customers here)
Both kinds share the same table, same behavior, same admin endpoints — the existing `IMultiTenant` query filter handles isolation. Tenant admins see only their own customers; host admins (no tenant context) see host-scoped customers and can opt-in to cross-tenant browsing via `IDataFilter.Disable<IMultiTenant>()`.
## Module split (Granit convention)
- `Granit.Contacts` — abstractions: `Contact` aggregate (with `IMultiTenant`), `ContactExternalMapping`, `BillingAddress` (moved here from Invoicing), `ContactStatus`, `ContactId` value object, reader/writer interfaces, domain + integration events, `GranitCustomersModule`
- `Granit.Contacts.EntityFrameworkCore` — `ContactsDbContext`, `EfContactStore`, `EfContactQueryableSource`, entity configurations
- `Granit.Contacts.Endpoints` — admin CRUD + external mapping management endpoints (visible at both host and tenant scope; permissions split `Contacts.Manage` per `MultiTenancySides`)
- `Granit.Contacts.Privacy` — `IPersonalDataExportHandler` + `IPersonalDataDeletionHandler` for the customer's personal-data fields (email, billing address). DPO-driven.
- `Granit.Contacts.Tests` (+ `.Tests.Integration` if needed)
## User stories
- #1226 - Design Contact aggregate, value objects, and events
- #1227 - EF Core persistence — ContactsDbContext, EfContactStore, configurations
- #1228 - Admin endpoints — CRUD + external mapping management
- #1229 - Privacy handlers — IPersonalDataExportHandler + IPersonalDataDeletionHandler
- #1230 - Validate dual-use semantics — host-scope and tenant-scope coexistence
- #1231 - Default customer resolver — bridge from tenantId to host-scoped customer
## Acceptance criteria
- All four packages compile and reference each other per `[DependsOn]` convention
- `dotnet test` green for the new test projects, coverage ≥ 80% on the module
- Architecture tests pass (`Granit.ArchitectureTests`); pairing rule satisfied (`CustomerQueryDefinition` + `CustomerExportDefinition`)
- `dotnet format --verify-no-changes` clean
- New test projects added to `.github/test-shards.json` (likely the `business` shard)
- Endpoints exposed at both `/contacts` (tenant scope) and `/admin/contacts` (host scope) per Granit's MultiTenancySides convention