using System.Text.Json; using Microsoft.EntityFrameworkCore; using WordVersioning.Api.Data; using WordVersioning.Api.Models; namespace WordVersioning.Api.Services; /// entry หนึ่ง field ใน `DocumentVersion.FieldsSnapshotJson` (เอกสารที่ผูก template) public record FieldSnapshotEntry(string Value, bool Missing); public record FieldSyncResult(string? SnapshotJson); public interface IFieldSyncService { /// FR-11 — เทียบ fields ที่ส่งมากับ TemplateField ของ template ที่ document ผูกอยู่: /// stage การ upsert `DocumentFieldValue` (latest value) ลง change tracker (**caller เป็นคน SaveChanges**) /// แล้วคืน snapshot JSON ของ version นี้ไปเก็บใน `DocumentVersion.FieldsSnapshotJson` /// /// policy ตาม spec: field ขาด (ผู้ใช้ลบ content control) → เก็บค่าเดิม + mark missing **ไม่ fail**; /// field เกินนิยาม → ignore; `UpdatedByUserId` จาก JWT เสมอ Task StageFieldSyncAsync( Document document, Guid sourceVersionId, IReadOnlyDictionary sentFields, DateTimeOffset now, CancellationToken ct); } public class FieldSyncService(AppDbContext db, ICurrentUser currentUser) : IFieldSyncService { // camelCase ให้ตรงกับ JSON ที่ API คืนทั้งระบบ private static readonly JsonSerializerOptions SnapshotJsonOptions = new(JsonSerializerDefaults.Web); public async Task StageFieldSyncAsync( Document document, Guid sourceVersionId, IReadOnlyDictionary sentFields, DateTimeOffset now, CancellationToken ct) { if (document.TemplateId is null) { // เอกสารไม่ผูก template (track เองจาก add-in) — ไม่มีนิยามให้เทียบ // เก็บ map ดิบตามที่ส่งมาใน snapshot ไว้ดู history ได้ (Phase 2 behavior) return sentFields.Count == 0 ? new FieldSyncResult(null) : new FieldSyncResult(JsonSerializer.Serialize(sentFields, SnapshotJsonOptions)); } var fieldDefs = await db.TemplateFields.AsNoTracking() .Where(f => f.TemplateId == document.TemplateId) .OrderBy(f => f.SortOrder) .ToListAsync(ct); if (fieldDefs.Count == 0) return new FieldSyncResult(null); // tracked — update ทีหลังให้เข้า SaveChanges เดียวกับ version insert var existingValues = await db.DocumentFieldValues .Where(v => v.DocumentId == document.Id) .ToDictionaryAsync(v => v.FieldKey, ct); var snapshot = new Dictionary(fieldDefs.Count); foreach (var def in fieldDefs) { if (sentFields.TryGetValue(def.Key, out var sentValue)) { sentValue ??= string.Empty; if (existingValues.TryGetValue(def.Key, out var row)) { row.Value = sentValue; row.SourceVersionId = sourceVersionId; row.UpdatedByUserId = currentUser.UserId; row.UpdatedAt = now; } else { db.DocumentFieldValues.Add(new DocumentFieldValue { DocumentId = document.Id, FieldKey = def.Key, Value = sentValue, SourceVersionId = sourceVersionId, UpdatedByUserId = currentUser.UserId, UpdatedAt = now, }); } snapshot[def.Key] = new FieldSnapshotEntry(sentValue, Missing: false); } else { // field หายจากเอกสาร (ผู้ใช้ลบ content control) — เก็บค่าเดิม + mark missing ไม่ fail (AC-14) var oldValue = existingValues.TryGetValue(def.Key, out var row) ? row.Value : string.Empty; snapshot[def.Key] = new FieldSnapshotEntry(oldValue, Missing: true); } } return new FieldSyncResult(JsonSerializer.Serialize(snapshot, SnapshotJsonOptions)); } }