Fix formula export/import: embed fields as real JSON array, not escaped string

Previously FieldsJson was serialized as an escaped string in the export
file, which was fragile and unreadable. Now parsed into a JsonElement and
embedded as a proper JSON array under the key "fields". Import reads it
back with GetRawText() to reconstruct the stored string. This prevents
the null/empty fields bug caused by manually-edited or round-tripped files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 11:06:38 -04:00
parent cd4c233b60
commit 23e64829bb
@@ -3051,6 +3051,15 @@ public class CompanySettingsController : Controller
var companyId = _tenantContext.GetCurrentCompanyId()!.Value; var companyId = _tenantContext.GetCurrentCompanyId()!.Value;
var templates = await _unitOfWork.CustomItemTemplates.FindAsync(t => t.CompanyId == companyId); var templates = await _unitOfWork.CustomItemTemplates.FindAsync(t => t.CompanyId == companyId);
// Parse FieldsJson into a real JsonElement so it is embedded as a proper JSON array
// in the export file rather than as an escaped string. This makes the file human-readable
// and avoids round-trip corruption when files are manually edited.
static System.Text.Json.JsonElement ParseFields(string? raw)
{
try { return System.Text.Json.JsonDocument.Parse(raw ?? "[]").RootElement.Clone(); }
catch { return System.Text.Json.JsonDocument.Parse("[]").RootElement.Clone(); }
}
var export = new var export = new
{ {
exportedAt = DateTime.UtcNow, exportedAt = DateTime.UtcNow,
@@ -3062,7 +3071,7 @@ public class CompanySettingsController : Controller
t.Name, t.Name,
t.Description, t.Description,
t.OutputMode, t.OutputMode,
t.FieldsJson, Fields = ParseFields(t.FieldsJson),
t.Formula, t.Formula,
t.DefaultRate, t.DefaultRate,
t.RateLabel, t.RateLabel,
@@ -3142,7 +3151,8 @@ public class CompanySettingsController : Controller
Name = name, Name = name,
Description = item.TryGetProperty("description", out var d) ? d.GetString() : null, Description = item.TryGetProperty("description", out var d) ? d.GetString() : null,
OutputMode = item.TryGetProperty("outputMode", out var om) ? om.GetString() ?? "FixedRate" : "FixedRate", OutputMode = item.TryGetProperty("outputMode", out var om) ? om.GetString() ?? "FixedRate" : "FixedRate",
FieldsJson = item.TryGetProperty("fieldsJson", out var fj) ? fj.GetString() ?? "[]" : "[]", // "fields" is a real JSON array in the export; GetRawText() reconstructs the string
FieldsJson = item.TryGetProperty("fields", out var fj) ? fj.GetRawText() : "[]",
Formula = item.TryGetProperty("formula", out var f) ? f.GetString() ?? "" : "", Formula = item.TryGetProperty("formula", out var f) ? f.GetString() ?? "" : "",
DefaultRate = item.TryGetProperty("defaultRate", out var dr) && dr.ValueKind == System.Text.Json.JsonValueKind.Number ? dr.GetDecimal() : null, DefaultRate = item.TryGetProperty("defaultRate", out var dr) && dr.ValueKind == System.Text.Json.JsonValueKind.Number ? dr.GetDecimal() : null,
RateLabel = item.TryGetProperty("rateLabel", out var rl) ? rl.GetString() : null, RateLabel = item.TryGetProperty("rateLabel", out var rl) ? rl.GetString() : null,