Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 146 additions & 43 deletions user_profile/services/sede_mercadopago.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
SYNC_CACHE_PREFIX = 'sede_sync_'
LAST_NAME_SIMILARITY_THRESHOLD = 0.85
FIRST_NAME_SIMILARITY_THRESHOLD = 0.90
MAX_SUBSCRIPTION_PAYMENTS = 30
PRIMARY_SOURCES = {'subscription', 'customer'}
PAYMENT_SOURCES = {'payment'}

PAYMENT_METHOD_LABELS = {
'account_money': 'Dinero en cuenta',
Expand Down Expand Up @@ -219,53 +222,92 @@ def _extract_email_from_payment(payment):

def _payment_belongs_to_subscription(payment, subscription_id):
if not subscription_id:
return True
return False

metadata = payment.get('metadata') or {}
metadata_values = {
str(metadata.get('preapproval_id') or ''),
str(metadata.get('subscription_id') or ''),
str(metadata.get('preapproval_plan_id') or ''),
}
if subscription_id in metadata_values:
return True

description = str(payment.get('description') or '').lower()
description = str(payment.get('description') or '')
external_reference = str(payment.get('external_reference') or '')
if subscription_id in description or subscription_id in external_reference:
return True

order = payment.get('order') or {}
order_id = str(order.get('id') or '')
return subscription_id in order_id
order_type = str(order.get('type') or '').lower()
if order_type == 'mercadopago' and subscription_id in str(order.get('id') or ''):
return True

return False


def _get_collector_payer_id():
collector_id = settings.MERCADOPAGO.get('COLLECTOR_USER_ID')
if not collector_id:
return None
try:
return int(collector_id)
except (TypeError, ValueError):
return None


def fetch_payments_for_subscription(sdk, subscription_id, payer_id=None):
"""Fetch only payments that belong to this specific subscription."""
if not subscription_id:
return []

seen_payment_ids = set()
payments = []
collector_payer_id = _get_collector_payer_id()

if payer_id and collector_payer_id and int(payer_id) == collector_payer_id:
payer_id = None

search_filters = [{'sort': 'date_created', 'criteria': 'desc'}]
if payer_id:
search_filters.append({'payer.id': payer_id, 'sort': 'date_created', 'criteria': 'desc'})
search_filters.append({'q': subscription_id, 'sort': 'date_created', 'criteria': 'desc'})
for payment in _paginated_search(
sdk.payment().search,
{'payer.id': payer_id, 'sort': 'date_created', 'criteria': 'desc'},
limit=50,
):
payment_id = payment.get('id')
if payment_id in seen_payment_ids:
continue
if not _payment_belongs_to_subscription(payment, subscription_id):
continue
seen_payment_ids.add(payment_id)
payments.append(payment)
if len(payments) >= MAX_SUBSCRIPTION_PAYMENTS:
break

for filters in search_filters:
for payment in _paginated_search(sdk.payment().search, filters):
if not payments:
for payment in _paginated_search(
sdk.payment().search,
{'q': subscription_id, 'sort': 'date_created', 'criteria': 'desc'},
limit=50,
):
payment_id = payment.get('id')
if payment_id in seen_payment_ids:
continue
if filters.get('payer.id') and not _payment_belongs_to_subscription(payment, subscription_id):
if not _payment_belongs_to_subscription(payment, subscription_id):
continue
seen_payment_ids.add(payment_id)
payments.append(payment)
if len(payments) >= MAX_SUBSCRIPTION_PAYMENTS:
break

return payments


def _add_payer_hint(hints, source, email=None, doc_type=None, doc_number=None,
first_name=None, last_name=None):
if email:
hints['emails'].add(email.strip().lower())
normalized_email = email.strip().lower()
hints['emails'].add(normalized_email)
hints['email_sources'].setdefault(normalized_email, set()).add(source)
if doc_number:
hints['documents'].append({
'source': source,
Expand All @@ -283,6 +325,7 @@ def _add_payer_hint(hints, source, email=None, doc_type=None, doc_number=None,
def collect_payer_hints(sdk, subscription_summary, subscription_detail, payments):
hints = {
'emails': set(),
'email_sources': {},
'documents': [],
'names': [],
}
Expand Down Expand Up @@ -331,10 +374,16 @@ def collect_payer_hints(sdk, subscription_summary, subscription_detail, payments
return hints


def _find_user_by_email(email, user_index):
def _find_user_by_email(email, user_index, payer_first_name='', payer_last_name=''):
if not email:
return None
return user_index['by_email'].get(email.strip().lower())
user = user_index['by_email'].get(email.strip().lower())
if not user:
return None
if payer_last_name and user.last_name:
if name_similarity(payer_last_name, user.last_name) < 0.5:
return None
return user


def _find_user_by_document(document_number, user_index):
Expand Down Expand Up @@ -376,18 +425,15 @@ def _find_user_by_name(first_name, last_name, user_index):
return best_user, best_score


def match_payer_hints_to_user(hints, user_index):
for email in hints['emails']:
user = _find_user_by_email(email, user_index)
if user:
return user, 'email', {'email': email}
def _match_from_hints(hints, user_index, allowed_sources, payer_first_name='', payer_last_name=''):
allowed_documents = [doc for doc in hints['documents'] if doc.get('source') in allowed_sources]
allowed_names = [name for name in hints['names'] if name.get('source') in allowed_sources]
allowed_emails = {
email for email in hints['emails']
if any(source in allowed_sources for source in hints.get('email_sources', {}).get(email, []))
} if hints.get('email_sources') else set()

seen_documents = set()
for document in hints['documents']:
normalized = normalize_document_number(document.get('number'))
if not normalized or normalized in seen_documents:
continue
seen_documents.add(normalized)
for document in allowed_documents:
user = _find_user_by_document(document.get('number'), user_index)
if user:
return user, 'dni', {
Expand All @@ -397,19 +443,17 @@ def match_payer_hints_to_user(hints, user_index):
}

best_user = None
best_method = None
best_meta = None
best_score = 0.0

for name_hint in hints['names']:
for name_hint in allowed_names:
user, score = _find_user_by_name(
name_hint.get('first_name'),
name_hint.get('last_name'),
user_index,
)
if user and score > best_score:
best_user = user
best_method = 'name'
best_meta = {
'first_name': name_hint.get('first_name'),
'last_name': name_hint.get('last_name'),
Expand All @@ -419,7 +463,41 @@ def match_payer_hints_to_user(hints, user_index):
best_score = score

if best_user:
return best_user, best_method, best_meta
return best_user, 'name', best_meta

for email in allowed_emails:
user = _find_user_by_email(
email,
user_index,
payer_first_name=payer_first_name,
payer_last_name=payer_last_name,
)
if user:
return user, 'email', {'email': email}

return None, None, None


def match_payer_hints_to_user(hints, user_index, payer_first_name='', payer_last_name=''):
user, method, meta = _match_from_hints(
hints,
user_index,
PRIMARY_SOURCES,
payer_first_name=payer_first_name,
payer_last_name=payer_last_name,
)
if user:
return user, method, meta

user, method, meta = _match_from_hints(
hints,
user_index,
PAYMENT_SOURCES,
payer_first_name=payer_first_name,
payer_last_name=payer_last_name,
)
if user:
return user, method, meta

return None, None, _summarize_unmatched_hints(hints)

Expand Down Expand Up @@ -521,7 +599,22 @@ def match_subscription_to_user(sdk, subscription_summary, subscription_detail=No
)
payments = fetch_payments_for_subscription(sdk, sub_id, payer_id=payer_id)
hints = collect_payer_hints(sdk, subscription_summary, subscription_detail, payments)
user, match_method, match_meta = match_payer_hints_to_user(hints, user_index)
payer_first_name = (
subscription_detail.get('payer_first_name')
or subscription_summary.get('payer_first_name')
or ''
)
payer_last_name = (
subscription_detail.get('payer_last_name')
or subscription_summary.get('payer_last_name')
or ''
)
user, match_method, match_meta = match_payer_hints_to_user(
hints,
user_index,
payer_first_name=payer_first_name,
payer_last_name=payer_last_name,
)
details = _extract_subscription_details(subscription_summary, subscription_detail, payments)

document_number = None
Expand Down Expand Up @@ -580,6 +673,7 @@ def start_sync():
'total': len(subscriptions),
'processed': 0,
'active_subscription_ids': [],
'assigned_users': {},
'results': [],
'done': False,
},
Expand Down Expand Up @@ -661,19 +755,28 @@ def process_next_subscription(sync_id):
user_index=state.get('user_index'),
)
if user:
apply_subscription_to_profile(user.profile, details)
if details.get('status') in ACTIVE_SUBSCRIPTION_STATUSES:
state['active_subscription_ids'].append(sub_id)
result.update({
'matched': True,
'match_method': match_method,
'user_email': user.email,
'message': f'Vinculado con {user.email} ({match_method})',
})
if match_method == 'name' and details.get('match_meta'):
result['message'] += f" — score {details['match_meta'].get('score')}"
if details.get('payments_checked'):
result['message'] += f" — {details['payments_checked']} pago(s)"
assigned_users = state.setdefault('assigned_users', {})
previous_name = assigned_users.get(user.id)
if previous_name and payer_name and name_similarity(previous_name, payer_name) < 0.5:
result['message'] = (
f'Conflicto: {user.email} ya vinculado a "{previous_name}", '
f'no coincide con "{payer_name}"'
)
else:
apply_subscription_to_profile(user.profile, details)
assigned_users[user.id] = payer_name or user.get_full_name()
if details.get('status') in ACTIVE_SUBSCRIPTION_STATUSES:
state['active_subscription_ids'].append(sub_id)
result.update({
'matched': True,
'match_method': match_method,
'user_email': user.email,
'message': f'Vinculado con {user.email} ({match_method})',
})
if match_method == 'name' and details.get('match_meta'):
result['message'] += f" — score {details['match_meta'].get('score')}"
if details.get('payments_checked'):
result['message'] += f" — {details['payments_checked']} pago(s)"
else:
result['message'] = _format_unmatched_message(details or {})
except Exception as exc:
Expand Down
Loading