diff options
| -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) + } +} |
