1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
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.
|