fix(worker): fix Bandel ticket sync and reactivate the integration
The status PATCH needs statusName alongside statusId or the API 400s, so every write-back had been silently failing. The board's flat assigneeName is null on every live ticket; the real assignee list is ticketAssignees[]. Import now also picks up In Bearbeitung tickets, not just Offen ones. Both issues are live-verified against the real API, so the feature flag flips back on.
This commit is contained in:
@@ -49,10 +49,19 @@ public sealed class BandelTicketClient
|
||||
=> await SendAsync<List<TicketBoardItem>>(
|
||||
HttpMethod.Get, $"/api/Board/project/{projectId}", null, ct);
|
||||
|
||||
/// <summary>
|
||||
/// ⚠️ Die Route verlangt <c>statusName</c> zusätzlich zu <c>statusId</c>, sonst 400
|
||||
/// ("The StatusName field is required."). Der Wert wird nicht gegen die Id validiert —
|
||||
/// gesetzt wird immer nach <c>statusId</c> — er darf aber nicht fehlen oder leer sein.
|
||||
/// Live geprüft am 2026-09-01.
|
||||
/// </summary>
|
||||
public async Task SetStatusAsync(int ticketId, int statusId, CancellationToken ct)
|
||||
=> await SendAsync<JsonElement>(
|
||||
HttpMethod.Patch, $"/api/Ticket/{ticketId}/status",
|
||||
new StringContent($"{{\"statusId\":{statusId}}}", Encoding.UTF8, "application/json"), ct);
|
||||
new StringContent(
|
||||
$"{{\"statusId\":{statusId},\"statusName\":\"{TicketStatusMap.NameOf(statusId)}\"}}",
|
||||
Encoding.UTF8, "application/json"),
|
||||
ct);
|
||||
|
||||
private async Task<T> SendAsync<T>(HttpMethod method, string path, HttpContent? body, CancellationToken ct)
|
||||
{
|
||||
|
||||
@@ -16,6 +16,11 @@ public sealed record TicketIdentity(
|
||||
[property: JsonPropertyName("scopes")] IReadOnlyList<string> Scopes);
|
||||
|
||||
/// Ein Board-Item (Ausschnitt aus GetTicketSummaryDto — nur was der Import braucht).
|
||||
/// ⚠️ <see cref="AssigneeId"/>/<see cref="AssigneeName"/> sind live durchgehend null (geprüft über
|
||||
/// alle 63 Tickets der Abteilung Entwicklung, 2026-09-01). Die echten Zuweisungen stehen in
|
||||
/// <see cref="TicketAssignees"/>; nur dessen <c>assigneeName</c> hat dasselbe Format wie
|
||||
/// <see cref="TicketIdentity.UserName"/> (<c>mika.kuns</c>). <c>personInChargeName</c> wäre die
|
||||
/// falsche Quelle — dort steht mal der Login, mal der Anzeigename ("Mika Kuns").
|
||||
public sealed record TicketBoardItem(
|
||||
[property: JsonPropertyName("id")] int Id,
|
||||
[property: JsonPropertyName("title")] string Title,
|
||||
@@ -23,6 +28,11 @@ public sealed record TicketBoardItem(
|
||||
[property: JsonPropertyName("statusId")] int StatusId,
|
||||
[property: JsonPropertyName("statusName")] string? StatusName,
|
||||
[property: JsonPropertyName("assigneeID")] int? AssigneeId,
|
||||
[property: JsonPropertyName("assigneeName")] string? AssigneeName,
|
||||
[property: JsonPropertyName("ticketAssignees")] IReadOnlyList<TicketAssignee>? TicketAssignees = null);
|
||||
|
||||
public sealed record TicketAssignee(
|
||||
[property: JsonPropertyName("assigneeId")] int AssigneeId,
|
||||
[property: JsonPropertyName("assigneeName")] string? AssigneeName);
|
||||
|
||||
/// GET /api/Navigation/sidebar — Abteilung mit ihren Projekten.
|
||||
|
||||
@@ -16,7 +16,9 @@ public sealed record TicketImportResult(int Examined, int Created);
|
||||
/// </summary>
|
||||
public sealed class TicketImportService
|
||||
{
|
||||
private const int StatusOffen = 1;
|
||||
// Offen UND In Bearbeitung: ein Ticket, an dem schon gearbeitet wird, ist genauso ein
|
||||
// Arbeitsauftrag wie ein neues. Fertig/Archiviert bleiben draußen.
|
||||
private static readonly int[] ImportableStatuses = [1, TicketStatusMap.InBearbeitung];
|
||||
|
||||
private readonly TicketClientFactory _clients;
|
||||
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||
@@ -48,8 +50,8 @@ public sealed class TicketImportService
|
||||
var board = await client.GetProjectBoardAsync(projectId, ct);
|
||||
|
||||
var mine = board
|
||||
.Where(t => t.StatusId == StatusOffen)
|
||||
.Where(t => string.Equals(t.AssigneeName, identity.UserName, StringComparison.OrdinalIgnoreCase))
|
||||
.Where(t => ImportableStatuses.Contains(t.StatusId))
|
||||
.Where(t => IsAssignedTo(t, identity.UserName))
|
||||
.ToList();
|
||||
|
||||
var known = await ctx.Tasks.AsNoTracking()
|
||||
@@ -87,6 +89,16 @@ public sealed class TicketImportService
|
||||
return new TicketImportResult(board.Count, created);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Zuweisung ausschließlich über <c>ticketAssignees</c> — das flache <c>assigneeName</c> ist
|
||||
/// live auf jedem Ticket null (siehe <see cref="TicketBoardItem"/>). Ein Ticket kann mehrere
|
||||
/// Zuweisungen haben; eine davon muss der Token-Inhaber sein.
|
||||
/// </summary>
|
||||
private static bool IsAssignedTo(TicketBoardItem ticket, string userName)
|
||||
=> ticket.TicketAssignees?.Any(
|
||||
a => string.Equals(a.AssigneeName, userName, StringComparison.OrdinalIgnoreCase))
|
||||
?? false;
|
||||
|
||||
private static string BuildDescription(TicketBoardItem ticket)
|
||||
{
|
||||
var body = string.IsNullOrWhiteSpace(ticket.Description) ? "" : ticket.Description.Trim() + "\n\n";
|
||||
|
||||
@@ -13,6 +13,20 @@ public static class TicketStatusMap
|
||||
|
||||
private const string BandelPrefix = "bandel:";
|
||||
|
||||
/// <summary>
|
||||
/// Namen wie von <c>GET /api/ticketsystem/statuses</c> geliefert. Braucht der PATCH-Body,
|
||||
/// der <c>statusName</c> als required-Feld verlangt (siehe
|
||||
/// <see cref="BandelTicketClient.SetStatusAsync"/>). Nie leer — ein leerer Wert wäre ein 400.
|
||||
/// </summary>
|
||||
public static string NameOf(int statusId) => statusId switch
|
||||
{
|
||||
1 => "Offen",
|
||||
InBearbeitung => "InBearbeitung",
|
||||
Fertig => "Fertig",
|
||||
4 => "Archiviert",
|
||||
_ => "Keine",
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Zielstatus im Ticketsystem, oder null wenn dieser Task-Status nichts melden soll.
|
||||
/// "Offen" (1) wird nie geschrieben: das ist der Eingangszustand. Failed/Cancelled lassen
|
||||
|
||||
Reference in New Issue
Block a user