aboutsummaryrefslogtreecommitdiffstats
path: root/internal/state
diff options
context:
space:
mode:
authorSimen A. W. Olsen <hello@simenandre.no>2026-10-27 17:19:57 +0100
committerSimen A. W. Olsen <hello@simenandre.no>2026-10-27 17:19:57 +0100
commit0571b30b6da27dc4c9e617403deab09b974ac641 (patch)
treef3dfab18e22a48d06c64e563ba9bcf478c01a5bc /internal/state
downloadaccounted-multicase-intgr-0571b30b6da27dc4c9e617403deab09b974ac641.tar.gz
accounted-multicase-intgr-0571b30b6da27dc4c9e617403deab09b974ac641.zip
feat: send Multicase buntfil vouchers to AccountedHEADmain
Diffstat (limited to 'internal/state')
-rw-r--r--internal/state/state.go79
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)
+}