diff options
| author | Simen A. W. Olsen <hello@simenandre.no> | 2026-10-18 13:01:54 +0200 |
|---|---|---|
| committer | Simen A. W. Olsen <hello@simenandre.no> | 2026-10-18 13:01:54 +0200 |
| commit | 53323a0671a3bfe86e7cff75ee5269f60c01ba05 (patch) | |
| tree | 77f0764204fedee138c084c6eb16075ab578dea4 | |
| parent | c4f6746182070aaccaa88b5ab95322b8ee87200c (diff) | |
| download | fiken-53323a0671a3bfe86e7cff75ee5269f60c01ba05.tar.gz fiken-53323a0671a3bfe86e7cff75ee5269f60c01ba05.zip | |
Make UpdateContact read, merge and writev0.3.1
Fiken's PUT replaces the whole contact and rejects a body missing
required fields ("'name' er påkrevd"), so a partial update cannot work.
Merging over the raw JSON rather than over Contact keeps every field
Fiken holds, including the ones this package does not model — a struct
round-trip would silently blank an address, its contact people and its
notes.
| -rw-r--r-- | fiken/fiken.go | 60 | ||||
| -rw-r--r-- | fiken/fiken_test.go | 49 |
2 files changed, 100 insertions, 9 deletions
diff --git a/fiken/fiken.go b/fiken/fiken.go index 2ba549c..2a5d94b 100644 --- a/fiken/fiken.go +++ b/fiken/fiken.go @@ -246,14 +246,39 @@ type ContactUpdate struct { Email string `json:"email,omitempty"` } -// UpdateContact changes a contact in place. +// UpdateContact changes fields on a contact, leaving the rest as they are. +// +// Fiken's PUT **replaces** the whole contact and rejects a body missing +// required fields ("Feil ved validering av data: 'name' er påkrevd"), so the +// contact is read first and sent back with the changes applied. +// +// It merges over the raw JSON rather than over Contact, so every field Fiken +// holds survives — including the ones this package does not model. Round- +// tripping through a struct would silently blank a contact's address, its +// contact people and its notes. 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) + current, err := c.contactFields(ctx, companySlug, contactID) + if err != nil { + return err + } + + changes, err := asFields(update) + if err != nil { + return err + } + if len(changes) == 0 { + return nil + } + for field, value := range changes { + current[field] = value + } + + body, err := json.Marshal(current) if err != nil { - return fmt.Errorf("encode contact update: %w", err) + return fmt.Errorf("encode contact: %w", err) } path := fmt.Sprintf("/companies/%s/contacts/%d", companySlug, contactID) resp, err := c.do(ctx, http.MethodPut, path, nil, body) @@ -265,6 +290,35 @@ func (c *Client) UpdateContact(ctx context.Context, companySlug string, contactI return nil } +// contactFields reads a contact as the fields Fiken actually holds. +func (c *Client) contactFields(ctx context.Context, companySlug string, contactID int64) (map[string]any, error) { + path := fmt.Sprintf("/companies/%s/contacts/%d", companySlug, contactID) + resp, err := c.do(ctx, http.MethodGet, path, nil, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var out map[string]any + if err := json.NewDecoder(io.LimitReader(resp.Body, maxBodyBytes)).Decode(&out); err != nil { + return nil, fmt.Errorf("decode contact: %w", err) + } + return out, nil +} + +// asFields turns an update into the fields it sets, dropping the unset ones. +func asFields(update ContactUpdate) (map[string]any, error) { + raw, err := json.Marshal(update) + if err != nil { + return nil, fmt.Errorf("encode contact update: %w", err) + } + var fields map[string]any + if err := json.Unmarshal(raw, &fields); err != nil { + return nil, fmt.Errorf("decode contact update: %w", err) + } + return fields, 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 2d82616..5e2124a 100644 --- a/fiken/fiken_test.go +++ b/fiken/fiken_test.go @@ -375,12 +375,24 @@ func TestCreateContactCarriesTheCurrency(t *testing.T) { } } -func TestUpdateContactSendsOnlyWhatIsSet(t *testing.T) { - var raw map[string]any +// Fiken's PUT replaces the whole contact, so an update has to read it first +// and send everything back. Anything this package does not model — the +// address, contact people, notes — must survive the round trip. +func TestUpdateContactKeepsEverythingItDoesNotChange(t *testing.T) { + var sent map[string]any var method, path string c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"contactId":14883209386,"name":"Upstacked AS","currency":"NOK", + "customer":true,"supplier":false,"organizationNumber":"926030396", + "address":{"streetAddress":"Drammensveien 151","postCode":"0277", + "city":"Oslo","country":"Norge"}, + "contactPerson":[{"name":"Someone"}],"notes":[],"language":"Norwegian"}`)) //nolint:errcheck + return + } method, path = r.Method, r.URL.Path - json.NewDecoder(r.Body).Decode(&raw) //nolint:errcheck + json.NewDecoder(r.Body).Decode(&sent) //nolint:errcheck w.WriteHeader(http.StatusOK) }) @@ -392,9 +404,34 @@ func TestUpdateContactSendsOnlyWhatIsSet(t *testing.T) { 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) + if sent["currency"] != "EUR" { + t.Errorf("currency = %v, want it changed", sent["currency"]) + } + // Fiken rejects a body without these; a struct round-trip would drop them. + for _, field := range []string{"name", "organizationNumber", "address", "contactPerson"} { + if sent[field] == nil { + t.Errorf("the update dropped %q, which Fiken holds", field) + } + } + if sent["language"] != "Norwegian" { + t.Errorf("language = %v, want it left alone", sent["language"]) + } +} + +// An update that changes nothing makes no call, rather than rewriting a +// contact to the value it already had. +func TestUpdateContactWithNoChangesDoesNothing(t *testing.T) { + calls := 0 + c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { + calls++ + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"contactId":1,"name":"Upstacked AS"}`)) //nolint:errcheck + }) + if err := c.UpdateContact(context.Background(), "sokkel-as", 1, ContactUpdate{}); err != nil { + t.Fatalf("UpdateContact: %v", err) + } + if calls > 1 { + t.Errorf("made %d calls for an empty update, want no PUT", calls) } } |
