package test import ( "encoding/json" "fmt" "net/http" "net/url" "testing" "github.com/gofiber/fiber/v2" "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/utils" ) func TestProjectFlockSummary(t *testing.T) { app, db := setupIntegrationApp(t) areaID := createArea(t, app, "Area Project") locationID := createLocation(t, app, "Location Project", "Address", areaID) flockID := createFlock(t, app, "Flock Summary") categoryID := createProductCategory(t, app, "DOC Summary", "DOCS") fcrID := createFcr(t, app, "FCR Summary", []map[string]any{ {"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0}, }) kandangID := createKandang(t, app, "Kandang Summary", locationID, 1) createPayload := map[string]any{ "flock_id": flockID, "area_id": areaID, "product_category_id": categoryID, "fcr_id": fcrID, "location_id": locationID, "kandang_ids": []uint{kandangID}, } resp, body := doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", createPayload) if resp.StatusCode != fiber.StatusCreated { t.Fatalf("expected 201 when creating project flock, got %d: %s", resp.StatusCode, string(body)) } var createResp struct { Data struct { Id uint `json:"id"` Period int `json:"period"` Flock struct { Id uint `json:"id"` Name string `json:"name"` } `json:"flock"` Area struct { Id uint `json:"id"` Name string `json:"name"` } `json:"area"` ProductCategory struct { Id uint `json:"id"` Name string `json:"name"` Code string `json:"code"` } `json:"product_category"` Fcr struct { Id uint `json:"id"` Name string `json:"name"` } `json:"fcr"` Location struct { Id uint `json:"id"` Name string `json:"name"` Address string `json:"address"` } `json:"location"` Kandangs []struct { Id uint `json:"id"` Name string `json:"name"` Status string `json:"status"` } `json:"kandangs"` CreatedUser struct { Id uint `json:"id"` IdUser uint `json:"id_user"` Email string `json:"email"` Name string `json:"name"` } `json:"created_user"` } `json:"data"` } if err := json.Unmarshal(body, &createResp); err != nil { t.Fatalf("failed to parse create response: %v", err) } if createResp.Data.Flock.Id != flockID || createResp.Data.Flock.Name == "" { t.Fatalf("expected flock detail to be present, got %+v", createResp.Data.Flock) } if createResp.Data.Area.Id != areaID || createResp.Data.Area.Name == "" { t.Fatalf("expected area detail to be present, got %+v", createResp.Data.Area) } if createResp.Data.Location.Id != locationID || createResp.Data.Location.Name == "" { t.Fatalf("expected location detail to be present, got %+v", createResp.Data.Location) } if len(createResp.Data.Kandangs) != 1 || createResp.Data.Kandangs[0].Id != kandangID { t.Fatalf("expected kandang detail to be present, got %+v", createResp.Data.Kandangs) } if createResp.Data.Kandangs[0].Status == "" { t.Fatalf("expected kandang status to be present, got %+v", createResp.Data.Kandangs[0]) } if createResp.Data.Period != 1 { t.Fatalf("expected period 1 to be assigned automatically, got %d", createResp.Data.Period) } var pivotRecords []entities.ProjectFlockKandang if err := db.Where("project_flock_id = ?", createResp.Data.Id).Find(&pivotRecords).Error; err != nil { t.Fatalf("failed to fetch pivot records: %v", err) } if len(pivotRecords) != 1 { t.Fatalf("expected 1 pivot record, got %d", len(pivotRecords)) } firstPivotRecord := pivotRecords[0] if firstPivotRecord.KandangId != kandangID { t.Fatalf("expected pivot kandang id %d, got %d", kandangID, firstPivotRecord.KandangId) } if firstPivotRecord.DetachedAt != nil { t.Fatalf("expected pivot DetachedAt to be nil for active assignment, got %v", firstPivotRecord.DetachedAt) } secondKandangID := createKandang(t, app, "Kandang Summary 2", locationID, 1) secondPayload := map[string]any{ "flock_id": flockID, "area_id": areaID, "product_category_id": categoryID, "fcr_id": fcrID, "location_id": locationID, "kandang_ids": []uint{secondKandangID}, } resp, body = doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", secondPayload) if resp.StatusCode != fiber.StatusCreated { t.Fatalf("expected 201 when creating second project flock, got %d: %s", resp.StatusCode, string(body)) } var createRespSecond struct { Data struct { Id uint `json:"id"` Period int `json:"period"` } `json:"data"` } if err := json.Unmarshal(body, &createRespSecond); err != nil { t.Fatalf("failed to parse second create response: %v", err) } if createRespSecond.Data.Period != 2 { t.Fatalf("expected second period to be 2, got %d", createRespSecond.Data.Period) } pivotRecords = nil if err := db.Where("project_flock_id = ?", createRespSecond.Data.Id).Find(&pivotRecords).Error; err != nil { t.Fatalf("failed to fetch second pivot records: %v", err) } if len(pivotRecords) != 1 { t.Fatalf("expected 1 pivot record for second project, got %d", len(pivotRecords)) } secondPivotRecord := pivotRecords[0] if secondPivotRecord.KandangId != secondKandangID { t.Fatalf("expected second pivot kandang id %d, got %d", secondKandangID, secondPivotRecord.KandangId) } if secondPivotRecord.DetachedAt != nil { t.Fatalf("expected second pivot DetachedAt to be nil, got %v", secondPivotRecord.DetachedAt) } resp, body = doJSONRequest(t, app, http.MethodGet, "/api/production/project_flocks/flocks/"+uintToString(flockID)+"/periods", nil) if resp.StatusCode != fiber.StatusOK { t.Fatalf("expected 200 when fetching summary, got %d: %s", resp.StatusCode, string(body)) } var summary struct { Data struct { NextPeriod int `json:"next_period"` } `json:"data"` } if err := json.Unmarshal(body, &summary); err != nil { t.Fatalf("failed to parse summary response: %v", err) } if summary.Data.NextPeriod != 3 { t.Fatalf("expected next_period 3, got %d", summary.Data.NextPeriod) } resp, body = doJSONRequest(t, app, http.MethodDelete, "/api/production/project_flocks/"+uintToString(createResp.Data.Id), nil) if resp.StatusCode != fiber.StatusOK { t.Fatalf("expected 200 when deleting first project flock, got %d: %s", resp.StatusCode, string(body)) } firstKandang := fetchKandang(t, db, kandangID) if firstKandang.ProjectFlockId != nil { t.Fatalf("expected project_flock_id to be nil after delete, got %v", *firstKandang.ProjectFlockId) } if firstKandang.Status != string(utils.KandangStatusNonActive) { t.Fatalf("expected kandang status to revert to NON_ACTIVE, got %s", firstKandang.Status) } var firstPivot entities.ProjectFlockKandang if err := db.First(&firstPivot, firstPivotRecord.Id).Error; err != nil { t.Fatalf("failed to reload first pivot record: %v", err) } if firstPivot.DetachedAt == nil { t.Fatalf("expected first pivot DetachedAt to be set after delete") } if firstPivot.ProjectFlockId != createResp.Data.Id { t.Fatalf("expected first pivot project_flock_id %d, got %d", createResp.Data.Id, firstPivot.ProjectFlockId) } resp, body = doJSONRequest(t, app, http.MethodDelete, "/api/production/project_flocks/"+uintToString(createRespSecond.Data.Id), nil) if resp.StatusCode != fiber.StatusOK { t.Fatalf("expected 200 when deleting second project flock, got %d: %s", resp.StatusCode, string(body)) } secondKandang := fetchKandang(t, db, secondKandangID) if secondKandang.ProjectFlockId != nil { t.Fatalf("expected second project_flock_id to be nil after delete, got %v", *secondKandang.ProjectFlockId) } if secondKandang.Status != string(utils.KandangStatusNonActive) { t.Fatalf("expected second kandang status to revert to NON_ACTIVE, got %s", secondKandang.Status) } var secondPivot entities.ProjectFlockKandang if err := db.First(&secondPivot, secondPivotRecord.Id).Error; err != nil { t.Fatalf("failed to reload second pivot record: %v", err) } if secondPivot.DetachedAt == nil { t.Fatalf("expected second pivot DetachedAt to be set after delete") } if secondPivot.ProjectFlockId != createRespSecond.Data.Id { t.Fatalf("expected second pivot project_flock_id %d, got %d", createRespSecond.Data.Id, secondPivot.ProjectFlockId) } resp, body = doJSONRequest(t, app, http.MethodGet, "/api/production/project_flocks/flocks/"+uintToString(flockID)+"/periods", nil) if resp.StatusCode != fiber.StatusOK { t.Fatalf("expected 200 when fetching summary after delete, got %d: %s", resp.StatusCode, string(body)) } if err := json.Unmarshal(body, &summary); err != nil { t.Fatalf("failed to parse summary response after delete: %v", err) } if summary.Data.NextPeriod != 1 { t.Fatalf("expected next_period 1 after soft deletes, got %d", summary.Data.NextPeriod) } } func uintToString(v uint) string { return fmt.Sprintf("%d", v) } func TestProjectFlockSearchByRelatedFields(t *testing.T) { app, _ := setupIntegrationApp(t) areaID := createArea(t, app, "Area Search Target") locationID := createLocation(t, app, "Location Search Target", "Location Address Target", areaID) flockID := createFlock(t, app, "Flock Search Target") categoryID := createProductCategory(t, app, "Category Search Target", "CATGT") fcrID := createFcr(t, app, "FCR Search Target", []map[string]any{ {"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0}, }) kandangID := createKandang(t, app, "Kandang Search Target", locationID, 1) createPayload := map[string]any{ "flock_id": flockID, "area_id": areaID, "product_category_id": categoryID, "fcr_id": fcrID, "location_id": locationID, "kandang_ids": []uint{kandangID}, } resp, body := doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", createPayload) if resp.StatusCode != fiber.StatusCreated { t.Fatalf("expected 201 when creating project flock, got %d: %s", resp.StatusCode, string(body)) } var createResp struct { Data struct { Id uint `json:"id"` } `json:"data"` } if err := json.Unmarshal(body, &createResp); err != nil { t.Fatalf("failed to parse create response: %v", err) } searchTerms := []string{ "Flock Search Target", "Area Search Target", "Category Search Target", "CATGT", "FCR Search Target", "Kandang Search Target", "Location Search Target", "Location Address Target", "Tester", "1", } for _, term := range searchTerms { path := "/api/production/project_flocks?search=" + url.QueryEscape(term) resp, body := doJSONRequest(t, app, http.MethodGet, path, nil) if resp.StatusCode != fiber.StatusOK { t.Fatalf("expected 200 when searching for %q, got %d: %s", term, resp.StatusCode, string(body)) } var listResp struct { Data []struct { Id uint `json:"id"` } `json:"data"` Meta struct { TotalResults int64 `json:"total_results"` } `json:"meta"` } if err := json.Unmarshal(body, &listResp); err != nil { t.Fatalf("failed to parse list response for %q: %v", term, err) } if listResp.Meta.TotalResults == 0 { t.Fatalf("expected at least one result when searching for %q", term) } if len(listResp.Data) == 0 { t.Fatalf("expected data when searching for %q", term) } if listResp.Data[0].Id != createResp.Data.Id { t.Fatalf("expected project flock id %d for search term %q, got %d", createResp.Data.Id, term, listResp.Data[0].Id) } } } func TestProjectFlockSorting(t *testing.T) { app, _ := setupIntegrationApp(t) areaA := createArea(t, app, "Area Alpha") areaB := createArea(t, app, "Area Beta") locationA := createLocation(t, app, "Location Alpha", "Address Alpha", areaA) locationB := createLocation(t, app, "Location Beta", "Address Beta", areaB) flockOne := createFlock(t, app, "Flock Sort One") flockTwo := createFlock(t, app, "Flock Sort Two") categoryID := createProductCategory(t, app, "Category Sort", "CSORT") fcrID := createFcr(t, app, "FCR Sort", []map[string]any{ {"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0}, }) kandangOne := createKandang(t, app, "Kandang Sort One", locationA, 1) kandangTwo := createKandang(t, app, "Kandang Sort Two", locationB, 1) kandangThree := createKandang(t, app, "Kandang Sort Three", locationB, 1) projectOnePayload := map[string]any{ "flock_id": flockOne, "area_id": areaA, "product_category_id": categoryID, "fcr_id": fcrID, "location_id": locationA, "kandang_ids": []uint{kandangOne}, } resp, body := doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", projectOnePayload) if resp.StatusCode != fiber.StatusCreated { t.Fatalf("expected 201 for project one, got %d: %s", resp.StatusCode, string(body)) } projectOneID := parseProjectFlockID(t, body) projectTwoPayload := map[string]any{ "flock_id": flockTwo, "area_id": areaB, "product_category_id": categoryID, "fcr_id": fcrID, "location_id": locationB, "kandang_ids": []uint{kandangTwo, kandangThree}, } resp, body = doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", projectTwoPayload) if resp.StatusCode != fiber.StatusCreated { t.Fatalf("expected 201 for project two, got %d: %s", resp.StatusCode, string(body)) } projectTwoID := parseProjectFlockID(t, body) updatePeriodPayload := map[string]any{"period": 5} resp, body = doJSONRequest(t, app, http.MethodPatch, "/api/production/project_flocks/"+uintToString(projectTwoID), updatePeriodPayload) if resp.StatusCode != fiber.StatusOK { t.Fatalf("expected 200 when updating period, got %d: %s", resp.StatusCode, string(body)) } assertOrder := func(t *testing.T, app *fiber.App, query string, expectedFirst uint) { t.Helper() resp, body := doJSONRequest(t, app, http.MethodGet, "/api/production/project_flocks?"+query, nil) if resp.StatusCode != fiber.StatusOK { t.Fatalf("expected 200 for query %q, got %d: %s", query, resp.StatusCode, string(body)) } var listResp struct { Data []struct { Id uint `json:"id"` } `json:"data"` } if err := json.Unmarshal(body, &listResp); err != nil { t.Fatalf("failed to parse list response for %q: %v", query, err) } if len(listResp.Data) == 0 { t.Fatalf("expected data for query %q", query) } if listResp.Data[0].Id != expectedFirst { t.Fatalf("expected first id %d for query %q, got %d", expectedFirst, query, listResp.Data[0].Id) } } assertOrder(t, app, "sort_by=area&sort_order=asc", projectOneID) assertOrder(t, app, "sort_by=location&sort_order=desc", projectTwoID) assertOrder(t, app, "sort_by=period&sort_order=desc", projectTwoID) assertOrder(t, app, "sort_by=kandangs&sort_order=desc", projectTwoID) assertOrder(t, app, "sort_by=kandangs&sort_order=asc", projectOneID) } func parseProjectFlockID(t *testing.T, body []byte) uint { t.Helper() var resp struct { Data struct { Id uint `json:"id"` } `json:"data"` } if err := json.Unmarshal(body, &resp); err != nil { t.Fatalf("failed to parse project flock response: %v", err) } return resp.Data.Id }