diff options
| author | Simen A. W. Olsen <hello@simenandre.no> | 2026-10-18 12:58:37 +0200 |
|---|---|---|
| committer | Simen A. W. Olsen <hello@simenandre.no> | 2026-10-18 12:58:37 +0200 |
| commit | c4f6746182070aaccaa88b5ab95322b8ee87200c (patch) | |
| tree | e47a54be4a87c1af438baa6e38301360a522dba5 | |
| parent | 08b6286f1bfe3892d0d63726d79cd5f1d9acac01 (diff) | |
| download | fiken-0.3.0.tar.gz fiken-0.3.0.zip | |
Carry a contact's currency, and let it be read and changedv0.3.0
Fiken refuses an invoice draft whose currency differs from the contact's:
"Currency for given customer {} is different from currency for the invoice
draft {}". A contact created without one defaults to NOK, so anything
invoiced in another currency failed at the draft with nothing to explain
it. Adds Currency and Language to NewContact, plus Contact and
UpdateContact for fixing one already made.
| -rw-r--r-- | fiken/fiken.go | 62 | ||||
| -rw-r--r-- | fiken/fiken_test.go | 59 |
2 files changed, 121 insertions, 0 deletions
diff --git a/fiken/fiken.go b/fiken/fiken.go index 9b46607..2ba549c 100644 --- a/fiken/fiken.go +++ b/fiken/fiken.go @@ -135,6 +135,11 @@ type Contact struct { CustomerNumber int32 `json:"customerNumber"` Customer bool `json:"customer"` Inactive bool `json:"inactive"` + + // Currency is what this contact is invoiced in. A draft in any other + // currency is refused, so a caller about to invoice has to check it. + Currency string `json:"currency"` + Language string `json:"language"` } // NewContact is a contact to create. Fiken requires only the name. @@ -145,6 +150,17 @@ type NewContact struct { Customer bool `json:"customer"` Supplier bool `json:"supplier"` Address *Address `json:"address,omitempty"` + + // Currency must match the currency of every invoice sent to this contact. + // Fiken refuses a draft whose currency differs from the contact's with + // "Currency for given customer {} is different from currency for the + // invoice draft {}" — so a contact to be invoiced in anything other than + // Fiken's default has to say so when it is created. + Currency string `json:"currency,omitempty"` + + // Language is what Fiken writes the invoice in: "Norwegian", "English", + // "Swedish". Empty leaves Fiken's default, which is Norwegian. + Language string `json:"language,omitempty"` } // Address is a postal address. Fiken requires a country on one. @@ -203,6 +219,52 @@ type Draft struct { Lines []DraftLine `json:"lines"` } +// Contact fetches one contact. +func (c *Client) Contact(ctx context.Context, companySlug string, contactID int64) (Contact, error) { + assert.True(companySlug != "", "company slug must not be empty") + assert.True(contactID > 0, "contact id must be positive, got %d", contactID) + + path := fmt.Sprintf("/companies/%s/contacts/%d", companySlug, contactID) + resp, err := c.do(ctx, http.MethodGet, path, nil, nil) + if err != nil { + return Contact{}, err + } + defer resp.Body.Close() + + var out Contact + if err := json.NewDecoder(io.LimitReader(resp.Body, maxBodyBytes)).Decode(&out); err != nil { + return Contact{}, fmt.Errorf("decode contact: %w", err) + } + return out, nil +} + +// ContactUpdate is the fields of a contact to change. Only what is set is +// sent, so a caller changing one thing cannot blank the rest. +type ContactUpdate struct { + Currency string `json:"currency,omitempty"` + Language string `json:"language,omitempty"` + Email string `json:"email,omitempty"` +} + +// UpdateContact changes a contact in place. +func (c *Client) UpdateContact(ctx context.Context, companySlug string, contactID int64, update ContactUpdate) error { + assert.True(companySlug != "", "company slug must not be empty") + assert.True(contactID > 0, "contact id must be positive, got %d", contactID) + + body, err := json.Marshal(update) + if err != nil { + return fmt.Errorf("encode contact update: %w", err) + } + path := fmt.Sprintf("/companies/%s/contacts/%d", companySlug, contactID) + resp, err := c.do(ctx, http.MethodPut, path, nil, body) + if err != nil { + return err + } + defer resp.Body.Close() + io.Copy(io.Discard, io.LimitReader(resp.Body, maxBodyBytes)) //nolint:errcheck + return 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) diff --git a/fiken/fiken_test.go b/fiken/fiken_test.go index 1f24b86..2d82616 100644 --- a/fiken/fiken_test.go +++ b/fiken/fiken_test.go @@ -353,3 +353,62 @@ func TestCreateContactReadsTheLocationHeader(t *testing.T) { t.Errorf("contact was sent as %+v, want a customer with a country on its address", body) } } + +// Fiken refuses a draft whose currency differs from the contact's, so a +// contact to be invoiced in anything but Fiken's default has to say so when it +// is created. Finding that out needs a real call; keeping it needs this. +func TestCreateContactCarriesTheCurrency(t *testing.T) { + var body NewContact + c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { + json.NewDecoder(r.Body).Decode(&body) //nolint:errcheck + w.Header().Set("Location", "https://api.fiken.no/api/v2/companies/sokkel-as/contacts/1") + w.WriteHeader(http.StatusCreated) + }) + + if _, err := c.CreateContact(context.Background(), "sokkel-as", NewContact{ + Name: "Upstacked AS", Customer: true, Currency: "EUR", Language: "English", + }); err != nil { + t.Fatalf("CreateContact: %v", err) + } + if body.Currency != "EUR" || body.Language != "English" { + t.Errorf("sent %+v, want the currency and language carried", body) + } +} + +func TestUpdateContactSendsOnlyWhatIsSet(t *testing.T) { + var raw map[string]any + var method, path string + c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { + method, path = r.Method, r.URL.Path + json.NewDecoder(r.Body).Decode(&raw) //nolint:errcheck + w.WriteHeader(http.StatusOK) + }) + + err := c.UpdateContact(context.Background(), "sokkel-as", 14883209386, + ContactUpdate{Currency: "EUR"}) + if err != nil { + t.Fatalf("UpdateContact: %v", err) + } + if method != http.MethodPut || !strings.HasSuffix(path, "/contacts/14883209386") { + t.Errorf("sent %s %s", method, path) + } + // Only the currency: a caller changing one field must not blank the rest. + if len(raw) != 1 || raw["currency"] != "EUR" { + t.Errorf("body = %v, want only the currency", raw) + } +} + +func TestContactReadsTheCurrency(t *testing.T) { + c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"contactId":1,"name":"Upstacked AS","currency":"NOK","customer":true}`)) //nolint:errcheck + }) + + got, err := c.Contact(context.Background(), "sokkel-as", 1) + if err != nil { + t.Fatalf("Contact: %v", err) + } + if got.Currency != "NOK" { + t.Errorf("currency = %q, want it read back so a caller can check it", got.Currency) + } +} |
