Für eine Anbindung bin ich daran einen API-Client für .NET zu schreiben. Als Teil davon bilde ich die verschiedenen Entitäten als .NET-Objekte mit stark typisierten Properties ab.

Hier ist schon mal das erste Problem, die Antworten des API scheinen nicht dokumentiert zu sein. So liefert z.B. ein LIST (list.json) kein "success"-Property, entgegen dem READ (read.json). Damit kann man gut umgehen wenn man es weiss. Dass die Entitäten aber nicht dokumentiert sind, ist schon deutlich mühsamer.

Nun ist mir aufgefallen, dass READ und LIST nicht die selben Properties zurückgeben. Dass LIST nicht alle Details beinhaltet wäre ja kein Problem, aber warum ist READ nicht ein Superset von LIST? Beispiel Account als Screenshot, links READ, rechts LIST:
Account API

Beim READ fehlen Eröffnungs- und Endsalden (zwei Währungen) sowie die Ziele. Weiter ist der "currencyCode" unterschiedlich, und auch Zusatzinfos (categoryDisplay, taxName) fehlen. Ist das Absicht, wie soll man als API-Nutzer damit umgehen?

Auch das UPDATE ist nicht sehr Nutzerfreundlich, es scheint als ob man immer das gesamte Objekt schreiben muss, also technisch kein PATCH machen kann. Oder gibts die Möglichkeit, nur bestimmte Eigenschaften zu ändern und die anderen unberührt zu lassen?

Erneut bei Account wird ja targetMin und targetMax als Parameter übergeben, aber wie oben gezeigt ist diese Info nicht in der von READ zurückgegebenen Entität. Ich muss also sowohl ein LIST wie ein READ machen und deren Ergebnisse zusammenführen um ein nicht-destruktives Update machen zu können, z.B. den Namen anpassen oder sowas.

Edit: Und was hat es mit den InventoryServiceCategoryController, InventoryServiceController, InventoryStockCategoryController, InventoryStockController auf sich? Sind das nicht Aliase für die Inventory > Article und Inventory > Article > Category?

Dies hat einen technisch-historischen Hintergrund. Der Code wurde ursprünglich ohne API im Hinterkopf und hauptsächlich für das UI-Framework ExtJS entwickelt, die API haben wir erst nachträglich veröffentlicht, und sozusagen retrofitted. Dass die API noch einige Mängel wie hier beschrieben hat, ist uns auch bewusst, aber nicht mit unerheblichem Aufwand zu lösen.

Warum read.json und list.json so unterschiedlich sind: read.json gibt einfach das JPA-Entity zurück (inkl. seiner Relationen), während list.json eine custom SQL-Abfrage ist, die nur genau das zurückgibt, was in der Übersichtstabelle dargestellt werden soll. Darum kann die List-Abfrage auch Informationen enthalten, die das Entity nicht hat, da gewisse Informationen aggregiert werden.

Das UPDATE wurde ebenfalls zunächst nur für das CashCtrl UI entwickelt, wo sowieso immer alle Daten mitkommen, die API existierte noch gar nicht.

Die API werden wir in Zukunft einmal generalüberholen, sodass READ/LIST konsistenter sind und mit UPDATE auch nur einzelne Felder geändert werden können (v.a. auch für ein generisches Multi-Edit Feature). Zurzeit ist die API aber nicht mehr im Fokus, erst nach der Lohnbuchhaltung wieder.

Gegenfrage: Wie kommst du auf InventoryServiceController usw.? Diese Begriffe sind nirgends dokumentiert (weder in der API-Doku noch sonstwo) 😉 Diese Klassen sind schon lange Deprecated, bleiben aber aus Kompatibilitätsgründen da.

Danke für die Antwort, ich hatte anhand der Daten schon sowas vermutet.

Zuerst die Antwort auf die Gegenfrage - InventoryServiceController und seine Geschwister sind in der API-Dokumentation durchaus drin, wenn auch nicht sauber im Inhalt verlinkt.
https://app.cashctrl.com/static/help/en/api/index.html => Examples => herunterscrollen, oder Account => heraufscrollen

Dann zum eigentlichen Thema, wäre es sehr aufwändig das READ wenigstens immer mit den Informationen zu versehen welche beim UPDATE gebraucht werden, wie die targetMin und targetMax? Denn so ist es doof, man liest die Entität mit READ, macht ein UPDATE mit den Infos, und verliert dabei die Targets...

Noch was zur Überholung des API, ich würde sehr empfehlen das dann richtig stateless zu machen, und nicht stateful wie es aktuell ist (Rechnungsperiode).

Ah, jetzt seh ich's. Das ist ein Fehler im API-Doku-Generator - wird behoben. Diese Controller-Einträge dürften gar nicht dort drin sein.

Das mit targetMin/targetMax können wir einbauen, ja. Die gesamte API durchgehen und auf solche Inkonsistenzen zu prüfen, werden wir später einmal machen, aber wie gesagt dafür fehlt uns die Zeit momentan.

Auch guter Tipp bezgl. stateless wegen der Rechnungsperiode, vielen Dank! Wir werden das sicherlich angehen.

    14 Tage später

    Ich erlaube mir hier noch weitere Fragen anzuhängen...

    • In der Doku hat es mehrere schreibende Operationen mit der GET-Methode (z.B. Periode ändern, Setting ändern, Order zu Dossier hinzufügen oder entfernen). Das entspricht nicht den Erwartungen (und der HTTP-Spezifikation, welche für GET verlangt dass es "safe" und "idempotent" ist). Ist das ein Fehler in der Doku, kann ich die Aufrufe mit einem POST implementieren?

    • Der Filter bei der Suche ist ziemlich try-and-error, zum Beispiel funktioniert ein Filter auf das "date"-Property eines Order nicht wenn ich das Datum im gleichen Format übermittle wie es vom Server zurückgegeben wird. Der Server gibt das im JSON wie folgt zurück: "date": "2022-01-03 00:00:00.0" - im Filter eingesetzt kommt damit aber kein Resultat, ich muss date=2022-01-03 (ohne Zeit) übergeben damit es klappt. Ist das ein Bug oder gewollt?

    • Wenn ich ein OrderBookEntry erstelle wird eine accountId verlangt auch wenn eine gültige templateId (welche ja die accountId impliziert) mitgebe, gleiches scheint entgegen der Dokumentation für die Description zu gelten (der lokalisierte Name des Templates sollte ja benutzt werden, das scheint aber nicht der Fall zu sein). Das ist mühsam, denn so muss man diese Daten erst in Erfahrung bringen (und den richtigen lokalisierten Text für die Sprache finden)...

    • Das Aktualisieren eines existierenden OrderBookEntries schlägt fehl ("This document does not allow book entries.") wenn man nicht auch die orderId sendet; in der Dokumentation ist das allerdings überhaupt nicht erwähnt: https://app.cashctrl.com/static/help/en/api/index.html#/order/bookentry/update.json

    • Aus dem Journal-Import werde ich nicht schlau. Es gibt kein Delete, und wenn man Execute macht so wird die Import-Session auch nicht entfernt. Im UI scheint der Import durch das Schliessen des Tabs entfernt zu werden, wie ist das via API vorgesehen?

    • Die verschiedenen Endpunkte, welche kein JSON zurückgeben (z.B. balance, exchangerate, setting, sequencenumber) , geben in den Headern "text/html" an, obschon der Inhalt ja nicht HTML ist. Ausserdem stellt sich bei sequencenumber auch noch die Frage, ob GET die richtige HTTP-Methode ist...

    So, der API-Client für .NET, in C# geschrieben, ist ziemlich komplett. Alle Aufrufe aus der aktuellen API-Doku ausser des Personen-Imports sind implementiert, inklusive der dazugehörigen (typisierten) Entitäten. Ich habe Wert auf eine möglichst einfache Syntax gelegt, hier ein paar Beispiele:

    Die Listenabfragen sind einfach mit Filtern benutzbar:

    var rechnungen = cashctrl.OrderList(new () {
    		CategoryId = invoiceCategory,
    		Type = OrderType.Sales,
    		FiscalPeriodId = fiscalPeriod,
    		["associateId"] = kunde,
    		["date"] = datum.ToCashCtrlString(true)
    });

    Entitäten sind einfach zu lesen, erstellen, mutieren:

     // bestehende Buchung auslesen (ID bekannt) oder neue erstellen (ID unbekannt)
    var journal = journalId > 0
    	? cashctrl.JournalRead(journalId)
    	: new Journal { // Neue Buchung vobereiten
    		CreditId = accounts[2850],
    		DebitId = accounts[6200]
    	};
     // Properties setzen, egal ob alt oder neu
    journal.DateAdded = datum;
    journal.Title = $"Privatfahrzeug {kilometer} km";
    journal.Amount = kilometer * 0.70;
    // Generischer Helfer für Upsert von Entitäten
    journalId = cashctrl.UpdateOrCreate(journal, CashCtrlClientExtensions.JournalUpdate, CashCtrlClientExtensions.JournalCreate);

    Falls jemand den Client haben möchte, so kann ich ihn unter einer freizügigen OS-Lizenz auf GitHub zur Verfügung stellen, mich einfach anpingen.

    14 Tage später

    shoogenb Ich bin noch über eine weitere Inkonsistenz gestossen die mir Mühe macht, und zwar beim Auslesen eines InventoryArticle mit READ sind die Konteninfos nicht enthalten (salesAccountId und purchaseAccountId, welche auf der Kategorie definiert werden).

    Zusammen mit der Problematik, dass CashCtrl auch beim Setzen einer InventoryId bei einem OrderItem alle Felder mit den Infos des InventoryArticle ausgefüllt haben will, fehlt dann die Kontoinformation wenn man letzteren direkt mit READ bezogen hat.

    Super @avonwyss für deine Zusammenfassung der API-Merkwürdigkeiten 😊

    Hast du schon mal daran gedacht den Client auf GIT zu publizieren? Allenfalls ergibt sich eine Weiterentwicklung / Zusammenarbeit mit weiteren interessierten Entwicklern…

    Einen wichtigen Punkt den ich zu deinen professionellen Punkten noch hinzufügen möchte:
    Änderungen an einer API sollten zwingend an eine konsequente Versionierung gebunden sein. Sprich es darf nicht vorkommen das die CashCtrl Entwickler eine API-Änderung auf der aktuellen «/v1/» mal schnell produktiv schalten und somit den produktiven Betrieb einer Vielzahl von Apps allfällig riskieren.

    Meine Bitte wäre das ihr bei ALLEN zukünftigen Änderungen diese in einer neuen Version (Bspw. «/v2/») publiziert. Damit ist die Konsistenz für bestehende Apps gewährleistet und es ist dem Entwickler überlassen, wann er seine App von der einen Version auf, die nächst folgende portiert.

    @shoogenb, habt ihr dazu schon konkrete Pläne?

    Happy Weekend allerseits!

    • avonwyss hat auf diesen Beitrag geantwortet.

      FlowPro Danke für deinen Beitrag!

      Ja, ich habe schon daran gedacht das auf GitHub zu publizieren, wie ich es vor zwei Wochen erwähnte. Allerdings ist der Client im Moment noch in der Solution zuhause wo ich den Code verwende, und weil das noch "frisch" ist musste ich auch noch die eine oder andere Anpassung machen, z.B. sind gewisse der in der Doku als "MANDATORY" markierten Einträge optional etc.

      Das ist der Grund warum ich es noch nicht in ein eigenständiges Projekt herausgelöst habe, ich mache das aber gerne sobald jemand den Client auch benutzen möchte.

      Was die Versionierung angeht, so hast du natürlich grundsätzlich recht dass eine solche stattfinden muss, und das CashCtrl-Team hat ja in weiser Voraussicht die URL schon entsprechend gebaut. Allerdings sind Erweiterungen der zurückgegebenen Daten (insbesondere wenn es eine Vereinheitlichung von READ/LIST ist) keine Breaking Changes, nicht zuletzt auch weil die Ausgabe der Aufrufe aktuell nicht dokumentiert ist. Sobald Aufrufe oder Properties umbenannt werden (oder sogar wegfallen) wäre ein Versionssprung (und ggf. vorübergehend die Unterstützung mehrerer API-Versionen) notwendig, solange nur Sachen vereinheitlicht oder erweitert werden kann das in der bisherigen API-Version bleiben ohne dass es bestehenden Code bricht (ausserdem erwarte ich nicht sehr viel bestehende API-Anbindungen, schon nur wegen der wenigen Beiträge im Forum dazu).

      6 Tage später

      Hallo @avonwyss und @FlowPro

      Hier meine Antworten zu euren Fragen:

      In der Doku hat es mehrere schreibende Operationen mit der GET-Methode (z.B. Periode ändern, Setting ändern, Order zu Dossier hinzufügen oder entfernen). Das entspricht nicht den Erwartungen (und der HTTP-Spezifikation, welche für GET verlangt dass es "safe" und "idempotent" ist). Ist das ein Fehler in der Doku, kann ich die Aufrufe mit einem POST implementieren?

      Dies ist ein Fehler in der Doku, diese Operationen gehen alle mit POST. Wahrscheinlich weil sie sowohl mit GET als auch POST gehen, stellt die Doku GET dar. Werden wir beheben.

      Der Filter bei der Suche ist ziemlich try-and-error, zum Beispiel funktioniert ein Filter auf das "date"-Property eines Order nicht wenn ich das Datum im gleichen Format übermittle wie es vom Server zurückgegeben wird. Der Server gibt das im JSON wie folgt zurück: "date": "2022-01-03 00:00:00.0" - im Filter eingesetzt kommt damit aber kein Resultat, ich muss date=2022-01-03 (ohne Zeit) übergeben damit es klappt. Ist das ein Bug oder gewollt?

      Die JSONs wurden primär für das UI-Framework ExtJS formatiert, und dieses erwartet für ein Datum dieses lange Format mit Millisekunden. Die Daten in der Datenbank haben allerdings in den meisten Fällen keine Uhrzeit (sind vom Typ DATE und nicht DATETIME). Darum muss man zum filtern nur das Datum mitgeben, ohne Uhrzeit. Ist eine bedauerliche Inkonsistenz, aber zurzeit einfach so. Das könnten wir beheben, indem wir die Uhrzeit entfernen beim date-Filter. Ist notiert.

      Wenn ich ein OrderBookEntry erstelle wird eine accountId verlangt auch wenn eine gültige templateId (welche ja die accountId impliziert) mitgebe, gleiches scheint entgegen der Dokumentation für die Description zu gelten (der lokalisierte Name des Templates sollte ja benutzt werden, das scheint aber nicht der Fall zu sein). Das ist mühsam, denn so muss man diese Daten erst in Erfahrung bringen (und den richtigen lokalisierten Text für die Sprache finden)...

      Es gibt sicherlich noch einige solche Optimierungen für API-Nutzer. Denn auch hier spielt dies für das UI von CashCtrl keine Rolle, da diese Informationen im Dialog vorausgefüllt werden, also immer mitgeschickt werden.

      Das Aktualisieren eines existierenden OrderBookEntries schlägt fehl ("This document does not allow book entries.") wenn man nicht auch die orderId sendet; in der Dokumentation ist das allerdings überhaupt nicht erwähnt: https://app.cashctrl.com/static/help/en/api/index.html#/order/bookentry/update.json

      Ist notiert.

      Aus dem Journal-Import werde ich nicht schlau. Es gibt kein Delete, und wenn man Execute macht so wird die Import-Session auch nicht entfernt. Im UI scheint der Import durch das Schliessen des Tabs entfernt zu werden, wie ist das via API vorgesehen?

      Im UI wird nur die sogenannte Tab-Session geschlossen (existiert in der API nicht). Die Import-Session (oder besser: der Import) bleibt immer bestehen in der Datenbank, wenn Buchungen importiert wurden (wird lediglich gelöscht, wenn man keine einzige Buchung daraus importiert hat). Die Imports und Import-Buchungen werden im UI nirgends dargestellt, aber bleiben zur Nachforschung in der Datenbank. Auch haben wir vor, in Zukunft einmal eine Liste der Imports darzustellen und löschbar zu machen.

      Die verschiedenen Endpunkte, welche kein JSON zurückgeben (z.B. balance, exchangerate, setting, sequencenumber) , geben in den Headern "text/html" an, obschon der Inhalt ja nicht HTML ist. Ausserdem stellt sich bei sequencenumber auch noch die Frage, ob GET die richtige HTTP-Methode ist...

      Das wäre ein Bug. text/html ist wohl einfach der Default, wenn kein Content-Type gesetzt ist, und es sich nicht von der Endung ableiten lässt. Ich nehme aber an, dass man bei der Nutzung der API hier den Content-Type sowieso ignoriert.

      Ich bin noch über eine weitere Inkonsistenz gestossen die mir Mühe macht, und zwar beim Auslesen eines InventoryArticle mit READ sind die Konteninfos nicht enthalten (salesAccountId und purchaseAccountId, welche auf der Kategorie definiert werden). Zusammen mit der Problematik, dass CashCtrl auch beim Setzen einer InventoryId bei einem OrderItem alle Felder mit den Infos des InventoryArticle ausgefüllt haben will, fehlt dann die Kontoinformation wenn man letzteren direkt mit READ bezogen hat.

      Ist ebenfalls notiert. Hierzu könnte man als Workaround den InventoryCategory auch auslesen über die categoryId des InventoryArticle.

      Einen wichtigen Punkt den ich zu deinen professionellen Punkten noch hinzufügen möchte:
      Änderungen an einer API sollten zwingend an eine konsequente Versionierung gebunden sein. Sprich es darf nicht vorkommen das die CashCtrl Entwickler eine API-Änderung auf der aktuellen «/v1/» mal schnell produktiv schalten und somit den produktiven Betrieb einer Vielzahl von Apps allfällig riskieren.

      Alle API-Änderungen, die die bestehende API gebrochen haben, sind Bugs, also nicht gewollt. Diese haben wir auch jeweils behoben. Denn grundsätzlich für die Version 1 der API machen wir nur rückwärtskompatible Änderungen, die die API nicht brechen. Eine Version 2 gäbe es nur bei einem totalen API-Overhaul. Für die Version 1 kann es aber vorkommen, dass wir teilweise Parameter umbenennen (selten), oder wie im Fall des vCard-Imports eine komplett neue API erstellen. In diesen Fällen gehen aber auch die alten Parameter und die alte vCard-Import-API, nur sind diese nicht mehr dokumentiert, sondern nur der neue Approach bleibt dokumentiert. So sparen wir uns Aufwand.

      @avonwyss Mir fällt auf, dass die meisten Fragen eigentlich eher Bugreports sind 😉 Könntest du diese bitte fortan per Support-Anfrage berichten anstatt im Forum? Das Forum ist eigentlich eher für den Austausch in der Community gedacht, weniger als Bug-Tracker. Besten Dank!

        Danke shoogenb für die Antworten und das Notieren der Bugs.

        shoogenb @avonwyss Mir fällt auf, dass die meisten Fragen eigentlich eher Bugreports sind 😉 Könntest du diese bitte fortan per Support-Anfrage berichten anstatt im Forum? Das Forum ist eigentlich eher für den Austausch in der Community gedacht, weniger als Bug-Tracker. Besten Dank!

        Ja das kann ich natürlich machen. Allerdings war mir - insbesondere bei Eröffnung des Themas - nicht das Bug-Melden wichtig, sondern eine Klärung der Eigenheiten des API, welche auch für andere Benutzer interessant sein könnten. Aus dem Grund habe ich auch nicht neue Themen eröffnet sondern nur ergänzt über was ich stolperte.

        shoogenb Es gibt sicherlich noch einige solche Optimierungen für API-Nutzer. Denn auch hier spielt dies für das UI von CashCtrl keine Rolle, da diese Informationen im Dialog vorausgefüllt werden, also immer mitgeschickt werden.

        Naja, was ist dann der Nutzen eine templateIdetc. mitzusenden wenn diese Informationen vom Server nicht genutzt werden? Eigentlich habe ich als API-Konsument erwartet dass Business-Logik auf dem Server passiert, nicht auf dem Client. Sobald mehrere User gleichzeitig in einem Mandant arbeiten (oder ein User mehrmals eingeloggt ist) birgt dieser Ansatz das Potenzial veraltete Informationen anzuwenden, wogegen ein Handling auf dem Server das verhindern würde. Gleiches gilt natürlich auch für das Fehlen einer Patch-Funktionalität, so ergibt sich ein Last-Writer-Wins wo ggf. veraltete Informationen wiederbelebt werden.

        shoogenb Im UI wird nur die sogenannte Tab-Session geschlossen (existiert in der API nicht). Die Import-Session (oder besser: der Import) bleibt immer bestehen in der Datenbank, wenn Buchungen importiert wurden (wird lediglich gelöscht, wenn man keine einzige Buchung daraus importiert hat).

        Ich verstehe das aus technischer Sicht. Allerdings kann ich per /import/list.json auch UI-Import-Sessions auflisten, doch wenn im UI das Tab geschlossen wird so verschwindet diese auch aus der Liste, was mir den Eindruck gab dass die Import-Session gelöscht würde. Das Pendant dazu habe ich dann im API gesucht aber eben nicht gefunden.

        2 Jahre später

        FlowPro Hast du schon mal daran gedacht den Client auf GIT zu publizieren? Allenfalls ergibt sich eine Weiterentwicklung / Zusammenarbeit mit weiteren interessierten Entwicklern…

        Der Vollständigkeit halber, das ist inzwischen gemacht (inkl. Code für das PowerShell-Modul), und auch als NuGet-Paket: https://github.com/avonwyss/bsn.CashCtrl

        Eine Antwort schreiben…