Merge pull request 'fix(crm): адреса контактов через ContactInfo типа Address (#288)' (#72) from feat/catalog-addresses#288 into main
This commit was merged in pull request #72.
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
package catalog
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestMergeAddressesDedup(t *testing.T) {
|
||||
dst := []Address{{Street: "Weg 1", City: "Stadt", Zip: "1"}}
|
||||
got := mergeAddresses(dst, []Address{
|
||||
{Street: "weg 1", City: "stadt", Zip: "1"}, // duplicate (case-insensitive)
|
||||
{Street: "Weg 2", City: "Stadt", Zip: "2"}, // new
|
||||
})
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("got %d addresses, want 2: %+v", len(got), got)
|
||||
}
|
||||
if got[1].Street != "Weg 2" {
|
||||
t.Errorf("second = %+v", got[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntryAddressesYAML(t *testing.T) {
|
||||
doc := &Document{Entries: []Entry{{
|
||||
ID: "person:max maier", Kind: "person", Name: "Max Maier",
|
||||
Addresses: []Address{{Street: "Weg 1", City: "Stadt", Zip: "12345", Category: "Billing", Primary: true}},
|
||||
}}}
|
||||
merged := MergeDocs(doc)
|
||||
if len(merged.Entries) != 1 || len(merged.Entries[0].Addresses) != 1 {
|
||||
t.Fatalf("merged = %+v", merged.Entries)
|
||||
}
|
||||
}
|
||||
@@ -195,5 +195,16 @@ func ensureContactInfos(ctx context.Context, client *onlyoffice.Client, contactI
|
||||
return fmt.Errorf("add phone %s: %w", ph, err)
|
||||
}
|
||||
}
|
||||
for _, a := range e.Addresses {
|
||||
if strings.TrimSpace(a.Street) == "" && strings.TrimSpace(a.City) == "" && strings.TrimSpace(a.Zip) == "" {
|
||||
continue
|
||||
}
|
||||
if onlyoffice.HasContactAddress(existing, a.Street, a.City, a.Zip, a.Category) {
|
||||
continue
|
||||
}
|
||||
if _, err := client.AddContactAddress(ctx, contactID, a.Street, a.City, a.State, a.Zip, a.Country, a.Category, a.Primary); err != nil {
|
||||
return fmt.Errorf("add address %s: %w", strings.TrimSpace(a.Street), err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -12,6 +12,18 @@ import (
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Address is one postal address of a catalog row. Category is the OO
|
||||
// AddressCategory label (Home|Postal|Office|Billing|Other|Work).
|
||||
type Address struct {
|
||||
Street string `yaml:"street,omitempty"`
|
||||
City string `yaml:"city,omitempty"`
|
||||
State string `yaml:"state,omitempty"`
|
||||
Zip string `yaml:"zip,omitempty"`
|
||||
Country string `yaml:"country,omitempty"`
|
||||
Category string `yaml:"category,omitempty"`
|
||||
Primary bool `yaml:"primary,omitempty"`
|
||||
}
|
||||
|
||||
// Entry is one catalog row (person or company).
|
||||
type Entry struct {
|
||||
ID string `yaml:"id"`
|
||||
@@ -21,6 +33,7 @@ type Entry struct {
|
||||
Last string `yaml:"last,omitempty"`
|
||||
Emails []string `yaml:"emails,omitempty"`
|
||||
Phones []string `yaml:"phones,omitempty"`
|
||||
Addresses []Address `yaml:"addresses,omitempty"`
|
||||
Org string `yaml:"org,omitempty"`
|
||||
Sources []string `yaml:"sources,omitempty"`
|
||||
Zone string `yaml:"zone"`
|
||||
@@ -136,6 +149,7 @@ func mergeEntry(dst, src *Entry) {
|
||||
dst.Sources = uniqueStrings(append(dst.Sources, src.Sources...))
|
||||
dst.Emails = uniqueEmails(append(dst.Emails, src.Emails...))
|
||||
dst.Phones = uniqueStrings(append(dst.Phones, src.Phones...))
|
||||
dst.Addresses = mergeAddresses(dst.Addresses, src.Addresses)
|
||||
if dst.First == "" {
|
||||
dst.First = src.First
|
||||
}
|
||||
@@ -176,6 +190,25 @@ func mergeEntry(dst, src *Entry) {
|
||||
}
|
||||
}
|
||||
|
||||
// mergeAddresses appends src addresses not already present (by street/city/zip).
|
||||
func mergeAddresses(dst, src []Address) []Address {
|
||||
for _, a := range src {
|
||||
exists := false
|
||||
for _, b := range dst {
|
||||
if strings.EqualFold(strings.TrimSpace(a.Street), strings.TrimSpace(b.Street)) &&
|
||||
strings.EqualFold(strings.TrimSpace(a.City), strings.TrimSpace(b.City)) &&
|
||||
strings.EqualFold(strings.TrimSpace(a.Zip), strings.TrimSpace(b.Zip)) {
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !exists {
|
||||
dst = append(dst, a)
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
func uniqueEmails(in []string) []string {
|
||||
seen := map[string]struct{}{}
|
||||
var out []string
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package onlyoffice
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// addressCategoryCodes maps ASC.CRM.Core.AddressCategory names to their numeric
|
||||
// codes. The OO API expects the code, the UI/docs use the label.
|
||||
var addressCategoryCodes = map[string]int{
|
||||
"home": 0,
|
||||
"postal": 1,
|
||||
"office": 2,
|
||||
"billing": 3,
|
||||
"other": 4,
|
||||
"work": 5,
|
||||
}
|
||||
|
||||
// AddressCategoryCode returns the numeric code for an AddressCategory label
|
||||
// (Home|Postal|Office|Billing|Other|Work) or a numeric string. Unknown/empty
|
||||
// labels fall back to Billing, the category `oo companies create` used.
|
||||
func AddressCategoryCode(category string) int {
|
||||
s := strings.ToLower(strings.TrimSpace(category))
|
||||
if s == "" {
|
||||
return addressCategoryCodes["billing"]
|
||||
}
|
||||
if n, err := strconv.Atoi(s); err == nil {
|
||||
if n >= 0 && n <= 5 {
|
||||
return n
|
||||
}
|
||||
return addressCategoryCodes["billing"]
|
||||
}
|
||||
if n, ok := addressCategoryCodes[s]; ok {
|
||||
return n
|
||||
}
|
||||
return addressCategoryCodes["billing"]
|
||||
}
|
||||
|
||||
// ContactAddresses returns the postal address rows of a contact map.
|
||||
func ContactAddresses(contact map[string]any) []map[string]any {
|
||||
if rows, ok := contact["addresses"].([]any); ok {
|
||||
return mapsFromAnySlice(rows)
|
||||
}
|
||||
if rows, ok := contact["addresses"].([]map[string]any); ok {
|
||||
return rows
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HasContactAddress reports whether a contact already has the given postal
|
||||
// address. street+city+zip+category identify it; comparison is normalized.
|
||||
func HasContactAddress(contact map[string]any, street, city, zip, category string) bool {
|
||||
wantStreet, wantCity, wantZip := normalizeAddressPart(street), normalizeAddressPart(city), normalizeAddressPart(zip)
|
||||
wantCat := AddressCategoryCode(category)
|
||||
for _, row := range ContactAddresses(contact) {
|
||||
if normalizeAddressPart(fmt.Sprint(row["street"])) != wantStreet {
|
||||
continue
|
||||
}
|
||||
if normalizeAddressPart(fmt.Sprint(row["city"])) != wantCity {
|
||||
continue
|
||||
}
|
||||
if normalizeAddressPart(fmt.Sprint(row["zip"])) != wantZip {
|
||||
continue
|
||||
}
|
||||
if int(anyToFloat(row["category"])) != wantCat {
|
||||
continue
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func normalizeAddressPart(s string) string {
|
||||
s = strings.ToLower(strings.TrimSpace(s))
|
||||
return strings.Join(strings.Fields(s), " ")
|
||||
}
|
||||
|
||||
func anyToFloat(v any) float64 {
|
||||
switch n := v.(type) {
|
||||
case float64:
|
||||
return n
|
||||
case int:
|
||||
return float64(n)
|
||||
case string:
|
||||
f, _ := strconv.ParseFloat(strings.TrimSpace(n), 64)
|
||||
return f
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package onlyoffice
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestAddressCategoryCode(t *testing.T) {
|
||||
cases := map[string]int{
|
||||
"Home": 0, "Postal": 1, "Office": 2, "Billing": 3, "Other": 4, "Work": 5,
|
||||
"billing": 3, " work ": 5, "3": 3, "5": 5,
|
||||
"": 3, "nonsense": 3, "99": 3,
|
||||
}
|
||||
for in, want := range cases {
|
||||
if got := AddressCategoryCode(in); got != want {
|
||||
t.Errorf("AddressCategoryCode(%q) = %d, want %d", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasContactAddress(t *testing.T) {
|
||||
contact := map[string]any{
|
||||
"addresses": []any{
|
||||
map[string]any{
|
||||
"street": "Lubanas st. 125a-25", "city": "Riga",
|
||||
"zip": "LV-1021", "country": "Latvia", "category": float64(3),
|
||||
},
|
||||
},
|
||||
}
|
||||
if !HasContactAddress(contact, " Lubanas St. 125a-25 ", "riga", "lv-1021", "Billing") {
|
||||
t.Error("want match (normalized, case-insensitive)")
|
||||
}
|
||||
if HasContactAddress(contact, "Lubanas st. 125a-25", "Riga", "LV-1021", "Work") {
|
||||
t.Error("different category must not match")
|
||||
}
|
||||
if HasContactAddress(contact, "Lubanas st. 125a-25", "Riga", "00000", "Billing") {
|
||||
t.Error("different zip must not match")
|
||||
}
|
||||
if HasContactAddress(map[string]any{}, "x", "y", "z", "Billing") {
|
||||
t.Error("empty contact must not match")
|
||||
}
|
||||
if got := ContactAddresses(contact); len(got) != 1 {
|
||||
t.Fatalf("ContactAddresses = %d rows", len(got))
|
||||
}
|
||||
}
|
||||
+21
-10
@@ -443,21 +443,32 @@ func (c *Client) DeleteInvoiceItem(ctx context.Context, id string) (map[string]a
|
||||
return c.deleteObject(ctx, fmt.Sprintf("/api/2.0/crm/invoiceitem/%s.json", url.PathEscape(id)))
|
||||
}
|
||||
|
||||
// contactAddress is the JSON payload OO stores in a ContactInfo row of type
|
||||
// Address (ASC.Api.CRM.Wrappers.Address).
|
||||
type contactAddress struct {
|
||||
Street string `json:"street"`
|
||||
City string `json:"city"`
|
||||
State string `json:"state"`
|
||||
Zip string `json:"zip"`
|
||||
Country string `json:"country"`
|
||||
}
|
||||
|
||||
// AddContactAddress attaches a postal address to a contact.
|
||||
// category: Home|Postal|Office|Billing|Other|Work (or numeric string).
|
||||
// category: Home|Postal|Office|Billing|Other|Work.
|
||||
//
|
||||
// OO stores addresses as ContactInfo rows of infoType Address whose `data` is
|
||||
// the Address object as JSON (the dedicated /addressdata endpoint binds the
|
||||
// model from the body and is not accepted by all builds). The generic
|
||||
// /contact/{id}/data endpoint is the one `oo contacts info-add` uses.
|
||||
func (c *Client) AddContactAddress(ctx context.Context, contactID, street, city, state, zip, country, category string, isPrimary bool) (map[string]any, error) {
|
||||
if category == "" {
|
||||
category = "Billing"
|
||||
}
|
||||
fields := url.Values{}
|
||||
fields.Set("street", street)
|
||||
fields.Set("city", city)
|
||||
fields.Set("state", state)
|
||||
fields.Set("zip", zip)
|
||||
fields.Set("country", country)
|
||||
fields.Set("category", category)
|
||||
fields.Set("isPrimary", strconv.FormatBool(isPrimary))
|
||||
return c.postFormObject(ctx, fmt.Sprintf("/api/2.0/crm/contact/%s/address", url.PathEscape(contactID)), fields)
|
||||
payload, err := json.Marshal(contactAddress{Street: street, City: city, State: state, Zip: zip, Country: country})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.AddContactInfo(ctx, contactID, "Address", string(payload), category, isPrimary)
|
||||
}
|
||||
|
||||
// UpdateCompany updates company name and optional about text.
|
||||
|
||||
Reference in New Issue
Block a user