Implement classification redesign
This commit is contained in:
@@ -18,6 +18,7 @@ import (
|
||||
"finance-duck/internal/analytics"
|
||||
"finance-duck/internal/app"
|
||||
"finance-duck/internal/banking"
|
||||
"finance-duck/internal/classification"
|
||||
"finance-duck/internal/domain"
|
||||
)
|
||||
|
||||
@@ -69,6 +70,8 @@ func New(a *app.App, assets fs.FS, publicURL string) (http.Handler, error) {
|
||||
s.mux.HandleFunc("POST /api/reclassify/preview", s.preview)
|
||||
s.mux.HandleFunc("POST /api/reclassify/apply", s.apply)
|
||||
s.mux.HandleFunc("POST /api/reclassify/cancel", s.cancel)
|
||||
s.mux.HandleFunc("POST /api/taxonomy/propose", s.taxonomyPropose)
|
||||
s.mux.HandleFunc("POST /api/taxonomy/apply", s.taxonomyApply)
|
||||
s.mux.HandleFunc("GET /api/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
_, err := a.Snapshot(r.Context())
|
||||
if err != nil {
|
||||
@@ -319,6 +322,9 @@ func (s *Server) transaction(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
b.Enrichment.Classification = domain.Provenance{Source: "manual", Timestamp: time.Now().UTC().Format(time.RFC3339)}
|
||||
d.Transactions[i].Enrichment = b.Enrichment
|
||||
if b.Enrichment.MerchantID != "" {
|
||||
app.LearnAlias(d, t.Facts, b.Enrichment.MerchantID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -509,3 +515,24 @@ func (s *Server) cancel(w http.ResponseWriter, r *http.Request) {
|
||||
s.app.CancelPreview(b.ID)
|
||||
respond(w, map[string]bool{"ok": true}, nil)
|
||||
}
|
||||
func (s *Server) taxonomyPropose(w http.ResponseWriter, r *http.Request) {
|
||||
var b app.TaxonomyProposalRequest
|
||||
if !decode(w, r, &b) {
|
||||
return
|
||||
}
|
||||
v, e := s.app.ProposeTaxonomy(r.Context(), b)
|
||||
respond(w, v, e)
|
||||
}
|
||||
|
||||
func (s *Server) taxonomyApply(w http.ResponseWriter, r *http.Request) {
|
||||
var b struct {
|
||||
ID string `json:"id"`
|
||||
Revision string `json:"revision"`
|
||||
Approved classification.TaxonomyProposal `json:"approved"`
|
||||
}
|
||||
if !decode(w, r, &b) {
|
||||
return
|
||||
}
|
||||
v, e := s.app.ApplyTaxonomy(r.Context(), b.ID, b.Revision, b.Approved)
|
||||
respond(w, v, e)
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ func TestOriginAndHostGuardProtectNoLoginService(t *testing.T) {
|
||||
}{{"rebound host", "attacker.example", "", "application/json", 403}, {"cross origin", "localhost:8080", "https://attacker.example", "application/json", 403}, {"simple form CSRF", "localhost:8080", "", "text/plain", 415}, {"valid local mutation", "localhost:8080", "http://localhost:8080", "application/json", 200}}
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := httptest.NewRequest(http.MethodPost, "http://localhost:8080/api/settings", strings.NewReader(`{"model":"example/model","include_amount":false}`))
|
||||
r := httptest.NewRequest(http.MethodPost, "http://localhost:8080/api/settings", strings.NewReader(`{"model":"example/model"}`))
|
||||
r.Host = tt.host
|
||||
r.Header.Set("Content-Type", tt.content)
|
||||
r.Header.Set("Origin", tt.origin)
|
||||
@@ -117,7 +117,7 @@ func TestOpenRouterKeyIsWriteOnlyAndRequiresExplicitRemoval(t *testing.T) {
|
||||
configured(check("POST", endpoint, keyJSON, origin, http.StatusOK), true)
|
||||
configured(check("GET", "/api/state", "", origin, http.StatusOK), true)
|
||||
// Ordinary preference updates must not implicitly erase credentials.
|
||||
configured(check("POST", "/api/settings", `{"model":"example/model","include_amount":false}`, origin, http.StatusOK), true)
|
||||
configured(check("POST", "/api/settings", `{"model":"example/model"}`, origin, http.StatusOK), true)
|
||||
for _, body := range []string{
|
||||
`{}`,
|
||||
`{"api_key":null}`,
|
||||
|
||||
Reference in New Issue
Block a user