chore: save current changes

This commit is contained in:
2026-02-18 07:02:17 +03:00
parent 2e973b6d78
commit 71f73e2f1d
10 changed files with 822 additions and 374 deletions

View File

@@ -10,25 +10,39 @@ import (
"git.mchus.pro/mchus/quoteforge/internal/config"
)
func TestToCSV_UTF8BOM(t *testing.T) {
svc := NewExportService(config.ExportConfig{}, nil)
data := &ExportData{
Name: "Test",
Items: []ExportItem{
func newTestProjectData(items []ExportItem, article string, serverCount int) *ProjectExportData {
var unitTotal float64
for _, item := range items {
unitTotal += item.UnitPrice * float64(item.Quantity)
}
if serverCount < 1 {
serverCount = 1
}
return &ProjectExportData{
Configs: []ConfigExportBlock{
{
LotName: "LOT-001",
Description: "Test Item",
Category: "CAT",
Quantity: 1,
UnitPrice: 100.0,
TotalPrice: 100.0,
Article: article,
ServerCount: serverCount,
UnitPrice: unitTotal,
Items: items,
},
},
Total: 100.0,
CreatedAt: time.Now(),
}
}
func TestToCSV_UTF8BOM(t *testing.T) {
svc := NewExportService(config.ExportConfig{}, nil, nil)
data := newTestProjectData([]ExportItem{
{
LotName: "LOT-001",
Category: "CAT",
Quantity: 1,
UnitPrice: 100.0,
TotalPrice: 100.0,
},
}, "TEST-ARTICLE", 1)
var buf bytes.Buffer
if err := svc.ToCSV(&buf, data); err != nil {
@@ -40,40 +54,31 @@ func TestToCSV_UTF8BOM(t *testing.T) {
t.Fatalf("CSV too short to contain BOM")
}
// Check UTF-8 BOM: 0xEF 0xBB 0xBF
expectedBOM := []byte{0xEF, 0xBB, 0xBF}
actualBOM := csvBytes[:3]
if bytes.Compare(actualBOM, expectedBOM) != 0 {
if !bytes.Equal(actualBOM, expectedBOM) {
t.Errorf("UTF-8 BOM mismatch. Expected %v, got %v", expectedBOM, actualBOM)
}
}
func TestToCSV_SemicolonDelimiter(t *testing.T) {
svc := NewExportService(config.ExportConfig{}, nil)
svc := NewExportService(config.ExportConfig{}, nil, nil)
data := &ExportData{
Name: "Test",
Items: []ExportItem{
{
LotName: "LOT-001",
Description: "Test Item",
Category: "CAT",
Quantity: 2,
UnitPrice: 100.50,
TotalPrice: 201.00,
},
data := newTestProjectData([]ExportItem{
{
LotName: "LOT-001",
Category: "CAT",
Quantity: 2,
UnitPrice: 100.50,
TotalPrice: 201.00,
},
Total: 201.00,
CreatedAt: time.Now(),
}
}, "TEST-ARTICLE", 1)
var buf bytes.Buffer
if err := svc.ToCSV(&buf, data); err != nil {
t.Fatalf("ToCSV failed: %v", err)
}
// Skip BOM and read CSV with semicolon delimiter
csvBytes := buf.Bytes()
reader := csv.NewReader(bytes.NewReader(csvBytes[3:]))
reader.Comma = ';'
@@ -84,125 +89,52 @@ func TestToCSV_SemicolonDelimiter(t *testing.T) {
t.Fatalf("Failed to read header: %v", err)
}
if len(header) != 6 {
t.Errorf("Expected 6 columns, got %d", len(header))
if len(header) != 8 {
t.Errorf("Expected 8 columns, got %d", len(header))
}
expectedHeader := []string{"Артикул", "Описание", "Категория", "Количество", "Цена за единицу", "Сумма"}
expectedHeader := []string{"Line", "Type", "p/n", "Description", "Qty (1 pcs.)", "Qty (total)", "Price (1 pcs.)", "Price (total)"}
for i, col := range expectedHeader {
if i < len(header) && header[i] != col {
t.Errorf("Column %d: expected %q, got %q", i, col, header[i])
}
}
// Read item row
// Read server row
serverRow, err := reader.Read()
if err != nil {
t.Fatalf("Failed to read server row: %v", err)
}
if serverRow[0] != "10" {
t.Errorf("Expected line number 10, got %s", serverRow[0])
}
if serverRow[2] != "TEST-ARTICLE" {
t.Errorf("Expected article TEST-ARTICLE, got %s", serverRow[2])
}
// Read component row
itemRow, err := reader.Read()
if err != nil {
t.Fatalf("Failed to read item row: %v", err)
}
if itemRow[0] != "LOT-001" {
t.Errorf("Lot name mismatch: expected LOT-001, got %s", itemRow[0])
if itemRow[2] != "LOT-001" {
t.Errorf("Lot name mismatch: expected LOT-001, got %s", itemRow[2])
}
if itemRow[3] != "2" {
t.Errorf("Quantity mismatch: expected 2, got %s", itemRow[3])
if itemRow[4] != "2" {
t.Errorf("Quantity mismatch: expected 2, got %s", itemRow[4])
}
if itemRow[4] != "100,50" {
t.Errorf("Unit price mismatch: expected 100,50, got %s", itemRow[4])
if itemRow[6] != "100,5" {
t.Errorf("Unit price mismatch: expected 100,5, got %s", itemRow[6])
}
}
func TestToCSV_TotalRow(t *testing.T) {
svc := NewExportService(config.ExportConfig{}, nil)
func TestToCSV_ServerRow(t *testing.T) {
svc := NewExportService(config.ExportConfig{}, nil, nil)
data := &ExportData{
Name: "Test",
Items: []ExportItem{
{
LotName: "LOT-001",
Description: "Item 1",
Category: "CAT",
Quantity: 1,
UnitPrice: 100.0,
TotalPrice: 100.0,
},
{
LotName: "LOT-002",
Description: "Item 2",
Category: "CAT",
Quantity: 2,
UnitPrice: 50.0,
TotalPrice: 100.0,
},
},
Total: 200.0,
CreatedAt: time.Now(),
}
var buf bytes.Buffer
if err := svc.ToCSV(&buf, data); err != nil {
t.Fatalf("ToCSV failed: %v", err)
}
csvBytes := buf.Bytes()
reader := csv.NewReader(bytes.NewReader(csvBytes[3:]))
reader.Comma = ';'
// Skip header and item rows
reader.Read()
reader.Read()
reader.Read()
// Read total row
totalRow, err := reader.Read()
if err != nil {
t.Fatalf("Failed to read total row: %v", err)
}
// Total row should have "ИТОГО:" in position 4 and total value in position 5
if totalRow[4] != "ИТОГО:" {
t.Errorf("Expected 'ИТОГО:' in column 4, got %q", totalRow[4])
}
if totalRow[5] != "200,00" {
t.Errorf("Expected total 200,00, got %s", totalRow[5])
}
}
func TestToCSV_CategorySorting(t *testing.T) {
// Test category sorting without category repo (items maintain original order)
svc := NewExportService(config.ExportConfig{}, nil)
data := &ExportData{
Name: "Test",
Items: []ExportItem{
{
LotName: "LOT-001",
Category: "CAT-A",
Quantity: 1,
UnitPrice: 100.0,
TotalPrice: 100.0,
},
{
LotName: "LOT-002",
Category: "CAT-C",
Quantity: 1,
UnitPrice: 100.0,
TotalPrice: 100.0,
},
{
LotName: "LOT-003",
Category: "CAT-B",
Quantity: 1,
UnitPrice: 100.0,
TotalPrice: 100.0,
},
},
Total: 300.0,
CreatedAt: time.Now(),
}
data := newTestProjectData([]ExportItem{
{LotName: "LOT-001", Category: "CAT", Quantity: 1, UnitPrice: 100.0, TotalPrice: 100.0},
{LotName: "LOT-002", Category: "CAT", Quantity: 2, UnitPrice: 50.0, TotalPrice: 100.0},
}, "DL380-ART", 10)
var buf bytes.Buffer
if err := svc.ToCSV(&buf, data); err != nil {
@@ -216,30 +148,75 @@ func TestToCSV_CategorySorting(t *testing.T) {
// Skip header
reader.Read()
// Read server row
serverRow, err := reader.Read()
if err != nil {
t.Fatalf("Failed to read server row: %v", err)
}
if serverRow[0] != "10" {
t.Errorf("Expected line 10, got %s", serverRow[0])
}
if serverRow[2] != "DL380-ART" {
t.Errorf("Expected article DL380-ART, got %s", serverRow[2])
}
if serverRow[5] != "10" {
t.Errorf("Expected server count 10, got %s", serverRow[5])
}
// UnitPrice = 100 + 100 = 200
if serverRow[6] != "200" {
t.Errorf("Expected unit price 200, got %s", serverRow[6])
}
// TotalPrice = 200 * 10 = 2000
if serverRow[7] != "2 000" {
t.Errorf("Expected total price '2 000', got %q", serverRow[7])
}
}
func TestToCSV_CategorySorting(t *testing.T) {
svc := NewExportService(config.ExportConfig{}, nil, nil)
data := newTestProjectData([]ExportItem{
{LotName: "LOT-001", Category: "CAT-A", Quantity: 1, UnitPrice: 100.0, TotalPrice: 100.0},
{LotName: "LOT-002", Category: "CAT-C", Quantity: 1, UnitPrice: 100.0, TotalPrice: 100.0},
{LotName: "LOT-003", Category: "CAT-B", Quantity: 1, UnitPrice: 100.0, TotalPrice: 100.0},
}, "ART", 1)
var buf bytes.Buffer
if err := svc.ToCSV(&buf, data); err != nil {
t.Fatalf("ToCSV failed: %v", err)
}
csvBytes := buf.Bytes()
reader := csv.NewReader(bytes.NewReader(csvBytes[3:]))
reader.Comma = ';'
// Skip header and server row
reader.Read()
reader.Read()
// Without category repo, items maintain original order
row1, _ := reader.Read()
if row1[0] != "LOT-001" {
t.Errorf("Expected LOT-001 first, got %s", row1[0])
if row1[2] != "LOT-001" {
t.Errorf("Expected LOT-001 first, got %s", row1[2])
}
row2, _ := reader.Read()
if row2[0] != "LOT-002" {
t.Errorf("Expected LOT-002 second, got %s", row2[0])
if row2[2] != "LOT-002" {
t.Errorf("Expected LOT-002 second, got %s", row2[2])
}
row3, _ := reader.Read()
if row3[0] != "LOT-003" {
t.Errorf("Expected LOT-003 third, got %s", row3[0])
if row3[2] != "LOT-003" {
t.Errorf("Expected LOT-003 third, got %s", row3[2])
}
}
func TestToCSV_EmptyData(t *testing.T) {
svc := NewExportService(config.ExportConfig{}, nil)
svc := NewExportService(config.ExportConfig{}, nil, nil)
data := &ExportData{
Name: "Test",
Items: []ExportItem{},
Total: 0.0,
data := &ProjectExportData{
Configs: []ConfigExportBlock{},
CreatedAt: time.Now(),
}
@@ -252,44 +229,28 @@ func TestToCSV_EmptyData(t *testing.T) {
reader := csv.NewReader(bytes.NewReader(csvBytes[3:]))
reader.Comma = ';'
// Should have header and total row
header, err := reader.Read()
if err != nil {
t.Fatalf("Failed to read header: %v", err)
}
if len(header) != 6 {
t.Errorf("Expected 6 columns, got %d", len(header))
if len(header) != 8 {
t.Errorf("Expected 8 columns, got %d", len(header))
}
totalRow, err := reader.Read()
if err != nil {
t.Fatalf("Failed to read total row: %v", err)
}
if totalRow[4] != "ИТОГО:" {
t.Errorf("Expected ИТОГО: in total row, got %s", totalRow[4])
// No more rows expected
_, err = reader.Read()
if err != io.EOF {
t.Errorf("Expected EOF after header, got: %v", err)
}
}
func TestToCSVBytes_BackwardCompat(t *testing.T) {
svc := NewExportService(config.ExportConfig{}, nil)
svc := NewExportService(config.ExportConfig{}, nil, nil)
data := &ExportData{
Name: "Test",
Items: []ExportItem{
{
LotName: "LOT-001",
Description: "Test Item",
Category: "CAT",
Quantity: 1,
UnitPrice: 100.0,
TotalPrice: 100.0,
},
},
Total: 100.0,
CreatedAt: time.Now(),
}
data := newTestProjectData([]ExportItem{
{LotName: "LOT-001", Category: "CAT", Quantity: 1, UnitPrice: 100.0, TotalPrice: 100.0},
}, "ART", 1)
csvBytes, err := svc.ToCSVBytes(data)
if err != nil {
@@ -300,34 +261,20 @@ func TestToCSVBytes_BackwardCompat(t *testing.T) {
t.Fatalf("CSV bytes too short")
}
// Verify BOM is present
expectedBOM := []byte{0xEF, 0xBB, 0xBF}
actualBOM := csvBytes[:3]
if bytes.Compare(actualBOM, expectedBOM) != 0 {
if !bytes.Equal(actualBOM, expectedBOM) {
t.Errorf("UTF-8 BOM mismatch in ToCSVBytes")
}
}
func TestToCSV_WriterError(t *testing.T) {
svc := NewExportService(config.ExportConfig{}, nil)
svc := NewExportService(config.ExportConfig{}, nil, nil)
data := &ExportData{
Name: "Test",
Items: []ExportItem{
{
LotName: "LOT-001",
Description: "Test",
Category: "CAT",
Quantity: 1,
UnitPrice: 100.0,
TotalPrice: 100.0,
},
},
Total: 100.0,
CreatedAt: time.Now(),
}
data := newTestProjectData([]ExportItem{
{LotName: "LOT-001", Category: "CAT", Quantity: 1, UnitPrice: 100.0, TotalPrice: 100.0},
}, "ART", 1)
// Use a failing writer
failingWriter := &failingWriter{}
if err := svc.ToCSV(failingWriter, data); err == nil {
@@ -335,6 +282,122 @@ func TestToCSV_WriterError(t *testing.T) {
}
}
func TestToCSV_MultipleBlocks(t *testing.T) {
svc := NewExportService(config.ExportConfig{}, nil, nil)
data := &ProjectExportData{
Configs: []ConfigExportBlock{
{
Article: "ART-1",
ServerCount: 2,
UnitPrice: 500.0,
Items: []ExportItem{
{LotName: "LOT-A", Category: "CPU", Quantity: 1, UnitPrice: 500.0, TotalPrice: 500.0},
},
},
{
Article: "ART-2",
ServerCount: 3,
UnitPrice: 1000.0,
Items: []ExportItem{
{LotName: "LOT-B", Category: "MEM", Quantity: 2, UnitPrice: 500.0, TotalPrice: 1000.0},
},
},
},
CreatedAt: time.Now(),
}
var buf bytes.Buffer
if err := svc.ToCSV(&buf, data); err != nil {
t.Fatalf("ToCSV failed: %v", err)
}
csvBytes := buf.Bytes()
reader := csv.NewReader(bytes.NewReader(csvBytes[3:]))
reader.Comma = ';'
reader.FieldsPerRecord = -1 // allow variable fields
// Header
reader.Read()
// Block 1: server row
srv1, _ := reader.Read()
if srv1[0] != "10" {
t.Errorf("Block 1 line: expected 10, got %s", srv1[0])
}
if srv1[7] != "1 000" {
t.Errorf("Block 1 total: expected '1 000', got %q", srv1[7])
}
// Block 1: component row
comp1, _ := reader.Read()
if comp1[2] != "LOT-A" {
t.Errorf("Block 1 component: expected LOT-A, got %s", comp1[2])
}
// Separator row
sep, _ := reader.Read()
allEmpty := true
for _, v := range sep {
if v != "" {
allEmpty = false
}
}
if !allEmpty {
t.Errorf("Expected empty separator row, got %v", sep)
}
// Block 2: server row
srv2, _ := reader.Read()
if srv2[0] != "20" {
t.Errorf("Block 2 line: expected 20, got %s", srv2[0])
}
if srv2[7] != "3 000" {
t.Errorf("Block 2 total: expected '3 000', got %q", srv2[7])
}
}
func TestFormatPriceWithSpace(t *testing.T) {
tests := []struct {
input float64
expected string
}{
{0, "0"},
{100, "100"},
{1000, "1 000"},
{10470, "10 470"},
{104700, "104 700"},
{1000000, "1 000 000"},
}
for _, tt := range tests {
result := formatPriceWithSpace(tt.input)
if result != tt.expected {
t.Errorf("formatPriceWithSpace(%v): expected %q, got %q", tt.input, tt.expected, result)
}
}
}
func TestFormatPriceComma(t *testing.T) {
tests := []struct {
input float64
expected string
}{
{100.0, "100"},
{2074.5, "2074,5"},
{100.50, "100,5"},
{99.99, "99,99"},
{0, "0"},
}
for _, tt := range tests {
result := formatPriceComma(tt.input)
if result != tt.expected {
t.Errorf("formatPriceComma(%v): expected %q, got %q", tt.input, tt.expected, result)
}
}
}
// failingWriter always returns an error
type failingWriter struct{}