feat: add community contribution ranks
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
from typing import Literal, Optional
|
||||
|
||||
from ninja import Schema
|
||||
from datetime import datetime
|
||||
|
||||
@@ -64,6 +66,30 @@ class PaymentDetailOut(Schema):
|
||||
verified_at: str | None = None
|
||||
event: dict
|
||||
|
||||
|
||||
class UserPaymentEventSchema(Schema):
|
||||
title: str
|
||||
slug: str
|
||||
image_url: Optional[str] = None
|
||||
|
||||
|
||||
class UserPaymentSchema(Schema):
|
||||
event: UserPaymentEventSchema
|
||||
base_amount: int
|
||||
discount_amount: int
|
||||
amount: int
|
||||
status: Literal["initiated", "pending", "paid", "failed", "canceled"]
|
||||
created_at: datetime
|
||||
paid_at: Optional[datetime] = None
|
||||
|
||||
|
||||
class PaginatedUserPaymentsSchema(Schema):
|
||||
count: int
|
||||
page: int
|
||||
page_size: int
|
||||
total_pages: int
|
||||
results: list[UserPaymentSchema]
|
||||
|
||||
class CouponVerifyIn(Schema):
|
||||
event_id: int
|
||||
code: str
|
||||
|
||||
@@ -2,6 +2,7 @@ from django.conf import settings
|
||||
from django.shortcuts import redirect, get_object_or_404
|
||||
from django.utils import timezone
|
||||
from django.db.models import Count, Q
|
||||
from typing import List
|
||||
|
||||
from ninja import Query, Router
|
||||
from ninja.errors import HttpError
|
||||
@@ -22,11 +23,70 @@ from apps.payments.api.schemas import (
|
||||
DiscountCodeWriteSchema,
|
||||
PagedDiscountCodeSchema,
|
||||
PaymentDetailOut,
|
||||
PaginatedUserPaymentsSchema,
|
||||
UserPaymentSchema,
|
||||
)
|
||||
|
||||
payments_router = Router(tags=["Payments"])
|
||||
|
||||
|
||||
@payments_router.get("/me", response=List[UserPaymentSchema], auth=jwt_auth)
|
||||
def my_payments(request):
|
||||
status_names = {
|
||||
Payment.OrderStatusChoices.INIT: "initiated",
|
||||
Payment.OrderStatusChoices.PENDING: "pending",
|
||||
Payment.OrderStatusChoices.PAID: "paid",
|
||||
Payment.OrderStatusChoices.FAILED: "failed",
|
||||
Payment.OrderStatusChoices.CANCELED: "canceled",
|
||||
}
|
||||
payments = Payment.objects.filter(user=request.auth).select_related("event").order_by("-created_at")[:50]
|
||||
return [
|
||||
_user_payment_payload(request, payment, status_names)
|
||||
for payment in payments
|
||||
]
|
||||
|
||||
|
||||
def _user_payment_payload(request, payment, status_names):
|
||||
return {
|
||||
"event": {
|
||||
"title": payment.event.title,
|
||||
"slug": payment.event.slug,
|
||||
"image_url": request.build_absolute_uri(payment.event.featured_image.url)
|
||||
if payment.event.featured_image
|
||||
else None,
|
||||
},
|
||||
"base_amount": payment.base_amount,
|
||||
"discount_amount": payment.discount_amount,
|
||||
"amount": payment.amount,
|
||||
"status": status_names[payment.status],
|
||||
"created_at": payment.created_at,
|
||||
"paid_at": payment.verified_at,
|
||||
}
|
||||
|
||||
|
||||
@payments_router.get("/me/paginated", response=PaginatedUserPaymentsSchema, auth=jwt_auth)
|
||||
def my_payments_paginated(request, page: int = Query(1, ge=1)):
|
||||
page_size = 10
|
||||
status_names = {
|
||||
Payment.OrderStatusChoices.INIT: "initiated",
|
||||
Payment.OrderStatusChoices.PENDING: "pending",
|
||||
Payment.OrderStatusChoices.PAID: "paid",
|
||||
Payment.OrderStatusChoices.FAILED: "failed",
|
||||
Payment.OrderStatusChoices.CANCELED: "canceled",
|
||||
}
|
||||
queryset = Payment.objects.filter(user=request.auth).select_related("event").order_by("-created_at", "-id")
|
||||
count = queryset.count()
|
||||
total_pages = max(1, (count + page_size - 1) // page_size)
|
||||
start = (page - 1) * page_size
|
||||
return {
|
||||
"count": count,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"total_pages": total_pages,
|
||||
"results": [_user_payment_payload(request, item, status_names) for item in queryset[start : start + page_size]],
|
||||
}
|
||||
|
||||
|
||||
def _staff_required(user):
|
||||
return bool(user and (user.is_staff or user.is_superuser))
|
||||
|
||||
|
||||
@@ -135,3 +135,82 @@ class TestPaymentsAPIIntegrationTests:
|
||||
settings.ZARINPAL_STARTPAY = 'https://zarinpal/start/'
|
||||
settings.ZARINPAL_VERIFY_URL = 'https://zarinpal/verify'
|
||||
settings.ZARINPAL_CALLBACK_URL = 'https://frontend/callback'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.integration
|
||||
def test_my_payments_are_user_scoped_and_omit_gateway_identifiers(client, user_factory, auth_headers, event_factory):
|
||||
current_user = user_factory(username="payment_history_owner")
|
||||
other_user = user_factory(username="payment_history_other")
|
||||
own_event = event_factory(slug="payment-history-own", title="My workshop")
|
||||
other_event = event_factory(slug="payment-history-other", title="Someone else's workshop")
|
||||
Payment.objects.create(
|
||||
user=current_user,
|
||||
event=own_event,
|
||||
base_amount=120000,
|
||||
discount_amount=20000,
|
||||
amount=100000,
|
||||
status=Payment.OrderStatusChoices.PAID,
|
||||
authority="internal-authority-owner",
|
||||
ref_id="internal-reference-owner",
|
||||
card_pan="6037-****-****-1234",
|
||||
card_hash="internal-card-hash",
|
||||
)
|
||||
Payment.objects.create(
|
||||
user=other_user,
|
||||
event=other_event,
|
||||
base_amount=50000,
|
||||
amount=50000,
|
||||
status=Payment.OrderStatusChoices.PENDING,
|
||||
authority="internal-authority-other",
|
||||
)
|
||||
|
||||
response = client.get("/api/payments/me", **auth_headers(current_user))
|
||||
assert response.status_code == 200, response.content
|
||||
payload = response.json()
|
||||
assert len(payload) == 1
|
||||
assert payload[0]["event"]["title"] == own_event.title
|
||||
assert payload[0]["amount"] == 100000
|
||||
assert payload[0]["discount_amount"] == 20000
|
||||
assert payload[0]["status"] == "paid"
|
||||
assert set(payload[0]) == {"event", "base_amount", "discount_amount", "amount", "status", "created_at", "paid_at"}
|
||||
assert "internal-authority-owner" not in response.content.decode()
|
||||
assert "internal-reference-owner" not in response.content.decode()
|
||||
assert "internal-card-hash" not in response.content.decode()
|
||||
|
||||
assert client.get("/api/payments/me").status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.integration
|
||||
def test_my_payments_pagination_is_user_scoped(client, user_factory, auth_headers, event_factory):
|
||||
current_user = user_factory(username="payment_paging_owner")
|
||||
other_user = user_factory(username="payment_paging_other")
|
||||
event = event_factory(slug="payment-paging-event")
|
||||
for index in range(12):
|
||||
Payment.objects.create(
|
||||
user=current_user,
|
||||
event=event,
|
||||
base_amount=1000 + index,
|
||||
amount=1000 + index,
|
||||
status=Payment.OrderStatusChoices.PAID,
|
||||
)
|
||||
Payment.objects.create(
|
||||
user=other_user,
|
||||
event=event,
|
||||
base_amount=9000,
|
||||
amount=9000,
|
||||
status=Payment.OrderStatusChoices.PAID,
|
||||
)
|
||||
|
||||
first_page = client.get("/api/payments/me/paginated", {"page": 1}, **auth_headers(current_user))
|
||||
second_page = client.get("/api/payments/me/paginated", {"page": 2}, **auth_headers(current_user))
|
||||
|
||||
assert first_page.status_code == 200, first_page.content
|
||||
assert first_page.json()["count"] == 12
|
||||
assert first_page.json()["page_size"] == 10
|
||||
assert first_page.json()["total_pages"] == 2
|
||||
assert len(first_page.json()["results"]) == 10
|
||||
assert len(second_page.json()["results"]) == 2
|
||||
assert all(item["event"]["slug"] == event.slug for item in first_page.json()["results"])
|
||||
assert "authority" not in first_page.json()["results"][0]
|
||||
|
||||
Reference in New Issue
Block a user