internal: add models, validation, transformer and hash logic
This commit is contained in:
parent
40e9067e04
commit
fa59c984bc
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue