diff --git a/apps/promotions/filters.py b/apps/promotions/filters.py index 33050b7..c7bbe77 100644 --- a/apps/promotions/filters.py +++ b/apps/promotions/filters.py @@ -1,11 +1,36 @@ import django_filters -from .models import Sample +from .handlers import ProcessorTypeChoices +from .models import Promotion, PaymentStateChoices -class SampleFilter(django_filters.FilterSet): +class AdminPromotionFilter(django_filters.FilterSet): + user_uuid = django_filters.UUIDFilter(field_name='user_uuid') + plan = django_filters.UUIDFilter(field_name='plan__uuid') + promotion_type = django_filters.ChoiceFilter(field_name='plan__processor', choices=ProcessorTypeChoices.choices) + state = django_filters.ChoiceFilter(field_name='state', choices=PaymentStateChoices.choices) + event_label = django_filters.CharFilter(field_name='event__label', lookup_expr='exact') + created_after = django_filters.DateTimeFilter(field_name='created_at', lookup_expr='gte') + created_before = django_filters.DateTimeFilter(field_name='created_at', lookup_expr='lte') + class Meta: - model = Sample - fields = { - 'data': ['exact'], - } + model = Promotion + fields = ['user_uuid', 'plan', 'promotion_type', 'state', 'event_label', 'created_after', 'created_before'] + + +class AdminReferralFilter(django_filters.FilterSet): + # invited_by == Promotion.user_uuid: in the referral recipient DSL + # ("->event:referral") this is who gets paid, i.e. the referrer. + invited_by = django_filters.UUIDFilter(field_name='user_uuid') + # invited_user lives only in free-form JSON (event.data['user']), so this is a + # CharFilter, not UUIDFilter: JSONField key-transform lookups compare against + # the stored string, not a native UUID the adapter can serialize. + invited_user = django_filters.CharFilter(field_name='event__data__user') + plan = django_filters.UUIDFilter(field_name='plan__uuid') + state = django_filters.ChoiceFilter(field_name='state', choices=PaymentStateChoices.choices) + created_after = django_filters.DateTimeFilter(field_name='created_at', lookup_expr='gte') + created_before = django_filters.DateTimeFilter(field_name='created_at', lookup_expr='lte') + + class Meta: + model = Promotion + fields = ['invited_user', 'invited_by', 'plan', 'state', 'created_after', 'created_before'] diff --git a/docs/adminpanel_technical.md b/docs/adminpanel_technical.md index 2c00b09..d786643 100644 --- a/docs/adminpanel_technical.md +++ b/docs/adminpanel_technical.md @@ -1,21 +1,38 @@ # Admin panel — technical reference -Two read-only endpoints added for the admin panel: **Promotions** (every promotion received, per user) and **Referral System** (referral activity). Both live in `apps/promotions`, following this repo's existing `_user` / `_application` file-suffix convention with a new `_admin` suffix: +Two read-only endpoints added for the admin panel: **Promotions** (every promotion received, per user) and **Referral System** (referral activity). They live under `apps/promotions`, structured to match the admin/users folder split used by the sibling `advertising` service (`apps/crm`, `apps/escrow`, `apps/stores` there each split `views/`, `serializers/`, and `urls/` into `admin/`/`users/` subpackages, sharing one `DefaultRouter`). Applied here for the admin slice only — the pre-existing `views_user.py` / `views_application.py` / `urls_user.py` / `urls_application.py` (user-token and application-token APIs) were intentionally left untouched, since restructuring those would rename URL namespaces already relied on by `tests.py` and any external caller. -| File | Purpose | +| Path | Purpose | |---|---| -| `apps/promotions/views_admin.py` | `AdminPromotionListApiView`, `AdminPromotionDetailApiView`, `AdminReferralListApiView`, `AdminReferralDetailApiView` | -| `apps/promotions/urls_admin.py` | URL routes, `app_name = 'promotions-admin'` | -| `apps/promotions/filters_admin.py` | `AdminPromotionFilter`, `AdminReferralFilter` (django-filter `FilterSet`s) | -| `apps/promotions/serializers.py` | `AdminPromotionSerializer`, `AdminReferralSerializer`, `AdminPlanSummarySerializer` (added alongside the existing serializers, this repo does not split serializers by audience) | +| `apps/promotions/views/admin/promotion.py` | `AdminPromotionViewSet` | +| `apps/promotions/views/admin/referral.py` | `AdminReferralViewSet` | +| `apps/promotions/views/admin/__init__.py`, `apps/promotions/views/__init__.py` | re-export the two viewsets | +| `apps/promotions/urls/router.py` | shared `DefaultRouter` for the admin API | +| `apps/promotions/urls/admin_urls.py` | registers both viewsets on the router | +| `apps/promotions/urls/__init__.py` | combines `router.urls`, sets `app_name = 'promotions-admin'` | +| `apps/promotions/serializers/admin/promotion.py` | `AdminPlanSummarySerializer`, `AdminPromotionSerializer` | +| `apps/promotions/serializers/admin/referral.py` | `AdminReferralSerializer` | +| `apps/promotions/serializers/admin/__init__.py`, `apps/promotions/serializers/__init__.py` | re-export, alongside the pre-existing (now `serializers/common.py`) serializers | +| `apps/promotions/filters.py` | `AdminPromotionFilter`, `AdminReferralFilter` — stays a single flat file, matching how `advertising`'s `filters.py` (e.g. `apps/crm/filters.py`) is *not* split into subfolders even though views/serializers/urls are | Mounted in `main/urls.py`: ```python -path('api/v2/promotions/admin/', include('apps.promotions.urls_admin', namespace='promotions-admin')), +path('api/v2/promotions/admin/', include('apps.promotions.urls', namespace='promotions-admin')), ``` -No models or migrations were changed. Both endpoints are pure `ListAPIView`/`RetrieveAPIView` reads over the existing `Promotion` table. +No models or migrations were changed. Both resources are `GenericViewSet` + `ListModelMixin`/`RetrieveModelMixin` (matching `AdminTicketViewSet` in `advertising`'s `apps/crm/views/admin/ticket.py`) registered on one router, not separate `ListAPIView`/`RetrieveAPIView` classes. + +### URL names + +Router-generated, under the `promotions-admin` namespace: + +| Name | Path | +|---|---| +| `promotions-admin:admin-promotions-list` | `GET /api/v2/promotions/admin/promotions/` | +| `promotions-admin:admin-promotions-detail` | `GET /api/v2/promotions/admin/promotions//` | +| `promotions-admin:admin-referrals-list` | `GET /api/v2/promotions/admin/referrals/` | +| `promotions-admin:admin-referrals-detail` | `GET /api/v2/promotions/admin/referrals//` | --- @@ -207,3 +224,4 @@ GET /api/v2/promotions/admin/referrals/?invited_by=1bb3b561-2823-4a3e-be12-edc5a - **Detail routes are of limited independent use.** `AdminPromotionDetailApiView` / `AdminReferralDetailApiView` return the same shape as one row of the list endpoint; included for REST completeness (e.g. deep-linking from a table row in the admin UI) but the list endpoint with `plan`/`state` filters covers most real usage. - **New scopes (`admin.promotions:retrieve`, `admin.referrals:retrieve`) need to be registered on the central accounts OAuth2 provider** before any real token can carry them — that's outside this checkout. - **No automated tests were added** for these two endpoints (matching the request's scope: verified via `manage.py check`, URL resolution, and an `APIRequestFactory` smoke test against an in-memory SQLite DB with the app's Postgres/JSONField-`GinIndex` usage stubbed around, run manually — see `apps/promotions/tests.py` for the existing `APITestCase` pattern if formal tests are wanted later). +- **The admin/users folder split was applied to the admin slice only.** `views_user.py`, `views_application.py`, `urls_user.py`, `urls_application.py`, and the non-admin serializers (now `serializers/common.py`) were left in place rather than also converted to `views/users/`, `views/application/`, etc., because that would rename the `promotions` / `promotions-application` URL namespaces already used by `tests.py`'s `reverse()` calls (and possibly external callers) — a breaking change outside what was asked for here. diff --git a/main/urls.py b/main/urls.py index 5067a92..a0eda91 100644 --- a/main/urls.py +++ b/main/urls.py @@ -30,7 +30,7 @@ urlpatterns = [ path('oauth2/', include('oauth2_provider.urls', namespace='oauth2_provider')), path('promotions/', include('apps.promotions.urls_user', namespace='promotions')), path('api/v2/promotions/application//', include('apps.promotions.urls_application', namespace='promotions-application')), - path('api/v2/promotions/admin/', include('apps.promotions.urls_admin', namespace='promotions-admin')), + path('api/v2/promotions/admin/', include('apps.promotions.urls', namespace='promotions-admin')), ] urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)