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  }