diff options
| author | Simen A. W. Olsen <hello@simenandre.no> | 2026-10-17 14:19:09 +0200 |
|---|---|---|
| committer | Simen A. W. Olsen <hello@simenandre.no> | 2026-10-17 14:19:09 +0200 |
| commit | c0a01ac65bb492640b169274614e0d8022f5076d (patch) | |
| tree | f7d3f0e81d1a9dc05288ba24f1ecc57387f3e0d5 /README.md | |
| download | fiken-c0a01ac65bb492640b169274614e0d8022f5076d.tar.gz fiken-c0a01ac65bb492640b169274614e0d8022f5076d.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.md | 61 |
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. |
