Adds GET /api/v2/promotions/admin/promotions/ and .../referrals/, listing
every Promotion (type derived from Plan.processor) and referral activity
(invited_by = resolved payee, invited_user = raw event.data['user'] --
there's no dedicated referral model or referral_code field in this
codebase). Follows the existing _user/_application file-suffix convention
with a new _admin suffix, backed by real django-filter FilterSets.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ApplicationEventViewSet.perform_create() was passing the full request.user
model instance into Event.user (a plain UUIDField), which raises on every
real call. Use the same _resolve_user() -> user.uuid pattern already used by
status(), and raise UnprocessableEntity on save_event failure to match the
v1 endpoint's error-handling convention.
Fold the restricted-recipient access check into the recipients
queryset itself via Q(public OR restricted-and-allowed), joining
against AllowedUser instead of running a separate lookup query.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Checking each recipient via Recipient.is_user_allowed() inside the
loop issued one AllowedUser query per restricted recipient (N+1).
Fetch all matching AllowedUser rows for the plan's recipients in a
single query up front instead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
get_event_status_for_user returned None whenever the event was
already processed, no plan matched, or the user lacked access to the
recipient. Callers now always get a concrete amount: the real payout
if accessible, 0 otherwise.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add Recipient.access_type (restricted/public) and an AllowedUser
table linking users to the recipients they may be paid through.
Restricted recipients are now checked against AllowedUser both when
computing a promotion payout and in the event-status endpoints that
report eligibility before submission.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>