summaryrefslogtreecommitdiffstats
path: root/README.md
diff options
context:
space:
mode:
authorSimen A. W. Olsen <hello@simenandre.no>2026-10-17 14:19:09 +0200
committerSimen A. W. Olsen <hello@simenandre.no>2026-10-17 14:19:09 +0200
commitc0a01ac65bb492640b169274614e0d8022f5076d (patch)
treef7d3f0e81d1a9dc05288ba24f1ecc57387f3e0d5 /README.md
downloadfiken-f112e25ee77c31812d0c59265284cc410bbb3b97.tar.gz
fiken-f112e25ee77c31812d0c59265284cc410bbb3b97.zip
Fiken API client and OAuth2 for Gov0.1.0
Extracted from gcbill, where it was internal/fiken and internal/fikenauth, so adaa and sokkel-billing can use the same client instead of each carrying its own. The login command named when a grant cannot be renewed is now injected (Source.LoginCommand): it was hardcoded to gcbill's, which would have sent every other caller's operator to a command that does not exist.
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.