aboutsummaryrefslogtreecommitdiffstats
path: root/README.md
blob: 7e59e41ec62db236ad03b26bcbe39f9f72ed2ba1 (plain)
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.