diff options
Diffstat (limited to 'internal/state/state.go')
| -rw-r--r-- | internal/state/state.go | 79 |
1 files changed, 79 insertions, 0 deletions
diff --git a/internal/state/state.go b/internal/state/state.go new file mode 100644 index 0000000..f530d96 --- /dev/null +++ b/internal/state/state.go @@ -0,0 +1,79 @@ +// Package state records which Multicase vouchers have been sent, so a +// re-run of the same or an overlapping export does not book them twice. +// Accounted's idempotency keys expire after 24 hours; this file does not. +package state + +import ( + "encoding/json" + "errors" + "io/fs" + "os" + "path/filepath" + "time" +) + +type Record struct { + CompanyID string `json:"company_id"` + EntryID string `json:"entry_id"` + Status string `json:"status"` // "draft" or "posted" + VoucherSeries string `json:"voucher_series,omitempty"` + VoucherNumber int `json:"voucher_number,omitempty"` + Hash string `json:"hash"` + UpdatedAt time.Time `json:"updated_at"` +} + +type File struct { + path string + Entries map[string]Record `json:"entries"` +} + +func Load(path string) (*File, error) { + f := &File{path: path, Entries: map[string]Record{}} + b, err := os.ReadFile(path) + if errors.Is(err, fs.ErrNotExist) { + return f, nil + } + if err != nil { + return nil, err + } + if err := json.Unmarshal(b, f); err != nil { + return nil, err + } + if f.Entries == nil { + f.Entries = map[string]Record{} + } + return f, nil +} + +func key(companyID, sourceID string) string { + return companyID + "/" + sourceID +} + +func (f *File) Get(companyID, sourceID string) (Record, bool) { + r, ok := f.Entries[key(companyID, sourceID)] + return r, ok +} + +// Put stores the record and writes the file, so progress survives a crash +// halfway through a run. +func (f *File) Put(sourceID string, r Record) error { + r.UpdatedAt = time.Now().UTC() + f.Entries[key(r.CompanyID, sourceID)] = r + b, err := json.MarshalIndent(f, "", " ") + if err != nil { + return err + } + tmp, err := os.CreateTemp(filepath.Dir(f.path), ".state-*") + if err != nil { + return err + } + defer os.Remove(tmp.Name()) + if _, err := tmp.Write(b); err != nil { + tmp.Close() + return err + } + if err := tmp.Close(); err != nil { + return err + } + return os.Rename(tmp.Name(), f.path) +} |
