// Run with: go run generate.go invoice.json invoice.xml package main import ( "bytes" "crypto/rand" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) func main() { if err := run(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } func run() error { if len(os.Args) != 3 { return fmt.Errorf("usage: go run generate.go invoice.json invoice.xml") } source, err := os.ReadFile(os.Args[1]) if err != nil { return err } if len(source) > 5<<20 { return fmt.Errorf("input exceeds 5 MiB") } base := os.Getenv("FINANCEWOLF_API_BASE") if base == "" { base = "https://api.ironfang.uk/financewolf" } req, err := http.NewRequest(http.MethodPost, strings.TrimRight(base, "/")+"/v1/einvoices/generate?ruleset=latest&profile=peppol-bis-billing-3", bytes.NewReader(source)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") if key := os.Getenv("IRONFANG_API_KEY"); key != "" { req.Header.Set("Authorization", "Bearer "+key) id := os.Getenv("FINANCEWOLF_IDEMPOTENCY_KEY") if id == "" { var nonce [16]byte if _, err := rand.Read(nonce[:]); err != nil { return err } id = hex.EncodeToString(nonce[:]) } req.Header.Set("Idempotency-Key", id) } client := &http.Client{Timeout: 30 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }} response, err := client.Do(req) if err != nil { return err } defer response.Body.Close() raw, err := io.ReadAll(io.LimitReader(response.Body, (12<<20)+1)) if err != nil { return err } if len(raw) > 12<<20 { return fmt.Errorf("response exceeds 12 MiB") } if response.StatusCode != 200 { return fmt.Errorf("HTTP %d: %s", response.StatusCode, raw) } if strings.Split(response.Header.Get("Content-Type"), ";")[0] != "application/json" { return fmt.Errorf("unexpected response type") } type identity struct { SHA256 string `json:"sha256"` Bytes int `json:"bytes"` } var result struct { Schema string `json:"schema"` Status string `json:"status"` Outcome string `json:"outcome"` OperationID string `json:"operation_id"` Artifact struct { identity Data []byte `json:"data_base64"` } `json:"artifact"` Validation struct { Outcome string `json:"outcome"` Input identity `json:"input"` Ruleset struct { ID string `json:"id"` } `json:"ruleset"` Layers []struct { Layer string `json:"layer"` Status string `json:"status"` } `json:"layers"` } `json:"validation"` } if err := json.Unmarshal(raw, &result); err != nil { return err } if result.Schema != "financewolf/einvoice/generation-result/v1" || result.Status != "completed" || result.Outcome != "valid" || result.Validation.Outcome != "valid" || len(result.Validation.Layers) != 5 { return fmt.Errorf("generation did not complete successfully") } for i, name := range []string{"input", "xml", "xsd", "en16931", "peppol"} { if layer := result.Validation.Layers[i]; layer.Layer != name || layer.Status != "passed" { return fmt.Errorf("generation did not pass every validation layer") } } xml := result.Artifact.Data sum := sha256.Sum256(xml) digest := hex.EncodeToString(sum[:]) if len(xml) == 0 || len(xml) > 5<<20 || len(xml) != result.Artifact.Bytes || digest != result.Artifact.SHA256 || digest != result.Validation.Input.SHA256 || len(xml) != result.Validation.Input.Bytes { return fmt.Errorf("XML does not match its validation hash/length") } if err := os.WriteFile(os.Args[2], xml, 0600); err != nil { return err } return json.NewEncoder(os.Stdout).Encode(map[string]string{"xml": os.Args[2], "sha256": digest, "ruleset": result.Validation.Ruleset.ID, "operation_id": result.OperationID}) }