// Prove the key, then land a sheet of stock as one sync. .NET 8, no packages. // // FREEPORT_KEY=fp_test_... dotnet run -- stock-sheet-template.csv using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes; var baseUrl = Environment.GetEnvironmentVariable("FREEPORT_API") ?? "https://sandbox-api.freeport.gg/v1"; var key = Environment.GetEnvironmentVariable("FREEPORT_KEY") ?? throw new InvalidOperationException("set FREEPORT_KEY to your fp_test_ or fp_live_ key"); var sheet = args.Length > 0 ? args[0] : "stock-sheet-template.csv"; var numbers = new HashSet { "price_per_qty", "min_qty", "max_qty", "stock_qty", "low_stock_alert_qty", "delivery_eta_minutes" }; using var http = new HttpClient { BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/") }; http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", key); // 1. Who am I? Console.WriteLine(await Call(HttpMethod.Get, "ping")); // 2. The sheet, one line per row. Tiers are "min_qty:price;min_qty:price". var rows = File.ReadAllLines(sheet).Where(l => l.Length > 0).Select(ParseCsv).ToList(); var header = rows[0]; var lines = new JsonArray(); foreach (var cells in rows.Skip(1)) { var line = new JsonObject(); for (var i = 0; i < header.Count; i++) { var value = i < cells.Count ? cells[i] : ""; if (value.Length == 0 || header[i] == "tiers") continue; line[header[i]] = numbers.Contains(header[i]) ? JsonValue.Create(int.Parse(value)) : JsonValue.Create(value); } var tiers = header.IndexOf("tiers") is var t && t >= 0 && t < cells.Count ? cells[t] : ""; if (tiers.Length > 0) { line["tiers"] = new JsonArray(tiers.Split(';').Select(r => { var parts = r.Split(':'); return (JsonNode)new JsonObject { ["min_qty"] = int.Parse(parts[0]), ["price"] = parts[1] }; }).ToArray()); } lines.Add(line); } // 3. Open, stage (up to a thousand a call), commit with an idempotency key. var sync = await Call(HttpMethod.Post, "stock/syncs", new JsonObject { ["mode"] = "replace" }); var syncId = sync!["id"]!.GetValue(); for (var start = 0; start < lines.Count; start += 1000) { var chunk = new JsonArray(lines.Skip(start).Take(1000).Select(n => JsonNode.Parse(n!.ToJsonString())).ToArray()); var staged = await Call(HttpMethod.Put, $"stock/syncs/{syncId}/lines", new JsonObject { ["lines"] = chunk }); var results = staged!["results"]!.AsArray(); Console.WriteLine($"staged {results.Count(r => r!["status"]!.GetValue() == "staged")} of {results.Count}"); } await Call(HttpMethod.Post, $"stock/syncs/{syncId}/commit", null, ("Idempotency-Key", $"commit-{syncId}")); // 4. The worker applies it; poll the sync, not the listings. while (true) { var state = await Call(HttpMethod.Get, $"stock/syncs/{syncId}"); if (state!["status"]!.GetValue() != "committing") { Console.WriteLine(state["summary"]); break; } await Task.Delay(1000); } // 5. Any line that failed says why. var failed = await Call(HttpMethod.Get, $"stock/syncs/{syncId}/lines?action=failed"); foreach (var line in failed!["data"]!.AsArray()) Console.WriteLine($"{line!["external_id"]}: {line["error"]}"); async Task Call(HttpMethod method, string path, JsonNode? body = null, (string, string)? header = null) { using var request = new HttpRequestMessage(method, path); if (body is not null) request.Content = JsonContent.Create(body); if (header is { } h) request.Headers.Add(h.Item1, h.Item2); using var response = await http.SendAsync(request); var json = await response.Content.ReadFromJsonAsync(); if (!response.IsSuccessStatusCode) throw new Exception($"{method} {path}: {json?["error"]?["code"]} {json?["error"]?["message"]}"); return json; } static List ParseCsv(string text) { var cells = new List(); var cell = new System.Text.StringBuilder(); var quoted = false; for (var i = 0; i < text.Length; i++) { var ch = text[i]; if (quoted && ch == '"' && i + 1 < text.Length && text[i + 1] == '"') { cell.Append('"'); i++; continue; } if (ch == '"') { quoted = !quoted; continue; } if (ch == ',' && !quoted) { cells.Add(cell.ToString()); cell.Clear(); continue; } cell.Append(ch); } cells.Add(cell.ToString()); return cells; }