55import tempfile
66import pytest
77from app import db as _db_ext
8- from app .models import Vehicle , FuelLog
8+ from app .models import Vehicle , FuelLog , Expense , Trip , ChargingSession
99
1010
1111# ---------------------------------------------------------------------------
@@ -690,6 +690,7 @@ def test_full_import_flow(self, auth_client, sample_vehicle):
690690 assert logs [0 ].odometer == 198042
691691 assert abs (logs [0 ].volume - 26.12 ) < 0.01
692692 assert abs (logs [0 ].total_cost - 44.12 ) < 0.01
693+ assert logs [0 ].price_per_unit == pytest .approx (44.12 / 26.12 , abs = 0.001 )
693694 assert logs [0 ].is_full_tank is True
694695 assert logs [0 ].notes == 'Testtankung'
695696
@@ -715,3 +716,97 @@ def test_auto_suggest_maps_german_columns(self, auth_client, sample_vehicle):
715716 # The mapping page should auto-select fields for German column names
716717 # Check that "date" is pre-selected for "Datum"
717718 assert 'selected' in html
719+
720+
721+ # ---------------------------------------------------------------------------
722+ # Duplicate detection during CSV import
723+ # ---------------------------------------------------------------------------
724+
725+ class TestCsvImportDuplicateDetection :
726+ """Importing the same CSV twice should skip duplicates on the second run."""
727+
728+ def _preview_and_execute (self , auth_client , vehicle_id , data_type , csv_bytes ,
729+ mappings , date_format = 'auto' ):
730+ """Run preview→execute and return the final (followed) response."""
731+ preview_resp = auth_client .post (
732+ '/api/import/csv/preview' ,
733+ data = {
734+ 'data_type' : data_type ,
735+ 'vehicle_id' : str (vehicle_id ),
736+ 'file' : (io .BytesIO (csv_bytes ), 'import.csv' ),
737+ },
738+ content_type = 'multipart/form-data' ,
739+ )
740+ assert preview_resp .status_code == 200
741+ form = {'data_type' : data_type , 'vehicle_id' : str (vehicle_id ),
742+ 'date_format' : date_format }
743+ form .update (mappings )
744+ return auth_client .post (
745+ '/api/import/csv/execute' , data = form ,
746+ content_type = 'multipart/form-data' ,
747+ follow_redirects = True ,
748+ )
749+
750+ def test_fuel_logs_skips_duplicates (self , auth_client , sample_vehicle ):
751+ csv = b'date,odometer,volume,total_cost\n 2024-03-01,20000,35.0,52.50\n '
752+ mappings = {'mapping_0' : 'date' , 'mapping_1' : 'odometer' ,
753+ 'mapping_2' : 'volume' , 'mapping_3' : 'total_cost' }
754+
755+ self ._preview_and_execute (auth_client , sample_vehicle .id ,
756+ 'fuel_logs' , csv , mappings )
757+ assert FuelLog .query .filter_by (vehicle_id = sample_vehicle .id ).count () == 1
758+
759+ resp = self ._preview_and_execute (auth_client , sample_vehicle .id ,
760+ 'fuel_logs' , csv , mappings )
761+ html = resp .data .decode ()
762+ assert '0 fuel logs imported' in html .lower ()
763+ assert '1 duplicate' in html .lower ()
764+ assert FuelLog .query .filter_by (vehicle_id = sample_vehicle .id ).count () == 1
765+
766+ def test_expenses_skips_duplicates (self , auth_client , sample_vehicle ):
767+ csv = b'date,category,description,cost\n 2024-03-01,maintenance,Oil change,75.00\n '
768+ mappings = {'mapping_0' : 'date' , 'mapping_1' : 'category' ,
769+ 'mapping_2' : 'description' , 'mapping_3' : 'cost' }
770+
771+ self ._preview_and_execute (auth_client , sample_vehicle .id ,
772+ 'expenses' , csv , mappings )
773+ assert Expense .query .filter_by (vehicle_id = sample_vehicle .id ).count () == 1
774+
775+ resp = self ._preview_and_execute (auth_client , sample_vehicle .id ,
776+ 'expenses' , csv , mappings )
777+ html = resp .data .decode ()
778+ assert '0 expenses imported' in html .lower ()
779+ assert '1 duplicate' in html .lower ()
780+ assert Expense .query .filter_by (vehicle_id = sample_vehicle .id ).count () == 1
781+
782+ def test_trips_skips_duplicates (self , auth_client , sample_vehicle ):
783+ csv = b'date,start_odometer,end_odometer,purpose\n 2024-03-01,20000,20050,business\n '
784+ mappings = {'mapping_0' : 'date' , 'mapping_1' : 'start_odometer' ,
785+ 'mapping_2' : 'end_odometer' , 'mapping_3' : 'purpose' }
786+
787+ self ._preview_and_execute (auth_client , sample_vehicle .id ,
788+ 'trips' , csv , mappings )
789+ assert Trip .query .filter_by (vehicle_id = sample_vehicle .id ).count () == 1
790+
791+ resp = self ._preview_and_execute (auth_client , sample_vehicle .id ,
792+ 'trips' , csv , mappings )
793+ html = resp .data .decode ()
794+ assert '0 trips imported' in html .lower ()
795+ assert '1 duplicate' in html .lower ()
796+ assert Trip .query .filter_by (vehicle_id = sample_vehicle .id ).count () == 1
797+
798+ def test_charging_sessions_skips_duplicates (self , auth_client , sample_vehicle ):
799+ csv = b'date,kwh_added,total_cost,location\n 2024-03-01,40.0,12.00,Home\n '
800+ mappings = {'mapping_0' : 'date' , 'mapping_1' : 'kwh_added' ,
801+ 'mapping_2' : 'total_cost' , 'mapping_3' : 'location' }
802+
803+ self ._preview_and_execute (auth_client , sample_vehicle .id ,
804+ 'charging_sessions' , csv , mappings )
805+ assert ChargingSession .query .filter_by (vehicle_id = sample_vehicle .id ).count () == 1
806+
807+ resp = self ._preview_and_execute (auth_client , sample_vehicle .id ,
808+ 'charging_sessions' , csv , mappings )
809+ html = resp .data .decode ()
810+ assert '0 charging sessions imported' in html .lower ()
811+ assert '1 duplicate' in html .lower ()
812+ assert ChargingSession .query .filter_by (vehicle_id = sample_vehicle .id ).count () == 1
0 commit comments