package cabana_test import ( "encoding/json" "fmt" "net/http" "testing" "git.golem15.com/golem15/summercms/modules/cabana" ) // TestMoneyFieldSave extends the plan 01 money smoke on Postgres (Phase 18 // D-07, D-10): an untouched value sent back with another change, or left // out of the body, is re-saved byte-identical; a negative amount and the // scale-2 field round-trip exactly; null clears; a 422 on update writes // nothing; and the list payload serves the stored strings with each row's // own currency. func TestMoneyFieldSave(t *testing.T) { e := newPaymentsEnv(t) load := func(t *testing.T, id uint) paymentsInvoice { t.Helper() var row paymentsInvoice if err := e.db.First(&row, id).Error; err != nil { t.Fatal(err) } return row } rec := e.send(t, http.MethodPost, "/acme/payments/invoices", map[string]any{"number": "S-1", "currency": "PLN", "total": "85.4193", "fee": "12.5"}, true) if rec.Code != http.StatusCreated { t.Fatalf("create status=%d body=%s", rec.Code, rec.Body.String()) } id := dataID(t, rec.Body.Bytes()) if row := load(t, id); row.Total.text != "85.4193" || row.Fee.text != "12.5000" { t.Fatalf("stored = %+v", row) } // Another field changes, the untouched amount comes back as loaded. rec = e.send(t, http.MethodPut, fmt.Sprintf("/acme/payments/invoices/%d", id), map[string]any{"number": "S-1b", "currency": "EUR", "total": "85.4193", "fee": "12.5000"}, true) if rec.Code != http.StatusOK { t.Fatalf("update status=%d body=%s", rec.Code, rec.Body.String()) } if row := load(t, id); row.Number != "S-1b" || row.Currency != "EUR" || row.Total.text != "85.4193" || row.Fee.text != "12.5000" { t.Fatalf("after update = %+v", row) } // Left out of the body, the amount is not touched. rec = e.send(t, http.MethodPut, fmt.Sprintf("/acme/payments/invoices/%d", id), map[string]any{"number": "S-1c"}, true) if rec.Code != http.StatusOK { t.Fatalf("partial update status=%d body=%s", rec.Code, rec.Body.String()) } if row := load(t, id); row.Total.text != "85.4193" || row.Fee.text != "12.5000" { t.Fatalf("after partial update = %+v", row) } // Negative amount and the scale-2 field, exactly. rec = e.send(t, http.MethodPut, fmt.Sprintf("/acme/payments/invoices/%d", id), map[string]any{"number": "S-1c", "total": "-0.5000", "fee": "0.01"}, true) if rec.Code != http.StatusOK { t.Fatalf("negative status=%d body=%s", rec.Code, rec.Body.String()) } if row := load(t, id); row.Total.text != "-0.5000" || row.Fee.text != "0.0100" { t.Fatalf("after negative = %+v", row) } // A 422 on update writes nothing. for _, body := range []map[string]any{ {"number": "S-X", "total": "1.00001"}, {"number": "S-X", "fee": "0.001"}, {"number": "S-X", "total": "1e2"}, {"number": "S-X", "total": "1000000.0000"}, } { rec = e.send(t, http.MethodPut, fmt.Sprintf("/acme/payments/invoices/%d", id), body, true) if rec.Code != http.StatusUnprocessableEntity { t.Fatalf("%v status=%d body=%s", body, rec.Code, rec.Body.String()) } if row := load(t, id); row.Number != "S-1c" || row.Total.text != "-0.5000" || row.Fee.text != "0.0100" { t.Fatalf("a refused update wrote: %+v", row) } } // null clears. rec = e.send(t, http.MethodPut, fmt.Sprintf("/acme/payments/invoices/%d", id), map[string]any{"number": "S-1c", "fee": nil}, true) if rec.Code != http.StatusOK { t.Fatalf("clear status=%d body=%s", rec.Code, rec.Body.String()) } if row := load(t, id); row.Fee.valid || row.Total.text != "-0.5000" { t.Fatalf("after clear = %+v", row) } // The list payload: stored strings and each row's own currency. if rec := e.send(t, http.MethodPost, "/acme/payments/invoices", map[string]any{"number": "S-2", "currency": "PLN", "total": "99999999.9999"}, true); rec.Code != http.StatusUnprocessableEntity { t.Fatalf("above the column max status=%d body=%s", rec.Code, rec.Body.String()) } if rec := e.send(t, http.MethodPost, "/acme/payments/invoices", map[string]any{"number": "S-2", "currency": "PLN", "total": "999999.9999", "fee": "1"}, true); rec.Code != http.StatusCreated { t.Fatalf("create S-2 status=%d body=%s", rec.Code, rec.Body.String()) } rec = e.send(t, http.MethodGet, "/acme/payments/invoices?sort=number&dir=asc", nil, true) if rec.Code != http.StatusOK { t.Fatalf("list status=%d body=%s", rec.Code, rec.Body.String()) } var list cabana.ListEnvelope[[]cabana.AdminRecord] if err := json.Unmarshal(rec.Body.Bytes(), &list); err != nil { t.Fatal(err) } if len(list.Data) != 2 { t.Fatalf("rows = %+v", list.Data) } if row := list.Data[0]; row["number"] != "S-1c" || row["total"] != "-0.5000" || row["currency"] != "EUR" || row["fee"] != nil { t.Fatalf("row 1 = %+v", row) } if row := list.Data[1]; row["number"] != "S-2" || row["total"] != "999999.9999" || row["currency"] != "PLN" || row["fee"] != "1.0000" { t.Fatalf("row 2 = %+v", row) } }