package service import ( "errors" "net/http" "net/http/httptest" "testing" "time" "github.com/glebarez/sqlite" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/validations" "gorm.io/gorm" ) func TestCreateOneRejectsWhenSameDateHasActiveEmptyKandang(t *testing.T) { svc, db := setupDailyChecklistServiceTest(t) insertDailyChecklistRow(t, db, 1, mustDate(t, "2026-01-10"), dailyChecklistCategoryEmptyKandang, strPtr("DRAFT"), nil) result, serviceErr, resp := runCreateOneRequest(t, svc, &validation.Create{ Date: "2026-01-10", KandangId: 1, Category: "cleaning", Status: "DRAFT", }) if result != nil { t.Fatalf("expected nil result, got %+v", result) } assertFiberErrorCode(t, serviceErr, fiber.StatusConflict) if resp.StatusCode != fiber.StatusConflict { t.Fatalf("expected HTTP status %d, got %d", fiber.StatusConflict, resp.StatusCode) } } func TestCreateOneRejectsWhenSameDateHasRejectedEmptyKandang(t *testing.T) { svc, db := setupDailyChecklistServiceTest(t) insertDailyChecklistRow(t, db, 1, mustDate(t, "2026-01-10"), dailyChecklistCategoryEmptyKandang, strPtr(dailyChecklistStatusRejected), nil) result, serviceErr, resp := runCreateOneRequest(t, svc, &validation.Create{ Date: "2026-01-10", KandangId: 1, Category: "cleaning", Status: "DRAFT", }) if result != nil { t.Fatalf("expected nil result, got %+v", result) } assertFiberErrorCode(t, serviceErr, fiber.StatusConflict) if resp.StatusCode != fiber.StatusConflict { t.Fatalf("expected HTTP status %d, got %d", fiber.StatusConflict, resp.StatusCode) } } func TestCreateOneAllowsWhenOnlySoftDeletedEmptyKandangExists(t *testing.T) { svc, db := setupDailyChecklistServiceTest(t) deletedAt := mustDateTime(t, "2026-01-11 10:00:00") insertDailyChecklistRow(t, db, 1, mustDate(t, "2026-01-10"), dailyChecklistCategoryEmptyKandang, strPtr("DRAFT"), &deletedAt) result, serviceErr, resp := runCreateOneRequest(t, svc, &validation.Create{ Date: "2026-01-10", KandangId: 1, Category: "cleaning", Status: "DRAFT", }) if serviceErr != nil { t.Fatalf("expected no error, got %v", serviceErr) } if resp.StatusCode != fiber.StatusCreated { t.Fatalf("expected HTTP status %d, got %d", fiber.StatusCreated, resp.StatusCode) } if result == nil { t.Fatal("expected non-nil result") } if result.Category != "cleaning" { t.Fatalf("expected category cleaning, got %s", result.Category) } var activeCount int64 if err := db.Model(&entity.DailyChecklist{}). Where("kandang_id = ? AND date = ? AND category = ? AND deleted_at IS NULL", 1, mustDate(t, "2026-01-10"), "cleaning"). Count(&activeCount).Error; err != nil { t.Fatalf("failed counting active checklists: %v", err) } if activeCount != 1 { t.Fatalf("expected 1 active cleaning checklist, got %d", activeCount) } } func TestCreateOneRejectsBulkEmptyKandangWhenDateRangeHasConflict(t *testing.T) { svc, db := setupDailyChecklistServiceTest(t) insertDailyChecklistRow(t, db, 1, mustDate(t, "2026-01-03"), dailyChecklistCategoryEmptyKandang, strPtr("APPROVED"), nil) result, serviceErr, resp := runCreateOneRequest(t, svc, &validation.Create{ Date: "2026-01-01", KandangId: 1, Category: "cleaning", Status: "DRAFT", EmptyKandang: true, EmptyKandangEndDate: "2026-01-05", }) if result != nil { t.Fatalf("expected nil result, got %+v", result) } assertFiberErrorCode(t, serviceErr, fiber.StatusConflict) if resp.StatusCode != fiber.StatusConflict { t.Fatalf("expected HTTP status %d, got %d", fiber.StatusConflict, resp.StatusCode) } var activeInRange int64 if err := db.Model(&entity.DailyChecklist{}). Where("kandang_id = ? AND date BETWEEN ? AND ? AND deleted_at IS NULL", 1, mustDate(t, "2026-01-01"), mustDate(t, "2026-01-05")). Count(&activeInRange).Error; err != nil { t.Fatalf("failed counting checklists in range: %v", err) } if activeInRange != 1 { t.Fatalf("expected only pre-existing row to remain in range, got %d rows", activeInRange) } } func TestCreateOneRejectsBulkEmptyKandangWhenRangeHasNonEmptyChecklist(t *testing.T) { svc, db := setupDailyChecklistServiceTest(t) insertDailyChecklistRow(t, db, 1, mustDate(t, "2026-01-03"), "cleaning", strPtr("APPROVED"), nil) result, serviceErr, resp := runCreateOneRequest(t, svc, &validation.Create{ Date: "2026-01-01", KandangId: 1, Category: "cleaning", Status: "DRAFT", EmptyKandang: true, EmptyKandangEndDate: "2026-01-05", }) if result != nil { t.Fatalf("expected nil result, got %+v", result) } assertFiberErrorCode(t, serviceErr, fiber.StatusConflict) if resp.StatusCode != fiber.StatusConflict { t.Fatalf("expected HTTP status %d, got %d", fiber.StatusConflict, resp.StatusCode) } } func TestCreateOneRejectsBulkEmptyKandangWhenRangeHasRejectedChecklist(t *testing.T) { svc, db := setupDailyChecklistServiceTest(t) insertDailyChecklistRow(t, db, 1, mustDate(t, "2026-01-03"), "cleaning", strPtr(dailyChecklistStatusRejected), nil) result, serviceErr, resp := runCreateOneRequest(t, svc, &validation.Create{ Date: "2026-01-01", KandangId: 1, Category: "cleaning", Status: "DRAFT", EmptyKandang: true, EmptyKandangEndDate: "2026-01-05", }) if result != nil { t.Fatalf("expected nil result, got %+v", result) } assertFiberErrorCode(t, serviceErr, fiber.StatusConflict) if resp.StatusCode != fiber.StatusConflict { t.Fatalf("expected HTTP status %d, got %d", fiber.StatusConflict, resp.StatusCode) } } func TestCreateOneAllowsBulkEmptyKandangWhenRangeHasOnlySoftDeletedChecklist(t *testing.T) { svc, db := setupDailyChecklistServiceTest(t) deletedAt := mustDateTime(t, "2026-01-11 10:00:00") insertDailyChecklistRow(t, db, 1, mustDate(t, "2026-01-03"), "cleaning", strPtr("APPROVED"), &deletedAt) result, serviceErr, resp := runCreateOneRequest(t, svc, &validation.Create{ Date: "2026-01-01", KandangId: 1, Category: "cleaning", Status: "DRAFT", EmptyKandang: true, EmptyKandangEndDate: "2026-01-05", }) if serviceErr != nil { t.Fatalf("expected no error, got %v", serviceErr) } if resp.StatusCode != fiber.StatusCreated { t.Fatalf("expected HTTP status %d, got %d", fiber.StatusCreated, resp.StatusCode) } if result == nil { t.Fatal("expected non-nil result") } if result.Category != dailyChecklistCategoryEmptyKandang { t.Fatalf("expected category %s, got %s", dailyChecklistCategoryEmptyKandang, result.Category) } var activeInRange int64 if err := db.Model(&entity.DailyChecklist{}). Where("kandang_id = ? AND date BETWEEN ? AND ? AND deleted_at IS NULL", 1, mustDate(t, "2026-01-01"), mustDate(t, "2026-01-05")). Count(&activeInRange).Error; err != nil { t.Fatalf("failed counting checklists in range: %v", err) } if activeInRange != 5 { t.Fatalf("expected 5 active checklists created for range, got %d", activeInRange) } } func TestCreateOneReusesExistingChecklistWhenNoEmptyKandangConflict(t *testing.T) { svc, db := setupDailyChecklistServiceTest(t) existingID := insertDailyChecklistRow(t, db, 1, mustDate(t, "2026-01-10"), "cleaning", strPtr("APPROVED"), nil) result, serviceErr, resp := runCreateOneRequest(t, svc, &validation.Create{ Date: "2026-01-10", KandangId: 1, Category: "cleaning", Status: "DRAFT", }) if serviceErr != nil { t.Fatalf("expected no error, got %v", serviceErr) } if resp.StatusCode != fiber.StatusCreated { t.Fatalf("expected HTTP status %d, got %d", fiber.StatusCreated, resp.StatusCode) } if result == nil { t.Fatal("expected non-nil result") } if result.Id != existingID { t.Fatalf("expected existing checklist id %d to be reused, got %d", existingID, result.Id) } var activeCount int64 if err := db.Model(&entity.DailyChecklist{}). Where("kandang_id = ? AND date = ? AND category = ? AND deleted_at IS NULL", 1, mustDate(t, "2026-01-10"), "cleaning"). Count(&activeCount).Error; err != nil { t.Fatalf("failed counting active checklists: %v", err) } if activeCount != 1 { t.Fatalf("expected 1 active cleaning checklist, got %d", activeCount) } } func setupDailyChecklistServiceTest(t *testing.T) (DailyChecklistService, *gorm.DB) { t.Helper() db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=private"), &gorm.Config{}) if err != nil { t.Fatalf("failed opening sqlite db: %v", err) } statements := []string{ `CREATE TABLE areas ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, created_by INTEGER NOT NULL, created_at DATETIME NULL, updated_at DATETIME NULL, deleted_at DATETIME NULL )`, `CREATE TABLE locations ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, address TEXT NOT NULL, area_id INTEGER NOT NULL, created_by INTEGER NOT NULL, created_at DATETIME NULL, updated_at DATETIME NULL, deleted_at DATETIME NULL )`, `CREATE TABLE kandang_groups ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, status TEXT NOT NULL, location_id INTEGER NOT NULL, pic_id INTEGER NOT NULL, created_by INTEGER NOT NULL, created_at DATETIME NULL, updated_at DATETIME NULL, deleted_at DATETIME NULL )`, `CREATE TABLE daily_checklists ( id INTEGER PRIMARY KEY AUTOINCREMENT, kandang_id INTEGER NOT NULL, checklist_id INTEGER NULL, date DATE NOT NULL, name TEXT NULL, status TEXT NULL, category TEXT NOT NULL, total_score INTEGER NULL, document_path TEXT NULL, reject_reason TEXT NULL, created_by INTEGER NULL, deleted_by INTEGER NULL, created_at DATETIME NULL, updated_at DATETIME NULL, deleted_at DATETIME NULL )`, `INSERT INTO areas (id, name, created_by, created_at, updated_at, deleted_at) VALUES (1, 'Area A', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL)`, `INSERT INTO locations (id, name, address, area_id, created_by, created_at, updated_at, deleted_at) VALUES (1, 'Farm A', 'Address', 1, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL)`, `INSERT INTO kandang_groups (id, name, status, location_id, pic_id, created_by, created_at, updated_at, deleted_at) VALUES (1, 'Kandang A', 'ACTIVE', 1, 1, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL)`, } for _, stmt := range statements { if err := db.Exec(stmt).Error; err != nil { t.Fatalf("failed preparing schema: %v", err) } } repo := repository.NewDailyChecklistRepository(db) svc := NewDailyChecklistService(repo, nil, validator.New(), nil) return svc, db } func runCreateOneRequest(t *testing.T, svc DailyChecklistService, req *validation.Create) (*entity.DailyChecklist, error, *http.Response) { t.Helper() app := fiber.New() var ( result *entity.DailyChecklist serviceErr error ) app.Post("/", func(c *fiber.Ctx) error { result, serviceErr = svc.CreateOne(c, req) if serviceErr != nil { return serviceErr } return c.SendStatus(fiber.StatusCreated) }) resp, err := app.Test(httptest.NewRequest(http.MethodPost, "/", nil)) if err != nil { t.Fatalf("failed running fiber request: %v", err) } return result, serviceErr, resp } func insertDailyChecklistRow(t *testing.T, db *gorm.DB, kandangID uint, date time.Time, category string, status *string, deletedAt *time.Time) uint { t.Helper() row := &entity.DailyChecklist{ KandangId: kandangID, Date: date, Category: category, Status: status, } if deletedAt != nil { row.DeletedAt = gorm.DeletedAt{ Time: *deletedAt, Valid: true, } } if err := db.Create(row).Error; err != nil { t.Fatalf("failed inserting daily checklist row: %v", err) } return row.Id } func assertFiberErrorCode(t *testing.T, err error, expectedCode int) { t.Helper() if err == nil { t.Fatal("expected error, got nil") } var fiberErr *fiber.Error if !errors.As(err, &fiberErr) { t.Fatalf("expected *fiber.Error, got %T (%v)", err, err) } if fiberErr.Code != expectedCode { t.Fatalf("expected fiber error code %d, got %d", expectedCode, fiberErr.Code) } } func mustDate(t *testing.T, raw string) time.Time { t.Helper() value, err := time.Parse(dailyChecklistDateLayout, raw) if err != nil { t.Fatalf("failed parsing date %q: %v", raw, err) } return value } func mustDateTime(t *testing.T, raw string) time.Time { t.Helper() value, err := time.Parse("2006-01-02 15:04:05", raw) if err != nil { t.Fatalf("failed parsing datetime %q: %v", raw, err) } return value } func strPtr(value string) *string { v := value return &v }