package main import ( "context" "errors" "fmt" "path/filepath" "strings" "testing" "github.com/xuri/excelize/v2" ) func TestParseAdjustmentPriceFile_ValidSingleRow(t *testing.T) { filePath := createWorkbook( t, "adjustment_prices", []string{"adjustment_id", "weight"}, [][]string{{"101", "12.345"}}, ) sheet, rows, issues, err := parseAdjustmentPriceFile(filePath, "") if err != nil { t.Fatalf("expected no error, got %v", err) } if sheet != "adjustment_prices" { t.Fatalf("expected selected sheet adjustment_prices, got %q", sheet) } if len(issues) != 0 { t.Fatalf("expected no issues, got %+v", issues) } if len(rows) != 1 { t.Fatalf("expected 1 row, got %d", len(rows)) } if rows[0].AdjustmentID != 101 { t.Fatalf("expected adjustment_id 101, got %d", rows[0].AdjustmentID) } if rows[0].Weight != 12.345 { t.Fatalf("expected weight 12.345, got %v", rows[0].Weight) } } func TestParseAdjustmentPriceFile_ValidMultiRow(t *testing.T) { filePath := createWorkbook( t, "adjustment_prices", []string{" Adjustment_ID ", "WEIGHT"}, [][]string{{"101", "10"}, {"102", "11.5"}}, ) _, rows, issues, err := parseAdjustmentPriceFile(filePath, "adjustment_prices") if err != nil { t.Fatalf("expected no error, got %v", err) } if len(issues) != 0 { t.Fatalf("expected no issues, got %+v", issues) } if len(rows) != 2 { t.Fatalf("expected 2 rows, got %d", len(rows)) } } func TestParseAdjustmentPriceFile_MissingRequiredHeader(t *testing.T) { filePath := createWorkbook( t, "adjustment_prices", []string{"adjustment_id", "price"}, [][]string{{"101", "12"}}, ) _, rows, issues, err := parseAdjustmentPriceFile(filePath, "") if err != nil { t.Fatalf("expected no error, got %v", err) } if len(rows) != 0 { t.Fatalf("expected 0 parsed rows when header invalid, got %d", len(rows)) } if !hasIssue(issues, 0, "weight", "required header is missing") { t.Fatalf("expected missing weight header issue, got %+v", issues) } } func TestParseAdjustmentPriceFile_InvalidAdjustmentID(t *testing.T) { filePath := createWorkbook( t, "adjustment_prices", []string{"adjustment_id", "weight"}, [][]string{{"abc", "10"}, {"0", "12"}}, ) _, rows, issues, err := parseAdjustmentPriceFile(filePath, "") if err != nil { t.Fatalf("expected no error, got %v", err) } if len(rows) != 0 { t.Fatalf("expected no valid rows, got %d", len(rows)) } if !hasIssue(issues, 2, "adjustment_id", "must be a positive integer") { t.Fatalf("expected non numeric adjustment_id issue, got %+v", issues) } if !hasIssue(issues, 3, "adjustment_id", "must be greater than 0") { t.Fatalf("expected adjustment_id >0 issue, got %+v", issues) } } func TestParseAdjustmentPriceFile_InvalidWeight(t *testing.T) { filePath := createWorkbook( t, "adjustment_prices", []string{"adjustment_id", "weight"}, [][]string{{"101", "abc"}, {"102", "-1"}}, ) _, rows, issues, err := parseAdjustmentPriceFile(filePath, "") if err != nil { t.Fatalf("expected no error, got %v", err) } if len(rows) != 0 { t.Fatalf("expected no valid rows, got %d", len(rows)) } if !hasIssue(issues, 2, "weight", "must be numeric") { t.Fatalf("expected weight numeric issue, got %+v", issues) } if !hasIssue(issues, 3, "weight", "must be greater than or equal to 0") { t.Fatalf("expected weight >=0 issue, got %+v", issues) } } func TestParseAdjustmentPriceFile_DuplicateAdjustmentID_LastRowWins(t *testing.T) { filePath := createWorkbook( t, "adjustment_prices", []string{"adjustment_id", "weight"}, [][]string{{"101", "10"}, {"102", "20"}, {"101", "30"}}, ) _, rows, issues, err := parseAdjustmentPriceFile(filePath, "") if err != nil { t.Fatalf("expected no error, got %v", err) } if len(issues) != 0 { t.Fatalf("expected no issues, got %+v", issues) } if len(rows) != 2 { t.Fatalf("expected 2 deduped rows, got %d", len(rows)) } row101, ok := findRowByAdjustmentID(rows, 101) if !ok { t.Fatalf("expected adjustment_id 101 to exist, got %+v", rows) } if row101.Weight != 30 { t.Fatalf("expected duplicate adjustment_id to keep last weight 30, got %v", row101.Weight) } if row101.RowNumber != 4 { t.Fatalf("expected duplicate adjustment_id to keep last row number 4, got %d", row101.RowNumber) } } func TestSplitRowsByExistingIDs_SkipMissing(t *testing.T) { rows := []adjustmentPriceImportRow{ {RowNumber: 2, AdjustmentID: 101, Weight: 10}, {RowNumber: 3, AdjustmentID: 102, Weight: 11}, {RowNumber: 4, AdjustmentID: 103, Weight: 12}, } existing := map[uint]struct{}{101: {}, 103: {}} processable, skipped := splitRowsByExistingIDs(rows, existing) if len(processable) != 2 { t.Fatalf("expected 2 processable rows, got %d", len(processable)) } if len(skipped) != 1 { t.Fatalf("expected 1 skipped row, got %d", len(skipped)) } if skipped[0].AdjustmentID != 102 { t.Fatalf("expected adjustment_id 102 skipped, got %+v", skipped) } } func TestApplyIfRequested_DryRunDoesNotWrite(t *testing.T) { runner := &fakeTransactionRunner{} rows := []adjustmentPriceImportRow{{RowNumber: 2, AdjustmentID: 101, Weight: 10}} results, err := applyIfRequested(context.Background(), false, runner, rows) if err != nil { t.Fatalf("expected no error, got %v", err) } if results != nil { t.Fatalf("expected nil results on dry-run, got %+v", results) } if runner.txCalls != 0 { t.Fatalf("expected no transaction call during dry-run, got %d", runner.txCalls) } } func TestApplyImportRows_Success(t *testing.T) { runner := &fakeTransactionRunner{ changedByID: map[uint]bool{101: true, 102: false}, } rows := []adjustmentPriceImportRow{ {RowNumber: 2, AdjustmentID: 101, Weight: 10}, {RowNumber: 3, AdjustmentID: 102, Weight: 11}, } results, err := applyImportRows(context.Background(), runner, rows) if err != nil { t.Fatalf("expected no error, got %v", err) } if runner.txCalls != 1 { t.Fatalf("expected 1 transaction call, got %d", runner.txCalls) } if len(runner.committedCalls) != 2 { t.Fatalf("expected 2 committed updates, got %d", len(runner.committedCalls)) } if len(results) != 2 { t.Fatalf("expected 2 row results, got %d", len(results)) } if !results[0].Changed || results[1].Changed { t.Fatalf("unexpected changed flags: %+v", results) } } func TestApplyImportRows_RollbackOnError(t *testing.T) { runner := &fakeTransactionRunner{ errByID: map[uint]error{102: errors.New("boom")}, } rows := []adjustmentPriceImportRow{ {RowNumber: 2, AdjustmentID: 101, Weight: 10}, {RowNumber: 3, AdjustmentID: 102, Weight: 11}, } _, err := applyImportRows(context.Background(), runner, rows) if err == nil { t.Fatal("expected error due to update failure") } if !strings.Contains(err.Error(), "row 3 adjustment_id=102 update failed") { t.Fatalf("unexpected error message: %v", err) } if runner.txCalls != 1 { t.Fatalf("expected 1 transaction call, got %d", runner.txCalls) } if len(runner.committedCalls) != 0 { t.Fatalf("expected no committed updates on rollback, got %d", len(runner.committedCalls)) } } func createWorkbook(t *testing.T, sheetName string, headers []string, rows [][]string) string { t.Helper() f := excelize.NewFile() defaultSheet := f.GetSheetName(f.GetActiveSheetIndex()) if sheetName == "" { sheetName = defaultSheet } else if sheetName != defaultSheet { f.SetSheetName(defaultSheet, sheetName) } for idx, header := range headers { cell, err := excelize.CoordinatesToCellName(idx+1, 1) if err != nil { t.Fatalf("failed resolving header cell: %v", err) } if err := f.SetCellValue(sheetName, cell, header); err != nil { t.Fatalf("failed setting header cell: %v", err) } } for rowIdx, row := range rows { for colIdx, value := range row { cell, err := excelize.CoordinatesToCellName(colIdx+1, rowIdx+2) if err != nil { t.Fatalf("failed resolving data cell: %v", err) } if err := f.SetCellValue(sheetName, cell, value); err != nil { t.Fatalf("failed setting data cell: %v", err) } } } path := filepath.Join(t.TempDir(), "adjustment_prices.xlsx") if err := f.SaveAs(path); err != nil { t.Fatalf("failed saving workbook: %v", err) } if err := f.Close(); err != nil { t.Fatalf("failed closing workbook: %v", err) } return path } func hasIssue(issues []validationIssue, row int, field, messageContains string) bool { for _, issue := range issues { if issue.Row != row { continue } if issue.Field != field { continue } if strings.Contains(issue.Message, messageContains) { return true } } return false } func findRowByAdjustmentID(rows []adjustmentPriceImportRow, adjustmentID uint) (adjustmentPriceImportRow, bool) { for _, row := range rows { if row.AdjustmentID == adjustmentID { return row, true } } return adjustmentPriceImportRow{}, false } type updateCall struct { adjustmentID uint price float64 } type fakeAdjustmentPriceStore struct { changedByID map[uint]bool errByID map[uint]error calls []updateCall } func (s *fakeAdjustmentPriceStore) UpdatePrice(_ context.Context, adjustmentID uint, price float64) (bool, error) { s.calls = append(s.calls, updateCall{adjustmentID: adjustmentID, price: price}) if err, exists := s.errByID[adjustmentID]; exists { return false, fmt.Errorf("forced update failure for adjustment_id=%d: %w", adjustmentID, err) } if changed, exists := s.changedByID[adjustmentID]; exists { return changed, nil } return true, nil } type fakeTransactionRunner struct { txCalls int changedByID map[uint]bool errByID map[uint]error committedCalls []updateCall } func (r *fakeTransactionRunner) InTx(ctx context.Context, fn func(store adjustmentPriceStore) error) error { r.txCalls++ txStore := &fakeAdjustmentPriceStore{ changedByID: r.changedByID, errByID: r.errByID, calls: make([]updateCall, 0), } if err := fn(txStore); err != nil { return err } r.committedCalls = append(r.committedCalls, txStore.calls...) return nil } var _ txRunner = (*fakeTransactionRunner)(nil) var _ adjustmentPriceStore = (*fakeAdjustmentPriceStore)(nil)