diff --git a/deprepagos/urls.py b/deprepagos/urls.py index 75b664e..a4d0688 100644 --- a/deprepagos/urls.py +++ b/deprepagos/urls.py @@ -6,6 +6,7 @@ from tickets.admin import admin_caja_view, email_has_account, admin_caja_order_view, admin_direct_tickets_view, \ admin_direct_tickets_buyer_view, admin_direct_tickets_congrats_view from user_profile.admin_sede_matches import admin_sede_matches_view +from user_profile.admin_sede_subscriptions import admin_sede_subscriptions_view urlpatterns = [ path('admin/caja/', admin_caja_view, name='admin_caja_view'), @@ -17,6 +18,7 @@ path('admin/direct_tickets/buyer/', admin_direct_tickets_buyer_view, name='admin_direct_tickets_buyer_view'), path('admin/direct_tickets/congrats//', admin_direct_tickets_congrats_view, name='admin_direct_tickets_congrats_view'), + path('admin/sede/subscriptions/', admin_sede_subscriptions_view, name='admin_sede_subscriptions_view'), path('admin/sede/matches/', admin_sede_matches_view, name='admin_sede_matches_view'), path('admin/', admin.site.urls), diff --git a/tickets/templates/admin/base_site.html b/tickets/templates/admin/base_site.html index fd6fa51..64393ad 100644 --- a/tickets/templates/admin/base_site.html +++ b/tickets/templates/admin/base_site.html @@ -24,6 +24,9 @@

{{ site_header|default:_('D {% endif %} {% if user.is_staff %} +
  • + LA SEDE — SUBSCRIPTIONS +
  • LA SEDE — MATCH AUDIT
  • diff --git a/user_profile/admin.py b/user_profile/admin.py index 1ee44b5..c6fd390 100644 --- a/user_profile/admin.py +++ b/user_profile/admin.py @@ -3,7 +3,7 @@ from django.contrib.auth.models import User from django.utils.html import format_html_join -from .models import Profile, SedeSubscription, SedeUnmatchedSubscription +from .models import Profile, SedeSubscription, SedeSubscriptionPlan, SedeUnmatchedSubscription User.__str__ = lambda self: f'{self.first_name} {self.last_name} ({self.email})' @@ -119,4 +119,11 @@ class SedeUnmatchedSubscriptionAdmin(admin.ModelAdmin): 'subscription_id', 'payer_email', 'payer_first_name', 'payer_last_name', 'document_number', 'plan_id', 'tier_name', ) - readonly_fields = [field.name for field in SedeUnmatchedSubscription._meta.fields] \ No newline at end of file + readonly_fields = [field.name for field in SedeUnmatchedSubscription._meta.fields] + + +@admin.register(SedeSubscriptionPlan) +class SedeSubscriptionPlanAdmin(admin.ModelAdmin): + list_display = ('plan_id', 'plan_name', 'is_enabled', 'subscriptions_count', 'last_seen_at') + list_filter = ('is_enabled',) + search_fields = ('plan_id', 'plan_name') \ No newline at end of file diff --git a/user_profile/admin_sede_subscriptions.py b/user_profile/admin_sede_subscriptions.py new file mode 100644 index 0000000..9daf224 --- /dev/null +++ b/user_profile/admin_sede_subscriptions.py @@ -0,0 +1,51 @@ +from django.contrib import messages +from django.contrib.admin.views.decorators import staff_member_required +from django.shortcuts import redirect, render +from django.views.decorators.http import require_http_methods + +from user_profile.models import SedeSubscriptionPlan +from user_profile.services.sede_mercadopago import refresh_sede_subscription_plans + + +@staff_member_required +@require_http_methods(['GET', 'POST']) +def admin_sede_subscriptions_view(request): + if request.method == 'POST': + action = request.POST.get('action') or '' + + if action == 'refresh': + try: + summary = refresh_sede_subscription_plans() + messages.success( + request, + ( + f"Refresh listo: {summary.get('refreshed_plans', 0)} plan(es) " + f"desde {summary.get('total_subscriptions', 0)} suscripción(es)." + ), + ) + except Exception as exc: + messages.error(request, f'Error refrescando suscripciones: {exc}') + return redirect('admin_sede_subscriptions_view') + + if action == 'toggle_plan': + plan_id = request.POST.get('plan_id') or '' + enabled = request.POST.get('enabled') == '1' + plan = SedeSubscriptionPlan.objects.filter(plan_id=plan_id).first() + if not plan: + messages.error(request, f'Plan no encontrado: {plan_id}') + else: + plan.is_enabled = enabled + plan.save(update_fields=['is_enabled', 'updated_at']) + state = 'habilitado' if enabled else 'deshabilitado' + messages.success(request, f'Plan {plan.plan_id} {state}.') + return redirect('admin_sede_subscriptions_view') + + plans = SedeSubscriptionPlan.objects.order_by('-is_enabled', 'plan_name', 'plan_id') + return render( + request, + 'admin/admin_sede_subscriptions.html', + { + 'title': 'La Sede — Subscription Plans', + 'plans': plans, + }, + ) diff --git a/user_profile/migrations/0007_sedesubscriptionplan.py b/user_profile/migrations/0007_sedesubscriptionplan.py new file mode 100644 index 0000000..aa8f72c --- /dev/null +++ b/user_profile/migrations/0007_sedesubscriptionplan.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2.15 on 2026-05-23 23:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user_profile', '0006_sedeunmatchedsubscription'), + ] + + operations = [ + migrations.CreateModel( + name='SedeSubscriptionPlan', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('plan_id', models.CharField(max_length=64, unique=True)), + ('plan_name', models.CharField(blank=True, default='', max_length=255)), + ('is_enabled', models.BooleanField(default=False)), + ('subscriptions_count', models.PositiveIntegerField(default=0)), + ('last_seen_at', models.DateTimeField(blank=True, null=True)), + ], + options={ + 'ordering': ('-is_enabled', 'plan_name', 'plan_id'), + }, + ), + ] diff --git a/user_profile/models.py b/user_profile/models.py index cbb683b..1103a69 100644 --- a/user_profile/models.py +++ b/user_profile/models.py @@ -132,4 +132,18 @@ class Meta: ordering = ('-last_seen_at', '-updated_at') def __str__(self): - return f'Unmatched {self.subscription_id} ({self.status})' \ No newline at end of file + return f'Unmatched {self.subscription_id} ({self.status})' + + +class SedeSubscriptionPlan(BaseModel): + plan_id = models.CharField(max_length=64, unique=True) + plan_name = models.CharField(max_length=255, blank=True, default='') + is_enabled = models.BooleanField(default=False) + subscriptions_count = models.PositiveIntegerField(default=0) + last_seen_at = models.DateTimeField(null=True, blank=True) + + class Meta: + ordering = ('-is_enabled', 'plan_name', 'plan_id') + + def __str__(self): + return f'{self.plan_name or self.plan_id} ({self.plan_id})' \ No newline at end of file diff --git a/user_profile/services/sede_mercadopago.py b/user_profile/services/sede_mercadopago.py index 4da38a7..8be236d 100644 --- a/user_profile/services/sede_mercadopago.py +++ b/user_profile/services/sede_mercadopago.py @@ -15,7 +15,12 @@ from django.utils import timezone from django.utils.dateparse import parse_datetime -from user_profile.models import Profile, SedeSubscription, SedeUnmatchedSubscription +from user_profile.models import ( + Profile, + SedeSubscription, + SedeSubscriptionPlan, + SedeUnmatchedSubscription, +) logger = logging.getLogger(__name__) @@ -42,6 +47,16 @@ def get_sede_plan_ids(): + try: + enabled_db_plan_ids = list( + SedeSubscriptionPlan.objects.filter(is_enabled=True).values_list('plan_id', flat=True) + ) + except Exception: + enabled_db_plan_ids = [] + + if enabled_db_plan_ids: + return enabled_db_plan_ids + plan_ids = list(getattr(settings, 'SEDE_SUBSCRIPTION_PLAN_IDS', []) or []) default_plan = getattr(settings, 'SEDE_DEFAULT_PLAN_ID', '') if default_plan and default_plan not in plan_ids: @@ -49,6 +64,42 @@ def get_sede_plan_ids(): return plan_ids +def refresh_sede_subscription_plans(log=None): + log = log or logger + sdk = get_mp_sdk() + subscriptions = fetch_all_subscriptions(sdk=sdk) + + plan_catalog = {} + for sub in subscriptions: + plan_id = (sub.get('preapproval_plan_id') or '').strip() + if not plan_id: + continue + plan_name = (sub.get('reason') or '').strip() + item = plan_catalog.setdefault(plan_id, {'name': '', 'count': 0}) + item['count'] += 1 + if plan_name and (not item['name'] or len(plan_name) > len(item['name'])): + item['name'] = plan_name + + refreshed = 0 + now = timezone.now() + for plan_id, info in plan_catalog.items(): + SedeSubscriptionPlan.objects.update_or_create( + plan_id=plan_id, + defaults={ + 'plan_name': info['name'] or '', + 'subscriptions_count': info['count'], + 'last_seen_at': now, + }, + ) + refreshed += 1 + + log.info('Refreshed %d MercadoPago subscription plan(s)', refreshed) + return { + 'total_subscriptions': len(subscriptions), + 'refreshed_plans': refreshed, + } + + def normalize_document_number(document_number): if not document_number: return '' @@ -154,14 +205,18 @@ def _paginated_search(search_fn, filters, limit=50): return results -def fetch_all_subscriptions(sdk=None): +def fetch_all_subscriptions(sdk=None, plan_ids=None): """Fetch every subscription on the MercadoPago account.""" sdk = sdk or get_mp_sdk() seen_ids = set() subscriptions = [] + allowed_plan_ids = set(plan_ids or []) for item in _paginated_search(sdk.preapproval().search, {}): sub_id = item.get('id') + plan_id = item.get('preapproval_plan_id') or '' + if allowed_plan_ids and plan_id not in allowed_plan_ids: + continue if sub_id and sub_id not in seen_ids: seen_ids.add(sub_id) subscriptions.append(item) @@ -875,7 +930,8 @@ def run_match_audit(log=None): return {'error': 'MERCADOPAGO_ACCESS_TOKEN not configured'} sdk = get_mp_sdk() - subscriptions = fetch_all_subscriptions(sdk) + plan_ids = get_sede_plan_ids() + subscriptions = fetch_all_subscriptions(sdk, plan_ids=plan_ids) user_index = build_user_match_index() matched_count = 0 @@ -970,7 +1026,7 @@ def run_full_sync(log=None): sdk = get_mp_sdk() log.info('Fetching subscriptions from MercadoPago...') - subscriptions = fetch_all_subscriptions(sdk) + subscriptions = fetch_all_subscriptions(sdk, plan_ids=plan_ids) total = len(subscriptions) log.info('Found %d subscription(s)', total) diff --git a/user_profile/templates/admin/admin_sede_subscriptions.html b/user_profile/templates/admin/admin_sede_subscriptions.html new file mode 100644 index 0000000..4d89558 --- /dev/null +++ b/user_profile/templates/admin/admin_sede_subscriptions.html @@ -0,0 +1,57 @@ +{% extends "admin/base_site.html" %} + +{% block content %} +
    +

    La Sede — Subscription Plans

    +

    + Refrescá los planes de suscripción de MercadoPago y habilitá/deshabilitá cuáles se usan para matchear usuarios. +

    + +
    + {% csrf_token %} + + +
    + +
    + + + + + + + + + + + + + {% for plan in plans %} + + + + + + + + + {% empty %} + + + + {% endfor %} + +
    EnabledPlan IDNameSubscriptionsLast seenAction
    {% if plan.is_enabled %}Sí{% else %}No{% endif %}{{ plan.plan_id }}{{ plan.plan_name|default:"—" }}{{ plan.subscriptions_count }}{{ plan.last_seen_at|date:"d/m/Y H:i"|default:"—" }} +
    + {% csrf_token %} + + + + +
    +
    No hay planes cargados todavía. Presioná Refresh subscriptions.
    +
    +
    +{% endblock %}