تخطَّ إلى المحتوى

Read invoices by seq and catch a gap

هذا المحتوى غير متوفر بلغتك بعد.

Slflo issues the invoices — a van sale is invoiced at the shop, a delivered order when it’s delivered — and the receipts and credit notes that go with them. Your ERP books them. Each of the three streams is numbered by a gap-free counter per company, seq, given once when the document is issued and never changed. Keep one integer per stream and you know exactly what is new — and, because there are no holes, you can prove you hold every one.

Stream Read seq is given when Cursor
Invoices GET /invoices?filter[seq_after]=N the invoice is issued GET /invoices/cursor
Payments GET /payments?filter[seq_after]=N the receipt is issued GET /payments/cursor
Returns GET /returns?filter[seq_after]=N the return is credited (it has a credit_note_number) GET /returns/cursor

Before you start: a token with invoices:read/write, payments:read/write and returns:read/write — the sandbox token has them all. Unlike order acks, these acks work in any ERP mode.

The code is samples/python/invoices_by_seq.py and samples/csharp/InvoicesBySeq.cs, run against a real Slflo in our build.

  1. Choose where to start. 0 reads the company’s whole history. To start from today, ask the cursor for the last number issued and store it:

    GET /invoices/cursor

    نافذة طرفيّة
    curl -X GET 'https://api.slflo.com/api/integration/v1/invoices/cursor' \
    -H 'Authorization: Bearer $SLFLO_TOKEN'

    Response

    HTTP 200
    {
    "data": {
    "last_seq": 41,
    "high_water": 9140
    }
    }
  2. Read what was issued after your number, in issue order. Check each document’s seq is the next one. Book it, remember the journal number, and move on. Stop at the first surprise.

    invoices_by_seq.py — read
    def read_new(api, erp, stream, last_seq):
    """Everything issued after last_seq, in issue order; stops at the first gap."""
    entity, kind, amount = STREAMS[stream]
    read, gap = 0, None
    while True:
    page = api.get(f"/{stream}", {"filter[seq_after]": last_seq, "per_page": 100})
    documents = page["data"]
    for document in documents:
    if document["seq"] != last_seq + 1:
    # Can't happen on our side: the counter has no holes. If it does, your
    # bookkeeping skipped one: stop here, alert, and re-read from last_seq.
    gap = {"expected": last_seq + 1, "got": document["seq"]}
    break
    journal = fake_erp.post_journal(erp, kind, document["id"], document["version"], amount(document), document["seq"])
    document["journal"] = journal
    last_seq = document["seq"]
    read += 1
    booked = [d for d in documents if "journal" in d]
    if booked:
    ack(api, entity, booked)
    if gap or not page["meta"]["has_more"]:
    return read, last_seq, gap
  3. Ack what you booked, many at once. POST /acks takes up to 500 of one kind; each item gets its own result, and one bad item never fails the rest.

    POST /acks

    نافذة طرفيّة
    curl -X POST 'https://api.slflo.com/api/integration/v1/acks' \
    -H 'Authorization: Bearer $SLFLO_TOKEN' \
    -H 'Idempotency-Key: invoice-acks:5f1c0e…' \
    -H 'Content-Type: application/json' \
    -d '{"entity":"invoice","items":[{"id":"inv_01J9","outcome":"ACCEPTED","external_ref":"JRN-000881","external_number":"JRN-000881"}]}'

    Response

    HTTP 200
    {
    "data": [
    {
    "id": "inv_01J9",
    "result": "applied",
    "status": "ISSUED",
    "version": 9120,
    "error": null
    }
    ],
    "meta": {
    "applied": 1,
    "unchanged": 0,
    "failed": 0
    }
    }
  4. Pick up what changed after its ack. seq shows each document once, at issue. A payment reversed, a cheque bounced, a return approved later — those change the document’s version and make it pending again. Ask for filter[ack]=pending, book the change (a reversing journal for a reversal), and ack it again at its new version with the same journal number.

    invoices_by_seq.py — changes since the ack
    def changed_since_ack(api, erp, stream):
    """A document that changes after its ack (a payment reversed, a cheque bounced, a return
    approved) is pending again. seq never sees it twice; filter[ack]=pending does."""
    entity, kind, amount = STREAMS[stream]
    changed = []
    for page in api.pages(f"/{stream}", {"filter[ack]": "pending"}):
    for document in page["data"]:
    journal = fake_erp.journal_of(erp, kind, document["id"])
    if journal is None:
    continue # never booked: it arrives through its seq (a return once it is credited)
    if document.get("reversal"):
    fake_erp.post_journal(erp, f"{kind}-reversal", document["id"], document["version"], amount(document))
    document["journal"] = journal # the answer stays the journal that booked it
    changed.append(document)
    if changed:
    ack(api, entity, changed)
    return len(changed)
Terminal window
export SLFLO_TOKEN='sfi_live_…' SLFLO_BASE='https://api.slflo.com/api/integration/v1'
python3 invoices_by_seq.py

On the hosted sandbox the first run reads the day’s two invoices, two receipts and one credit note:

{"changed": {"invoices": 0, "payments": 0, "returns": 0},
"invoices": {"gap": null, "last_seq": 2, "read": 2},
"payments": {"gap": null, "last_seq": 2, "read": 2},
"returns": {"gap": null, "last_seq": 1, "read": 1}}

A second run reads nothing new. After the office reverses a payment (on the sandbox, ask us), the next run reads no new receipt but reports "changed": {"payments": 1} — the reversal, booked as a reversing journal.

There are two kinds, and they mean different things.

  • In the answer: you asked after 41 and the first document is 43. The counter has no holes, so this can’t come from Slflo; it means your stored number is wrong (two jobs sharing it, a restored settings table). The sample stops the stream, reports "gap": {"expected": 42, "got": 43}, exits non-zero and saves nothing past 41. Alert someone; don’t skip ahead.

  • In your books: the numbers you read are all there, but a journal went missing on your side (deleted by hand, a partial restore). Because seq has no holes, you can list every number from 1 to your last one and look for the missing. To fetch number n again, ask for filter[seq_after]=n-1&per_page=1:

    invoices_by_seq.py — check
    def check(api, erp, state):
    """Holes in the ERP's own books (a journal deleted by hand, a restore): re-read and re-book them."""
    found = {}
    for stream, (entity, kind, amount) in STREAMS.items():
    booked = set(fake_erp.booked_seqs(erp, kind))
    missing = [n for n in range(1, state.get(stream, 0) + 1) if n not in booked]
    for seq in missing:
    page = api.get(f"/{stream}", {"filter[seq_after]": seq - 1, "per_page": 1})
    document = page["data"][0]
    fake_erp.post_journal(erp, kind, document["id"], document["version"], amount(document), document["seq"])
    found[stream] = missing
    return found
    Terminal window
    python3 invoices_by_seq.py check # {"missing": {"invoices": [1], "payments": [], "returns": []}}
What you see Why What to do
A return you know about doesn’t arrive by seq It hasn’t been credited yet (waiting for approval) It gets a seq when credited; until then it’s visible by version or as pending
409 ALREADY_ACKNOWLEDGED on an item You answered this version before with a different journal number Keep one journal number per document; re-ack with the same one
409 EXTERNAL_REF_CONFLICT on an item That journal number is already on another document One journal per document
400 INVALID_REQUEST naming seq_after filter[seq_after] sent with a version cursor Send one cursor, not both
The same document read twice Your number wasn’t saved after the last page Harmless if your journal posting is keyed by (document id, version), as the sample’s is