@@ -100,12 +100,14 @@ def __init__(self,
100100 self .session = session
101101 self .current_duplicate_strategy = 'update' # Default strategy
102102
103- def import_propertyradar_csv (self , file : FileStorage , list_name : Optional [str ] = None , progress_callback : Optional [callable ] = None ) -> Result :
103+ def import_propertyradar_csv (self , file : FileStorage , list_name : Optional [str ] = None , duplicate_strategy : Optional [ str ] = 'update' , progress_callback : Optional [callable ] = None ) -> Result :
104104 """Import PropertyRadar CSV file with dual contacts per row
105105
106106 Args:
107107 file: Uploaded CSV file
108108 list_name: Optional name for the import list
109+ duplicate_strategy: How to handle duplicates ('update', 'skip', 'replace'). Default: 'update'
110+ progress_callback: Optional callback for progress updates
109111
110112 Returns:
111113 Result with import statistics or error
@@ -117,7 +119,7 @@ def import_propertyradar_csv(self, file: FileStorage, list_name: Optional[str] =
117119 content = content .decode ('utf-8-sig' ) # Handle BOM
118120
119121 # Pass list_name only if it's provided (not if it's None)
120- return self .import_csv (content , file .filename , 'system' , list_name = list_name , progress_callback = progress_callback )
122+ return self .import_csv (content , file .filename , 'system' , list_name = list_name , duplicate_strategy = duplicate_strategy , progress_callback = progress_callback )
121123
122124 except Exception as e :
123125 logger .error (f"Failed to import PropertyRadar CSV: { e } " )
@@ -247,13 +249,25 @@ def import_csv(self, csv_content: str, filename: str, imported_by: str,
247249 if progress_callback :
248250 progress_callback (total_csv_rows , total_csv_rows )
249251
250- # Update import record
252+ # Update import record with correct statistics
253+ # successful_imports should be the number of CSV rows successfully processed
254+ # not the total number of entities created/updated
255+ successful_rows = stats ['total_rows' ] - len (stats ['errors' ])
251256 self .csv_import_repository .update_import_status (
252257 csv_import .id ,
253258 stats ['total_rows' ],
254- stats [ 'properties_created' ] + stats [ 'properties_updated' ] ,
259+ successful_rows ,
255260 len (stats ['errors' ]),
256- {'errors' : stats ['errors' ]} if stats ['errors' ] else None
261+ {
262+ 'errors' : stats ['errors' ] if stats ['errors' ] else [],
263+ 'properties_created' : stats ['properties_created' ],
264+ 'properties_updated' : stats ['properties_updated' ],
265+ 'properties_skipped' : stats ['properties_skipped' ],
266+ 'contacts_created' : stats ['contacts_created' ],
267+ 'contacts_updated' : stats ['contacts_updated' ],
268+ 'contacts_skipped' : stats ['contacts_skipped' ],
269+ 'duplicate_strategy' : self .current_duplicate_strategy
270+ }
257271 )
258272
259273 # Add contacts to campaign list if applicable
@@ -875,7 +889,7 @@ def import_row(self, row: Dict, csv_import: CSVImport) -> Result:
875889
876890 # Handle primary contact
877891 primary_contact = None
878- primary_contact_operation = ' skipped'
892+ primary_contact_operation = None # Don't default to skipped
879893 if data .get ('primary_contact' ):
880894 try :
881895 primary_contact , primary_contact_operation = self ._process_contact (data ['primary_contact' ], csv_import )
@@ -887,10 +901,11 @@ def import_row(self, row: Dict, csv_import: CSVImport) -> Result:
887901 row_errors .append (f"Failed to create primary contact for { row .get ('Address' , 'Unknown' )} " )
888902 except Exception as e :
889903 row_errors .append (f"Primary contact error: { str (e )} " )
904+ primary_contact_operation = 'error'
890905
891906 # Handle secondary contact
892907 secondary_contact = None
893- secondary_contact_operation = ' skipped'
908+ secondary_contact_operation = None # Don't default to skipped
894909 if data .get ('secondary_contact' ):
895910 try :
896911 secondary_contact , secondary_contact_operation = self ._process_contact (data ['secondary_contact' ], csv_import )
@@ -900,6 +915,7 @@ def import_row(self, row: Dict, csv_import: CSVImport) -> Result:
900915 )
901916 except Exception as e :
902917 row_errors .append (f"Secondary contact error: { str (e )} " )
918+ secondary_contact_operation = 'error'
903919
904920 # Return result with any errors captured, including contact objects and operation types
905921 result_data = {
@@ -1144,40 +1160,49 @@ def _process_batch(self, batch: List[Dict], csv_import: CSVImport, return_contac
11441160 elif property_operation == 'updated' :
11451161 stats ['properties_updated' ] += 1
11461162 elif property_operation == 'existing' :
1147- # Property exists - count as update for statistics purposes
1148- stats ['properties_updated' ] += 1
1163+ # Property exists - handle based on duplicate strategy
1164+ if self .current_duplicate_strategy == 'skip' :
1165+ stats ['properties_skipped' ] += 1
1166+ else :
1167+ stats ['properties_updated' ] += 1
11491168 elif property_operation == 'skipped' :
11501169 stats ['properties_skipped' ] += 1
11511170
11521171 # Count ACTUAL contact operations using operation types
1153- primary_operation = result .value .get ('primary_contact_operation' , 'skipped' )
1172+ primary_operation = result .value .get ('primary_contact_operation' )
11541173 if primary_operation == 'created' :
11551174 stats ['contacts_created' ] += 1
11561175 elif primary_operation == 'existing' :
1157- # For 'replace' strategy, existing contacts count as updated
1158- if self .current_duplicate_strategy == 'replace' :
1176+ # Handle based on duplicate strategy
1177+ if self .current_duplicate_strategy == 'skip' :
1178+ stats ['contacts_skipped' ] += 1
1179+ elif self .current_duplicate_strategy in ['update' , 'replace' ]:
11591180 stats ['contacts_updated' ] += 1
11601181 else :
11611182 stats ['contacts_updated' ] += 1
11621183 elif primary_operation == 'skipped' :
11631184 stats ['contacts_skipped' ] += 1
1185+ # If primary_operation is None, don't count it (no contact data)
11641186
11651187 # Get primary contact for list tracking
11661188 if return_contacts and 'primary_contact' in result .value and result .value ['primary_contact' ]:
11671189 logger .debug (f"Adding primary contact to list: { result .value ['primary_contact' ]} " )
11681190 imported_contacts .append (result .value ['primary_contact' ])
11691191
1170- secondary_operation = result .value .get ('secondary_contact_operation' , 'skipped' )
1192+ secondary_operation = result .value .get ('secondary_contact_operation' )
11711193 if secondary_operation == 'created' :
11721194 stats ['contacts_created' ] += 1
11731195 elif secondary_operation == 'existing' :
1174- # For 'replace' strategy, existing contacts count as updated
1175- if self .current_duplicate_strategy == 'replace' :
1196+ # Handle based on duplicate strategy
1197+ if self .current_duplicate_strategy == 'skip' :
1198+ stats ['contacts_skipped' ] += 1
1199+ elif self .current_duplicate_strategy in ['update' , 'replace' ]:
11761200 stats ['contacts_updated' ] += 1
11771201 else :
11781202 stats ['contacts_updated' ] += 1
11791203 elif secondary_operation == 'skipped' :
11801204 stats ['contacts_skipped' ] += 1
1205+ # If secondary_operation is None, don't count it (no contact data)
11811206
11821207 # Get secondary contact for list tracking
11831208 if return_contacts and 'secondary_contact' in result .value and result .value ['secondary_contact' ]:
@@ -1261,18 +1286,17 @@ def _process_property(self, property_data: Dict) -> Tuple[Property, str]:
12611286 if existing :
12621287 # Handle existing property based on duplicate strategy
12631288 if self .current_duplicate_strategy == 'skip' :
1264- operation_type = 'skipped'
1289+ return existing , 'skipped'
12651290 elif self .current_duplicate_strategy in ['update' , 'replace' ]:
12661291 # Update existing property
12671292 for key , value in property_data .items ():
12681293 if value is not None : # Only update non-null values
12691294 setattr (existing , key , value )
12701295 self .property_repository .update (existing )
1271- operation_type = 'updated'
1296+ return existing , 'updated'
12721297 else :
1273- # For any other strategy (like 'existing'), just return existing without update
1274- operation_type = 'existing'
1275- return existing , operation_type
1298+ # For any other strategy, just return existing without update
1299+ return existing , 'existing'
12761300 else :
12771301 # Create new property with retry on constraint violation
12781302 try :
@@ -1290,18 +1314,17 @@ def _process_property(self, property_data: Dict) -> Tuple[Property, str]:
12901314 logger .debug (f"Found existing property after retry: { existing .id } " )
12911315 # Handle existing property based on duplicate strategy
12921316 if self .current_duplicate_strategy == 'skip' :
1293- operation_type = 'skipped'
1317+ return existing , 'skipped'
12941318 elif self .current_duplicate_strategy in ['update' , 'replace' ]:
12951319 # Update with current data
12961320 for key , value in property_data .items ():
12971321 if value is not None :
12981322 setattr (existing , key , value )
12991323 self .property_repository .update (existing )
1300- operation_type = 'updated'
1324+ return existing , 'updated'
13011325 else :
1302- # For any other strategy (like 'existing'), just return existing without update
1303- operation_type = 'existing'
1304- return existing , operation_type
1326+ # For any other strategy, just return existing without update
1327+ return existing , 'existing'
13051328 raise create_error
13061329
13071330 except Exception as e :
@@ -1336,11 +1359,13 @@ def _process_contact(self, contact_data: Dict, csv_import: Optional['CSVImport']
13361359 # Don't commit here - let batch processing handle commits
13371360 # Return operation type based on duplicate strategy
13381361 if self .current_duplicate_strategy == 'skip' :
1339- operation_type = 'skipped'
1362+ return existing , 'skipped'
1363+ elif self .current_duplicate_strategy in ['update' , 'replace' ]:
1364+ # For PropertyRadar, we don't actually update contact data, just link to property
1365+ # But we still count it as updated for statistics
1366+ return existing , 'existing' # Will be counted as updated in batch processing
13401367 else :
1341- # For 'update' or any other strategy, return 'existing' to indicate found existing contact
1342- operation_type = 'existing'
1343- return existing , operation_type
1368+ return existing , 'existing'
13441369 else :
13451370 # Create new contact with retry on constraint violation
13461371 logger .debug (f"Creating new contact with phone { contact_data ['phone' ]} " )
@@ -1362,11 +1387,13 @@ def _process_contact(self, contact_data: Dict, csv_import: Optional['CSVImport']
13621387 csv_import .contacts .append (existing )
13631388 # Return operation type based on duplicate strategy
13641389 if self .current_duplicate_strategy == 'skip' :
1365- operation_type = 'skipped'
1390+ return existing , 'skipped'
1391+ elif self .current_duplicate_strategy in ['update' , 'replace' ]:
1392+ # For PropertyRadar, we don't actually update contact data, just link to property
1393+ # But we still count it as updated for statistics
1394+ return existing , 'existing' # Will be counted as updated in batch processing
13661395 else :
1367- # For 'update' or any other strategy, return 'existing' to indicate found existing contact
1368- operation_type = 'existing'
1369- return existing , operation_type
1396+ return existing , 'existing'
13701397 raise create_error
13711398
13721399 except Exception as e :
0 commit comments