Skip to content
Merged
Show file tree
Hide file tree
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
2 changes: 2 additions & 0 deletions deprepagos/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand All @@ -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/<int:new_order_id>/', 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),
Expand Down
3 changes: 3 additions & 0 deletions tickets/templates/admin/base_site.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ <h1 id="site-name"><a href="{% url 'admin:index' %}">{{ site_header|default:_('D
</li>
{% endif %}
{% if user.is_staff %}
<li>
<a href="{% url 'admin_sede_subscriptions_view' %}">LA SEDE — SUBSCRIPTIONS</a>
</li>
<li>
<a href="{% url 'admin_sede_matches_view' %}">LA SEDE — MATCH AUDIT</a>
</li>
Expand Down
11 changes: 9 additions & 2 deletions user_profile/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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})'
Expand Down Expand Up @@ -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]
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')
51 changes: 51 additions & 0 deletions user_profile/admin_sede_subscriptions.py
Original file line number Diff line number Diff line change
@@ -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,
},
)
29 changes: 29 additions & 0 deletions user_profile/migrations/0007_sedesubscriptionplan.py
Original file line number Diff line number Diff line change
@@ -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'),
},
),
]
16 changes: 15 additions & 1 deletion user_profile/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,18 @@ class Meta:
ordering = ('-last_seen_at', '-updated_at')

def __str__(self):
return f'Unmatched {self.subscription_id} ({self.status})'
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})'
64 changes: 60 additions & 4 deletions user_profile/services/sede_mercadopago.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand All @@ -42,13 +47,59 @@


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:
plan_ids.append(default_plan)
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 ''
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down
57 changes: 57 additions & 0 deletions user_profile/templates/admin/admin_sede_subscriptions.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{% extends "admin/base_site.html" %}

{% block content %}
<div>
<h1>La Sede — Subscription Plans</h1>
<p>
Refrescá los planes de suscripción de MercadoPago y habilitá/deshabilitá cuáles se usan para matchear usuarios.
</p>

<form method="post" style="margin: 1rem 0;">
{% csrf_token %}
<input type="hidden" name="action" value="refresh">
<button type="submit" class="button default">Refresh subscriptions</button>
</form>

<div style="overflow-x:auto;">
<table class="table">
<thead>
<tr>
<th>Enabled</th>
<th>Plan ID</th>
<th>Name</th>
<th>Subscriptions</th>
<th>Last seen</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for plan in plans %}
<tr>
<td>{% if plan.is_enabled %}Sí{% else %}No{% endif %}</td>
<td><code>{{ plan.plan_id }}</code></td>
<td>{{ plan.plan_name|default:"—" }}</td>
<td>{{ plan.subscriptions_count }}</td>
<td>{{ plan.last_seen_at|date:"d/m/Y H:i"|default:"—" }}</td>
<td>
<form method="post" style="display:inline;">
{% csrf_token %}
<input type="hidden" name="action" value="toggle_plan">
<input type="hidden" name="plan_id" value="{{ plan.plan_id }}">
<input type="hidden" name="enabled" value="{% if plan.is_enabled %}0{% else %}1{% endif %}">
<button type="submit" class="button">
{% if plan.is_enabled %}Disable{% else %}Enable{% endif %}
</button>
</form>
</td>
</tr>
{% empty %}
<tr>
<td colspan="6">No hay planes cargados todavía. Presioná <em>Refresh subscriptions</em>.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
Loading