Org structure: divisions → products/services → teams + custom model base URL

The object spine becomes definable (data model was designed-for from day one):
- Division and Product entities (Product carries kind: Product|Service, optional DivisionId);
  Team gains nullable ProductId — pre-structure teams keep working. AddDivisionsAndProducts
  migration; org-scoped validation; owner-only writes (audited); list endpoints.
- /structure page: define divisions, products/services (with division), teams (under a
  product). Org chart now renders the full spine — org → divisions → products → teams →
  seats — with parentless layers linking up to the org.
- BYOK custom URL: the SeatsPage model-connection form gains a Base URL field (provider
  list: stub/openai/ollama/vllm/custom). Backend already supported it end to end —
  ApiConfig.Endpoint flows into the OpenAI-compatible adapter ({base}/v1/chat/completions),
  so any OpenAI-compatible gateway or self-hosted model works; the config list shows it.

Verified: ArchitectureTests 8/8, IntegrationTests 45/45 (new OrgStructureTests: spine
creation, kind tags, org-scoped validation 400s, Member 403), client build green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-10 18:13:52 +03:30
parent 4416d99360
commit 1e65654114
15 changed files with 1153 additions and 21 deletions
@@ -0,0 +1,22 @@
using TeamUp.SharedKernel.Domain;
namespace TeamUp.Modules.OrgBoard.Domain;
/// <summary>An organizational division (Technical, Finance, HR, …). Products/services live under it.</summary>
internal sealed class Division : Entity
{
public Guid OrganizationId { get; private set; }
public string Name { get; private set; } = null!;
public DateTimeOffset CreatedAtUtc { get; private set; }
private Division()
{
}
public Division(Guid organizationId, string name, DateTimeOffset createdAtUtc)
{
OrganizationId = organizationId;
Name = name;
CreatedAtUtc = createdAtUtc;
}
}
@@ -0,0 +1,33 @@
using TeamUp.SharedKernel.Domain;
namespace TeamUp.Modules.OrgBoard.Domain;
/// <summary>Engineering divisions organize around products; other divisions around services.</summary>
internal enum ProductKind
{
Product,
Service,
}
/// <summary>A product or service — the same entity with a kind tag. Teams live under it.</summary>
internal sealed class Product : Entity
{
public Guid OrganizationId { get; private set; }
public Guid? DivisionId { get; private set; }
public string Name { get; private set; } = null!;
public ProductKind Kind { get; private set; }
public DateTimeOffset CreatedAtUtc { get; private set; }
private Product()
{
}
public Product(Guid organizationId, Guid? divisionId, string name, ProductKind kind, DateTimeOffset createdAtUtc)
{
OrganizationId = organizationId;
DivisionId = divisionId;
Name = name;
Kind = kind;
CreatedAtUtc = createdAtUtc;
}
}
@@ -2,10 +2,14 @@ using TeamUp.SharedKernel.Domain;
namespace TeamUp.Modules.OrgBoard.Domain;
/// <summary>A team within an organization. Team-level memberships are granted at its id (Team scope).</summary>
/// <summary>
/// A team within an organization, optionally under a product/service. Team-level memberships are
/// granted at its id (Team scope). ProductId is nullable so pre-structure teams keep working.
/// </summary>
internal sealed class Team : Entity
{
public Guid OrganizationId { get; private set; }
public Guid? ProductId { get; private set; }
public string Name { get; private set; } = null!;
public DateTimeOffset CreatedAtUtc { get; private set; }
@@ -13,10 +17,11 @@ internal sealed class Team : Entity
{
}
public Team(Guid organizationId, string name, DateTimeOffset createdAtUtc)
public Team(Guid organizationId, string name, DateTimeOffset createdAtUtc, Guid? productId = null)
{
OrganizationId = organizationId;
Name = name;
CreatedAtUtc = createdAtUtc;
ProductId = productId;
}
}
@@ -7,9 +7,17 @@ internal sealed record CreateOrganizationRequest(Guid OrganizationId, string Nam
internal sealed record OrganizationResponse(Guid Id, string Name);
internal sealed record CreateTeamRequest(Guid OrganizationId, string Name);
internal sealed record CreateTeamRequest(Guid OrganizationId, string Name, Guid? ProductId = null);
internal sealed record TeamResponse(Guid Id, Guid OrganizationId, string Name);
internal sealed record TeamResponse(Guid Id, Guid OrganizationId, string Name, Guid? ProductId = null);
internal sealed record CreateDivisionRequest(Guid OrganizationId, string Name);
internal sealed record DivisionResponse(Guid Id, Guid OrganizationId, string Name);
internal sealed record CreateProductRequest(Guid OrganizationId, string Name, ProductKind Kind, Guid? DivisionId = null);
internal sealed record ProductResponse(Guid Id, Guid OrganizationId, Guid? DivisionId, string Name, string Kind);
internal sealed record CreateTaskRequest(Guid TeamId, string Title, string? Description, WorkItemType Type);
@@ -18,6 +18,10 @@ internal static class OrgBoardEndpoints
group.MapGet("/ping", () => TypedResults.Ok(new ModulePing("orgboard")));
group.MapPost("/organizations", CreateOrganization).RequireAuthorization();
group.MapPost("/divisions", CreateDivision).RequireAuthorization();
group.MapGet("/divisions", ListDivisions).RequireAuthorization();
group.MapPost("/products", CreateProduct).RequireAuthorization();
group.MapGet("/products", ListProducts).RequireAuthorization();
group.MapPost("/teams", CreateTeam).RequireAuthorization();
group.MapGet("/teams", ListTeams).RequireAuthorization();
group.MapPost("/tasks", CreateTask).RequireAuthorization();
@@ -87,11 +91,97 @@ internal static class OrgBoardEndpoints
return Results.BadRequest("Organization does not exist; create it first.");
}
var team = new Team(request.OrganizationId, request.Name.Trim(), clock.GetUtcNow());
if (request.ProductId is { } productId
&& !await db.Products.AnyAsync(p => p.Id == productId && p.OrganizationId == request.OrganizationId, ct))
{
return Results.BadRequest("Product not found in this organization.");
}
var team = new Team(request.OrganizationId, request.Name.Trim(), clock.GetUtcNow(), request.ProductId);
db.Teams.Add(team);
await db.SaveChangesAsync(ct);
await audit.WriteAsync(new AuditEvent("team.created", "Team", team.Id, user.MemberId, team.Name), ct);
return Results.Ok(new TeamResponse(team.Id, team.OrganizationId, team.Name));
return Results.Ok(new TeamResponse(team.Id, team.OrganizationId, team.Name, team.ProductId));
}
private static async Task<IResult> CreateDivision(
CreateDivisionRequest request, ICurrentUser user, IPermissionService permissions,
IAuditLog audit, OrgBoardDbContext db, TimeProvider clock, CancellationToken ct)
{
if (!permissions.Has(Capability.CreateProductsAndTeams, ScopeRef.Org(request.OrganizationId)))
{
return Results.Forbid();
}
if (string.IsNullOrWhiteSpace(request.Name))
{
return Results.BadRequest("Name is required.");
}
var division = new Division(request.OrganizationId, request.Name.Trim(), clock.GetUtcNow());
db.Divisions.Add(division);
await db.SaveChangesAsync(ct);
await audit.WriteAsync(new AuditEvent("division.created", "Division", division.Id, user.MemberId, division.Name), ct);
return Results.Ok(new DivisionResponse(division.Id, division.OrganizationId, division.Name));
}
private static async Task<IResult> ListDivisions(
Guid organizationId, IPermissionService permissions, OrgBoardDbContext db, CancellationToken ct)
{
if (!permissions.Has(Capability.ViewBoard, ScopeRef.Org(organizationId)))
{
return Results.Forbid();
}
var divisions = await db.Divisions
.Where(d => d.OrganizationId == organizationId)
.OrderBy(d => d.CreatedAtUtc)
.Select(d => new DivisionResponse(d.Id, d.OrganizationId, d.Name))
.ToListAsync(ct);
return Results.Ok(divisions);
}
private static async Task<IResult> CreateProduct(
CreateProductRequest request, ICurrentUser user, IPermissionService permissions,
IAuditLog audit, OrgBoardDbContext db, TimeProvider clock, CancellationToken ct)
{
if (!permissions.Has(Capability.CreateProductsAndTeams, ScopeRef.Org(request.OrganizationId)))
{
return Results.Forbid();
}
if (string.IsNullOrWhiteSpace(request.Name))
{
return Results.BadRequest("Name is required.");
}
if (request.DivisionId is { } divisionId
&& !await db.Divisions.AnyAsync(d => d.Id == divisionId && d.OrganizationId == request.OrganizationId, ct))
{
return Results.BadRequest("Division not found in this organization.");
}
var product = new Product(request.OrganizationId, request.DivisionId, request.Name.Trim(), request.Kind, clock.GetUtcNow());
db.Products.Add(product);
await db.SaveChangesAsync(ct);
await audit.WriteAsync(new AuditEvent("product.created", "Product", product.Id, user.MemberId, product.Name), ct);
return Results.Ok(new ProductResponse(product.Id, product.OrganizationId, product.DivisionId, product.Name, product.Kind.ToString()));
}
private static async Task<IResult> ListProducts(
Guid organizationId, IPermissionService permissions, OrgBoardDbContext db, CancellationToken ct)
{
if (!permissions.Has(Capability.ViewBoard, ScopeRef.Org(organizationId)))
{
return Results.Forbid();
}
var products = await db.Products
.Where(p => p.OrganizationId == organizationId)
.OrderBy(p => p.CreatedAtUtc)
.Select(p => new ProductResponse(p.Id, p.OrganizationId, p.DivisionId, p.Name, p.Kind.ToString()))
.ToListAsync(ct);
return Results.Ok(products);
}
private static async Task<IResult> ListTeams(
@@ -105,7 +195,7 @@ internal static class OrgBoardEndpoints
var teams = await db.Teams
.Where(t => t.OrganizationId == organizationId)
.OrderBy(t => t.CreatedAtUtc)
.Select(t => new TeamResponse(t.Id, t.OrganizationId, t.Name))
.Select(t => new TeamResponse(t.Id, t.OrganizationId, t.Name, t.ProductId))
.ToListAsync(ct);
return Results.Ok(teams);
@@ -0,0 +1,317 @@
// <auto-generated />
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using TeamUp.Modules.OrgBoard.Persistence;
#nullable disable
namespace TeamUp.Modules.OrgBoard.Persistence.Migrations
{
[DbContext(typeof(OrgBoardDbContext))]
[Migration("20260610142338_AddDivisionsAndProducts")]
partial class AddDivisionsAndProducts
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("orgboard")
.HasAnnotation("ProductVersion", "10.0.8")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("TeamUp.Modules.OrgBoard.Domain.Agent", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("ApiConfigId")
.HasColumnType("uuid");
b.Property<string>("Autonomy")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<DateTimeOffset>("CreatedAtUtc")
.HasColumnType("timestamp with time zone");
b.PrimitiveCollection<List<string>>("Docs")
.IsRequired()
.HasColumnType("text[]");
b.Property<Guid?>("FallbackApiConfigId")
.HasColumnType("uuid");
b.Property<string>("Monogram")
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(120)
.HasColumnType("character varying(120)");
b.Property<Guid>("SeatId")
.HasColumnType("uuid");
b.PrimitiveCollection<List<string>>("SkillKeys")
.IsRequired()
.HasColumnType("text[]");
b.Property<DateTimeOffset>("UpdatedAtUtc")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("SeatId")
.IsUnique();
b.ToTable("agents", "orgboard");
});
modelBuilder.Entity("TeamUp.Modules.OrgBoard.Domain.Division", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAtUtc")
.HasColumnType("timestamp with time zone");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<Guid>("OrganizationId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("OrganizationId");
b.ToTable("divisions", "orgboard");
});
modelBuilder.Entity("TeamUp.Modules.OrgBoard.Domain.Organization", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAtUtc")
.HasColumnType("timestamp with time zone");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.HasKey("Id");
b.ToTable("organizations", "orgboard");
});
modelBuilder.Entity("TeamUp.Modules.OrgBoard.Domain.Product", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAtUtc")
.HasColumnType("timestamp with time zone");
b.Property<Guid?>("DivisionId")
.HasColumnType("uuid");
b.Property<string>("Kind")
.IsRequired()
.HasMaxLength(16)
.HasColumnType("character varying(16)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<Guid>("OrganizationId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("DivisionId");
b.HasIndex("OrganizationId");
b.ToTable("products", "orgboard");
});
modelBuilder.Entity("TeamUp.Modules.OrgBoard.Domain.Seat", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid?>("AgentId")
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAtUtc")
.HasColumnType("timestamp with time zone");
b.Property<Guid?>("MemberId")
.HasColumnType("uuid");
b.Property<string>("RoleName")
.IsRequired()
.HasMaxLength(120)
.HasColumnType("character varying(120)");
b.Property<string>("State")
.IsRequired()
.HasMaxLength(16)
.HasColumnType("character varying(16)");
b.Property<Guid>("TeamId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("TeamId");
b.ToTable("seats", "orgboard");
});
modelBuilder.Entity("TeamUp.Modules.OrgBoard.Domain.Team", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAtUtc")
.HasColumnType("timestamp with time zone");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<Guid>("OrganizationId")
.HasColumnType("uuid");
b.Property<Guid?>("ProductId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("OrganizationId");
b.HasIndex("ProductId");
b.ToTable("teams", "orgboard");
});
modelBuilder.Entity("TeamUp.Modules.OrgBoard.Domain.WorkItem", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid?>("AssigneeId")
.HasColumnType("uuid");
b.Property<string>("AssigneeKind")
.IsRequired()
.HasMaxLength(16)
.HasColumnType("character varying(16)");
b.Property<DateTimeOffset>("CreatedAtUtc")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("CreatedByMemberId")
.HasColumnType("uuid");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<Guid?>("ParentId")
.HasColumnType("uuid");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(16)
.HasColumnType("character varying(16)");
b.Property<Guid>("TeamId")
.HasColumnType("uuid");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("character varying(300)");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(16)
.HasColumnType("character varying(16)");
b.Property<DateTimeOffset>("UpdatedAtUtc")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("TeamId");
b.HasIndex("AssigneeKind", "AssigneeId");
b.ToTable("work_items", "orgboard");
});
modelBuilder.Entity("TeamUp.Modules.OrgBoard.Domain.WorkItemTransition", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid?>("ActorMemberId")
.HasColumnType("uuid");
b.Property<string>("FromStatus")
.IsRequired()
.HasMaxLength(16)
.HasColumnType("character varying(16)");
b.Property<DateTimeOffset>("OccurredAtUtc")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("TeamId")
.HasColumnType("uuid");
b.Property<string>("ToStatus")
.IsRequired()
.HasMaxLength(16)
.HasColumnType("character varying(16)");
b.Property<Guid>("WorkItemId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("TeamId");
b.HasIndex("WorkItemId");
b.ToTable("work_item_transitions", "orgboard");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,100 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace TeamUp.Modules.OrgBoard.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddDivisionsAndProducts : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Guid>(
name: "ProductId",
schema: "orgboard",
table: "teams",
type: "uuid",
nullable: true);
migrationBuilder.CreateTable(
name: "divisions",
schema: "orgboard",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
OrganizationId = table.Column<Guid>(type: "uuid", nullable: false),
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
CreatedAtUtc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_divisions", x => x.Id);
});
migrationBuilder.CreateTable(
name: "products",
schema: "orgboard",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
OrganizationId = table.Column<Guid>(type: "uuid", nullable: false),
DivisionId = table.Column<Guid>(type: "uuid", nullable: true),
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
Kind = table.Column<string>(type: "character varying(16)", maxLength: 16, nullable: false),
CreatedAtUtc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_products", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_teams_ProductId",
schema: "orgboard",
table: "teams",
column: "ProductId");
migrationBuilder.CreateIndex(
name: "IX_divisions_OrganizationId",
schema: "orgboard",
table: "divisions",
column: "OrganizationId");
migrationBuilder.CreateIndex(
name: "IX_products_DivisionId",
schema: "orgboard",
table: "products",
column: "DivisionId");
migrationBuilder.CreateIndex(
name: "IX_products_OrganizationId",
schema: "orgboard",
table: "products",
column: "OrganizationId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "divisions",
schema: "orgboard");
migrationBuilder.DropTable(
name: "products",
schema: "orgboard");
migrationBuilder.DropIndex(
name: "IX_teams_ProductId",
schema: "orgboard",
table: "teams");
migrationBuilder.DropColumn(
name: "ProductId",
schema: "orgboard",
table: "teams");
}
}
}
@@ -75,6 +75,30 @@ namespace TeamUp.Modules.OrgBoard.Persistence.Migrations
b.ToTable("agents", "orgboard");
});
modelBuilder.Entity("TeamUp.Modules.OrgBoard.Domain.Division", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAtUtc")
.HasColumnType("timestamp with time zone");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<Guid>("OrganizationId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("OrganizationId");
b.ToTable("divisions", "orgboard");
});
modelBuilder.Entity("TeamUp.Modules.OrgBoard.Domain.Organization", b =>
{
b.Property<Guid>("Id")
@@ -94,6 +118,40 @@ namespace TeamUp.Modules.OrgBoard.Persistence.Migrations
b.ToTable("organizations", "orgboard");
});
modelBuilder.Entity("TeamUp.Modules.OrgBoard.Domain.Product", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAtUtc")
.HasColumnType("timestamp with time zone");
b.Property<Guid?>("DivisionId")
.HasColumnType("uuid");
b.Property<string>("Kind")
.IsRequired()
.HasMaxLength(16)
.HasColumnType("character varying(16)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<Guid>("OrganizationId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("DivisionId");
b.HasIndex("OrganizationId");
b.ToTable("products", "orgboard");
});
modelBuilder.Entity("TeamUp.Modules.OrgBoard.Domain.Seat", b =>
{
b.Property<Guid>("Id")
@@ -146,10 +204,15 @@ namespace TeamUp.Modules.OrgBoard.Persistence.Migrations
b.Property<Guid>("OrganizationId")
.HasColumnType("uuid");
b.Property<Guid?>("ProductId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("OrganizationId");
b.HasIndex("ProductId");
b.ToTable("teams", "orgboard");
});
@@ -8,6 +8,8 @@ internal sealed class OrgBoardDbContext(DbContextOptions<OrgBoardDbContext> opti
: DbContext(options), IModuleDbContext
{
public DbSet<Organization> Organizations => Set<Organization>();
public DbSet<Division> Divisions => Set<Division>();
public DbSet<Product> Products => Set<Product>();
public DbSet<Team> Teams => Set<Team>();
public DbSet<Seat> Seats => Set<Seat>();
public DbSet<Agent> Agents => Set<Agent>();
@@ -25,12 +27,31 @@ internal sealed class OrgBoardDbContext(DbContextOptions<OrgBoardDbContext> opti
organization.Property(o => o.Name).HasMaxLength(200).IsRequired();
});
modelBuilder.Entity<Division>(division =>
{
division.ToTable("divisions");
division.HasKey(d => d.Id);
division.Property(d => d.Name).HasMaxLength(200).IsRequired();
division.HasIndex(d => d.OrganizationId);
});
modelBuilder.Entity<Product>(product =>
{
product.ToTable("products");
product.HasKey(p => p.Id);
product.Property(p => p.Name).HasMaxLength(200).IsRequired();
product.Property(p => p.Kind).HasConversion<string>().HasMaxLength(16);
product.HasIndex(p => p.OrganizationId);
product.HasIndex(p => p.DivisionId);
});
modelBuilder.Entity<Team>(team =>
{
team.ToTable("teams");
team.HasKey(t => t.Id);
team.Property(t => t.Name).HasMaxLength(200).IsRequired();
team.HasIndex(t => t.OrganizationId);
team.HasIndex(t => t.ProductId);
});
modelBuilder.Entity<Seat>(seat =>