internal: add models, validation, transformer and hash logic

This commit is contained in:
admin 2026-04-08 14:31:02 +02:00
parent 40e9067e04
commit fa59c984bc
4 changed files with 328 additions and 0 deletions

67
internal/hash.go Normal file
View File

@ -0,0 +1,67 @@
package internal
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"strings"
"time"
)
type HashService struct {
lastRecord *LastRecord
}
type LastRecord struct {
EmisorNIF string
NumSerie string
Fecha time.Time
Huella string
FechaGen time.Time
}
func NewHashService() *HashService {
return &HashService{}
}
func (s *HashService) SetLastRecord(r *LastRecord) {
s.lastRecord = r
}
func (s *HashService) GetLastRecord() *LastRecord {
return s.lastRecord
}
func (s *HashService) CalculateHash(data *InvoiceData, previousHash string) string {
fechaGen := data.FechaGen.Format(time.RFC3339)
fields := fmt.Sprintf("%s|%s|%s|%s|%.2f|%.2f|%s|%s",
data.EmisorNIF,
data.NumSerie,
data.Fecha.Format("02-01-2006"),
data.TipoFactura,
data.CuotaTotal,
data.ImporteTotal,
previousHash,
fechaGen,
)
hash := sha256.Sum256([]byte(fields))
return strings.ToUpper(hex.EncodeToString(hash[:]))
}
func (s *HashService) IsFirstRecord() bool {
return s.lastRecord == nil
}
func (s *HashService) GetPreviousHash() string {
if s.lastRecord == nil {
return ""
}
return s.lastRecord.Huella
}
type LastRecordStorage interface {
GetLastRecord(emisorNIF string) (*LastRecord, error)
SaveLastRecord(r *LastRecord) error
}

158
internal/models.go Normal file
View File

@ -0,0 +1,158 @@
package internal
import (
"errors"
"fmt"
"regexp"
"strconv"
"time"
)
var (
ErrEmptyNIF = errors.New("NIF cannot be empty")
ErrInvalidNIF = errors.New("invalid NIF format")
ErrEmptyNumSerie = errors.New("number series cannot be empty")
ErrInvalidFecha = errors.New("invalid date format, expected dd-mm-yyyy")
ErrInvalidTipo = errors.New("invalid invoice type")
ErrInvalidImporte = errors.New("invalid amount format")
ErrEmptyNombre = errors.New("business name cannot be empty")
ErrInvalidIVACampo = errors.New("invalid IVA field: must be numeric")
)
var nifRegex = regexp.MustCompile(`^[A-Z0-9]\d{7}[A-Z]$`)
var tipoFacturaValidos = map[string]bool{
"F1": true, "F2": true,
"R1": true, "R2": true, "R3": true, "R4": true, "R5": true,
}
type InvoiceInput struct {
Tipo string `json:"tipo"`
Factura FacturaInput `json:"factura"`
Sistema SistemaInput `json:"sistema"`
}
type FacturaInput struct {
EmisorNIF string `json:"emisor_nif"`
NumSerie string `json:"num_serie"`
FechaExpedicion string `json:"fecha_expedicion"`
TipoFactura string `json:"tipo_factura"`
Descripcion string `json:"descripcion"`
Destinatario *DestinatarioInput `json:"destinatario,omitempty"`
IVA []IVAInput `json:"iva"`
ImporteTotal float64 `json:"importe_total"`
}
type DestinatarioInput struct {
Nombre string `json:"nombre"`
NIF string `json:"nif"`
}
type IVAInput struct {
Base float64 `json:"base"`
Cuota float64 `json:"cuota"`
Tipo float64 `json:"tipo"`
}
type SistemaInput struct {
Nombre string `json:"nombre"`
NIFProveedor string `json:"nif_proveedor"`
Version string `json:"version"`
}
type ValidationError struct {
Field string `json:"field"`
Message string `json:"message"`
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
func ValidateInvoiceInput(in *InvoiceInput) []error {
var errs []error
if in.Tipo == "" {
errs = append(errs, &ValidationError{"tipo", "operation type is required"})
} else if in.Tipo != "alta" && in.Tipo != "anulacion" {
errs = append(errs, &ValidationError{"tipo", "must be 'alta' or 'anulacion'"})
}
if in.Factura.EmisorNIF == "" {
errs = append(errs, &ValidationError{"factura.emisor_nif", "cannot be empty"})
} else if !isValidNIF(in.Factura.EmisorNIF) {
errs = append(errs, &ValidationError{"factura.emisor_nif", "invalid NIF format"})
}
if in.Factura.NumSerie == "" {
errs = append(errs, &ValidationError{"factura.num_serie", "cannot be empty"})
}
if in.Factura.FechaExpedicion == "" {
errs = append(errs, &ValidationError{"factura.fecha_expedicion", "cannot be empty"})
} else if !isValidDate(in.Factura.FechaExpedicion) {
errs = append(errs, &ValidationError{"factura.fecha_expedicion", "invalid format, expected dd-mm-yyyy"})
}
if in.Factura.TipoFactura == "" {
errs = append(errs, &ValidationError{"factura.tipo_factura", "cannot be empty"})
} else if !tipoFacturaValidos[in.Factura.TipoFactura] {
errs = append(errs, &ValidationError{"factura.tipo_factura", "invalid invoice type"})
}
if len(in.Factura.IVA) == 0 {
errs = append(errs, &ValidationError{"factura.iva", "at least one IVA entry is required"})
}
for i, iva := range in.Factura.IVA {
if iva.Base < 0 {
errs = append(errs, &ValidationError{fmt.Sprintf("factura.iva[%d].base", i), "must be positive"})
}
if iva.Cuota < 0 {
errs = append(errs, &ValidationError{fmt.Sprintf("factura.iva[%d].cuota", i), "must be positive"})
}
if iva.Tipo < 0 {
errs = append(errs, &ValidationError{fmt.Sprintf("factura.iva[%d].tipo", i), "must be positive"})
}
}
if in.Factura.ImporteTotal <= 0 {
errs = append(errs, &ValidationError{"factura.importe_total", "must be greater than 0"})
}
if in.Factura.Destinatario != nil {
if in.Factura.Destinatario.NIF != "" && !isValidNIF(in.Factura.Destinatario.NIF) {
errs = append(errs, &ValidationError{"factura.destinatario.nif", "invalid NIF format"})
}
}
if in.Sistema.Nombre == "" {
errs = append(errs, &ValidationError{"sistema.nombre", "cannot be empty"})
}
if in.Sistema.NIFProveedor == "" {
errs = append(errs, &ValidationError{"sistema.nif_proveedor", "cannot be empty"})
}
if in.Sistema.Version == "" {
errs = append(errs, &ValidationError{"sistema.version", "cannot be empty"})
}
return errs
}
func isValidNIF(nif string) bool {
if len(nif) != 9 {
return false
}
return nifRegex.MatchString(nif)
}
func isValidDate(date string) bool {
_, err := time.Parse("02-01-2006", date)
return err == nil
}
func ParseFloat(s string) (float64, error) {
if s == "" {
return 0, ErrInvalidImporte
}
return strconv.ParseFloat(s, 64)
}

100
internal/transformer.go Normal file
View File

@ -0,0 +1,100 @@
package internal
import "time"
type InvoiceData struct {
Tipo string
EmisorNIF string
NumSerie string
Fecha time.Time
TipoFactura string
Descripcion string
Destinatario *Destinatario
IVA []IVARegularizacion
CuotaTotal float64
ImporteTotal float64
Sistema Sistema
FechaGen time.Time
Huella string
}
type Destinatario struct {
Nombre string
NIF string
}
type IVARegularizacion struct {
Base float64
Cuota float64
Tipo float64
ClaveRegimen string
Calificacion string
}
type Sistema struct {
Nombre string
NIFProveedor string
NombreSistema string
IDSistema string
Version string
NumeroInstalacion string
TipoUsoVerifactu string
TipoUsoMultiOT string
IndicadorMultiOT string
}
func TransformToInvoiceData(in *InvoiceInput) (*InvoiceData, error) {
fecha, _ := time.Parse("02-01-2006", in.Factura.FechaExpedicion)
destinatario := in.Factura.Destinatario
var dest *Destinatario
if destinatario != nil {
dest = &Destinatario{
Nombre: destinatario.Nombre,
NIF: destinatario.NIF,
}
}
ivaList := make([]IVARegularizacion, len(in.Factura.IVA))
for i, iva := range in.Factura.IVA {
ivaList[i] = IVARegularizacion{
Base: iva.Base,
Cuota: iva.Cuota,
Tipo: iva.Tipo,
ClaveRegimen: "01",
Calificacion: "S1",
}
}
cuotaTotal := 0.0
for _, iva := range in.Factura.IVA {
cuotaTotal += iva.Cuota
}
sistema := Sistema{
Nombre: in.Sistema.Nombre,
NIFProveedor: in.Sistema.NIFProveedor,
NombreSistema: in.Sistema.Nombre,
IDSistema: "1",
Version: in.Sistema.Version,
NumeroInstalacion: "1",
TipoUsoVerifactu: "S",
TipoUsoMultiOT: "N",
IndicadorMultiOT: "N",
}
return &InvoiceData{
Tipo: in.Tipo,
EmisorNIF: in.Factura.EmisorNIF,
NumSerie: in.Factura.NumSerie,
Fecha: fecha,
TipoFactura: in.Factura.TipoFactura,
Descripcion: in.Factura.Destinatario.NIF,
Destinatario: dest,
IVA: ivaList,
CuotaTotal: cuotaTotal,
ImporteTotal: in.Factura.ImporteTotal,
Sistema: sistema,
FechaGen: time.Now(),
}, nil
}

3
main.go Normal file
View File

@ -0,0 +1,3 @@
package main
func main() {}