using System.Security.Cryptography; using System.Text; using Microsoft.EntityFrameworkCore; using TeamUp.Modules.OrgBoard.Domain; using TeamUp.Modules.OrgBoard.Persistence; namespace TeamUp.Modules.OrgBoard.Profiles; /// Upserts a product profile by (org, key, version) — the one place product profiles are written. internal sealed class ProductProfileWriter(OrgBoardDbContext db, TimeProvider clock) { /// /// When true the row must not already exist: a colliding (org, key, version) trips the unique /// index (DbUpdateException) instead of overwriting — the install path uses this so a race can't /// clobber an existing profile. /// public async Task UpsertAsync( ProductProfileManifest manifest, string body, Guid? organizationId, ProfileOrigin origin, Guid? authoredByMemberId, bool insertOnly = false, CancellationToken cancellationToken = default) { var now = clock.GetUtcNow(); var canonical = $"{manifest.Id}\n{manifest.Version}\n{manifest.Name}\n{manifest.Summary}\n{body}"; var contentHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(canonical))); var profile = insertOnly ? null : await db.ProductProfiles.FirstOrDefaultAsync( p => p.OrganizationId == organizationId && p.ProfileKey == manifest.Id && p.Version == manifest.Version, cancellationToken); var isNew = profile is null; profile ??= ProductProfile.Create(manifest.Id, manifest.Version, organizationId, now); profile.Apply(manifest, body, contentHash, origin, authoredByMemberId, now); if (isNew) { db.ProductProfiles.Add(profile); } await db.SaveChangesAsync(cancellationToken); return profile; } }