From a5491c9dccdf4678bf3b4380d208cf2562afeee4 Mon Sep 17 00:00:00 2001 From: "Simen A. W. Olsen" Date: Tue, 3 Nov 2026 22:08:49 +0100 Subject: Never retry a call that creates something --- fiken/fiken.go | 28 ++++++++++++++++++++++++---- fiken/fiken_test.go | 16 ++++++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/fiken/fiken.go b/fiken/fiken.go index 2a5d94b..4e1d0dd 100644 --- a/fiken/fiken.go +++ b/fiken/fiken.go @@ -95,7 +95,7 @@ func withSleep(f func(time.Duration)) Option { // personal API token is. func New(token string, opts ...Option) (*Client, error) { if strings.TrimSpace(token) == "" { - return nil, errors.New("no Fiken API token: set FIKEN_API_TOKEN or fiken.token_file") + return nil, errors.New("no Fiken API token") } return NewWithTokens(StaticToken(token), opts...) } @@ -181,7 +181,7 @@ func (c *Client) CreateContact(ctx context.Context, companySlug string, contact if err != nil { return 0, fmt.Errorf("encode contact: %w", err) } - resp, err := c.do(ctx, http.MethodPost, "/companies/"+companySlug+"/contacts", nil, body) + resp, err := c.doOnce(ctx, http.MethodPost, "/companies/"+companySlug+"/contacts", nil, body) if err != nil { return 0, err } @@ -319,6 +319,26 @@ func asFields(update ContactUpdate) (map[string]any, error) { return fields, nil } +// User is the Fiken account a token belongs to. +type User struct { + Name string `json:"name"` + Email string `json:"email"` +} + +// User returns the account the token was issued to. +func (c *Client) User(ctx context.Context) (User, error) { + resp, err := c.do(ctx, http.MethodGet, "/user", nil, nil) + if err != nil { + return User{}, err + } + defer resp.Body.Close() + var u User + if err := json.NewDecoder(io.LimitReader(resp.Body, maxBodyBytes)).Decode(&u); err != nil { + return User{}, fmt.Errorf("fiken GET /user: unreadable response: %w", err) + } + return u, nil +} + // Companies lists the companies the token can act for. func (c *Client) Companies(ctx context.Context) ([]Company, error) { return page[Company](ctx, c, "/companies", nil) @@ -427,7 +447,7 @@ func (c *Client) CreateDraft(ctx context.Context, companySlug string, draft Draf if err != nil { return 0, fmt.Errorf("encode invoice draft: %w", err) } - resp, err := c.do(ctx, http.MethodPost, "/companies/"+companySlug+"/invoices/drafts", nil, body) + resp, err := c.doOnce(ctx, http.MethodPost, "/companies/"+companySlug+"/invoices/drafts", nil, body) if err != nil { return 0, err } @@ -455,7 +475,7 @@ func (c *Client) DeleteDraft(ctx context.Context, companySlug string, draftID in func (c *Client) CreateInvoiceFromDraft(ctx context.Context, companySlug string, draftID int64) (int64, error) { assert.True(draftID > 0, "draft id must be positive, got %d", draftID) path := fmt.Sprintf("/companies/%s/invoices/drafts/%d/createInvoice", companySlug, draftID) - resp, err := c.do(ctx, http.MethodPost, path, nil, nil) + resp, err := c.doOnce(ctx, http.MethodPost, path, nil, nil) if err != nil { return 0, err } diff --git a/fiken/fiken_test.go b/fiken/fiken_test.go index 5e2124a..7103339 100644 --- a/fiken/fiken_test.go +++ b/fiken/fiken_test.go @@ -449,3 +449,19 @@ func TestContactReadsTheCurrency(t *testing.T) { t.Errorf("currency = %q, want it read back so a caller can check it", got.Currency) } } + +// A 502 does not say whether the draft was made, so asking again could make two. +func TestCreatingIsNeverRetried(t *testing.T) { + calls := 0 + c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { + calls++ + w.WriteHeader(http.StatusBadGateway) + }) + ctx := context.Background() + c.CreateDraft(ctx, "sokkel-as", Draft{CustomerID: 1, Lines: []DraftLine{{Description: "x", Quantity: 1}}}) + c.CreateContact(ctx, "sokkel-as", NewContact{Name: "x"}) + c.CreateInvoiceFromDraft(ctx, "sokkel-as", 1) + if calls != 3 { + t.Errorf("%d requests for three creates, want 3", calls) + } +} -- cgit v1.3.1