2026-04-08 12:31:05 +00:00
|
|
|
package verifactu
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"crypto/tls"
|
2026-04-17 11:03:06 +00:00
|
|
|
"crypto/x509"
|
2026-04-08 12:31:05 +00:00
|
|
|
"fmt"
|
|
|
|
|
"io"
|
2026-04-17 11:03:06 +00:00
|
|
|
"log"
|
2026-04-08 12:31:05 +00:00
|
|
|
"net/http"
|
2026-04-17 11:03:06 +00:00
|
|
|
"os"
|
|
|
|
|
"time"
|
2026-05-19 20:03:28 +00:00
|
|
|
|
|
|
|
|
"golang.org/x/crypto/pkcs12"
|
2026-04-08 12:31:05 +00:00
|
|
|
)
|
|
|
|
|
|
2026-04-17 11:03:06 +00:00
|
|
|
type Client struct {
|
|
|
|
|
BaseURL string
|
|
|
|
|
HTTPClient *http.Client
|
|
|
|
|
Certificate *tls.Certificate
|
2026-04-08 12:31:05 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-17 11:03:06 +00:00
|
|
|
type ClientConfig struct {
|
|
|
|
|
BaseURL string
|
|
|
|
|
Timeout time.Duration
|
|
|
|
|
CertificatePath string
|
|
|
|
|
CertificatePassword string
|
2026-04-08 12:31:05 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-17 11:03:06 +00:00
|
|
|
func NewClient(cfg ClientConfig) (*Client, error) {
|
|
|
|
|
httpClient := &http.Client{
|
|
|
|
|
Timeout: cfg.Timeout,
|
|
|
|
|
Transport: &http.Transport{
|
|
|
|
|
TLSClientConfig: &tls.Config{
|
|
|
|
|
InsecureSkipVerify: false,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var cert *tls.Certificate
|
|
|
|
|
if cfg.CertificatePath != "" {
|
|
|
|
|
c, err := LoadCertificate(cfg.CertificatePath, cfg.CertificatePassword)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("loading certificate: %w", err)
|
|
|
|
|
}
|
|
|
|
|
cert = c
|
2026-04-08 12:31:05 +00:00
|
|
|
}
|
2026-04-17 11:03:06 +00:00
|
|
|
|
2026-04-08 12:31:05 +00:00
|
|
|
return &Client{
|
2026-04-17 11:03:06 +00:00
|
|
|
BaseURL: cfg.BaseURL,
|
|
|
|
|
HTTPClient: httpClient,
|
|
|
|
|
Certificate: cert,
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func LoadCertificate(certPath, password string) (*tls.Certificate, error) {
|
2026-05-19 20:03:28 +00:00
|
|
|
der, err := os.ReadFile(certPath)
|
2026-04-17 11:03:06 +00:00
|
|
|
if err != nil {
|
2026-05-19 20:03:28 +00:00
|
|
|
return nil, fmt.Errorf("reading cert file: %w", err)
|
2026-04-17 11:03:06 +00:00
|
|
|
}
|
|
|
|
|
|
2026-05-19 20:03:28 +00:00
|
|
|
return parseP12(der, password)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func LoadCertificateFromBytes(der []byte, password string) (*tls.Certificate, error) {
|
|
|
|
|
return parseP12(der, password)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func parseP12(der []byte, password string) (*tls.Certificate, error) {
|
|
|
|
|
key, cert, err := pkcs12.Decode(der, password)
|
2026-04-17 11:03:06 +00:00
|
|
|
if err != nil {
|
2026-05-19 20:03:28 +00:00
|
|
|
return nil, fmt.Errorf("decoding PKCS#12: %w", err)
|
2026-04-17 11:03:06 +00:00
|
|
|
}
|
2026-05-19 20:03:28 +00:00
|
|
|
|
|
|
|
|
if cert == nil {
|
|
|
|
|
return nil, fmt.Errorf("no certificate found in PKCS#12")
|
2026-04-08 12:31:05 +00:00
|
|
|
}
|
2026-04-17 11:03:06 +00:00
|
|
|
|
2026-05-19 20:03:28 +00:00
|
|
|
tlsCert := &tls.Certificate{
|
|
|
|
|
Certificate: [][]byte{cert.Raw},
|
|
|
|
|
PrivateKey: key,
|
|
|
|
|
Leaf: cert,
|
2026-04-17 11:03:06 +00:00
|
|
|
}
|
|
|
|
|
|
2026-05-19 20:03:28 +00:00
|
|
|
log.Printf("cert loaded: subject=%s has private key=%v", cert.Subject, key != nil)
|
|
|
|
|
|
|
|
|
|
return tlsCert, nil
|
2026-04-08 12:31:05 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-17 11:03:06 +00:00
|
|
|
func (c *Client) SetCertificate(certPath, password string) error {
|
|
|
|
|
cert, err := LoadCertificate(certPath, password)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
2026-04-08 12:31:05 +00:00
|
|
|
}
|
2026-04-17 11:03:06 +00:00
|
|
|
c.Certificate = cert
|
|
|
|
|
return nil
|
2026-04-08 12:31:05 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-17 11:03:06 +00:00
|
|
|
func (c *Client) SendAlta(data AltaData) (*Response, error) {
|
|
|
|
|
env, err := BuildAltaSOAPRequest(data)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("building Alta request: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return c.SendRequest(env)
|
2026-04-08 12:31:05 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-17 11:03:06 +00:00
|
|
|
func (c *Client) SendAnulacion(data AltaData) (*Response, error) {
|
|
|
|
|
env, err := BuildAnulacionSOAPRequest(data)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("building Anulacion request: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return c.SendRequest(env)
|
2026-04-08 12:31:05 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-17 11:03:06 +00:00
|
|
|
func (c *Client) SendRequest(env *SOAPEnvelope) (*Response, error) {
|
|
|
|
|
body, err := env.ToBytes()
|
2026-04-08 12:31:05 +00:00
|
|
|
if err != nil {
|
2026-04-17 11:03:06 +00:00
|
|
|
return nil, fmt.Errorf("marshaling request: %w", err)
|
2026-04-08 12:31:05 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-17 11:03:06 +00:00
|
|
|
req, err := http.NewRequest("POST", c.BaseURL, bytes.NewReader(body))
|
2026-04-08 12:31:05 +00:00
|
|
|
if err != nil {
|
2026-04-17 11:03:06 +00:00
|
|
|
return nil, fmt.Errorf("creating request: %w", err)
|
2026-04-08 12:31:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
req.Header.Set("Content-Type", "text/xml; charset=utf-8")
|
|
|
|
|
req.Header.Set("SOAPAction", "")
|
|
|
|
|
|
2026-04-17 11:03:06 +00:00
|
|
|
if c.Certificate != nil {
|
|
|
|
|
log.Printf("Using client certificate with TLS")
|
|
|
|
|
tlsConfig := &tls.Config{
|
|
|
|
|
Certificates: []tls.Certificate{*c.Certificate},
|
|
|
|
|
InsecureSkipVerify: true,
|
|
|
|
|
}
|
|
|
|
|
c.HTTPClient.Transport = &http.Transport{
|
|
|
|
|
TLSClientConfig: tlsConfig,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resp, err := c.HTTPClient.Do(req)
|
2026-04-08 12:31:05 +00:00
|
|
|
if err != nil {
|
2026-04-17 11:03:06 +00:00
|
|
|
return nil, fmt.Errorf("sending request: %w", err)
|
2026-04-08 12:31:05 +00:00
|
|
|
}
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
2026-04-17 11:03:06 +00:00
|
|
|
log.Printf("AEAT response status: %d", resp.StatusCode)
|
|
|
|
|
|
|
|
|
|
respBody, err := io.ReadAll(resp.Body)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("reading response: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if resp.StatusCode != 200 {
|
|
|
|
|
return nil, fmt.Errorf("HTTP error: %d - %s", resp.StatusCode, string(respBody))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
response, err := ParseResponse(respBody)
|
2026-04-08 12:31:05 +00:00
|
|
|
if err != nil {
|
2026-04-17 11:03:06 +00:00
|
|
|
return nil, fmt.Errorf("parsing response: %w", err)
|
2026-04-08 12:31:05 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-17 11:03:06 +00:00
|
|
|
return response, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *Client) SetRootCA(caPath string) error {
|
|
|
|
|
caData, err := os.ReadFile(caPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("reading CA file: %w", err)
|
2026-04-08 12:31:05 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-17 11:03:06 +00:00
|
|
|
pool := x509.NewCertPool()
|
|
|
|
|
pool.AppendCertsFromPEM(caData)
|
|
|
|
|
|
|
|
|
|
if transport, ok := c.HTTPClient.Transport.(*http.Transport); ok {
|
|
|
|
|
if transport.TLSClientConfig == nil {
|
|
|
|
|
transport.TLSClientConfig = &tls.Config{}
|
|
|
|
|
}
|
|
|
|
|
transport.TLSClientConfig.RootCAs = pool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func GetTestURL() string {
|
|
|
|
|
return "https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func GetProdURL() string {
|
|
|
|
|
return "https://www1.agenciatributaria.gob.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP"
|
2026-04-08 12:31:05 +00:00
|
|
|
}
|