From c72cdebd4b3cbe027e3dc5d73b9a5ea13d7f4ed4 Mon Sep 17 00:00:00 2001 From: Ruslan Efanov Date: Mon, 16 Dec 2024 17:38:10 +0300 Subject: [PATCH 1/6] parsing nubmers --- core/util/phone.go | 222 ++++++++++++++++++++++++++++++++++++++++ core/util/phone_test.go | 74 ++++++++++++++ go.mod | 6 ++ go.sum | 15 +++ 4 files changed, 317 insertions(+) create mode 100644 core/util/phone.go create mode 100644 core/util/phone_test.go diff --git a/core/util/phone.go b/core/util/phone.go new file mode 100644 index 0000000..81f470a --- /dev/null +++ b/core/util/phone.go @@ -0,0 +1,222 @@ +package util + +import ( + "errors" + "fmt" + phoneiso3166 "github.com/onlinecity/go-phone-iso3166" + pn "github.com/ttacon/libphonenumber" + "regexp" + "slices" + "strconv" + "strings" +) + +const ( + CountryPhoneCodeDE = 49 + CountryPhoneCodeAG = 54 + CountryPhoneCodeMX = 52 + // CountryPhoneCodeMXWA For Whatsapp + CountryPhoneCodeMXWA = 521 + CountryPhoneCodeUS = "1443" + CountryPhoneCodePS = 970 + CountryPhoneCodeUZ = 998 + PalestineRegion = "PS" + BangladeshRegion = "BD" +) + +var ( + ErrPhoneTooShort = errors.New("phone is too short - must be at least 5 symbols") + ErrCannotDetermineCountry = errors.New("cannot determine phone country code") + ErrCannotParsePhone = errors.New("cannot parse phone number") + undefinedUSCodes = []string{"1445", "1945", "1840", "1448", "1279", "1839"} +) + +// FormatNumberForWA forms a number in E164 format without `+` symbol to send to whatsapp +func FormatNumberForWA(number string) (string, error) { + parsedPhone, err := ParsePhone(number) + + if err != nil { + return "", err + } + + var formattedPhoneNumber string + switch parsedPhone.GetCountryCode() { + case CountryPhoneCodeAG: + formattedPhoneNumber = Add9AGIFNeed(parsedPhone) + case CountryPhoneCodeMX: + c := int32(CountryPhoneCodeMXWA) + parsedPhone.CountryCode = &c + fallthrough + default: + formattedPhoneNumber = pn.Format(parsedPhone, pn.E164) + } + + return formattedPhoneNumber[1:], nil +} + +// FormatNumberForMG forms a number in E164 format without `+` symbol to send to Message Gateway +// TODO Возможно, нет смысла в этих функция, так как в КР и 360 будет своя логика +func FormatNumberForMG(number string) (string, error) { + parsedPhone, err := ParsePhone(number) + + if err != nil { + return "", err + } + + var formattedPhoneNumber string + switch parsedPhone.GetCountryCode() { + case CountryPhoneCodeAG: + formattedPhoneNumber = Remove9AGIfNeed(parsedPhone) + case CountryPhoneCodeMX: + c := int32(CountryPhoneCodeMXWA) + parsedPhone.CountryCode = &c + fallthrough + default: + formattedPhoneNumber = pn.Format(parsedPhone, pn.E164) + } + + return formattedPhoneNumber[1:], nil +} + +// ParsePhone this function parses the number as a string +// Mexican numbers may not have a 1 after the country code 52 +func ParsePhone(phoneNumber string) (*pn.PhoneNumber, error) { + trimmedPhone := regexp.MustCompile(`\D+`).ReplaceAllString(phoneNumber, "") + if len(trimmedPhone) < 5 { + return nil, ErrPhoneTooShort + } + + countryCode := getCountryCode(trimmedPhone) + if countryCode == "" { + return nil, ErrCannotDetermineCountry + } + + // For russian numbers as 8800xxxxxxx + if strings.EqualFold(BangladeshRegion, countryCode) && IsRussianNumberWith8Prefix(trimmedPhone) { + countryCode = phoneiso3166.E164.LookupString("7" + trimmedPhone[1:]) + } + + parsedPhone, err := pn.Parse(trimmedPhone, countryCode) + + if err != nil { + return nil, ErrCannotParsePhone + } + + if CountryPhoneCodeDE == parsedPhone.GetCountryCode() { + number, err := getGermanNationalNumber(trimmedPhone, parsedPhone) + if err != nil { + return nil, err + } + + parsedPhone.NationalNumber = &number + } + + if CountryPhoneCodeUZ == parsedPhone.GetCountryCode() { + number, err := getUzbekistanNationalNumber(trimmedPhone, parsedPhone) + if err != nil { + return nil, err + } + + parsedPhone.NationalNumber = &number + } + + return parsedPhone, err +} + +func IsRussianNumberWith8Prefix(phone string) bool { + return strings.HasPrefix(phone, "8") && len(phone) == 11 && phoneiso3166.E164.LookupString("7"+phone[1:]) == "RU" +} + +func IsMexicoNumber(phone string, parsed *pn.PhoneNumber) bool { + phoneNumber := regexp.MustCompile(`\D+`).ReplaceAllString(phone, "") + return len(phoneNumber) == 13 && parsed.GetCountryCode() == 52 && strings.HasPrefix(phoneNumber, "521") +} + +func IsUSNumber(phone string) bool { + return slices.Contains(undefinedUSCodes, phone[:4]) && + phoneiso3166.E164.LookupString(CountryPhoneCodeUS+phone[4:]) == "US" +} + +func IsPLNumber(phone string) bool { + num, err := pn.Parse(phone, "PS") + return err == nil && num.GetCountryCode() == CountryPhoneCodePS && fmt.Sprintf("%d", CountryPhoneCodePS) == phone[0:3] +} + +func Remove9AGIfNeed(parsedPhone *pn.PhoneNumber) string { + formattedPhone := pn.Format(parsedPhone, pn.E164) + numberWOCountry := fmt.Sprintf("%d", parsedPhone.GetNationalNumber()) + + if len(numberWOCountry) == 11 && string(numberWOCountry[0]) == "9" { + formattedPhone = fmt.Sprintf("+%d%s", CountryPhoneCodeAG, numberWOCountry[1:]) + } + + return formattedPhone +} + +func Add9AGIFNeed(parsedPhone *pn.PhoneNumber) string { + formattedPhone := pn.Format(parsedPhone, pn.E164) + numberWOCountry := fmt.Sprintf("%d", parsedPhone.GetNationalNumber()) + + if len(numberWOCountry) == 10 { + formattedPhone = fmt.Sprintf("+%d%s", CountryPhoneCodeAG, "9"+numberWOCountry) + } + + return formattedPhone +} + +// getGermanNationalNumber some German numbers may not be parsed correctly. +// For example, for 491736276098 libphonenumber.PhoneNumber.NationalNumber +// will contain the country code(49). This function fix it and return correct libphonenumber.PhoneNumber +func getGermanNationalNumber(phone string, parsedPhone *pn.PhoneNumber) (uint64, error) { + result := parsedPhone.GetNationalNumber() + + if len(fmt.Sprintf("%d", parsedPhone.GetNationalNumber())) == len(phone) { + deduplicateCountryNumber := fmt.Sprintf("%d", parsedPhone.GetNationalNumber())[2:] + + number, err := strconv.Atoi(deduplicateCountryNumber) + if err != nil { + return 0, err + } + + result = uint64(number) + } + + return result, nil +} + +// For UZ numbers where 8 is deleted after the country code +func getUzbekistanNationalNumber(phone string, parsedPhone *pn.PhoneNumber) (uint64, error) { + result := parsedPhone.GetNationalNumber() + numberWithEight := fmt.Sprintf("8%d", parsedPhone.GetNationalNumber()) + + if len(fmt.Sprintf("%d%s", parsedPhone.GetCountryCode(), numberWithEight)) == len(phone) { + number, err := strconv.Atoi(numberWithEight) + if err != nil { + return 0, err + } + + result = uint64(number) + } + + return result, nil +} + +func getCountryCode(phone string) string { + countryCode := phoneiso3166.E164.LookupString(phone) + + if countryCode == "" { + if IsRussianNumberWith8Prefix(phone) { + countryCode = phoneiso3166.E164.LookupString("7" + phone[1:]) + } + + if IsUSNumber(phone) { + countryCode = phoneiso3166.E164.LookupString(CountryPhoneCodeUS + phone[4:]) + } + + if IsPLNumber(phone) { + countryCode = PalestineRegion + } + } + + return countryCode +} diff --git a/core/util/phone_test.go b/core/util/phone_test.go new file mode 100644 index 0000000..148a45d --- /dev/null +++ b/core/util/phone_test.go @@ -0,0 +1,74 @@ +package util + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestParsePhone(t *testing.T) { + t.Run("russian numers", func(t *testing.T) { + n := "+88002541213" + pn, err := ParsePhone(n) + assert.Equal(t, uint64(8002541213), pn.GetNationalNumber()) + assert.Equal(t, int32(7), pn.GetCountryCode()) + + n = "+78002541213" + pn, err = ParsePhone(n) + assert.NoError(t, err) + assert.NotNil(t, pn) + assert.Equal(t, uint64(8002541213), pn.GetNationalNumber()) + assert.Equal(t, int32(7), pn.GetCountryCode()) + + n = "89521548787" + pn, err = ParsePhone(n) + assert.NoError(t, err) + assert.Equal(t, uint64(9521548787), pn.GetNationalNumber()) + assert.Equal(t, int32(7), pn.GetCountryCode()) + }) + + t.Run("german numbers", func(t *testing.T) { + n := "491736276098" + pn, err := ParsePhone(n) + assert.NoError(t, err) + assert.Equal(t, uint64(1736276098), pn.GetNationalNumber()) + assert.Equal(t, int32(CountryPhoneCodeDE), pn.GetCountryCode()) + + n = "4915229457499" + pn, err = ParsePhone(n) + assert.NoError(t, err) + assert.Equal(t, uint64(15229457499), pn.GetNationalNumber()) + assert.Equal(t, int32(CountryPhoneCodeDE), pn.GetCountryCode()) + }) + + t.Run("mexican number", func(t *testing.T) { + n := "5219982418333" + pn, err := ParsePhone(n) + assert.NoError(t, err) + assert.Equal(t, uint64(9982418333), pn.GetNationalNumber()) + assert.Equal(t, int32(CountryPhoneCodeMX), pn.GetCountryCode()) + }) + + t.Run("palestine number", func(t *testing.T) { + n := "970567800663" + pn, err := ParsePhone(n) + assert.NoError(t, err) + assert.Equal(t, uint64(567800663), pn.GetNationalNumber()) + assert.Equal(t, int32(CountryPhoneCodePS), pn.GetCountryCode()) + }) + + t.Run("argentine number", func(t *testing.T) { + n := "5491131157821" + pn, err := ParsePhone(n) + assert.NoError(t, err) + assert.Equal(t, uint64(91131157821), pn.GetNationalNumber()) + assert.Equal(t, int32(CountryPhoneCodeAG), pn.GetCountryCode()) + }) + + t.Run("uzbekistan number", func(t *testing.T) { + n := "998882207724" + pn, err := ParsePhone(n) + assert.NoError(t, err) + assert.Equal(t, uint64(882207724), pn.GetNationalNumber()) + assert.Equal(t, int32(CountryPhoneCodeUZ), pn.GetCountryCode()) + }) +} diff --git a/go.mod b/go.mod index ee79d44..81bfc74 100644 --- a/go.mod +++ b/go.mod @@ -23,10 +23,12 @@ require ( github.com/jessevdk/go-flags v1.6.1 github.com/jinzhu/gorm v1.9.11 github.com/nicksnyder/go-i18n/v2 v2.4.1 + github.com/onlinecity/go-phone-iso3166 v0.0.1 github.com/retailcrm/api-client-go/v2 v2.1.17 github.com/retailcrm/mg-transport-api-client-go v1.3.19 github.com/retailcrm/zabbix-metrics-collector v1.0.0 github.com/stretchr/testify v1.10.0 + github.com/ttacon/libphonenumber v1.2.1 go.uber.org/atomic v1.11.0 go.uber.org/zap v1.27.0 golang.org/x/text v0.21.0 @@ -62,8 +64,11 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-sql-driver/mysql v1.5.0 // indirect + github.com/golang/protobuf v1.5.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect + github.com/hashicorp/go-immutable-radix v1.1.0 // indirect + github.com/hashicorp/golang-lru v0.5.1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -77,6 +82,7 @@ require ( github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/objx v0.5.2 // indirect + github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect go.uber.org/multierr v1.10.0 // indirect diff --git a/go.sum b/go.sum index 0995ba5..280254b 100644 --- a/go.sum +++ b/go.sum @@ -173,6 +173,8 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomarkdown/markdown v0.0.0-20241205020045-f7e15b2f3e62 h1:pbAFUZisjG4s6sxvRJvf2N7vhpCvx2Oxb3PmS6pDO1g= github.com/gomarkdown/markdown v0.0.0-20241205020045-f7e15b2f3e62/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= @@ -184,6 +186,7 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -215,7 +218,12 @@ github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE= github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= +github.com/hashicorp/go-immutable-radix v1.1.0 h1:vN9wG1D6KG6YHRTWr8512cxGOVgTMEfgEdSj/hr8MPc= +github.com/hashicorp/go-immutable-radix v1.1.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -280,6 +288,8 @@ github.com/nicksnyder/go-i18n/v2 v2.4.1 h1:zwzjtX4uYyiaU02K5Ia3zSkpJZrByARkRB4V3 github.com/nicksnyder/go-i18n/v2 v2.4.1/go.mod h1:++Pl70FR6Cki7hdzZRnEEqdc2dJt+SAGotyFg/SvZMk= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/onlinecity/go-phone-iso3166 v0.0.1 h1:srN6o8NjxBWIrlK6Z+zD9wGMSGYi4itWA/fRyaxetqs= +github.com/onlinecity/go-phone-iso3166 v0.0.1/go.mod h1:n8+yIOCu9O63MH3WVwlWq1YVF6ZuAG5xlZ4mZ5ZzKF8= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= @@ -329,6 +339,10 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 h1:5u+EJUQiosu3JFX0XS0qTf5FznsMOzTjGqavBGuCbo0= +github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2/go.mod h1:4kyMkleCiLkgY6z8gK5BkI01ChBtxR0ro3I1ZDcGM3w= +github.com/ttacon/libphonenumber v1.2.1 h1:fzOfY5zUADkCkbIafAed11gL1sW+bJ26p6zWLBMElR4= +github.com/ttacon/libphonenumber v1.2.1/go.mod h1:E0TpmdVMq5dyVlQ7oenAkhsLu86OkUl+yR4OAxyEg/M= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= @@ -581,6 +595,7 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= From 719d89fafbd68d746fc605ef0271b29254f08658 Mon Sep 17 00:00:00 2001 From: Ruslan Efanov Date: Wed, 18 Dec 2024 17:27:18 +0300 Subject: [PATCH 2/6] fix lint errors --- core/util/phone.go | 32 +++++++++++++++++--------------- core/util/phone_test.go | 21 ++++++++++++--------- 2 files changed, 29 insertions(+), 24 deletions(-) diff --git a/core/util/phone.go b/core/util/phone.go index 81f470a..2febb3a 100644 --- a/core/util/phone.go +++ b/core/util/phone.go @@ -3,19 +3,21 @@ package util import ( "errors" "fmt" - phoneiso3166 "github.com/onlinecity/go-phone-iso3166" - pn "github.com/ttacon/libphonenumber" "regexp" "slices" "strconv" "strings" + + phoneiso3166 "github.com/onlinecity/go-phone-iso3166" + pn "github.com/ttacon/libphonenumber" ) const ( - CountryPhoneCodeDE = 49 - CountryPhoneCodeAG = 54 - CountryPhoneCodeMX = 52 - // CountryPhoneCodeMXWA For Whatsapp + MinPhoneSymbolCount = 5 + CountryPhoneCodeDE = 49 + CountryPhoneCodeAG = 54 + CountryPhoneCodeMX = 52 + // CountryPhoneCodeMXWA For Whatsapp. CountryPhoneCodeMXWA = 521 CountryPhoneCodeUS = "1443" CountryPhoneCodePS = 970 @@ -31,7 +33,7 @@ var ( undefinedUSCodes = []string{"1445", "1945", "1840", "1448", "1279", "1839"} ) -// FormatNumberForWA forms a number in E164 format without `+` symbol to send to whatsapp +// FormatNumberForWA forms a number in E164 format without `+` symbol to send to whatsapp. func FormatNumberForWA(number string) (string, error) { parsedPhone, err := ParsePhone(number) @@ -55,7 +57,7 @@ func FormatNumberForWA(number string) (string, error) { } // FormatNumberForMG forms a number in E164 format without `+` symbol to send to Message Gateway -// TODO Возможно, нет смысла в этих функция, так как в КР и 360 будет своя логика +// TODO Возможно, нет смысла в этих функция, так как в КР и 360 будет своя логика. func FormatNumberForMG(number string) (string, error) { parsedPhone, err := ParsePhone(number) @@ -79,10 +81,10 @@ func FormatNumberForMG(number string) (string, error) { } // ParsePhone this function parses the number as a string -// Mexican numbers may not have a 1 after the country code 52 +// Mexican numbers may not have a 1 after the country code 52. func ParsePhone(phoneNumber string) (*pn.PhoneNumber, error) { trimmedPhone := regexp.MustCompile(`\D+`).ReplaceAllString(phoneNumber, "") - if len(trimmedPhone) < 5 { + if len(trimmedPhone) < MinPhoneSymbolCount { return nil, ErrPhoneTooShort } @@ -157,7 +159,7 @@ func Add9AGIFNeed(parsedPhone *pn.PhoneNumber) string { formattedPhone := pn.Format(parsedPhone, pn.E164) numberWOCountry := fmt.Sprintf("%d", parsedPhone.GetNationalNumber()) - if len(numberWOCountry) == 10 { + if len(numberWOCountry) == 10 { // nolint:mnd formattedPhone = fmt.Sprintf("+%d%s", CountryPhoneCodeAG, "9"+numberWOCountry) } @@ -166,7 +168,7 @@ func Add9AGIFNeed(parsedPhone *pn.PhoneNumber) string { // getGermanNationalNumber some German numbers may not be parsed correctly. // For example, for 491736276098 libphonenumber.PhoneNumber.NationalNumber -// will contain the country code(49). This function fix it and return correct libphonenumber.PhoneNumber +// will contain the country code(49). This function fix it and return correct libphonenumber.PhoneNumber. func getGermanNationalNumber(phone string, parsedPhone *pn.PhoneNumber) (uint64, error) { result := parsedPhone.GetNationalNumber() @@ -178,13 +180,13 @@ func getGermanNationalNumber(phone string, parsedPhone *pn.PhoneNumber) (uint64, return 0, err } - result = uint64(number) + result = uint64(number) //nolint:gosec } return result, nil } -// For UZ numbers where 8 is deleted after the country code +// For UZ numbers where 8 is deleted after the country code. func getUzbekistanNationalNumber(phone string, parsedPhone *pn.PhoneNumber) (uint64, error) { result := parsedPhone.GetNationalNumber() numberWithEight := fmt.Sprintf("8%d", parsedPhone.GetNationalNumber()) @@ -195,7 +197,7 @@ func getUzbekistanNationalNumber(phone string, parsedPhone *pn.PhoneNumber) (uin return 0, err } - result = uint64(number) + result = uint64(number) //nolint:gosec } return result, nil diff --git a/core/util/phone_test.go b/core/util/phone_test.go index 148a45d..589fa67 100644 --- a/core/util/phone_test.go +++ b/core/util/phone_test.go @@ -1,27 +1,30 @@ package util import ( - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "testing" + + "github.com/stretchr/testify/assert" ) func TestParsePhone(t *testing.T) { t.Run("russian numers", func(t *testing.T) { n := "+88002541213" pn, err := ParsePhone(n) + require.NoError(t, err) assert.Equal(t, uint64(8002541213), pn.GetNationalNumber()) assert.Equal(t, int32(7), pn.GetCountryCode()) n = "+78002541213" pn, err = ParsePhone(n) - assert.NoError(t, err) + require.NoError(t, err) assert.NotNil(t, pn) assert.Equal(t, uint64(8002541213), pn.GetNationalNumber()) assert.Equal(t, int32(7), pn.GetCountryCode()) n = "89521548787" pn, err = ParsePhone(n) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, uint64(9521548787), pn.GetNationalNumber()) assert.Equal(t, int32(7), pn.GetCountryCode()) }) @@ -29,13 +32,13 @@ func TestParsePhone(t *testing.T) { t.Run("german numbers", func(t *testing.T) { n := "491736276098" pn, err := ParsePhone(n) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, uint64(1736276098), pn.GetNationalNumber()) assert.Equal(t, int32(CountryPhoneCodeDE), pn.GetCountryCode()) n = "4915229457499" pn, err = ParsePhone(n) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, uint64(15229457499), pn.GetNationalNumber()) assert.Equal(t, int32(CountryPhoneCodeDE), pn.GetCountryCode()) }) @@ -43,7 +46,7 @@ func TestParsePhone(t *testing.T) { t.Run("mexican number", func(t *testing.T) { n := "5219982418333" pn, err := ParsePhone(n) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, uint64(9982418333), pn.GetNationalNumber()) assert.Equal(t, int32(CountryPhoneCodeMX), pn.GetCountryCode()) }) @@ -51,7 +54,7 @@ func TestParsePhone(t *testing.T) { t.Run("palestine number", func(t *testing.T) { n := "970567800663" pn, err := ParsePhone(n) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, uint64(567800663), pn.GetNationalNumber()) assert.Equal(t, int32(CountryPhoneCodePS), pn.GetCountryCode()) }) @@ -59,7 +62,7 @@ func TestParsePhone(t *testing.T) { t.Run("argentine number", func(t *testing.T) { n := "5491131157821" pn, err := ParsePhone(n) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, uint64(91131157821), pn.GetNationalNumber()) assert.Equal(t, int32(CountryPhoneCodeAG), pn.GetCountryCode()) }) @@ -67,7 +70,7 @@ func TestParsePhone(t *testing.T) { t.Run("uzbekistan number", func(t *testing.T) { n := "998882207724" pn, err := ParsePhone(n) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, uint64(882207724), pn.GetNationalNumber()) assert.Equal(t, int32(CountryPhoneCodeUZ), pn.GetCountryCode()) }) From f25059448b64f9611bbb9c391a9c05505f472321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D1=83=D1=81=D0=BB=D0=B0=D0=BD?= Date: Thu, 19 Dec 2024 12:38:51 +0300 Subject: [PATCH 3/6] edit parse function --- core/util/phone.go | 59 +++++++++++++---------------------------- core/util/phone_test.go | 15 ++++++++++- 2 files changed, 33 insertions(+), 41 deletions(-) diff --git a/core/util/phone.go b/core/util/phone.go index 2febb3a..c303147 100644 --- a/core/util/phone.go +++ b/core/util/phone.go @@ -33,8 +33,8 @@ var ( undefinedUSCodes = []string{"1445", "1945", "1840", "1448", "1279", "1839"} ) -// FormatNumberForWA forms a number in E164 format without `+` symbol to send to whatsapp. -func FormatNumberForWA(number string) (string, error) { +// FormatNumberForWA forms in the specified format according to the rules https://faq.whatsapp.com/1294841057948784 +func FormatNumberForWA(number string, format pn.PhoneNumberFormat) (string, error) { parsedPhone, err := ParsePhone(number) if err != nil { @@ -45,43 +45,16 @@ func FormatNumberForWA(number string) (string, error) { switch parsedPhone.GetCountryCode() { case CountryPhoneCodeAG: formattedPhoneNumber = Add9AGIFNeed(parsedPhone) - case CountryPhoneCodeMX: - c := int32(CountryPhoneCodeMXWA) - parsedPhone.CountryCode = &c - fallthrough default: - formattedPhoneNumber = pn.Format(parsedPhone, pn.E164) + formattedPhoneNumber = pn.Format(parsedPhone, format) } - return formattedPhoneNumber[1:], nil -} - -// FormatNumberForMG forms a number in E164 format without `+` symbol to send to Message Gateway -// TODO Возможно, нет смысла в этих функция, так как в КР и 360 будет своя логика. -func FormatNumberForMG(number string) (string, error) { - parsedPhone, err := ParsePhone(number) - - if err != nil { - return "", err - } - - var formattedPhoneNumber string - switch parsedPhone.GetCountryCode() { - case CountryPhoneCodeAG: - formattedPhoneNumber = Remove9AGIfNeed(parsedPhone) - case CountryPhoneCodeMX: - c := int32(CountryPhoneCodeMXWA) - parsedPhone.CountryCode = &c - fallthrough - default: - formattedPhoneNumber = pn.Format(parsedPhone, pn.E164) - } - - return formattedPhoneNumber[1:], nil + return formattedPhoneNumber, nil } // ParsePhone this function parses the number as a string -// Mexican numbers may not have a 1 after the country code 52. +// For mexican numbers automatic add 1 after to the country code (521). +// But for argentine numbers there is no automatic addition 9 to the country code. func ParsePhone(phoneNumber string) (*pn.PhoneNumber, error) { trimmedPhone := regexp.MustCompile(`\D+`).ReplaceAllString(phoneNumber, "") if len(trimmedPhone) < MinPhoneSymbolCount { @@ -93,13 +66,7 @@ func ParsePhone(phoneNumber string) (*pn.PhoneNumber, error) { return nil, ErrCannotDetermineCountry } - // For russian numbers as 8800xxxxxxx - if strings.EqualFold(BangladeshRegion, countryCode) && IsRussianNumberWith8Prefix(trimmedPhone) { - countryCode = phoneiso3166.E164.LookupString("7" + trimmedPhone[1:]) - } - parsedPhone, err := pn.Parse(trimmedPhone, countryCode) - if err != nil { return nil, ErrCannotParsePhone } @@ -122,6 +89,11 @@ func ParsePhone(phoneNumber string) (*pn.PhoneNumber, error) { parsedPhone.NationalNumber = &number } + if IsMexicoNumber(trimmedPhone, parsedPhone) { + c := int32(CountryPhoneCodeMXWA) + parsedPhone.CountryCode = &c + } + return parsedPhone, err } @@ -131,7 +103,9 @@ func IsRussianNumberWith8Prefix(phone string) bool { func IsMexicoNumber(phone string, parsed *pn.PhoneNumber) bool { phoneNumber := regexp.MustCompile(`\D+`).ReplaceAllString(phone, "") - return len(phoneNumber) == 13 && parsed.GetCountryCode() == 52 && strings.HasPrefix(phoneNumber, "521") + return len(phoneNumber) == 13 && + parsed.GetCountryCode() == CountryPhoneCodeMX && + strings.HasPrefix(phoneNumber, "521") } func IsUSNumber(phone string) bool { @@ -220,5 +194,10 @@ func getCountryCode(phone string) string { } } + // For russian numbers as 8800xxxxxxx + if strings.EqualFold(BangladeshRegion, countryCode) && IsRussianNumberWith8Prefix(phone) { + countryCode = phoneiso3166.E164.LookupString("7" + phone[1:]) + } + return countryCode } diff --git a/core/util/phone_test.go b/core/util/phone_test.go index 589fa67..c4532e0 100644 --- a/core/util/phone_test.go +++ b/core/util/phone_test.go @@ -27,6 +27,13 @@ func TestParsePhone(t *testing.T) { require.NoError(t, err) assert.Equal(t, uint64(9521548787), pn.GetNationalNumber()) assert.Equal(t, int32(7), pn.GetCountryCode()) + + n = "+7-900-123-45-67" + pn, err = ParsePhone(n) + require.NoError(t, err) + assert.Equal(t, uint64(9001234567), pn.GetNationalNumber()) + assert.Equal(t, int32(7), pn.GetCountryCode()) + }) t.Run("german numbers", func(t *testing.T) { @@ -48,7 +55,13 @@ func TestParsePhone(t *testing.T) { pn, err := ParsePhone(n) require.NoError(t, err) assert.Equal(t, uint64(9982418333), pn.GetNationalNumber()) - assert.Equal(t, int32(CountryPhoneCodeMX), pn.GetCountryCode()) + assert.Equal(t, int32(CountryPhoneCodeMXWA), pn.GetCountryCode()) + + n = "+521 (998) 241 83 33" + pn, err = ParsePhone(n) + require.NoError(t, err) + assert.Equal(t, uint64(9982418333), pn.GetNationalNumber()) + assert.Equal(t, int32(CountryPhoneCodeMXWA), pn.GetCountryCode()) }) t.Run("palestine number", func(t *testing.T) { From 61618bc7f823ab1c1b530ede4b53f54dfd0f2da5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D1=83=D1=81=D0=BB=D0=B0=D0=BD?= Date: Mon, 23 Dec 2024 15:49:05 +0300 Subject: [PATCH 4/6] fix parsing --- core/util/phone.go | 57 ++++++++++++++++++++++++++--------------- core/util/phone_test.go | 43 ++++++++++++++++++++++++++++--- 2 files changed, 75 insertions(+), 25 deletions(-) diff --git a/core/util/phone.go b/core/util/phone.go index c303147..105993a 100644 --- a/core/util/phone.go +++ b/core/util/phone.go @@ -17,24 +17,24 @@ const ( CountryPhoneCodeDE = 49 CountryPhoneCodeAG = 54 CountryPhoneCodeMX = 52 - // CountryPhoneCodeMXWA For Whatsapp. - CountryPhoneCodeMXWA = 521 - CountryPhoneCodeUS = "1443" - CountryPhoneCodePS = 970 - CountryPhoneCodeUZ = 998 - PalestineRegion = "PS" - BangladeshRegion = "BD" + CountryPhoneCodeUS = "1443" + CountryPhoneCodePS = 970 + CountryPhoneCodeUZ = 998 + PalestineRegion = "PS" + BangladeshRegion = "BD" ) var ( ErrPhoneTooShort = errors.New("phone is too short - must be at least 5 symbols") ErrCannotDetermineCountry = errors.New("cannot determine phone country code") ErrCannotParsePhone = errors.New("cannot parse phone number") - undefinedUSCodes = []string{"1445", "1945", "1840", "1448", "1279", "1839"} + + TrimmedPhoneRegexp = regexp.MustCompile(`\D+`) + UndefinedUSCodes = []string{"1445", "1945", "1840", "1448", "1279", "1839"} ) -// FormatNumberForWA forms in the specified format according to the rules https://faq.whatsapp.com/1294841057948784 -func FormatNumberForWA(number string, format pn.PhoneNumberFormat) (string, error) { +// FormatNumberForWA forms in the format according to the rules https://faq.whatsapp.com/1294841057948784 +func FormatNumberForWA(number string) (string, error) { parsedPhone, err := ParsePhone(number) if err != nil { @@ -46,7 +46,7 @@ func FormatNumberForWA(number string, format pn.PhoneNumberFormat) (string, erro case CountryPhoneCodeAG: formattedPhoneNumber = Add9AGIFNeed(parsedPhone) default: - formattedPhoneNumber = pn.Format(parsedPhone, format) + formattedPhoneNumber = pn.Format(parsedPhone, pn.E164) } return formattedPhoneNumber, nil @@ -56,7 +56,7 @@ func FormatNumberForWA(number string, format pn.PhoneNumberFormat) (string, erro // For mexican numbers automatic add 1 after to the country code (521). // But for argentine numbers there is no automatic addition 9 to the country code. func ParsePhone(phoneNumber string) (*pn.PhoneNumber, error) { - trimmedPhone := regexp.MustCompile(`\D+`).ReplaceAllString(phoneNumber, "") + trimmedPhone := TrimmedPhoneRegexp.ReplaceAllString(phoneNumber, "") if len(trimmedPhone) < MinPhoneSymbolCount { return nil, ErrPhoneTooShort } @@ -89,9 +89,13 @@ func ParsePhone(phoneNumber string) (*pn.PhoneNumber, error) { parsedPhone.NationalNumber = &number } - if IsMexicoNumber(trimmedPhone, parsedPhone) { - c := int32(CountryPhoneCodeMXWA) - parsedPhone.CountryCode = &c + if IsMexicoNumber(parsedPhone) { + number, err := getMexicanNationalNumber(parsedPhone) + if err != nil { + return nil, err + } + + parsedPhone.NationalNumber = &number } return parsedPhone, err @@ -101,15 +105,12 @@ func IsRussianNumberWith8Prefix(phone string) bool { return strings.HasPrefix(phone, "8") && len(phone) == 11 && phoneiso3166.E164.LookupString("7"+phone[1:]) == "RU" } -func IsMexicoNumber(phone string, parsed *pn.PhoneNumber) bool { - phoneNumber := regexp.MustCompile(`\D+`).ReplaceAllString(phone, "") - return len(phoneNumber) == 13 && - parsed.GetCountryCode() == CountryPhoneCodeMX && - strings.HasPrefix(phoneNumber, "521") +func IsMexicoNumber(parsed *pn.PhoneNumber) bool { + return parsed.GetCountryCode() == CountryPhoneCodeMX } func IsUSNumber(phone string) bool { - return slices.Contains(undefinedUSCodes, phone[:4]) && + return slices.Contains(UndefinedUSCodes, phone[:4]) && phoneiso3166.E164.LookupString(CountryPhoneCodeUS+phone[4:]) == "US" } @@ -177,6 +178,20 @@ func getUzbekistanNationalNumber(phone string, parsedPhone *pn.PhoneNumber) (uin return result, nil } +func getMexicanNationalNumber(parsedPhone *pn.PhoneNumber) (uint64, error) { + result := parsedPhone.GetNationalNumber() + phoneWithDigit := fmt.Sprintf("1%d", parsedPhone.GetNationalNumber()) + + num, err := strconv.Atoi(phoneWithDigit) + + if err != nil { + return 0, err + } + + result = uint64(num) + return result, nil +} + func getCountryCode(phone string) string { countryCode := phoneiso3166.E164.LookupString(phone) diff --git a/core/util/phone_test.go b/core/util/phone_test.go index c4532e0..e9d13b4 100644 --- a/core/util/phone_test.go +++ b/core/util/phone_test.go @@ -54,14 +54,20 @@ func TestParsePhone(t *testing.T) { n := "5219982418333" pn, err := ParsePhone(n) require.NoError(t, err) - assert.Equal(t, uint64(9982418333), pn.GetNationalNumber()) - assert.Equal(t, int32(CountryPhoneCodeMXWA), pn.GetCountryCode()) + assert.Equal(t, uint64(19982418333), pn.GetNationalNumber()) + assert.Equal(t, int32(CountryPhoneCodeMX), pn.GetCountryCode()) n = "+521 (998) 241 83 33" pn, err = ParsePhone(n) require.NoError(t, err) - assert.Equal(t, uint64(9982418333), pn.GetNationalNumber()) - assert.Equal(t, int32(CountryPhoneCodeMXWA), pn.GetCountryCode()) + assert.Equal(t, uint64(19982418333), pn.GetNationalNumber()) + assert.Equal(t, int32(CountryPhoneCodeMX), pn.GetCountryCode()) + + n = "529982418333" + pn, err = ParsePhone(n) + require.NoError(t, err) + assert.Equal(t, uint64(19982418333), pn.GetNationalNumber()) + assert.Equal(t, int32(CountryPhoneCodeMX), pn.GetCountryCode()) }) t.Run("palestine number", func(t *testing.T) { @@ -88,3 +94,32 @@ func TestParsePhone(t *testing.T) { assert.Equal(t, int32(CountryPhoneCodeUZ), pn.GetCountryCode()) }) } + +func TestFormatNumberForWA(t *testing.T) { + numbers := map[string]string{ + "79040000000": "+79040000000", + "491736276098": "+491736276098", + "89185553535": "+79185553535", + "4915229457499": "+4915229457499", + "5491131157821": "+5491131157821", + "541131157821": "+5491131157821", + "5219982418333": "+5219982418333", + "529982418333": "+5219982418333", + "14452385043": "+14452385043", + "19452090748": "+19452090748", + "19453003681": "+19453003681", + "19452141217": "+19452141217", + "18407778097": "+18407778097", + "14482074337": "+14482074337", + "18406665259": "+18406665259", + "19455009160": "+19455009160", + "19452381431": "+19452381431", + "12793006305": "+12793006305", + } + + for orig, expected := range numbers { + actual, err := FormatNumberForWA(orig) + assert.NoError(t, err) + assert.Equal(t, expected, actual) + } +} From 81ce0c4ba8cb19947d426b7a397b9bd3f6db1b46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D1=83=D1=81=D0=BB=D0=B0=D0=BD?= Date: Mon, 23 Dec 2024 17:54:10 +0300 Subject: [PATCH 5/6] fix linters --- core/util/phone.go | 4 +--- core/util/phone_test.go | 7 ++++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/core/util/phone.go b/core/util/phone.go index 105993a..37b2025 100644 --- a/core/util/phone.go +++ b/core/util/phone.go @@ -179,7 +179,6 @@ func getUzbekistanNationalNumber(phone string, parsedPhone *pn.PhoneNumber) (uin } func getMexicanNationalNumber(parsedPhone *pn.PhoneNumber) (uint64, error) { - result := parsedPhone.GetNationalNumber() phoneWithDigit := fmt.Sprintf("1%d", parsedPhone.GetNationalNumber()) num, err := strconv.Atoi(phoneWithDigit) @@ -188,8 +187,7 @@ func getMexicanNationalNumber(parsedPhone *pn.PhoneNumber) (uint64, error) { return 0, err } - result = uint64(num) - return result, nil + return uint64(num), nil //nolint:gosec } func getCountryCode(phone string) string { diff --git a/core/util/phone_test.go b/core/util/phone_test.go index e9d13b4..9c00c9c 100644 --- a/core/util/phone_test.go +++ b/core/util/phone_test.go @@ -1,9 +1,10 @@ package util import ( - "github.com/stretchr/testify/require" "testing" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/assert" ) @@ -119,7 +120,7 @@ func TestFormatNumberForWA(t *testing.T) { for orig, expected := range numbers { actual, err := FormatNumberForWA(orig) - assert.NoError(t, err) - assert.Equal(t, expected, actual) + require.NoError(t, err) + require.Equal(t, expected, actual) } } From cc638daffb2dfc5d4e722ebccc455982f6806a8e Mon Sep 17 00:00:00 2001 From: Ruslan Efanov Date: Tue, 24 Dec 2024 09:17:03 +0300 Subject: [PATCH 6/6] fix comment --- core/util/phone.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/util/phone.go b/core/util/phone.go index 37b2025..eb524e6 100644 --- a/core/util/phone.go +++ b/core/util/phone.go @@ -53,8 +53,10 @@ func FormatNumberForWA(number string) (string, error) { } // ParsePhone this function parses the number as a string -// For mexican numbers automatic add 1 after to the country code (521). -// But for argentine numbers there is no automatic addition 9 to the country code. +// For Mexican numbers `1` is always added to the national number because it is always removed during parsing. +// Attention when formatted in libphonenumber.INTERNATIONAL 1 will not be after the country code, even though +// it is in the national number. +// But for Argentine numbers there is no automatic addition 9 to the country code. func ParsePhone(phoneNumber string) (*pn.PhoneNumber, error) { trimmedPhone := TrimmedPhoneRegexp.ReplaceAllString(phoneNumber, "") if len(trimmedPhone) < MinPhoneSymbolCount {