summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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)
+ }
+}