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") 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, "category": "growing", "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"` Category string `json:"category"` Flock struct { Id uint `json:"id"` Name string `json:"name"` } `json:"flock"` Area struct { Id uint `json:"id"` Name string `json:"name"` } `json:"area"` 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.Category != string(utils.ProjectFlockCategoryGrowing) { t.Fatalf("expected category to be %s, got %s", utils.ProjectFlockCategoryGrowing, createResp.Data.Category) } 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 != string(utils.KandangStatusPengajuan) { t.Fatalf("expected kandang status to be PENGAJUAN, got %s", createResp.Data.Kandangs[0].Status) } if createResp.Data.Period != 1 { t.Fatalf("expected period 1 to be assigned automatically, got %d", createResp.Data.Period) } createdKandang := fetchKandang(t, db, kandangID) if createdKandang.Status != string(utils.KandangStatusPengajuan) { t.Fatalf("expected kandang status in DB to be PENGAJUAN, got %s", createdKandang.Status) } 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) } secondKandangID := createKandang(t, app, "Kandang Summary 2", locationID, 1) secondPayload := map[string]any{ "flock_id": flockID, "area_id": areaID, "category": "laying", "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"` Category string `json:"category"` } `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) } if createRespSecond.Data.Category != string(utils.ProjectFlockCategoryLaying) { t.Fatalf("expected category to be %s, got %s", utils.ProjectFlockCategoryLaying, createRespSecond.Data.Category) } 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) } secondKandang := fetchKandang(t, db, secondKandangID) if secondKandang.Status != string(utils.KandangStatusPengajuan) { t.Fatalf("expected second kandang status in DB to be PENGAJUAN, got %s", secondKandang.Status) } 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 remainingFirst int64 if err := db.Model(&entities.ProjectFlockKandang{}). Where("project_flock_id = ? AND kandang_id = ?", createResp.Data.Id, kandangID). Count(&remainingFirst).Error; err != nil { t.Fatalf("failed to count first pivot records after delete: %v", err) } if remainingFirst != 0 { t.Fatalf("expected no pivot records remaining after delete, found %d", remainingFirst) } 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 remainingSecond int64 if err := db.Model(&entities.ProjectFlockKandang{}). Where("project_flock_id = ? AND kandang_id = ?", createRespSecond.Data.Id, secondKandangID). Count(&remainingSecond).Error; err != nil { t.Fatalf("failed to count second pivot records after delete: %v", err) } if remainingSecond != 0 { t.Fatalf("expected no second pivot records remaining after delete, found %d", remainingSecond) } 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") 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, "category": "growing", "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", string(utils.ProjectFlockCategoryGrowing), "growing", "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") 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, "category": "growing", "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, "category": "laying", "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 }