summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSimen A. W. Olsen <hello@simenandre.no>2026-10-18 12:58:37 +0200
committerSimen A. W. Olsen <hello@simenandre.no>2026-10-18 12:58:37 +0200
commitc4f6746182070aaccaa88b5ab95322b8ee87200c (patch)
treee47a54be4a87c1af438baa6e38301360a522dba5
parent08b6286f1bfe3892d0d63726d79cd5f1d9acac01 (diff)
downloadfiken-c4f6746182070aaccaa88b5ab95322b8ee87200c.tar.gz
fiken-c4f6746182070aaccaa88b5ab95322b8ee87200c.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.go62
-rw-r--r--fiken/fiken_test.go59
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)
+ }
+}