Browse Source

feat(transaction): implement receipt management for transactions

- Added `TransactionReceipt` model to store payment receipts uploaded by users.
- Introduced endpoints for uploading receipts and listing receipts associated with transactions.
- Updated `TransactionParticipant` model to include a new status 'waiting_approval'.
- Enhanced serializers to handle receipt uploads and retrievals, including validation for file uploads.
- Updated admin interface to manage transaction receipts effectively.
- Added comprehensive API documentation for transaction receipt operations.
master
mortezaei 6 months ago
parent
commit
c0cd7ff8f9
  1. 21
      apps/library/migrations/0009_alter_book_language.py
  2. 61
      apps/transaction/admin.py
  3. 529
      apps/transaction/doc.py
  4. 35
      apps/transaction/migrations/0003_alter_transactionparticipant_status_and_more.py
  5. 44
      apps/transaction/models.py
  6. 57
      apps/transaction/serializers.py
  7. 3
      apps/transaction/urls.py
  8. 165
      apps/transaction/views.py
  9. 79
      templates/docs.html

21
apps/library/migrations/0009_alter_book_language.py

@ -0,0 +1,21 @@
# Generated by Django 5.1.8 on 2025-12-03 23:32
import dj_language.field
import django.db.models.deletion
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('dj_language', '0002_auto_20220120_1344'),
('library', '0008_auto_20251203_1533'),
]
operations = [
migrations.AlterField(
model_name='book',
name='language',
field=dj_language.field.LanguageField(blank=True, default=69, limit_choices_to={'status': True}, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='dj_language.language', verbose_name='Language'),
),
]

61
apps/transaction/admin.py

@ -3,10 +3,10 @@ from django.utils.translation import gettext_lazy as _
from django.utils.html import format_html
from django.contrib import messages
from unfold.admin import ModelAdmin, StackedInline
from unfold.admin import ModelAdmin, StackedInline, TabularInline
from unfold.decorators import display
from apps.transaction.models import TransactionParticipant, ParticipantInfo
from apps.transaction.models import TransactionParticipant, ParticipantInfo, TransactionReceipt
from apps.course.models import Participant
from utils.admin import project_admin_site
@ -21,13 +21,25 @@ class ParticipantInfoInline(StackedInline):
show_change_link = True
class TransactionReceiptInline(TabularInline):
model = TransactionReceipt
extra = 0
fields = ['file', 'description', 'uploaded_at']
readonly_fields = ['uploaded_at']
classes = ['collapse']
tab = True
show_change_link = True
verbose_name = _('Payment Receipt')
verbose_name_plural = _('Payment Receipts')
@admin.register(TransactionParticipant)
class TransactionParticipantAdmin(ModelAdmin):
list_display = ('user', 'course', 'payment_status', 'price_display', 'participant_status', 'created_at', 'updated_at')
list_display = ('user', 'course', 'payment_status', 'price_display', 'participant_status', 'receipts_count', 'created_at', 'updated_at')
list_filter = ('status', 'course', 'created_at')
search_fields = ('user__email', 'course__title')
readonly_fields = [ 'created_at', 'updated_at']
inlines = [ParticipantInfoInline]
readonly_fields = ['created_at', 'updated_at']
inlines = [ParticipantInfoInline, TransactionReceiptInline]
autocomplete_fields = ['user',]
show_change_link = True
ordering = ('-created_at',)
@ -48,7 +60,17 @@ class TransactionParticipantAdmin(ModelAdmin):
return format_html('<span class="unfold-badge unfold-badge--success">Paid</span>')
elif obj.status == 'failed':
return format_html('<span class="unfold-badge unfold-badge--danger">Failed</span>')
elif obj.status == 'waiting_approval':
return format_html('<span class="unfold-badge unfold-badge--info">Waiting Approval</span>')
return format_html('<span class="unfold-badge unfold-badge--warning">Pending</span>')
@display(description=_("Receipts Count"))
def receipts_count(self, obj):
"""Display count of uploaded receipts"""
count = obj.receipts.count()
if count > 0:
return format_html('<span class="unfold-badge unfold-badge--info">{} receipts</span>', count)
return format_html('<span class="unfold-badge unfold-badge--secondary">No receipts</span>')
@display(description=_("Price"), ordering="price")
def price_display(self, obj):
@ -106,3 +128,32 @@ class TransactionParticipantAdmin(ModelAdmin):
return super().get_queryset(request).filter(is_deleted=False)
project_admin_site.register(TransactionParticipant, TransactionParticipantAdmin)
@admin.register(TransactionReceipt)
class TransactionReceiptAdmin(ModelAdmin):
list_display = ('transaction', 'file', 'uploaded_at', 'description_preview')
list_filter = ('uploaded_at', 'transaction__status')
search_fields = ('transaction__user__email', 'transaction__course__title', 'description')
readonly_fields = ['uploaded_at']
autocomplete_fields = ['transaction']
ordering = ('-uploaded_at',)
fieldsets = (
(None, {
'fields': ('transaction', 'file', 'description')
}),
(_('Timestamps'), {
'fields': ('uploaded_at',),
'classes': ('collapse',)
}),
)
@display(description=_("Description"))
def description_preview(self, obj):
"""Display truncated description"""
if obj.description:
return obj.description[:50] + '...' if len(obj.description) > 50 else obj.description
return '-'
project_admin_site.register(TransactionReceipt, TransactionReceiptAdmin)

529
apps/transaction/doc.py

@ -0,0 +1,529 @@
def doc_upload_transaction_receipts():
return """
# 🐈 Scenario
🛠 آپلود رسید پرداخت برای تراکنش
این API برای آپلود یک یا چند رسید پرداخت برای یک تراکنش استفاده میشود.
پس از آپلود موفقیتآمیز، وضعیت تراکنش به 'waiting_approval' (در انتظار تایید) تغییر میکند.
---
## 🚀 روند آپلود (دو مرحله‌ای)
### مرحله 1️⃣: آپلود فایل به سرور موقت
ابتدا باید فایلهای خود را به endpoint زیر آپلود کنید:
```
POST /upload-tmp-media/
Content-Type: multipart/form-data
Body:
- file: [فایل رسید]
```
**پاسخ:**
```json
{
"url": "/static/tmp/xyz123-receipt.jpg",
"name": "receipt.jpg",
"size": "1024000",
"mime_type": "image/jpeg"
}
```
### مرحله 2️⃣: ثبت URL فایل‌ها در تراکنش
سپس URL های دریافتی را به این endpoint ارسال کنید:
```
POST /api/transactions/<transaction_id>/receipts/upload/
Content-Type: application/json
```
---
## 🚀 درخواست API (مرحله 2)
### URL:
```
POST /api/transactions/<transaction_id>/receipts/upload/
```
### پارامترهای URL:
| کلید | نوع داده | توضیحات |
|------------------|-----------|----------------------------------------------------------|
| `transaction_id` | Integer | شناسه تراکنش که میخواهید رسید برای آن ثبت کنید |
### پارامترهای درخواست (JSON Body):
| کلید | نوع داده | الزامی | توضیحات |
|---------------|-----------|--------|----------------------------------------------------------|
| `files` | String[] | بله | لیست URL های فایلهای آپلود شده از مرحله 1 (حداکثر 10 فایل) |
| `description` | String | خیر | توضیحات اختیاری درباره رسیدها |
---
## 💡 نکات مهم:
1. **روند دو مرحلهای**:
- **مرحله 1**: ابتدا فایلها را به `/upload-tmp-media/` آپلود کنید
- **مرحله 2**: سپس URL های دریافتی را به این API ارسال کنید
2. **محدودیت فایلها**:
- حداکثر 10 فایل میتوانید در هر درخواست ثبت کنید
3. **وضعیت تراکنش**:
- فقط میتوانید برای تراکنشهایی با وضعیت 'pending' یا 'waiting_approval' رسید آپلود کنید
- پس از ثبت موفقیتآمیز، وضعیت تراکنش به 'waiting_approval' تغییر میکند
4. **احراز هویت**:
- باید توکن احراز هویت را در هدر درخواست ارسال کنید
- فقط میتوانید برای تراکنشهای خودتان رسید آپلود کنید
---
## 📊 پاسخ‌ها
| کد وضعیت | توضیحات |
|---------------|-----------------------------------------------------------|
| `201` | موفقیتآمیز - رسیدها با موفقیت ثبت شدند |
| `400` | دادههای نامعتبر یا تراکنش قادر به دریافت رسید نیست |
| `403` | عدم دسترسی - شما صاحب این تراکنش نیستید |
| `404` | تراکنش یافت نشد |
---
## 📄 نمونه درخواست کامل (JSON):
```json
{
"files": [
"/static/tmp/xyz123-receipt1.jpg",
"/static/tmp/abc456-receipt2.jpg"
],
"description": "Payment receipt for Python course"
}
```
---
## 📄 نمونه پاسخ موفقیت‌آمیز
```json
{
"success": true,
"message": "Receipts uploaded successfully",
"transaction_status": "waiting_approval",
"receipts": [
{
"id": 1,
"file": "http://example.com/media/receipts/1/receipt1.jpg",
"description": "Payment receipt for course enrollment",
"uploaded_at": "2025-12-03T10:30:00Z"
},
{
"id": 2,
"file": "http://example.com/media/receipts/1/receipt2.jpg",
"description": "Payment receipt for course enrollment",
"uploaded_at": "2025-12-03T10:30:05Z"
}
]
}
```
---
## 📄 نمونه درخواست کامل (cURL):
### مرحله 1 - آپلود فایل:
```bash
curl -X POST \\
'http://your-api.com/upload-tmp-media/' \\
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \\
-F 'file=@/path/to/receipt1.jpg'
```
### مرحله 2 - ثبت رسید:
```bash
curl -X POST \\
'http://your-api.com/api/transactions/123/receipts/upload/' \\
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \\
-H 'Content-Type: application/json' \\
-d '{
"files": ["/static/tmp/xyz123-receipt1.jpg"],
"description": "Payment receipt for Python course"
}'
```
---
## 📄 نمونه پاسخ خطا (403 - عدم دسترسی):
```json
{
"message": "You don't have permission to upload receipts for this transaction"
}
```
---
## 📄 نمونه پاسخ خطا (400 - وضعیت نامعتبر):
```json
{
"message": "Cannot upload receipts for transaction with status 'success'"
}
```
"""
def doc_list_transaction_receipts():
return """
# 🐈 Scenario
🛠 لیست رسیدهای پرداخت یک تراکنش
این API برای دریافت لیست تمام رسیدهای آپلود شده برای یک تراکنش خاص استفاده میشود.
---
## 🚀 درخواست API
### URL:
```
GET /api/transactions/<transaction_id>/receipts/
```
### پارامترهای URL:
| کلید | نوع داده | توضیحات |
|------------------|-----------|----------------------------------------------------------|
| `transaction_id` | Integer | شناسه تراکنش که میخواهید رسیدهای آن را مشاهده کنید |
---
## 💡 نکات مهم:
1. **احراز هویت**:
- باید توکن احراز هویت را در هدر درخواست ارسال کنید
- فقط میتوانید رسیدهای تراکنشهای خودتان را مشاهده کنید
2. **مرتبسازی**:
- رسیدها بر اساس تاریخ آپلود (جدیدترین اول) مرتب میشوند
---
## 📊 پاسخ‌ها
| کد وضعیت | توضیحات |
|---------------|-----------------------------------------------------------|
| `200` | موفقیتآمیز - لیست رسیدها بازگردانده شد |
| `403` | عدم دسترسی - شما صاحب این تراکنش نیستید |
| `404` | تراکنش یافت نشد |
---
## 📄 نمونه پاسخ موفقیت‌آمیز
```json
[
{
"id": 1,
"file": "http://example.com/media/receipts/1/receipt1.jpg",
"description": "Payment receipt for course enrollment",
"uploaded_at": "2025-12-03T10:30:00Z"
},
{
"id": 2,
"file": "http://example.com/media/receipts/1/receipt2.jpg",
"description": "Second payment receipt",
"uploaded_at": "2025-12-03T10:25:00Z"
}
]
```
---
## 📄 توضیحات مقادیر پاسخ
| کلید | نوع داده | توضیحات |
|---------------|------------|----------------------------------------------------------|
| `id` | Integer | شناسه یکتای رسید |
| `file` | String | URL کامل فایل رسید آپلود شده |
| `description` | String | توضیحات اختیاری درباره رسید (ممکن است خالی باشد) |
| `uploaded_at` | DateTime | تاریخ و زمان آپلود رسید |
---
## 📄 نمونه درخواست (cURL):
```bash
curl -X GET \\
'http://your-api.com/api/transactions/123/receipts/' \\
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN'
```
---
## 📄 نمونه پاسخ خطا (403 - عدم دسترسی):
```json
{
"message": "You don't have permission to view receipts for this transaction"
}
```
---
## 📄 نمونه پاسخ خطا (404 - تراکنش یافت نشد):
```json
{
"message": "Transaction not found"
}
```
"""
def doc_transaction_list():
return """
# 🐈 Scenario
🛠 لیست تراکنشهای کاربر
این API برای دریافت لیست تمام تراکنشهای کاربر احراز هویت شده استفاده میشود.
---
## 🚀 درخواست API
### URL:
```
GET /api/transactions/list/
```
---
## 💡 نکات مهم:
1. **احراز هویت**:
- باید توکن احراز هویت را در هدر درخواست ارسال کنید
- فقط تراکنشهای خودتان را مشاهده میکنید
2. **فیلترینگ خودکار**:
- تراکنشهای حذف شده (soft deleted) نمایش داده نمیشوند
3. **وضعیتهای تراکنش**:
- `pending`: در انتظار پرداخت
- `waiting_approval`: در انتظار تایید (رسید آپلود شده)
- `success`: پرداخت موفق و تایید شده
- `failed`: پرداخت ناموفق
---
## 📊 پاسخ‌ها
| کد وضعیت | توضیحات |
|---------------|-----------------------------------------------------------|
| `200` | موفقیتآمیز - لیست تراکنشها بازگردانده شد |
| `401` | عدم احراز هویت |
---
## 📄 توضیحات مقادیر پاسخ
| کلید | نوع داده | توضیحات |
|---------------|------------|----------------------------------------------------------|
| `id` | Integer | شناسه یکتای تراکنش |
| `course` | Object | اطلاعات دوره مرتبط با تراکنش |
| `status` | String | وضعیت تراکنش (pending, waiting_approval, success, failed) |
| `price` | Decimal | مبلغ تراکنش |
| `created_at` | DateTime | تاریخ و زمان ایجاد تراکنش |
| `updated_at` | DateTime | تاریخ و زمان آخرین بهروزرسانی تراکنش |
---
## 📄 نمونه پاسخ موفقیت‌آمیز
```json
[
{
"id": 1,
"course": {
"id": 5,
"title": "Python Programming Basics",
"slug": "python-programming-basics",
"thumbnail": "http://example.com/media/courses/thumbnails/python.jpg",
"price": "99.00",
"final_price": "79.00"
},
"status": "waiting_approval",
"price": "79.00",
"created_at": "2025-12-01T10:00:00Z",
"updated_at": "2025-12-03T10:30:00Z"
},
{
"id": 2,
"course": {
"id": 8,
"title": "Django Web Development",
"slug": "django-web-development",
"thumbnail": "http://example.com/media/courses/thumbnails/django.jpg",
"price": "149.00",
"final_price": "149.00"
},
"status": "success",
"price": "149.00",
"created_at": "2025-11-28T14:20:00Z",
"updated_at": "2025-11-29T09:15:00Z"
}
]
```
---
## 📄 نمونه درخواست (cURL):
```bash
curl -X GET \\
'http://your-api.com/api/transactions/list/' \\
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN'
```
"""
def doc_create_transaction():
return """
# 🐈 Scenario
🛠 ثبتنام در دوره و ایجاد تراکنش
این API برای ثبتنام کاربر در یک دوره و ایجاد تراکنش استفاده میشود.
---
## 🚀 درخواست API
### URL:
```
POST /api/transactions/<slug>/join/
```
### پارامترهای URL:
| کلید | نوع داده | توضیحات |
|---------|-----------|----------------------------------------------------------|
| `slug` | String | اسلاگ دورهای که میخواهید در آن ثبتنام کنید |
### پارامترهای درخواست (JSON Body):
| کلید | نوع داده | الزامی | توضیحات |
|---------------------|-----------|--------|----------------------------------------------------------|
| `participant_infos` | Array | بله | لیست اطلاعات شرکتکنندگان |
### ساختار `participant_infos`:
| کلید | نوع داده | الزامی | توضیحات |
|---------------|-----------|--------|----------------------------------------------------------|
| `fullname` | String | بله | نام کامل شرکتکننده |
| `email` | String | بله | ایمیل شرکتکننده (برای دوره رایگان باید با ایمیل کاربر احراز هویت شده یکسان باشد) |
| `phone_number`| String | خیر | شماره تلفن شرکتکننده |
| `gender` | String | خیر | جنسیت شرکتکننده (male, female) |
| `birthdate` | Date | خیر | تاریخ تولد شرکتکننده (فرمت: YYYY-MM-DD) |
---
## 💡 نکات مهم:
1. **دوره رایگان**:
- اگر دوره رایگان باشد و فقط یک شرکتکننده در لیست باشد و ایمیل او با کاربر احراز هویت شده یکسان باشد، تراکنش به صورت خودکار تایید میشود (status = 'success')
- کاربر به صورت خودکار به عنوان دانشجو در دوره ثبت میشود
2. **دوره پولی**:
- تراکنش با وضعیت 'pending' ایجاد میشود
- کاربر باید رسید پرداخت خود را آپلود کند
- پس از آپلود رسید، وضعیت به 'waiting_approval' تغییر میکند
- پس از تایید توسط ادمین، وضعیت به 'success' تغییر میکند
3. **احراز هویت**:
- باید توکن احراز هویت را در هدر درخواست ارسال کنید
---
## 📊 پاسخ‌ها
| کد وضعیت | توضیحات |
|---------------|-----------------------------------------------------------|
| `201` | موفقیتآمیز - تراکنش ایجاد شد |
| `400` | دادههای نامعتبر |
| `404` | دوره یافت نشد |
---
## 📄 نمونه درخواست (JSON Body):
```json
{
"participant_infos": [
{
"fullname": "علی رضایی",
"email": "ali@example.com",
"phone_number": "+989123456789",
"gender": "male",
"birthdate": "1995-05-15"
}
]
}
```
---
## 📄 نمونه پاسخ موفقیت‌آمیز (دوره رایگان):
```json
{
"message": "Transaction Participant created successfully.",
"transaction_id": 123,
"participant_infos": [
{
"fullname": "علی رضایی",
"email": "ali@example.com",
"phone_number": "+989123456789",
"gender": "male",
"birthdate": "1995-05-15"
}
]
}
```
---
## 📄 نمونه پاسخ موفقیت‌آمیز (دوره پولی):
```json
{
"message": "Transaction Participant created successfully.",
"transaction_id": 124,
"participant_infos": [
{
"fullname": "سارا احمدی",
"email": "sara@example.com",
"phone_number": "+989123456789",
"gender": "female",
"birthdate": "1998-03-20"
}
]
}
```
توجه: برای دوره پولی، شما باید با استفاده از `transaction_id` بازگشتی، رسید پرداخت خود را از طریق API آپلود رسید آپلود کنید.
---
## 📄 نمونه درخواست (cURL):
```bash
curl -X POST \\
'http://your-api.com/api/transactions/python-programming-basics/join/' \\
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \\
-H 'Content-Type: application/json' \\
-d '{
"participant_infos": [
{
"fullname": "علی رضایی",
"email": "ali@example.com",
"phone_number": "+989123456789",
"gender": "male",
"birthdate": "1995-05-15"
}
]
}'
```
"""

35
apps/transaction/migrations/0003_alter_transactionparticipant_status_and_more.py

@ -0,0 +1,35 @@
# Generated by Django 5.1.8 on 2025-12-03 23:32
import apps.transaction.models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('transaction', '0002_remove_transactionparticipant_is_paid_and_more'),
]
operations = [
migrations.AlterField(
model_name='transactionparticipant',
name='status',
field=models.CharField(choices=[('pending', 'Pending'), ('waiting_approval', 'Waiting for Approval'), ('success', 'Success'), ('failed', 'Failed')], default='pending', max_length=20, verbose_name='Transaction Status'),
),
migrations.CreateModel(
name='TransactionReceipt',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('file', models.FileField(help_text='Upload payment receipt image or document', upload_to=apps.transaction.models.receipt_file_upload_to, verbose_name='Receipt File')),
('uploaded_at', models.DateTimeField(auto_now_add=True, verbose_name='Uploaded At')),
('description', models.TextField(blank=True, help_text='Optional description or notes about the receipt', null=True, verbose_name='Description')),
('transaction', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='receipts', to='transaction.transactionparticipant', verbose_name='Transaction')),
],
options={
'verbose_name': 'Transaction Receipt',
'verbose_name_plural': 'Transaction Receipts',
'ordering': ['-uploaded_at'],
},
),
]

44
apps/transaction/models.py

@ -1,4 +1,5 @@
from django.db import models
import os
from django.utils.translation import gettext_lazy as _
@ -8,6 +9,10 @@ from phonenumber_field.modelfields import PhoneNumberField
from utils.validators import validate_possible_number
def receipt_file_upload_to(instance, filename):
return os.path.join(f"receipts/{instance.transaction.id}/{filename}")
@ -16,6 +21,7 @@ class TransactionParticipant(models.Model):
class TransactionStatus(models.TextChoices):
PENDING = 'pending', _('Pending')
WAITING_APPROVAL = 'waiting_approval', _('Waiting for Approval')
SUCCESS = 'success', _('Success')
FAILED = 'failed', _('Failed')
@ -56,9 +62,9 @@ class ParticipantInfo(models.Model):
FEMALE = 'female', 'Female'
transaction_participant = models.ForeignKey(
TransactionParticipant,
on_delete=models.CASCADE,
related_name='participant_infos',
TransactionParticipant,
on_delete=models.CASCADE,
related_name='participant_infos',
verbose_name="Transaction Participant"
)
fullname = models.CharField(max_length=255, verbose_name="Full Name", help_text="Enter the full name of the user.")
@ -73,6 +79,38 @@ class ParticipantInfo(models.Model):
return f"{self.fullname} (Transaction: {self.transaction_participant.id}) - {self.email}"
class TransactionReceipt(models.Model):
"""
Model for storing payment receipts uploaded by users for transactions
"""
transaction = models.ForeignKey(
TransactionParticipant,
on_delete=models.CASCADE,
related_name='receipts',
verbose_name=_('Transaction')
)
file = models.FileField(
upload_to=receipt_file_upload_to,
verbose_name=_('Receipt File'),
help_text=_('Upload payment receipt image or document')
)
uploaded_at = models.DateTimeField(auto_now_add=True, verbose_name=_('Uploaded At'))
description = models.TextField(
blank=True,
null=True,
verbose_name=_('Description'),
help_text=_('Optional description or notes about the receipt')
)
class Meta:
verbose_name = _('Transaction Receipt')
verbose_name_plural = _('Transaction Receipts')
ordering = ['-uploaded_at']
def __str__(self):
return f"Receipt for Transaction #{self.transaction.id} - {self.uploaded_at.strftime('%Y-%m-%d %H:%M')}"

57
apps/transaction/serializers.py

@ -1,8 +1,9 @@
from rest_framework import serializers
from apps.transaction.models import TransactionParticipant, ParticipantInfo
from apps.transaction.models import TransactionParticipant, ParticipantInfo, TransactionReceipt
from apps.course.serializers import CourseDetailSerializer
from utils import FileFieldSerializer
@ -39,11 +40,57 @@ class TransactionParticipantSerializer(serializers.ModelSerializer):
class TransactionListSerializer(serializers.ModelSerializer):
course = serializers.SerializerMethodField()
receipts = serializers.SerializerMethodField()
class Meta:
model = TransactionParticipant
fields = ['id', 'course', 'status', 'price', 'created_at', 'updated_at']
fields = ['id', 'course', 'status', 'price', 'receipts', 'created_at', 'updated_at']
def get_course(self, obj):
return CourseDetailSerializer(obj.course, context=self.context).data
def get_receipts(self, obj):
receipts = obj.receipts.all()
return TransactionReceiptSerializer(receipts, many=True, context=self.context).data
class TransactionReceiptSerializer(serializers.ModelSerializer):
"""
Serializer for uploading payment receipts
Uses FileFieldSerializer to handle pre-uploaded files from /upload-tmp-media/
"""
file = FileFieldSerializer()
class Meta:
model = TransactionReceipt
fields = ['id', 'file', 'description', 'uploaded_at']
read_only_fields = ['id', 'uploaded_at']
class UploadReceiptsSerializer(serializers.Serializer):
"""
Serializer for uploading multiple receipt files for a transaction.
Files should be pre-uploaded using /upload-tmp-media/ endpoint,
then their URLs should be sent here.
"""
files = serializers.ListField(
child=FileFieldSerializer(),
allow_empty=False,
max_length=10,
help_text="List of file URLs (max 10 files) - files should be pre-uploaded via /upload-tmp-media/"
)
description = serializers.CharField(
required=False,
allow_blank=True,
max_length=1000,
help_text="Optional description for the receipts"
)
def validate_files(self, files):
"""
Validate uploaded file URLs
"""
if len(files) > 10:
raise serializers.ValidationError("You can upload a maximum of 10 files.")
return files

3
apps/transaction/urls.py

@ -9,7 +9,8 @@ urlpatterns = [
path('<slug:slug>/join/', views.TransactionParticipantCreateView.as_view(), name='transaction-participant-create'),
path('list/', views.TransactiontListView.as_view(), name='transaction-list'),
path('<int:pk>/delete/', views.SoftDeleteTransactionParticipantView.as_view(), name='soft-delete-transaction-participant'),
path('<int:transaction_id>/receipts/upload/', views.UploadTransactionReceiptsView.as_view(), name='upload-transaction-receipts'),
path('<int:transaction_id>/receipts/', views.TransactionReceiptsListView.as_view(), name='transaction-receipts-list'),
]

165
apps/transaction/views.py

@ -3,12 +3,23 @@ from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from apps.course.models import Participant, Course
from apps.transaction.models import TransactionParticipant
from apps.transaction.serializers import TransactionParticipantSerializer, TransactionListSerializer
from apps.transaction.models import TransactionParticipant, TransactionReceipt
from apps.transaction.serializers import (
TransactionParticipantSerializer,
TransactionListSerializer,
UploadReceiptsSerializer,
TransactionReceiptSerializer
)
from utils.exceptions import AppAPIException
from apps.account.models import User
from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi
from apps.transaction.doc import (
doc_upload_transaction_receipts,
doc_list_transaction_receipts,
doc_transaction_list,
doc_create_transaction
)
@ -17,7 +28,9 @@ class TransactionParticipantCreateView(generics.CreateAPIView):
serializer_class = TransactionParticipantSerializer
permission_classes = [IsAuthenticated]
@swagger_auto_schema(
operation_description=doc_create_transaction()
)
def create(self, request, *args, **kwargs):
user = request.user
course_slug = self.kwargs.get('slug') # Get the slug from the URL
@ -65,11 +78,13 @@ class TransactionParticipantCreateView(generics.CreateAPIView):
class TransactiontListView(generics.ListAPIView):
queryset = TransactionParticipant.objects.all() # یا هر فیلتر که بخواهید اضافه کنید
queryset = TransactionParticipant.objects.all()
serializer_class = TransactionListSerializer
permission_classes = [IsAuthenticated] # برای دسترسی کاربران احراز هویت شده
permission_classes = [IsAuthenticated]
@swagger_auto_schema(
operation_description=doc_transaction_list()
)
def get_queryset(self):
queryset = super().get_queryset()
queryset = queryset.filter(user=self.request.user, is_deleted=False)
@ -79,16 +94,16 @@ class TransactiontListView(generics.ListAPIView):
class SoftDeleteTransactionParticipantView(APIView):
permission_classes = [IsAuthenticated]
@swagger_auto_schema(
operation_summary="Soft delete a transaction participant",
operation_description="Marks a transaction participant as deleted without removing it from the database",
manual_parameters=[
openapi.Parameter(
'id',
openapi.IN_PATH,
description="Transaction Participant ID",
type=openapi.TYPE_INTEGER,
'id',
openapi.IN_PATH,
description="Transaction Participant ID",
type=openapi.TYPE_INTEGER,
required=True
)
],
@ -121,6 +136,132 @@ class SoftDeleteTransactionParticipantView(APIView):
detail={'message': "You don't have permission to delete this transaction"},
status_code=status.HTTP_403_FORBIDDEN
)
except TransactionParticipant.DoesNotExist:
raise AppAPIException({'message': "Transaction participant not found"})
class UploadTransactionReceiptsView(APIView):
permission_classes = [IsAuthenticated]
@swagger_auto_schema(
operation_summary="Upload payment receipts for a transaction",
operation_description=doc_upload_transaction_receipts(),
request_body=UploadReceiptsSerializer,
responses={
201: openapi.Response(
description="Receipts uploaded successfully",
examples={
"application/json": {
"success": True,
"message": "Receipts uploaded successfully",
"transaction_status": "waiting_approval",
"receipts": [
{
"id": 1,
"file": "http://example.com/media/receipts/1/receipt.jpg",
"description": "Payment receipt",
"uploaded_at": "2025-12-03T10:30:00Z"
}
]
}
}
),
400: "Invalid data or transaction cannot accept receipts",
403: "Permission denied",
404: "Transaction not found"
}
)
def post(self, request, transaction_id):
try:
transaction = TransactionParticipant.objects.get(pk=transaction_id, is_deleted=False)
except TransactionParticipant.DoesNotExist:
raise AppAPIException({'message': "Transaction not found"})
# Check if user owns this transaction
if transaction.user != request.user:
raise AppAPIException(
detail={'message': "You don't have permission to upload receipts for this transaction"},
status_code=status.HTTP_403_FORBIDDEN
)
# Check if transaction is in a state that can accept receipts
if transaction.status not in [
TransactionParticipant.TransactionStatus.PENDING,
TransactionParticipant.TransactionStatus.WAITING_APPROVAL
]:
raise AppAPIException(
detail={'message': f"Cannot upload receipts for transaction with status '{transaction.status}'"},
status_code=status.HTTP_400_BAD_REQUEST
)
# Validate using serializer
serializer = UploadReceiptsSerializer(data=request.data, context={'request': request})
serializer.is_valid(raise_exception=True)
# Create receipt records
receipts = []
description = serializer.validated_data.get('description', '')
file_urls = serializer.validated_data.get('files', [])
for file_url in file_urls:
receipt = TransactionReceipt.objects.create(
transaction=transaction,
file=file_url,
description=description
)
receipts.append(receipt)
# Update transaction status to waiting_approval
transaction.status = TransactionParticipant.TransactionStatus.WAITING_APPROVAL
transaction.save()
# Serialize receipts for response
receipts_data = TransactionReceiptSerializer(receipts, many=True, context={'request': request}).data
return Response({
'success': True,
'message': 'Receipts uploaded successfully',
'transaction_status': transaction.status,
'receipts': receipts_data
}, status=status.HTTP_201_CREATED)
class TransactionReceiptsListView(generics.ListAPIView):
serializer_class = TransactionReceiptSerializer
permission_classes = [IsAuthenticated]
@swagger_auto_schema(
operation_summary="List receipts for a transaction",
operation_description=doc_list_transaction_receipts(),
manual_parameters=[
openapi.Parameter(
'transaction_id',
openapi.IN_PATH,
description="Transaction ID",
type=openapi.TYPE_INTEGER,
required=True
)
],
responses={
200: TransactionReceiptSerializer(many=True),
403: "Permission denied",
404: "Transaction not found"
}
)
def get_queryset(self):
transaction_id = self.kwargs.get('transaction_id')
try:
transaction = TransactionParticipant.objects.get(pk=transaction_id, is_deleted=False)
except TransactionParticipant.DoesNotExist:
raise AppAPIException({'message': "Transaction not found"})
# Check if user owns this transaction
if transaction.user != self.request.user:
raise AppAPIException(
detail={'message': "You don't have permission to view receipts for this transaction"},
status_code=status.HTTP_403_FORBIDDEN
)
return TransactionReceipt.objects.filter(transaction=transaction)

79
templates/docs.html

@ -64,6 +64,85 @@
</div>
</div>
</div>
<div class="col-12">
<div class="card">
<div class="card-header bg-light header-elements-inline">
مستندات API تراکنش‌ها و رسیدهای پرداخت
</div>
<div class="card-body">
<h4>🔹 ثبت‌نام در دوره و ایجاد تراکنش</h4>
<p><strong>Endpoint:</strong> <code>POST /api/transactions/&lt;slug&gt;/join/</code></p>
<p>این API برای ثبت‌نام کاربر در دوره و ایجاد تراکنش استفاده می‌شود.</p>
<ul>
<li>برای دوره‌های رایگان، تراکنش به صورت خودکار تایید می‌شود</li>
<li>برای دوره‌های پولی، تراکنش با وضعیت <code>pending</code> ایجاد می‌شود</li>
</ul>
<hr>
<h4>🔹 آپلود رسید پرداخت</h4>
<p><strong>Endpoint:</strong> <code>POST /api/transactions/&lt;transaction_id&gt;/receipts/upload/</code></p>
<p>برای آپلود رسید پرداخت دوره‌های پولی استفاده می‌شود.</p>
<ul>
<li>حداکثر 10 فایل قابل آپلود در هر درخواست</li>
<li>حداکثر حجم هر فایل: 10 مگابایت</li>
<li>پس از آپلود موفق، وضعیت تراکنش به <code>waiting_approval</code> تغییر می‌کند</li>
</ul>
<hr>
<h4>🔹 مشاهده رسیدهای یک تراکنش</h4>
<p><strong>Endpoint:</strong> <code>GET /api/transactions/&lt;transaction_id&gt;/receipts/</code></p>
<p>برای دریافت لیست تمام رسیدهای آپلود شده برای یک تراکنش.</p>
<hr>
<h4>🔹 لیست تراکنش‌های کاربر</h4>
<p><strong>Endpoint:</strong> <code>GET /api/transactions/list/</code></p>
<p>برای دریافت لیست تمام تراکنش‌های کاربر احراز هویت شده.</p>
<hr>
<h4>🔹 وضعیت‌های تراکنش</h4>
<table class="table table-bordered">
<thead>
<tr>
<th>وضعیت</th>
<th>توضیحات</th>
</tr>
</thead>
<tbody>
<tr>
<td><span class="badge badge-warning">pending</span></td>
<td>در انتظار پرداخت - کاربر باید رسید را آپلود کند</td>
</tr>
<tr>
<td><span class="badge badge-info">waiting_approval</span></td>
<td>در انتظار تایید - رسید آپلود شده و منتظر تایید ادمین</td>
</tr>
<tr>
<td><span class="badge badge-success">success</span></td>
<td>پرداخت موفق و تایید شده - کاربر به دوره دسترسی دارد</td>
</tr>
<tr>
<td><span class="badge badge-danger">failed</span></td>
<td>پرداخت ناموفق یا رد شده</td>
</tr>
</tbody>
</table>
<hr>
<h4>📌 نکات مهم برای ادمین</h4>
<ul>
<li>زمانی که کاربر رسید آپلود می‌کند، وضعیت تراکنش به <code>waiting_approval</code> تغییر می‌کند</li>
<li>ادمین باید رسیدها را در پنل ادمین بررسی کرده و وضعیت را به <code>success</code> یا <code>failed</code> تغییر دهد</li>
<li>زمانی که وضعیت به <code>success</code> تغییر کند، کاربر به صورت خودکار به عنوان دانشجو در دوره ثبت می‌شود</li>
<li>تمام رسیدهای آپلود شده در پنل ادمین قابل مشاهده هستند</li>
</ul>
<p class="mt-3">
<a href="/swagger/" target="_blank" class="btn btn-primary">مشاهده مستندات کامل Swagger</a>
</p>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
Loading…
Cancel
Save