aboutsummaryrefslogtreecommitdiffstats
path: root/README.md
diff options
context:
space:
mode:
Diffstat (limited to 'README.md')
-rw-r--r--README.md61
1 files changed, 61 insertions, 0 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..7e59e41
--- /dev/null
+++ b/README.md
@@ -0,0 +1,61 @@
+# fiken
+
+A Go client for [Fiken](https://fiken.no)'s API v2, and the OAuth2 flow that
+gets a token for it.
+
+```sh
+go get simenandre.no/fiken
+```
+
+Two packages, and the split is the point:
+
+- **`fiken`** knows how to put a bearer token in a header and how to read
+ Fiken's answers. Where the token came from is not its business.
+- **`fikenauth`** gets and keeps a token: the authorization-code flow over a
+ loopback redirect, refreshing an expired grant, and a `0600` file to store it
+ in. Refreshing is a side effect of asking for a token rather than a step the
+ caller has to remember, which is the only way it gets done everywhere.
+
+```go
+store := fikenauth.NewStore(filepath.Join(dir, "fiken-token.json"))
+source := fikenauth.NewSource(cfg, store)
+source.LoginCommand = "myapp fiken login" // named in the error if the grant dies
+
+client, err := fiken.NewWithTokens(source)
+```
+
+A personal API token works too, with no OAuth at all:
+
+```go
+client, err := fiken.New(os.Getenv("FIKEN_TOKEN"))
+```
+
+## What it is careful about
+
+**Amounts are integers.** `DraftLine.UnitPrice` is the invoice currency's minor
+unit and `Quantity` is normally 1, so the amount is carried entirely by the
+price and no float ever enters the payload. Fiken's minor unit has two decimals
+whatever the currency, so a price carried at more precision than that has to be
+rounded by the caller — where the rounding rule can be stated and tested,
+rather than here where it would be invisible.
+
+**A token never reaches a log line.** `Token.String` hides its own value, so
+`%v` on one prints nothing useful.
+
+**404 is its own error.** `ErrNoSuchResource` wraps it, because deleting a draft
+that is already gone is usually the state the caller wanted, not a failure.
+`ErrUnauthorized` is likewise separate from a transport error: one means log in
+again, the other means try again.
+
+**Retries are bounded and only for what is retryable.** A 5xx or 429 is retried;
+a 4xx is not, because sending the same bad request again cannot help.
+
+## Status
+
+Extracted from a working invoicing tool and used by three of them, so the
+surface is what those needed rather than all of Fiken: companies, contacts,
+invoices, drafts, credit notes. Pull requests that widen it are welcome; the
+constraints above are not up for negotiation.
+
+Tests run against `httptest` servers — including retries, 4xx, and a declined
+consent — and reach nothing real.