init push

This commit is contained in:
2026-07-16 22:16:45 +07:00
commit 8b068bdb10
1021 changed files with 332816 additions and 0 deletions

View File

@@ -0,0 +1,615 @@
package handlers
import (
"context"
"encoding/json"
"io"
"net/http"
"strings"
"testing"
"time"
"github.com/gofiber/fiber/v2"
actionsignoff "wucher/internal/domain/action_signoff"
"wucher/internal/domain/complaint"
"wucher/internal/shared/pkg/uuidv7"
)
type complaintSignoffMock struct {
getByComplaintIDsFn func(context.Context, [][]byte) (map[string]*actionsignoff.ActionSignoff, error)
unsignByComplaintFn func(context.Context, []byte) (*actionsignoff.ActionSignoff, error)
}
func (m *complaintSignoffMock) GetByComplaintIDs(ctx context.Context, ids [][]byte) (map[string]*actionsignoff.ActionSignoff, error) {
if m.getByComplaintIDsFn != nil {
return m.getByComplaintIDsFn(ctx, ids)
}
return map[string]*actionsignoff.ActionSignoff{}, nil
}
func (m *complaintSignoffMock) UnsignByComplaint(ctx context.Context, complaintID []byte) (*actionsignoff.ActionSignoff, error) {
if m.unsignByComplaintFn != nil {
return m.unsignByComplaintFn(ctx, complaintID)
}
return nil, nil
}
type complaintServiceMock struct {
createFn func(context.Context, *complaint.Complaint) error
updateFn func(context.Context, *complaint.Complaint) error
getByIDFn func(context.Context, []byte) (*complaint.Complaint, error)
hasOpenFn func(context.Context, []byte) (bool, error)
listFn func(context.Context, complaint.ListFilter, string, int, int) ([]complaint.Complaint, int64, error)
listByHelicopterFn func(context.Context, []byte, int, int) ([]complaint.Complaint, int64, error)
resolveNamesFn func(context.Context, []complaint.Complaint) (map[string]string, error)
}
func (m *complaintServiceMock) Create(ctx context.Context, row *complaint.Complaint) error {
if m.createFn != nil {
return m.createFn(ctx, row)
}
return nil
}
func (m *complaintServiceMock) Update(ctx context.Context, row *complaint.Complaint) error {
if m.updateFn != nil {
return m.updateFn(ctx, row)
}
return nil
}
func (m *complaintServiceMock) Delete(context.Context, []byte, []byte) error { return nil }
func (m *complaintServiceMock) GetByID(ctx context.Context, id []byte) (*complaint.Complaint, error) {
if m.getByIDFn != nil {
return m.getByIDFn(ctx, id)
}
return nil, nil
}
func (m *complaintServiceMock) ListByHelicopter(ctx context.Context, helicopterID []byte, limit, offset int) ([]complaint.Complaint, int64, error) {
if m.listByHelicopterFn != nil {
return m.listByHelicopterFn(ctx, helicopterID, limit, offset)
}
return nil, 0, nil
}
func (m *complaintServiceMock) List(ctx context.Context, filter complaint.ListFilter, sort string, limit, offset int) ([]complaint.Complaint, int64, error) {
if m.listFn != nil {
return m.listFn(ctx, filter, sort, limit, offset)
}
return nil, 0, nil
}
func (m *complaintServiceMock) HelicopterHasOpenComplaint(ctx context.Context, helicopterID []byte) (bool, error) {
if m.hasOpenFn != nil {
return m.hasOpenFn(ctx, helicopterID)
}
return false, nil
}
func (m *complaintServiceMock) ActiveGroundingByHelicopterIDs(context.Context, [][]byte) ([]complaint.Complaint, error) {
return nil, nil
}
func (m *complaintServiceMock) OpenByHelicopterIDs(context.Context, [][]byte) ([]complaint.Complaint, error) {
return nil, nil
}
func (m *complaintServiceMock) HelicopterHasUnactionedComplaint(context.Context, []byte) (bool, error) {
return false, nil
}
func (m *complaintServiceMock) MarkFixedByHelicopter(context.Context, []byte, []byte, []byte) error {
return nil
}
func (m *complaintServiceMock) MarkFixedByComplaint(context.Context, []byte, []byte, []byte) error {
return nil
}
func (m *complaintServiceMock) MarkFixedByFlight(context.Context, []byte, []byte, []byte) error {
return nil
}
func (m *complaintServiceMock) ResolveUserNames(ctx context.Context, rows []complaint.Complaint) (map[string]string, error) {
if m.resolveNamesFn != nil {
return m.resolveNamesFn(ctx, rows)
}
return map[string]string{}, nil
}
func newComplaintApp(h *ComplaintHandler, userID []byte) *fiber.App {
app := fiber.New()
app.Use(func(c *fiber.Ctx) error {
c.Locals("user_id", userID)
return c.Next()
})
app.Post("/api/v1/complaints/create", h.Create)
app.Get("/api/v1/complaints/get/:id", h.GetByID)
app.Patch("/api/v1/complaints/update/:id", h.Update)
app.Post("/api/v1/complaints/action-taken/create/:id", h.ActionTaken)
app.Patch("/api/v1/complaints/action-taken/update/:id", h.UpdateActionTaken)
app.Post("/api/v1/complaints/nsr/sign/:id", h.NSRSign)
app.Post("/api/v1/complaints/nsr/unsign/:id", h.NSRUnsign)
app.Get("/api/v1/complaints/hold-items/get-all", h.ListHoldItems)
app.Get("/api/v1/complaints/hold-items/get-all/dt", h.ListHoldItemsDatatable)
return app
}
func TestComplaintHandler_ListHoldItems_FiltersOpenMEL(t *testing.T) {
var got complaint.ListFilter
svc := &complaintServiceMock{
listFn: func(_ context.Context, filter complaint.ListFilter, _ string, _, _ int) ([]complaint.Complaint, int64, error) {
got = filter
return nil, 0, nil
},
}
app := newComplaintApp(NewComplaintHandler(svc), uuidv7.MustBytes())
req, _ := http.NewRequest(http.MethodGet, "/api/v1/complaints/hold-items/get-all?date_from=2026-06-01&date_to=2026-06-30&person=Ganahl&search=vibration", nil)
resp, err := app.Test(req)
if err != nil {
t.Fatal(err)
}
bodyBytes, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d body=%s", resp.StatusCode, string(bodyBytes))
}
if !got.HoldItemsOnly {
t.Fatalf("expected HoldItemsOnly=true in filter, got %#v", got)
}
if got.DateFrom != "2026-06-01" || got.DateTo != "2026-06-30" || got.Person != "Ganahl" || got.Search != "vibration" {
t.Fatalf("expected Von/bis/person/search filters passed, got %#v", got)
}
}
// Action-taken now only records the corrective action TEXT — signing moved to
// POST /action-signoffs/sign (with complaint_id). No sign field, no 422.
func TestComplaintHandler_ActionTaken_RecordsTextWithoutSign(t *testing.T) {
complaintID := uuidv7.MustBytes()
complaintIDStr, _ := uuidv7.BytesToString(complaintID)
helicopterID := uuidv7.MustBytes()
var recorded *complaint.Complaint
svc := &complaintServiceMock{
getByIDFn: func(context.Context, []byte) (*complaint.Complaint, error) {
if recorded != nil {
return recorded, nil
}
return &complaint.Complaint{ID: complaintID, HelicopterID: helicopterID, Description: "vibration", MELSeverity: 2, MELClassifiedAt: func() *time.Time { t := time.Now().UTC(); return &t }()}, nil
},
updateFn: func(_ context.Context, row *complaint.Complaint) error {
if row.ActionTaken == nil || *row.ActionTaken != "tightened mount" {
t.Fatalf("unexpected action_taken: %#v", row.ActionTaken)
}
recorded = row
return nil
},
}
app := newComplaintApp(NewComplaintHandler(svc), uuidv7.MustBytes())
// No `sign` field anymore.
body := `{"data":{"type":"complaint_action_taken","attributes":{"action_taken":"tightened mount"}}}`
req, _ := http.NewRequest(http.MethodPost, "/api/v1/complaints/action-taken/create/"+complaintIDStr, strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
if err != nil {
t.Fatal(err)
}
bodyBytes, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d body=%s", resp.StatusCode, string(bodyBytes))
}
if recorded == nil {
t.Fatal("expected update to be called recording the action text")
}
}
// A pending (unclassified) complaint — mel_classified_at NULL — cannot record action
// taken until MEL is classified; the gate returns 422 and never touches the row.
func TestComplaintHandler_ActionTaken_BlockedWhenMELPending(t *testing.T) {
complaintID := uuidv7.MustBytes()
complaintIDStr, _ := uuidv7.BytesToString(complaintID)
helicopterID := uuidv7.MustBytes()
svc := &complaintServiceMock{
getByIDFn: func(context.Context, []byte) (*complaint.Complaint, error) {
// Pending: no MELClassifiedAt.
return &complaint.Complaint{ID: complaintID, HelicopterID: helicopterID, Description: "vibration"}, nil
},
updateFn: func(context.Context, *complaint.Complaint) error {
t.Fatal("update must not be called while MEL is pending")
return nil
},
}
app := newComplaintApp(NewComplaintHandler(svc), uuidv7.MustBytes())
body := `{"data":{"type":"complaint_action_taken","attributes":{"action_taken":"tightened mount"}}}`
req, _ := http.NewRequest(http.MethodPost, "/api/v1/complaints/action-taken/create/"+complaintIDStr, strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusUnprocessableEntity {
bodyBytes, _ := io.ReadAll(resp.Body)
t.Fatalf("expected 422 (classify MEL first), got %d body=%s", resp.StatusCode, string(bodyBytes))
}
}
// NSR is its own flow: nsr/sign sets the deferral on a classified complaint; a pending
// (unclassified) complaint cannot be deferred and returns 422.
func TestComplaintHandler_NSRSign_SetsDeferralWhenClassified(t *testing.T) {
complaintID := uuidv7.MustBytes()
complaintIDStr, _ := uuidv7.BytesToString(complaintID)
helicopterID := uuidv7.MustBytes()
classified := time.Now().UTC()
var saved *complaint.Complaint
svc := &complaintServiceMock{
getByIDFn: func(context.Context, []byte) (*complaint.Complaint, error) {
if saved != nil {
return saved, nil
}
return &complaint.Complaint{ID: complaintID, HelicopterID: helicopterID, Description: "vibration", MELSeverity: 2, MELClassifiedAt: &classified}, nil
},
updateFn: func(_ context.Context, row *complaint.Complaint) error {
if !row.IsNSR || row.NSRDecidedAt == nil {
t.Fatalf("expected NSR set with decided_at, got %#v", row)
}
saved = row
return nil
},
}
app := newComplaintApp(NewComplaintHandler(svc), uuidv7.MustBytes())
body := `{"data":{"type":"complaint_nsr_sign","attributes":{"nsr_reason":"deferred under MEL B"}}}`
req, _ := http.NewRequest(http.MethodPost, "/api/v1/complaints/nsr/sign/"+complaintIDStr, strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(resp.Body)
t.Fatalf("expected 200, got %d body=%s", resp.StatusCode, string(b))
}
if saved == nil || !saved.IsNSR {
t.Fatal("expected NSR to be recorded")
}
}
func TestComplaintHandler_NSRSign_BlockedWhenMELPending(t *testing.T) {
complaintID := uuidv7.MustBytes()
complaintIDStr, _ := uuidv7.BytesToString(complaintID)
helicopterID := uuidv7.MustBytes()
svc := &complaintServiceMock{
getByIDFn: func(context.Context, []byte) (*complaint.Complaint, error) {
// Pending: no MELClassifiedAt.
return &complaint.Complaint{ID: complaintID, HelicopterID: helicopterID, Description: "vibration"}, nil
},
updateFn: func(context.Context, *complaint.Complaint) error {
t.Fatal("update must not be called: cannot defer a pending complaint")
return nil
},
}
app := newComplaintApp(NewComplaintHandler(svc), uuidv7.MustBytes())
body := `{"data":{"type":"complaint_nsr_sign","attributes":{}}}`
req, _ := http.NewRequest(http.MethodPost, "/api/v1/complaints/nsr/sign/"+complaintIDStr, strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusUnprocessableEntity {
b, _ := io.ReadAll(resp.Body)
t.Fatalf("expected 422 (classify MEL first), got %d body=%s", resp.StatusCode, string(b))
}
}
func TestComplaintHandler_NSRUnsign_ClearsDeferral(t *testing.T) {
complaintID := uuidv7.MustBytes()
complaintIDStr, _ := uuidv7.BytesToString(complaintID)
helicopterID := uuidv7.MustBytes()
classified := time.Now().UTC()
var saved *complaint.Complaint
svc := &complaintServiceMock{
getByIDFn: func(context.Context, []byte) (*complaint.Complaint, error) {
if saved != nil {
return saved, nil
}
return &complaint.Complaint{ID: complaintID, HelicopterID: helicopterID, Description: "vibration", MELSeverity: 2, MELClassifiedAt: &classified, IsNSR: true, NSRDecidedAt: &classified}, nil
},
updateFn: func(_ context.Context, row *complaint.Complaint) error {
if row.IsNSR || row.NSRDecidedAt != nil {
t.Fatalf("expected NSR cleared, got %#v", row)
}
saved = row
return nil
},
}
app := newComplaintApp(NewComplaintHandler(svc), uuidv7.MustBytes())
req, _ := http.NewRequest(http.MethodPost, "/api/v1/complaints/nsr/unsign/"+complaintIDStr, nil)
resp, err := app.Test(req)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(resp.Body)
t.Fatalf("expected 200, got %d body=%s", resp.StatusCode, string(b))
}
if saved == nil || saved.IsNSR {
t.Fatal("expected NSR to be cleared")
}
}
func TestComplaintHandler_UpdateActionTaken_RecordsTextWithoutSign(t *testing.T) {
complaintID := uuidv7.MustBytes()
complaintIDStr, _ := uuidv7.BytesToString(complaintID)
helicopterID := uuidv7.MustBytes()
var recorded *complaint.Complaint
svc := &complaintServiceMock{
getByIDFn: func(context.Context, []byte) (*complaint.Complaint, error) {
if recorded != nil {
return recorded, nil
}
return &complaint.Complaint{ID: complaintID, HelicopterID: helicopterID, Description: "vibration", MELSeverity: 2, MELClassifiedAt: func() *time.Time { t := time.Now().UTC(); return &t }()}, nil
},
updateFn: func(_ context.Context, row *complaint.Complaint) error {
if row.ActionTaken == nil || *row.ActionTaken != "re-torqued mount" {
t.Fatalf("unexpected action_taken: %#v", row.ActionTaken)
}
recorded = row
return nil
},
}
app := newComplaintApp(NewComplaintHandler(svc), uuidv7.MustBytes())
body := `{"data":{"type":"complaint_action_taken","attributes":{"action_taken":"re-torqued mount"}}}`
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/complaints/action-taken/update/"+complaintIDStr, strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
if err != nil {
t.Fatal(err)
}
bodyBytes, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d body=%s", resp.StatusCode, string(bodyBytes))
}
if recorded == nil {
t.Fatal("expected update to be called recording the action text")
}
}
// The complaint response reads its action-sign state from action_signoff (by
// complaint_id), not from the (now-unset) complaint.ActionSignedAt/By columns.
func TestComplaintHandler_GetByID_ReadsSignFromSignoff(t *testing.T) {
complaintID := uuidv7.MustBytes()
complaintIDStr, _ := uuidv7.BytesToString(complaintID)
helicopterID := uuidv7.MustBytes()
signer := uuidv7.MustBytes()
signedAt := time.Now().UTC()
action := "replaced part"
svc := &complaintServiceMock{
getByIDFn: func(context.Context, []byte) (*complaint.Complaint, error) {
return &complaint.Complaint{ID: complaintID, HelicopterID: helicopterID, Description: "vibration", MELSeverity: 2, ActionTaken: &action}, nil
},
}
var lookedUp [][]byte
signoffSvc := &complaintSignoffMock{
getByComplaintIDsFn: func(_ context.Context, ids [][]byte) (map[string]*actionsignoff.ActionSignoff, error) {
lookedUp = ids
return map[string]*actionsignoff.ActionSignoff{
string(complaintID): {ComplaintID: complaintID, HelicopterID: helicopterID, SignedAt: &signedAt, SignedBy: signer},
}, nil
},
}
app := newComplaintApp(NewComplaintHandler(svc).WithActionSignoffService(signoffSvc), uuidv7.MustBytes())
req, _ := http.NewRequest(http.MethodGet, "/api/v1/complaints/get/"+complaintIDStr, nil)
resp, err := app.Test(req)
if err != nil {
t.Fatal(err)
}
bodyBytes, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d body=%s", resp.StatusCode, string(bodyBytes))
}
if len(lookedUp) != 1 || string(lookedUp[0]) != string(complaintID) {
t.Fatalf("expected signoff lookup by complaint id, got %#v", lookedUp)
}
attrs := struct {
ActionSigned bool `json:"action_signed"`
ActionSignedAt *string `json:"action_signed_at"`
}{}
var payload struct {
Data struct {
Data struct {
Attributes json.RawMessage `json:"attributes"`
} `json:"data"`
} `json:"data"`
}
if err := json.Unmarshal(bodyBytes, &payload); err != nil {
t.Fatalf("bad json: %v body=%s", err, string(bodyBytes))
}
if err := json.Unmarshal(payload.Data.Data.Attributes, &attrs); err != nil {
t.Fatalf("bad attributes json: %v body=%s", err, string(bodyBytes))
}
if !attrs.ActionSigned {
t.Fatalf("expected action_signed=true from signoff, got body=%s", string(bodyBytes))
}
if attrs.ActionSignedAt == nil {
t.Fatalf("expected action_signed_at from signoff, got body=%s", string(bodyBytes))
}
}
type complaintAOGRecalcMock struct {
fn func(context.Context, []byte) error
}
func (m *complaintAOGRecalcMock) RecalculateAOGByHelicopter(ctx context.Context, helicopterID []byte) error {
if m.fn != nil {
return m.fn(ctx, helicopterID)
}
return nil
}
// Updating a complaint that was already closed reopens its lifecycle: the complaint is
// unfixed, its corrective-action sign-off is unsigned, and AOG is recomputed.
func TestComplaintHandler_Update_ReopensAndUnsignsWhenClosed(t *testing.T) {
complaintID := uuidv7.MustBytes()
complaintIDStr, _ := uuidv7.BytesToString(complaintID)
helicopterID := uuidv7.MustBytes()
easaID := uuidv7.MustBytes()
fixedAt := time.Now().UTC()
var updatedRow *complaint.Complaint
svc := &complaintServiceMock{
getByIDFn: func(context.Context, []byte) (*complaint.Complaint, error) {
// A fresh CLOSED complaint on each call (fixed by a past EASA release).
return &complaint.Complaint{ID: complaintID, HelicopterID: helicopterID, Description: "vibration", MELSeverity: 2, FixedAt: &fixedAt, FixedByEASAID: easaID}, nil
},
updateFn: func(_ context.Context, row *complaint.Complaint) error {
updatedRow = row
return nil
},
}
var unsignedComplaint []byte
signoffSvc := &complaintSignoffMock{
unsignByComplaintFn: func(_ context.Context, cid []byte) (*actionsignoff.ActionSignoff, error) {
unsignedComplaint = cid
return nil, nil
},
}
var recalced []byte
aog := &complaintAOGRecalcMock{fn: func(_ context.Context, id []byte) error { recalced = id; return nil }}
h := NewComplaintHandler(svc).WithActionSignoffService(signoffSvc).WithAOGRecalculator(aog)
app := newComplaintApp(h, uuidv7.MustBytes())
body := `{"data":{"type":"complaint_update","id":"` + complaintIDStr + `","attributes":{"description":"vibration + extra work"}}}`
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/complaints/update/"+complaintIDStr, strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
if err != nil {
t.Fatal(err)
}
bodyBytes, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d body=%s", resp.StatusCode, string(bodyBytes))
}
if updatedRow == nil {
t.Fatal("expected update to be called")
}
if updatedRow.FixedAt != nil || len(updatedRow.FixedByEASAID) != 0 {
t.Fatalf("expected complaint reopened (fixed_at/by cleared), got at=%v by=%x", updatedRow.FixedAt, updatedRow.FixedByEASAID)
}
if string(unsignedComplaint) != string(complaintID) {
t.Fatalf("expected sign-off unsigned for complaint, got %x", unsignedComplaint)
}
if string(recalced) != string(helicopterID) {
t.Fatalf("expected AOG recomputed for helicopter, got %x", recalced)
}
}
func TestComplaintHandler_UpdateActionTaken_ClearWhenNotSigned(t *testing.T) {
complaintID := uuidv7.MustBytes()
complaintIDStr, _ := uuidv7.BytesToString(complaintID)
action := "old action"
now := time.Now().UTC()
var cleared bool
svc := &complaintServiceMock{
getByIDFn: func(context.Context, []byte) (*complaint.Complaint, error) {
return &complaint.Complaint{ID: complaintID, HelicopterID: uuidv7.MustBytes(), MELSeverity: 2, MELClassifiedAt: &now, ActionTaken: &action}, nil
},
updateFn: func(_ context.Context, row *complaint.Complaint) error {
if row.ActionTaken != nil {
t.Fatalf("expected action_taken cleared to nil, got %v", *row.ActionTaken)
}
cleared = true
return nil
},
}
// unsigned sign-off → clearing allowed
app := newComplaintApp(NewComplaintHandler(svc).WithActionSignoffService(&complaintSignoffMock{}), uuidv7.MustBytes())
body := `{"data":{"type":"complaint_action_taken","attributes":{"action_taken":""}}}`
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/complaints/action-taken/update/"+complaintIDStr, strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
if err != nil {
t.Fatal(err)
}
bodyBytes, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d body=%s", resp.StatusCode, string(bodyBytes))
}
if !cleared {
t.Fatal("expected action_taken to be cleared")
}
}
func TestComplaintHandler_UpdateActionTaken_ClearBlockedWhenSigned(t *testing.T) {
complaintID := uuidv7.MustBytes()
complaintIDStr, _ := uuidv7.BytesToString(complaintID)
action := "old action"
now := time.Now().UTC()
svc := &complaintServiceMock{
getByIDFn: func(context.Context, []byte) (*complaint.Complaint, error) {
return &complaint.Complaint{ID: complaintID, HelicopterID: uuidv7.MustBytes(), MELSeverity: 2, MELClassifiedAt: &now, ActionTaken: &action}, nil
},
updateFn: func(context.Context, *complaint.Complaint) error {
t.Fatal("must not update when clearing a signed action")
return nil
},
}
signoffSvc := &complaintSignoffMock{
getByComplaintIDsFn: func(_ context.Context, _ [][]byte) (map[string]*actionsignoff.ActionSignoff, error) {
return map[string]*actionsignoff.ActionSignoff{
string(complaintID): {ComplaintID: complaintID, SignedAt: &now},
}, nil
},
}
app := newComplaintApp(NewComplaintHandler(svc).WithActionSignoffService(signoffSvc), uuidv7.MustBytes())
body := `{"data":{"type":"complaint_action_taken","attributes":{"action_taken":""}}}`
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/complaints/action-taken/update/"+complaintIDStr, strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
if err != nil {
t.Fatal(err)
}
bodyBytes, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusConflict {
t.Fatalf("expected 409, got %d body=%s", resp.StatusCode, string(bodyBytes))
}
if !strings.Contains(string(bodyBytes), "COMPLAINT_ACTION_SIGNED_LOCKED") {
t.Fatalf("expected COMPLAINT_ACTION_SIGNED_LOCKED, got %s", string(bodyBytes))
}
}
func TestComplaintHandler_ActionTaken_CreateStillRequiresText(t *testing.T) {
complaintIDStr, _ := uuidv7.BytesToString(uuidv7.MustBytes())
app := newComplaintApp(NewComplaintHandler(&complaintServiceMock{}), uuidv7.MustBytes())
body := `{"data":{"type":"complaint_action_taken","attributes":{"action_taken":""}}}`
req, _ := http.NewRequest(http.MethodPost, "/api/v1/complaints/action-taken/create/"+complaintIDStr, strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusUnprocessableEntity {
bodyBytes, _ := io.ReadAll(resp.Body)
t.Fatalf("create with empty action_taken must be 422, got %d body=%s", resp.StatusCode, string(bodyBytes))
}
}