Import ING and Kontist statements behind a reviewed column mapping
CSV import is now mapping-driven: N26, ING (metadata preamble, Windows-1252, German decimals) and Kontist exports are recognized locally, and any other layout can have its columns proposed by the configured model from a sample in which letters are replaced by x and digits by 0. Proposals are untrusted: every column must name a supplied header, money must come from one signed column or one debit/credit pair, and formats must be from a closed list. Uploading no longer imports. /api/import is replaced by prepare/confirm/cancel: prepare parses, deduplicates and previews the exact facts, and only confirming at the reviewed revision writes them. ING and AI-mapped facts carry no transaction reference, because repeating SEPA mandate references must never become a transaction identity.
This commit is contained in:
@@ -44,7 +44,9 @@ func New(a *app.App, assets fs.FS, publicURL string) (http.Handler, error) {
|
||||
s.mux.HandleFunc("POST /api/merchants", s.merchant)
|
||||
s.mux.HandleFunc("POST /api/transactions/{id}", s.transaction)
|
||||
s.mux.HandleFunc("POST /api/manage", s.manage)
|
||||
s.mux.HandleFunc("POST /api/import", s.importCSV)
|
||||
s.mux.HandleFunc("POST /api/import/prepare", s.importPrepare)
|
||||
s.mux.HandleFunc("POST /api/import/confirm", s.importConfirm)
|
||||
s.mux.HandleFunc("POST /api/import/cancel", s.importCancel)
|
||||
s.mux.HandleFunc("POST /api/backfill", s.backfill)
|
||||
s.mux.HandleFunc("POST /api/rebuild", func(w http.ResponseWriter, r *http.Request) { v, e := a.Rebuild(r.Context()); respond(w, v, e) })
|
||||
s.mux.HandleFunc("POST /api/sync", func(w http.ResponseWriter, r *http.Request) { v, e := a.Sync(s.manualBankContext(r)); respond(w, v, e) })
|
||||
@@ -139,7 +141,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
media, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if r.URL.Path != "/api/import" && media != "application/json" {
|
||||
if r.URL.Path != "/api/import/prepare" && media != "application/json" {
|
||||
http.Error(w, "application/json required", http.StatusUnsupportedMediaType)
|
||||
return
|
||||
}
|
||||
@@ -309,7 +311,7 @@ func (s *Server) manage(w http.ResponseWriter, r *http.Request) {
|
||||
v, e := s.app.ManageRegistry(r.Context(), b.Revision, b.Entity, b.Action, b.ID, b.TargetID)
|
||||
respond(w, v, e)
|
||||
}
|
||||
func (s *Server) importCSV(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *Server) importPrepare(w http.ResponseWriter, r *http.Request) {
|
||||
if e := r.ParseMultipartForm(2 << 20); e != nil {
|
||||
respond(w, nil, e)
|
||||
return
|
||||
@@ -321,9 +323,30 @@ func (s *Server) importCSV(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
v, e := s.app.ImportCSV(r.Context(), r.FormValue("revision"), r.FormValue("account_id"), f)
|
||||
v, e := s.app.PrepareCSVImport(r.Context(), r.FormValue("revision"), r.FormValue("account_id"), f)
|
||||
respond(w, v, e)
|
||||
}
|
||||
func (s *Server) importConfirm(w http.ResponseWriter, r *http.Request) {
|
||||
var b struct {
|
||||
ID string `json:"id"`
|
||||
Revision string `json:"revision"`
|
||||
}
|
||||
if !decode(w, r, &b) {
|
||||
return
|
||||
}
|
||||
v, e := s.app.ConfirmCSVImport(r.Context(), b.ID, b.Revision)
|
||||
respond(w, v, e)
|
||||
}
|
||||
func (s *Server) importCancel(w http.ResponseWriter, r *http.Request) {
|
||||
var b struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
if !decode(w, r, &b) {
|
||||
return
|
||||
}
|
||||
s.app.CancelCSVImport(b.ID)
|
||||
respond(w, map[string]bool{"ok": true}, nil)
|
||||
}
|
||||
func (s *Server) backfill(w http.ResponseWriter, r *http.Request) {
|
||||
var b struct {
|
||||
Revision string `json:"revision"`
|
||||
|
||||
Reference in New Issue
Block a user