git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/money/vat/vat.go (about) 1 package vat 2 3 import ( 4 "bytes" 5 "encoding/xml" 6 "errors" 7 "io" 8 "net/http" 9 "regexp" 10 "strings" 11 "text/template" 12 "time" 13 ) 14 15 type VATresponse struct { 16 CountryCode string 17 VATnumber string 18 RequestDate time.Time 19 Valid bool 20 Name string 21 Address string 22 } 23 24 const serviceUrl = "http://ec.europa.eu/taxation_customs/vies/services/checkVatService" 25 26 const envelope = ` 27 <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:v1="http://schemas.conversesolutions.com/xsd/dmticta/v1"> 28 <soapenv:Header/> 29 <soapenv:Body> 30 <checkVat xmlns="urn:ec.europa.eu:taxud:vies:services:checkVat:types"> 31 <countryCode>{{.countryCode}}</countryCode> 32 <vatNumber>{{.vatNumber}}</vatNumber> 33 </checkVat> 34 </soapenv:Body> 35 </soapenv:Envelope> 36 ` 37 38 var Timeout = 10 // seconds 39 40 var ( 41 ErrVATnumberNotValid = errors.New("VAT number is not valid.") 42 ErrVATserviceUnreachable = errors.New("VAT number validation service is offline.") 43 ErrVATserviceError = "VAT number validation service returns an error : " 44 ) 45 46 // Check returns *VATresponse for vat number 47 func CheckVAT(vatNumber string) (*VATresponse, error) { 48 vatNumber = sanitizeVatNumber(vatNumber) 49 50 e, err := getEnvelope(vatNumber) 51 if err != nil { 52 return nil, err 53 } 54 eb := bytes.NewBufferString(e) 55 client := http.Client{ 56 Timeout: time.Duration(time.Duration(Timeout) * time.Second), 57 } 58 res, err := client.Post(serviceUrl, "text/xml;charset=UTF-8", eb) 59 if err != nil { 60 return nil, ErrVATserviceUnreachable 61 } 62 defer res.Body.Close() 63 xmlRes, err := io.ReadAll(res.Body) 64 if err != nil { 65 return nil, err 66 } 67 68 if bytes.Contains(xmlRes, []byte("INVALID_INPUT")) { 69 return nil, ErrVATnumberNotValid 70 } 71 72 var rd struct { 73 XMLName xml.Name `xml:"Envelope"` 74 Soap struct { 75 XMLName xml.Name `xml:"Body"` 76 Soap struct { 77 XMLName xml.Name `xml:"checkVatResponse"` 78 CountryCode string `xml:"countryCode"` 79 VATnumber string `xml:"vatNumber"` 80 RequestDate string `xml:"requestDate"` // 2015-03-06+01:00 81 Valid bool `xml:"valid"` 82 Name string `xml:"name"` 83 Address string `xml:"address"` 84 } 85 SoapFault struct { 86 XMLName string `xml:"Fault"` 87 Code string `xml:"faultcode"` 88 Message string `xml:"faultstring"` 89 } 90 } 91 } 92 if err := xml.Unmarshal(xmlRes, &rd); err != nil { 93 return nil, err 94 } 95 96 if rd.Soap.SoapFault.Message != "" { 97 return nil, errors.New(ErrVATserviceError + rd.Soap.SoapFault.Message) 98 } 99 100 if rd.Soap.Soap.RequestDate == "" { 101 return nil, errors.New("service returned invalid request date") 102 } 103 104 pDate, err := time.Parse("2006-01-02-07:00", rd.Soap.Soap.RequestDate) 105 if err != nil { 106 return nil, err 107 } 108 109 r := &VATresponse{ 110 CountryCode: rd.Soap.Soap.CountryCode, 111 VATnumber: rd.Soap.Soap.VATnumber, 112 RequestDate: pDate, 113 Valid: rd.Soap.Soap.Valid, 114 Name: rd.Soap.Soap.Name, 115 Address: rd.Soap.Soap.Address, 116 } 117 118 return r, nil 119 } 120 121 // IsValid returns true if vat number is correct 122 func IsValidVAT(vatNumber string) (bool, error) { 123 r, err := CheckVAT(vatNumber) 124 if err != nil { 125 return false, err 126 } 127 return r.Valid, nil 128 } 129 130 // sanitizeVatNumber removes white space 131 func sanitizeVatNumber(vatNumber string) string { 132 vatNumber = strings.TrimSpace(vatNumber) 133 return regexp.MustCompile(" ").ReplaceAllString(vatNumber, "") 134 } 135 136 // getEnvelope parses envelope template 137 func getEnvelope(vatNumber string) (string, error) { 138 if len(vatNumber) < 3 { 139 return "", errors.New("VAT number is too short.") 140 } 141 142 t, err := template.New("envelope").Parse(envelope) 143 if err != nil { 144 return "", err 145 } 146 147 var result bytes.Buffer 148 if err := t.Execute(&result, map[string]string{ 149 "countryCode": strings.ToUpper(vatNumber[0:2]), 150 "vatNumber": vatNumber[2:], 151 }); err != nil { 152 return "", err 153 } 154 return result.String(), nil 155 }