diff --git a/src/food-market.infrastructure/Integrations/MoySklad/MoySkladClient.cs b/src/food-market.infrastructure/Integrations/MoySklad/MoySkladClient.cs index 618a589..bed9460 100644 --- a/src/food-market.infrastructure/Integrations/MoySklad/MoySkladClient.cs +++ b/src/food-market.infrastructure/Integrations/MoySklad/MoySkladClient.cs @@ -63,60 +63,53 @@ public async Task> WhoAmIAsync(string token, C : MoySkladApiResult.Ok(org); } + // MoySklad list endpoints по умолчанию возвращают только активных (archived=false). + // Чтобы получить архивных, нужен явный filter=archived=true. Делаем два прохода: + // сначала активные (default), затем архивные — и отдаём всё одним потоком. + public async IAsyncEnumerable StreamProductsAsync( string token, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct) { - const int pageSize = 1000; - var offset = 0; - while (true) - { - using var req = Build(HttpMethod.Get, $"entity/product?limit={pageSize}&offset={offset}", token); - using var res = await _http.SendAsync(req, ct); - res.EnsureSuccessStatusCode(); - var page = await res.Content.ReadFromJsonAsync>(Json, ct); - if (page is null || page.Rows.Count == 0) yield break; - foreach (var p in page.Rows) yield return p; - if (page.Rows.Count < pageSize) yield break; - offset += pageSize; - } + await foreach (var p in StreamPagedAsync(token, "entity/product", archivedOnly: false, ct)) yield return p; + await foreach (var p in StreamPagedAsync(token, "entity/product", archivedOnly: true, ct)) yield return p; } public async IAsyncEnumerable StreamCounterpartiesAsync( string token, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct) { - const int pageSize = 1000; - var offset = 0; - while (true) - { - using var req = Build(HttpMethod.Get, $"entity/counterparty?limit={pageSize}&offset={offset}", token); - using var res = await _http.SendAsync(req, ct); - res.EnsureSuccessStatusCode(); - var page = await res.Content.ReadFromJsonAsync>(Json, ct); - if (page is null || page.Rows.Count == 0) yield break; - foreach (var c in page.Rows) yield return c; - if (page.Rows.Count < pageSize) yield break; - offset += pageSize; - } + await foreach (var c in StreamPagedAsync(token, "entity/counterparty", archivedOnly: false, ct)) yield return c; + await foreach (var c in StreamPagedAsync(token, "entity/counterparty", archivedOnly: true, ct)) yield return c; } public async Task> GetAllFoldersAsync(string token, CancellationToken ct) { var all = new List(); - var offset = 0; - const int pageSize = 1000; - while (true) - { - using var req = Build(HttpMethod.Get, $"entity/productfolder?limit={pageSize}&offset={offset}", token); - using var res = await _http.SendAsync(req, ct); - res.EnsureSuccessStatusCode(); - var page = await res.Content.ReadFromJsonAsync>(Json, ct); - if (page is null || page.Rows.Count == 0) break; - all.AddRange(page.Rows); - if (page.Rows.Count < pageSize) break; - offset += pageSize; - } + await foreach (var f in StreamPagedAsync(token, "entity/productfolder", archivedOnly: false, ct)) all.Add(f); + await foreach (var f in StreamPagedAsync(token, "entity/productfolder", archivedOnly: true, ct)) all.Add(f); return all; } + + private async IAsyncEnumerable StreamPagedAsync( + string token, + string path, + bool archivedOnly, + [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct) + { + const int pageSize = 1000; + var offset = 0; + var filterSuffix = archivedOnly ? "&filter=archived=true" : ""; + while (true) + { + using var req = Build(HttpMethod.Get, $"{path}?limit={pageSize}&offset={offset}{filterSuffix}", token); + using var res = await _http.SendAsync(req, ct); + res.EnsureSuccessStatusCode(); + var page = await res.Content.ReadFromJsonAsync>(Json, ct); + if (page is null || page.Rows.Count == 0) yield break; + foreach (var row in page.Rows) yield return row; + if (page.Rows.Count < pageSize) yield break; + offset += pageSize; + } + } } diff --git a/src/food-market.infrastructure/Integrations/MoySklad/MoySkladImportService.cs b/src/food-market.infrastructure/Integrations/MoySklad/MoySkladImportService.cs index 20ff3a9..89ec7bb 100644 --- a/src/food-market.infrastructure/Integrations/MoySklad/MoySkladImportService.cs +++ b/src/food-market.infrastructure/Integrations/MoySklad/MoySkladImportService.cs @@ -69,7 +69,7 @@ static foodmarket.Domain.Catalog.CounterpartyType ResolveType(string? companyTyp await foreach (var c in _client.StreamCounterpartiesAsync(token, ct)) { total++; - if (c.Archived) { skipped++; continue; } + // Архивных не пропускаем — импортируем как IsActive=false (см. ApplyCounterparty). try { @@ -144,11 +144,12 @@ await foreach (var c in _client.StreamCounterpartiesAsync(token, ct)) .IgnoreQueryFilters() .ToDictionaryAsync(c => c.Name, c => c.Id, ct); - // Import folders first — build flat then link parents. + // Import folders first — build flat then link parents. Архивные тоже берём, + // помечаем IsActive=false — у MoySklad у productfolder есть archived. var folders = await _client.GetAllFoldersAsync(token, ct); var localGroupByMsId = new Dictionary(); var groupsCreated = 0; - foreach (var f in folders.Where(f => !f.Archived).OrderBy(f => f.PathName?.Length ?? 0)) + foreach (var f in folders.OrderBy(f => f.PathName?.Length ?? 0)) { if (f.Id is null) continue; var existing = await _db.ProductGroups.FirstOrDefaultAsync( @@ -163,7 +164,7 @@ await foreach (var c in _client.StreamCounterpartiesAsync(token, ct)) OrganizationId = orgId, Name = f.Name, Path = string.IsNullOrEmpty(f.PathName) ? f.Name : $"{f.PathName}/{f.Name}", - IsActive = true, + IsActive = !f.Archived, }; _db.ProductGroups.Add(g); localGroupByMsId[f.Id] = g.Id; @@ -188,7 +189,7 @@ await foreach (var c in _client.StreamCounterpartiesAsync(token, ct)) await foreach (var p in _client.StreamProductsAsync(token, ct)) { total++; - if (p.Archived) { skipped++; continue; } + // Архивных не пропускаем — импортируем как IsActive=false. var article = string.IsNullOrWhiteSpace(p.Article) ? p.Code : p.Article; var alreadyByArticle = !string.IsNullOrWhiteSpace(article) && existingByArticle.ContainsKey(article);