diff --git a/frontend/css/style.css b/frontend/css/style.css index 34ae48a..7a06632 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -2162,4 +2162,71 @@ tr:hover .action-cell { font-size: 0.7rem; color: var(--medium-gray); font-family: system-ui; +} + +/* Refresh Button Animation */ +@keyframes spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + +.icon-btn.refreshing i { + animation: spin 0.8s linear infinite; +} + +.icon-btn.refreshing { + pointer-events: none; + opacity: 0.7; +} + +/* Refresh button success feedback */ +.icon-btn.refresh-success { + color: #27ae60 !important; + background-color: rgba(39, 174, 96, 0.15) !important; +} + +.icon-btn.refresh-success i::before { + content: "\f00c"; + /* Font Awesome check icon */ +} + +/* Table refresh animation */ +@keyframes tableRefresh { + 0% { + opacity: 0.3; + } + + 50% { + opacity: 0.6; + } + + 100% { + opacity: 1; + } +} + +.table-refreshing { + animation: tableRefresh 0.5s ease-out; +} + +/* Row highlight on refresh */ +@keyframes rowHighlight { + 0% { + background-color: rgba(255, 107, 53, 0.2); + transform: translateX(-5px); + } + + 100% { + background-color: transparent; + transform: translateX(0); + } +} + +.table-refreshing tbody tr { + animation: rowHighlight 0.4s ease-out; } \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html index 7b7972a..f356e55 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -54,7 +54,7 @@ - 每周客户进度 + 每周进度 @@ -127,12 +127,7 @@ - - - - - - + @@ -843,6 +838,19 @@ + + 是否试用 + + + + 是 + + + + 否 + + + 开始时间 diff --git a/frontend/js/main.js b/frontend/js/main.js index 0039372..6807117 100644 --- a/frontend/js/main.js +++ b/frontend/js/main.js @@ -412,7 +412,6 @@ document.addEventListener('DOMContentLoaded', function () { currentSection = 'customer'; loadCustomers(); } else if (section === 'trialPeriods') { - console.log('Switching to trial periods section'); const trialPeriodsSection = document.getElementById('trialPeriodsSection'); if (trialPeriodsSection) { trialPeriodsSection.classList.add('active'); @@ -423,9 +422,7 @@ document.addEventListener('DOMContentLoaded', function () { // Load customers first, then trial periods if (typeof loadCustomersForDropdown === 'function' && typeof loadAllTrialPeriods === 'function') { - console.log('Loading customers first, then trial periods'); loadCustomersForDropdown().then(() => { - console.log('Customers loaded, now loading trial periods'); loadAllTrialPeriods(); }); } else { @@ -475,9 +472,7 @@ document.addEventListener('DOMContentLoaded', function () { // Populate customer filter dropdown function populateCustomerFilter() { - console.log('Populating filter, allCustomers:', allCustomers); const uniqueCustomers = [...new Set(allCustomers.map(c => c.customerName).filter(c => c))]; - console.log('Unique customers:', uniqueCustomers); customerFilter.innerHTML = '全部客户'; uniqueCustomers.forEach(customer => { @@ -643,9 +638,27 @@ document.addEventListener('DOMContentLoaded', function () { } if (refreshCustomersBtn) { - refreshCustomersBtn.addEventListener('click', () => { - if (currentSection === 'customer') { - loadCustomers(); + refreshCustomersBtn.addEventListener('click', async () => { + // Add refreshing animation + refreshCustomersBtn.classList.add('refreshing'); + const table = document.getElementById('customerTable'); + try { + await loadCustomers(); + // Show success feedback briefly + refreshCustomersBtn.classList.remove('refreshing'); + refreshCustomersBtn.classList.add('refresh-success'); + // Add table refresh animation + if (table) { + table.classList.add('table-refreshing'); + setTimeout(() => { + table.classList.remove('table-refreshing'); + }, 500); + } + setTimeout(() => { + refreshCustomersBtn.classList.remove('refresh-success'); + }, 1000); + } catch (error) { + refreshCustomersBtn.classList.remove('refreshing'); } }); } @@ -672,16 +685,12 @@ document.addEventListener('DOMContentLoaded', function () { // Load all customers for dashboard async function loadAllCustomers() { - console.log('loadAllCustomers called'); try { const response = await authenticatedFetch('/api/customers?page=1&pageSize=1000'); const data = await response.json(); - console.log('Dashboard data received:', data); - if (data.customers) { dashboardCustomers = data.customers; - console.log('Dashboard customers set:', dashboardCustomers.length, 'customers'); updateDashboardStats(dashboardCustomers); renderStatusChart(dashboardCustomers); renderTypeChart(dashboardCustomers); @@ -1070,7 +1079,6 @@ document.addEventListener('DOMContentLoaded', function () { // Dashboard functionality async function loadDashboardData() { - console.log('loadDashboardData called'); await loadAllCustomers(); await loadTrialCustomersForDashboard(); await loadFollowUpCount(); @@ -1264,9 +1272,7 @@ document.addEventListener('DOMContentLoaded', function () { // Render customer type chart function renderTypeChart(customers) { - console.log('renderTypeChart called with customers:', customers); const canvas = document.getElementById('typeChart'); - console.log('typeChart canvas element:', canvas); if (!canvas) { console.error('typeChart canvas not found'); @@ -1290,9 +1296,6 @@ document.addEventListener('DOMContentLoaded', function () { const labels = Object.keys(typeCount); const data = Object.values(typeCount); - console.log('Type chart labels:', labels); - console.log('Type chart data:', data); - typeChartInstance = new Chart(ctx, { type: 'doughnut', data: { @@ -1367,8 +1370,6 @@ document.addEventListener('DOMContentLoaded', function () { } } }); - - console.log('Type chart created successfully'); } // Render trend line chart @@ -1585,8 +1586,6 @@ document.addEventListener('DOMContentLoaded', function () { } } }); - - console.log('Trend chart created successfully'); } // Initialize the app @@ -1931,8 +1930,28 @@ document.addEventListener('DOMContentLoaded', function () { // Refresh follow-ups button if (refreshFollowupsBtn) { - refreshFollowupsBtn.addEventListener('click', () => { - loadFollowUps(); + refreshFollowupsBtn.addEventListener('click', async () => { + // Add refreshing animation + refreshFollowupsBtn.classList.add('refreshing'); + const table = document.getElementById('followupTable'); + try { + await loadFollowUps(); + // Show success feedback briefly + refreshFollowupsBtn.classList.remove('refreshing'); + refreshFollowupsBtn.classList.add('refresh-success'); + // Add table refresh animation + if (table) { + table.classList.add('table-refreshing'); + setTimeout(() => { + table.classList.remove('table-refreshing'); + }, 500); + } + setTimeout(() => { + refreshFollowupsBtn.classList.remove('refresh-success'); + }, 1000); + } catch (error) { + refreshFollowupsBtn.classList.remove('refreshing'); + } }); } @@ -2175,9 +2194,9 @@ document.addEventListener('DOMContentLoaded', function () { if (headerLogoutBtn) { headerLogoutBtn.addEventListener('click', () => { if (confirm('确定要退出登录吗?')) { - localStorage.removeItem('authToken'); + localStorage.removeItem('crmToken'); localStorage.removeItem('crmSearchHistory'); - window.location.reload(); + window.location.href = '/static/login.html'; } }); } diff --git a/frontend/js/tests.js b/frontend/js/tests.js new file mode 100644 index 0000000..268a5b5 --- /dev/null +++ b/frontend/js/tests.js @@ -0,0 +1,225 @@ +// Test suite for CRM Frontend JavaScript +// Run with: open this file in a browser or use a test runner like Jest + +// Simple test framework +const TestRunner = { + tests: [], + results: [], + + add(name, testFn) { + this.tests.push({ name, testFn }); + }, + + async run() { + console.log('🧪 Running CRM Frontend Tests...\n'); + this.results = []; + + for (const test of this.tests) { + try { + await test.testFn(); + this.results.push({ name: test.name, passed: true }); + console.log(`✅ ${test.name}`); + } catch (error) { + this.results.push({ name: test.name, passed: false, error: error.message }); + console.log(`❌ ${test.name}: ${error.message}`); + } + } + + const passed = this.results.filter(r => r.passed).length; + const total = this.results.length; + console.log(`\n📊 Results: ${passed}/${total} tests passed`); + + return this.results; + } +}; + +function assert(condition, message) { + if (!condition) { + throw new Error(message || 'Assertion failed'); + } +} + +function assertEqual(actual, expected, message) { + if (actual !== expected) { + throw new Error(message || `Expected ${expected}, got ${actual}`); + } +} + +// ========================================== +// Test: Sidebar menu text is "每周进度" +// ========================================== +TestRunner.add('Sidebar menu shows "每周进度" (not "每周客户进度")', () => { + const navItems = document.querySelectorAll('.nav-item'); + let found = false; + let hasOldText = false; + + navItems.forEach(item => { + const text = item.textContent.trim(); + if (text.includes('每周进度') && !text.includes('每周客户进度')) { + found = true; + } + if (text.includes('每周客户进度')) { + hasOldText = true; + } + }); + + assert(!hasOldText, 'Found old text "每周客户进度" which should be removed'); + assert(found, 'Could not find "每周进度" in sidebar'); +}); + +// ========================================== +// Test: Bell and Settings buttons removed from header +// ========================================== +TestRunner.add('Header does not have bell and settings icons', () => { + const headerRight = document.querySelector('.header-right'); + + if (headerRight) { + const bellBtn = headerRight.querySelector('.fa-bell'); + const cogBtn = headerRight.querySelector('.fa-cog'); + + assert(!bellBtn, 'Bell icon should be removed from header'); + assert(!cogBtn, 'Settings (cog) icon should be removed from header'); + } + + // Verify logout button still exists + const logoutBtn = document.getElementById('logoutBtn'); + assert(logoutBtn, 'Logout button should exist'); +}); + +// ========================================== +// Test: Add trial modal has "是否试用" option +// ========================================== +TestRunner.add('Add trial modal has "是否试用" radio options', () => { + const modal = document.getElementById('addTrialPeriodModal'); + assert(modal, 'Add trial period modal should exist'); + + const isTrialRadios = modal.querySelectorAll('input[name="isTrial"]'); + assert(isTrialRadios.length === 2, `Should have 2 radio buttons for "是否试用", found ${isTrialRadios.length}`); + + const trueRadio = modal.querySelector('input[name="isTrial"][value="true"]'); + const falseRadio = modal.querySelector('input[name="isTrial"][value="false"]'); + + assert(trueRadio, 'Should have a radio for isTrial=true'); + assert(falseRadio, 'Should have a radio for isTrial=false'); +}); + +// ========================================== +// Test: Edit trial modal has "是否试用" option +// ========================================== +TestRunner.add('Edit trial modal has "是否试用" radio options', () => { + const modal = document.getElementById('editTrialPeriodModal'); + assert(modal, 'Edit trial period modal should exist'); + + const editIsTrialRadios = modal.querySelectorAll('input[name="editIsTrial"]'); + assert(editIsTrialRadios.length === 2, `Should have 2 radio buttons for edit "是否试用", found ${editIsTrialRadios.length}`); + + const trueRadio = modal.querySelector('input[name="editIsTrial"][value="true"]'); + const falseRadio = modal.querySelector('input[name="editIsTrial"][value="false"]'); + + assert(trueRadio, 'Should have a radio for editIsTrial=true'); + assert(falseRadio, 'Should have a radio for editIsTrial=false'); +}); + +// ========================================== +// Test: Trial periods table has "是否试用" column +// ========================================== +TestRunner.add('Trial periods table has "是否试用" column header', () => { + const trialPeriodsSection = document.getElementById('trialPeriodsSection'); + if (trialPeriodsSection) { + const headers = trialPeriodsSection.querySelectorAll('th'); + let found = false; + + headers.forEach(header => { + if (header.textContent.includes('是否试用')) { + found = true; + } + }); + + assert(found, 'Trial periods table should have "是否试用" column header'); + } +}); + +// ========================================== +// Test: Logout button clears crmToken +// ========================================== +TestRunner.add('Logout button has correct event listener setup', () => { + const logoutBtn = document.getElementById('logoutBtn'); + assert(logoutBtn, 'Logout button should exist'); + + // Check that the button is wired up (we can't directly test the function without clicking) + assert(logoutBtn.id === 'logoutBtn', 'Logout button should have correct ID'); +}); + +// ========================================== +// Test: Refresh button for customers exists +// ========================================== +TestRunner.add('Refresh customers button exists', () => { + const refreshBtn = document.getElementById('refreshCustomersBtn'); + assert(refreshBtn, 'Refresh customers button should exist'); +}); + +// ========================================== +// Test: Refresh button for followups exists +// ========================================== +TestRunner.add('Refresh followups button exists', () => { + const refreshBtn = document.getElementById('refreshFollowupsBtn'); + assert(refreshBtn, 'Refresh followups button should exist'); +}); + +// ========================================== +// Test: Add trial button exists +// ========================================== +TestRunner.add('Add trial button exists', () => { + const addTrialBtn = document.getElementById('addTrialBtn'); + assert(addTrialBtn, 'Add trial button should exist'); +}); + +// ========================================== +// Test: Quick add dropdown exists +// ========================================== +TestRunner.add('Quick add dropdown exists in header', () => { + const quickAddBtn = document.getElementById('headerQuickAddBtn'); + const quickAddDropdown = document.getElementById('headerQuickAddDropdown'); + + assert(quickAddBtn, 'Quick add button should exist'); + assert(quickAddDropdown, 'Quick add dropdown should exist'); +}); + +// ========================================== +// Test: Global search input exists +// ========================================== +TestRunner.add('Global search input exists', () => { + const searchInput = document.getElementById('globalSearchInput'); + assert(searchInput, 'Global search input should exist'); +}); + +// ========================================== +// Test: All required sections exist +// ========================================== +TestRunner.add('All required sections exist', () => { + const customerSection = document.getElementById('customerSection'); + const dashboardSection = document.getElementById('dashboardSection'); + const followupSection = document.getElementById('followupSection'); + const trialPeriodsSection = document.getElementById('trialPeriodsSection'); + + assert(customerSection, 'Customer section should exist'); + assert(dashboardSection, 'Dashboard section should exist'); + assert(followupSection, 'Followup section should exist'); + assert(trialPeriodsSection, 'Trial periods section should exist'); +}); + +// ========================================== +// Run tests when DOM is loaded +// ========================================== +if (typeof document !== 'undefined' && document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => { + setTimeout(() => TestRunner.run(), 1000); + }); +} else if (typeof document !== 'undefined') { + setTimeout(() => TestRunner.run(), 1000); +} + +// Export for use in other modules +if (typeof module !== 'undefined' && module.exports) { + module.exports = { TestRunner, assert, assertEqual }; +} diff --git a/frontend/js/trial-periods-page.js b/frontend/js/trial-periods-page.js index 1bbc47a..05c62b6 100644 --- a/frontend/js/trial-periods-page.js +++ b/frontend/js/trial-periods-page.js @@ -83,7 +83,6 @@ function initTrialPeriodsPage() { // Load customers map for displaying customer names in table async function loadCustomersMap() { try { - console.log('Loading customers map...'); const response = await authenticatedFetch('/api/customers/list'); if (!response) { @@ -92,12 +91,9 @@ async function loadCustomersMap() { } const data = await response.json(); - console.log('Customers data:', data); // Use the complete customer map for ID->Name lookups customersMap = data.customerMap || {}; - - console.log('Number of customer ID mappings:', Object.keys(customersMap).length); } catch (error) { console.error('Error loading customers map:', error); } @@ -374,7 +370,6 @@ function updateTrialPagination() { // Open add trial modal function openAddTrialModal() { - console.log('Opening add trial modal'); document.getElementById('trialCustomerInput').value = ''; document.querySelector('input[name="isTrial"][value="true"]').checked = true; document.getElementById('trialStartTime').value = ''; @@ -395,6 +390,13 @@ function openEditTrialModal(periodId) { document.getElementById('editTrialStartTime').value = formatDateTimeLocal(startDate); document.getElementById('editTrialEndTime').value = formatDateTimeLocal(endDate); + // Set isTrial radio button + const isTrial = (period.isTrial !== undefined && period.isTrial !== null) ? period.isTrial : true; + const isTrialRadio = document.querySelector(`input[name="editIsTrial"][value="${isTrial}"]`); + if (isTrialRadio) { + isTrialRadio.checked = true; + } + document.getElementById('editTrialPeriodModal').style.display = 'block'; } diff --git a/frontend/js/trial-periods.js b/frontend/js/trial-periods.js index 7866460..8cab91b 100644 --- a/frontend/js/trial-periods.js +++ b/frontend/js/trial-periods.js @@ -232,6 +232,10 @@ async function updateTrialPeriod() { const startTime = document.getElementById('editTrialStartTime').value; const endTime = document.getElementById('editTrialEndTime').value; + // Get isTrial value from radio buttons + const isTrialRadio = document.querySelector('input[name="editIsTrial"]:checked'); + const isTrial = isTrialRadio ? isTrialRadio.value === 'true' : true; + if (!startTime || !endTime) { alert('请填写开始时间和结束时间'); return; @@ -239,7 +243,8 @@ async function updateTrialPeriod() { const formData = { startTime: new Date(startTime).toISOString(), - endTime: new Date(endTime).toISOString() + endTime: new Date(endTime).toISOString(), + isTrial: isTrial }; try { @@ -253,7 +258,14 @@ async function updateTrialPeriod() { if (response.ok) { document.getElementById('editTrialPeriodModal').style.display = 'none'; - await loadTrialPeriods(currentCustomerId); + // Load trial periods for specific customer (for embedded list) + if (currentCustomerId) { + await loadTrialPeriods(currentCustomerId); + } + // Also refresh the main trial periods page list + if (typeof loadAllTrialPeriods === 'function') { + await loadAllTrialPeriods(); + } alert('试用时间更新成功!'); } else { alert('更新试用时间时出错'); diff --git a/internal/handlers/auth_handler.go b/internal/handlers/auth_handler.go index bbe7b39..d6b7c33 100644 --- a/internal/handlers/auth_handler.go +++ b/internal/handlers/auth_handler.go @@ -44,7 +44,7 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) { } // 验证用户名和密码 - if req.Username != "admin" || req.Password != "admin123" { + if req.Username != "admin" || req.Password != "digua666" { http.Error(w, "用户名或密码错误", http.StatusUnauthorized) return } diff --git a/internal/storage/trial_period_storage.go b/internal/storage/trial_period_storage.go index 6409bbb..7fa5f8f 100644 --- a/internal/storage/trial_period_storage.go +++ b/internal/storage/trial_period_storage.go @@ -148,6 +148,9 @@ func (ts *trialPeriodStorage) UpdateTrialPeriod(id string, updates models.Update trialPeriods[i].EndTime = endTime } } + if updates.IsTrial != nil { + trialPeriods[i].IsTrial = *updates.IsTrial + } return ts.saveTrialPeriods(trialPeriods) } diff --git a/internal/storage/trial_period_storage_test.go b/internal/storage/trial_period_storage_test.go new file mode 100644 index 0000000..2d4f9f7 --- /dev/null +++ b/internal/storage/trial_period_storage_test.go @@ -0,0 +1,272 @@ +package storage + +import ( + "os" + "path/filepath" + "testing" + "time" + + "crm-go/models" +) + +func TestUpdateTrialPeriodWithIsTrial(t *testing.T) { + // Create a temporary directory for test data + tempDir, err := os.MkdirTemp("", "trial_test") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + // Create a test storage + testFilePath := filepath.Join(tempDir, "trial_periods.json") + storage := NewTrialPeriodStorage(testFilePath) + + // Create a trial period with isTrial = true + now := time.Now() + startTime := now + endTime := now.Add(7 * 24 * time.Hour) + + trialPeriod := models.TrialPeriod{ + CustomerID: "test-customer-001", + StartTime: startTime, + EndTime: endTime, + IsTrial: true, + } + + createdPeriod, err := storage.CreateTrialPeriod(trialPeriod) + if err != nil { + t.Fatalf("Failed to create trial period: %v", err) + } + + if createdPeriod.ID == "" { + t.Fatal("Created trial period should have an ID") + } + + if !createdPeriod.IsTrial { + t.Fatal("Created trial period should have IsTrial = true") + } + + // Test updating isTrial to false + isTrialFalse := false + updateRequest := models.UpdateTrialPeriodRequest{ + IsTrial: &isTrialFalse, + } + + err = storage.UpdateTrialPeriod(createdPeriod.ID, updateRequest) + if err != nil { + t.Fatalf("Failed to update trial period: %v", err) + } + + // Verify the update + updatedPeriod, err := storage.GetTrialPeriodByID(createdPeriod.ID) + if err != nil { + t.Fatalf("Failed to get updated trial period: %v", err) + } + + if updatedPeriod == nil { + t.Fatal("Updated trial period should not be nil") + } + + if updatedPeriod.IsTrial { + t.Fatal("Updated trial period should have IsTrial = false") + } + + // Test updating isTrial back to true + isTrialTrue := true + updateRequest2 := models.UpdateTrialPeriodRequest{ + IsTrial: &isTrialTrue, + } + + err = storage.UpdateTrialPeriod(createdPeriod.ID, updateRequest2) + if err != nil { + t.Fatalf("Failed to update trial period back to true: %v", err) + } + + // Verify the update + updatedPeriod2, err := storage.GetTrialPeriodByID(createdPeriod.ID) + if err != nil { + t.Fatalf("Failed to get updated trial period: %v", err) + } + + if !updatedPeriod2.IsTrial { + t.Fatal("Updated trial period should have IsTrial = true") + } +} + +func TestUpdateTrialPeriodWithStartEndTime(t *testing.T) { + // Create a temporary directory for test data + tempDir, err := os.MkdirTemp("", "trial_test_time") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + // Create a test storage + testFilePath := filepath.Join(tempDir, "trial_periods.json") + storage := NewTrialPeriodStorage(testFilePath) + + // Create a trial period + now := time.Now() + startTime := now + endTime := now.Add(7 * 24 * time.Hour) + + trialPeriod := models.TrialPeriod{ + CustomerID: "test-customer-002", + StartTime: startTime, + EndTime: endTime, + IsTrial: true, + } + + createdPeriod, err := storage.CreateTrialPeriod(trialPeriod) + if err != nil { + t.Fatalf("Failed to create trial period: %v", err) + } + + // Update start and end times along with isTrial + newStartTime := now.Add(1 * 24 * time.Hour).Format(time.RFC3339) + newEndTime := now.Add(14 * 24 * time.Hour).Format(time.RFC3339) + isTrialFalse := false + + updateRequest := models.UpdateTrialPeriodRequest{ + StartTime: &newStartTime, + EndTime: &newEndTime, + IsTrial: &isTrialFalse, + } + + err = storage.UpdateTrialPeriod(createdPeriod.ID, updateRequest) + if err != nil { + t.Fatalf("Failed to update trial period: %v", err) + } + + // Verify all updates + updatedPeriod, err := storage.GetTrialPeriodByID(createdPeriod.ID) + if err != nil { + t.Fatalf("Failed to get updated trial period: %v", err) + } + + if updatedPeriod == nil { + t.Fatal("Updated trial period should not be nil") + } + + if updatedPeriod.IsTrial { + t.Fatal("Updated trial period should have IsTrial = false") + } + + // Verify start time was updated (approximately) + expectedStartTime, _ := time.Parse(time.RFC3339, newStartTime) + if !updatedPeriod.StartTime.Equal(expectedStartTime) { + t.Fatalf("StartTime mismatch: expected %v, got %v", expectedStartTime, updatedPeriod.StartTime) + } + + // Verify end time was updated + expectedEndTime, _ := time.Parse(time.RFC3339, newEndTime) + if !updatedPeriod.EndTime.Equal(expectedEndTime) { + t.Fatalf("EndTime mismatch: expected %v, got %v", expectedEndTime, updatedPeriod.EndTime) + } +} + +func TestGetAllTrialPeriods(t *testing.T) { + // Create a temporary directory for test data + tempDir, err := os.MkdirTemp("", "trial_test_all") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + // Create a test storage + testFilePath := filepath.Join(tempDir, "trial_periods.json") + storage := NewTrialPeriodStorage(testFilePath) + + // Initially should be empty + periods, err := storage.GetAllTrialPeriods() + if err != nil { + t.Fatalf("Failed to get all trial periods: %v", err) + } + if len(periods) != 0 { + t.Fatalf("Expected 0 periods, got %d", len(periods)) + } + + // Create multiple trial periods + now := time.Now() + for i := 0; i < 3; i++ { + trialPeriod := models.TrialPeriod{ + CustomerID: "test-customer-bulk", + StartTime: now.Add(time.Duration(i) * 24 * time.Hour), + EndTime: now.Add(time.Duration(i+7) * 24 * time.Hour), + IsTrial: i%2 == 0, // Alternate true/false + } + _, err := storage.CreateTrialPeriod(trialPeriod) + if err != nil { + t.Fatalf("Failed to create trial period %d: %v", i, err) + } + // Add small delay to ensure different createdAt times + time.Sleep(10 * time.Millisecond) + } + + // Verify all periods are retrieved + periods, err = storage.GetAllTrialPeriods() + if err != nil { + t.Fatalf("Failed to get all trial periods: %v", err) + } + if len(periods) != 3 { + t.Fatalf("Expected 3 periods, got %d", len(periods)) + } + + // Verify periods are sorted by CreatedAt descending (newest first) + for i := 0; i < len(periods)-1; i++ { + if periods[i].CreatedAt.Before(periods[i+1].CreatedAt) { + t.Fatal("Periods should be sorted by CreatedAt descending") + } + } +} + +func TestDeleteTrialPeriod(t *testing.T) { + // Create a temporary directory for test data + tempDir, err := os.MkdirTemp("", "trial_test_delete") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + // Create a test storage + testFilePath := filepath.Join(tempDir, "trial_periods.json") + storage := NewTrialPeriodStorage(testFilePath) + + // Create a trial period + now := time.Now() + trialPeriod := models.TrialPeriod{ + CustomerID: "test-customer-delete", + StartTime: now, + EndTime: now.Add(7 * 24 * time.Hour), + IsTrial: true, + } + + createdPeriod, err := storage.CreateTrialPeriod(trialPeriod) + if err != nil { + t.Fatalf("Failed to create trial period: %v", err) + } + + // Verify it exists + existingPeriod, err := storage.GetTrialPeriodByID(createdPeriod.ID) + if err != nil { + t.Fatalf("Failed to get trial period: %v", err) + } + if existingPeriod == nil { + t.Fatal("Trial period should exist after creation") + } + + // Delete the trial period + err = storage.DeleteTrialPeriod(createdPeriod.ID) + if err != nil { + t.Fatalf("Failed to delete trial period: %v", err) + } + + // Verify it no longer exists + deletedPeriod, err := storage.GetTrialPeriodByID(createdPeriod.ID) + if err != nil { + t.Fatalf("Failed to get trial period after delete: %v", err) + } + if deletedPeriod != nil { + t.Fatal("Trial period should not exist after deletion") + } +} diff --git a/models/trial_period.go b/models/trial_period.go index 1f638fe..05656f4 100644 --- a/models/trial_period.go +++ b/models/trial_period.go @@ -24,4 +24,5 @@ type CreateTrialPeriodRequest struct { type UpdateTrialPeriodRequest struct { StartTime *string `json:"startTime,omitempty"` EndTime *string `json:"endTime,omitempty"` + IsTrial *bool `json:"isTrial,omitempty"` }