From 7924cda52053a18f7fd99c0be0faf2ef0966ef5e Mon Sep 17 00:00:00 2001 From: PointStar Date: Tue, 12 May 2026 21:03:16 +0300 Subject: [PATCH] fix(documents): set publishDate on Document create (Marcus-Law requires it) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The server returned: 400 {"messageTranslation":{"label":"validationFailure", "data":{"field":"publishDate","type":"required"}}} Marcus-Law's EspoCRM install marks Document.publishDate as required. By default the field is optional, but this tenant tightened it. Send today's date (UTC, YYYY-MM-DD form) so the validation passes — the user can still edit the published date in EspoCRM later if needed. Also add the same verbose-error pattern to CreateDocumentAsync that we already use on UploadAttachmentAsync: when the create returns non-2xx, log the response body + X-Status-Reason header so any future required- field mismatch shows up as a single readable line instead of "שמירת הקבצים נכשלה". Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Services/EspoCrmClient.cs | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/OutlookAddin.Core/Services/EspoCrmClient.cs b/src/OutlookAddin.Core/Services/EspoCrmClient.cs index af50309..d8b0450 100644 --- a/src/OutlookAddin.Core/Services/EspoCrmClient.cs +++ b/src/OutlookAddin.Core/Services/EspoCrmClient.cs @@ -262,21 +262,47 @@ namespace MarcusLaw.OutlookAddin.Core.Services if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("name required", nameof(name)); if (string.IsNullOrWhiteSpace(attachmentFileId)) throw new ArgumentException("attachmentFileId required", nameof(attachmentFileId)); + // Marcus-Law's EspoCRM has `publishDate` configured as required on + // Document. The field accepts a plain date (YYYY-MM-DD). Default + // to today; the user can edit it later in EspoCRM if needed. + var todayIso = DateTimeOffset.UtcNow.ToString("yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture); + var payload = new Dictionary { ["name"] = name, ["fileId"] = attachmentFileId, - ["status"] = "Active" + ["status"] = "Active", + ["publishDate"] = todayIso }; if (!string.IsNullOrWhiteSpace(parentType)) payload["parentType"] = parentType; if (!string.IsNullOrWhiteSpace(parentId)) payload["parentId"] = parentId; if (!string.IsNullOrWhiteSpace(folderId)) payload["folderId"] = folderId; var json = JsonSerializer.Serialize(payload, JsonOptions); + _logger.Information( + "CreateDocument POST /Document (name={Name}, fileId={FileId}, parent={ParentType}/{ParentId}, folder={FolderId})", + name, attachmentFileId, parentType, parentId, folderId ?? "(none)"); + var response = await ExecuteAsync(HttpMethod.Post, "Document", json, cancellationToken).ConfigureAwait(false); try { - await EnsureSuccessAsync(response).ConfigureAwait(false); + if (!response.IsSuccessStatusCode) + { + var errBody = await ReadBodyAsync(response).ConfigureAwait(false); + var reason = response.Headers.TryGetValues("X-Status-Reason", out var v) + ? string.Join(",", v) : "(none)"; + _logger.Warning( + "CreateDocument failed: {Status} (X-Status-Reason={Reason}) body={Body}", + response.StatusCode, reason, string.IsNullOrEmpty(errBody) ? "(empty)" : Truncate(errBody, 500)); + if (response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.Forbidden) + { + throw new EspoCrmAuthorizationException( + $"EspoCRM rejected credentials ({(int)response.StatusCode} {response.StatusCode}). Re-enter API key."); + } + throw new EspoCrmHttpException(response.StatusCode, errBody, + $"EspoCRM Document create returned {(int)response.StatusCode} {response.StatusCode}" + + (string.IsNullOrEmpty(reason) ? "" : " (reason: " + reason + ")")); + } var body = await ReadBodyAsync(response).ConfigureAwait(false); return JsonSerializer.Deserialize(body, JsonOptions) ?? throw new EspoCrmHttpException(response.StatusCode, body, "Empty DocumentEntity from EspoCRM");