// Invoices by the gap-free counter: keep one integer, read what was issued after it, book, ack. using System.Text.Json.Nodes; namespace Slflo.Samples; public sealed class SeqState { public long? LastSeq { get; set; } } public static class InvoicesBySeq { public static async Task> RunAsync(SlfloClient api) { var state = StateFiles.Load("seq-cs.json"); var erp = FakeErp.Open(); if (state.LastSeq is null) { // First run: start from now, not from the company's whole history. var cursor = await api.GetAsync("/invoices/cursor"); state.LastSeq = cursor["data"]!["last_seq"]!.GetValue(); StateFiles.Save("seq-cs.json", state); } var read = 0; object? gap = null; while (gap is null) { var page = await api.GetAsync("/invoices", new Dictionary { ["filter[seq_after]"] = state.LastSeq, ["per_page"] = 100 }); var items = new JsonArray(); foreach (var invoice in page["data"]!.AsArray()) { var seq = invoice!["seq"]!.GetValue(); if (seq != state.LastSeq + 1) { gap = new { expected = state.LastSeq + 1, got = seq }; break; } // alert var id = invoice["id"]!.GetValue(); var journal = erp.PostJournal("invoice", id, invoice["version"]!.GetValue()); items.Add(new JsonObject { ["id"] = id, ["outcome"] = "ACCEPTED", ["external_ref"] = journal, ["external_number"] = journal }); state.LastSeq = seq; read++; } if (items.Count > 0) { var first = items[0]!["id"]!.GetValue(); await api.PostAsync("/acks", new JsonObject { ["entity"] = "invoice", ["items"] = items }, $"invoice-acks:{first}:{state.LastSeq}"); } StateFiles.Save("seq-cs.json", state); if (!page["meta"]!["has_more"]!.GetValue()) break; } return new() { ["read"] = read, ["last_seq"] = state.LastSeq, ["gap"] = gap }; } }