2026-04-08 12:31:02 +00:00
|
|
|
package internal
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"crypto/sha256"
|
|
|
|
|
"encoding/hex"
|
2026-04-17 11:03:06 +00:00
|
|
|
"encoding/json"
|
2026-04-08 12:31:02 +00:00
|
|
|
"fmt"
|
2026-04-17 11:03:06 +00:00
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"regexp"
|
2026-04-08 12:31:02 +00:00
|
|
|
"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
|
|
|
|
|
}
|
2026-04-17 11:03:06 +00:00
|
|
|
|
|
|
|
|
type FileLastRecordStorage struct {
|
|
|
|
|
basePath string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewFileLastRecordStorage(basePath string) *FileLastRecordStorage {
|
|
|
|
|
return &FileLastRecordStorage{basePath: basePath}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *FileLastRecordStorage) GetLastRecord(emisorNIF string) (*LastRecord, error) {
|
|
|
|
|
if s.basePath == "" {
|
|
|
|
|
s.basePath = "./data/"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
filePath := filepath.Join(s.basePath, sanitizeFilename(emisorNIF)+".json")
|
|
|
|
|
data, err := os.ReadFile(filePath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
return nil, fmt.Errorf("reading last record: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var record LastRecord
|
|
|
|
|
if err := json.Unmarshal(data, &record); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("unmarshaling last record: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &record, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *FileLastRecordStorage) SaveLastRecord(r *LastRecord) error {
|
|
|
|
|
if s.basePath == "" {
|
|
|
|
|
s.basePath = "./data/"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := os.MkdirAll(s.basePath, 0750); err != nil {
|
|
|
|
|
return fmt.Errorf("creating directory: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
filePath := filepath.Join(s.basePath, sanitizeFilename(r.EmisorNIF)+".json")
|
|
|
|
|
data, err := json.Marshal(r)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("marshaling last record: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := os.WriteFile(filePath, data, 0640); err != nil {
|
|
|
|
|
return fmt.Errorf("writing last record: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func sanitizeFilename(name string) string {
|
|
|
|
|
re := regexp.MustCompile(`[^a-zA-Z0-9]`)
|
|
|
|
|
return re.ReplaceAllString(name, "_")
|
|
|
|
|
}
|