commit
0590e7aa42
33 changed files with 781 additions and 235 deletions
@ -0,0 +1 @@ |
||||
venv*/ |
||||
@ -0,0 +1,25 @@ |
||||
from rest_framework import serializers |
||||
|
||||
from apps.payment.models import AuthorBalance |
||||
|
||||
|
||||
class AuthorBalanceSerializer(serializers.ModelSerializer): |
||||
|
||||
class Meta: |
||||
model = AuthorBalance |
||||
fields = ( |
||||
'id', |
||||
'author', |
||||
'type', |
||||
'amount', |
||||
'commission', |
||||
'status', |
||||
'payment', |
||||
) |
||||
|
||||
read_only_fields = ( |
||||
'id', |
||||
'author', |
||||
'type', |
||||
'payment', |
||||
) |
||||
@ -1,3 +1,61 @@ |
||||
from django.contrib import admin |
||||
from polymorphic.admin import ( |
||||
PolymorphicParentModelAdmin, |
||||
PolymorphicChildModelAdmin, |
||||
PolymorphicChildModelFilter, |
||||
) |
||||
|
||||
# Register your models here. |
||||
from .models import AuthorBalance, CoursePayment, SchoolPayment, Payment |
||||
|
||||
|
||||
@admin.register(AuthorBalance) |
||||
class AuthorBalanceAdmin(admin.ModelAdmin): |
||||
list_display = ( |
||||
'author', |
||||
'type', |
||||
'amount', |
||||
'commission', |
||||
'status', |
||||
'payment', |
||||
) |
||||
|
||||
|
||||
class PaymentChildAdmin(PolymorphicChildModelAdmin): |
||||
base_model = Payment |
||||
show_in_index = True |
||||
list_display = ( |
||||
'id', |
||||
'user', |
||||
'amount', |
||||
'status', |
||||
) |
||||
base_fieldsets = ( |
||||
(None, {'fields': ('user', 'amount', 'status', 'data',)}), |
||||
) |
||||
readonly_fields = ('amount', 'status', 'data',) |
||||
|
||||
|
||||
@admin.register(CoursePayment) |
||||
class CoursePaymentAdmin(PaymentChildAdmin): |
||||
base_model = CoursePayment |
||||
list_display = PaymentChildAdmin.list_display + ('course',) |
||||
|
||||
|
||||
@admin.register(SchoolPayment) |
||||
class SchoolPaymentAdmin(PaymentChildAdmin): |
||||
base_model = SchoolPayment |
||||
list_display = PaymentChildAdmin.list_display + ( |
||||
'weekdays', |
||||
'date_start', |
||||
'date_end', |
||||
) |
||||
|
||||
|
||||
@admin.register(Payment) |
||||
class PaymentAdmin(PolymorphicParentModelAdmin): |
||||
base_model = Payment |
||||
polymorphic_list = True |
||||
child_models = ( |
||||
CoursePayment, |
||||
SchoolPayment, |
||||
) |
||||
|
||||
@ -0,0 +1,16 @@ |
||||
# Generated by Django 2.0.2 on 2018-02-20 17:37 |
||||
|
||||
from django.db import migrations |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
('payment', '0001_initial'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.DeleteModel( |
||||
name='Purchase', |
||||
), |
||||
] |
||||
@ -0,0 +1,71 @@ |
||||
# Generated by Django 2.0.2 on 2018-02-20 19:12 |
||||
|
||||
from django.conf import settings |
||||
import django.contrib.postgres.fields |
||||
import django.contrib.postgres.fields.jsonb |
||||
from django.db import migrations, models |
||||
import django.db.models.deletion |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
initial = True |
||||
|
||||
dependencies = [ |
||||
('course', '0034_auto_20180215_1503'), |
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL), |
||||
('contenttypes', '0002_remove_content_type_name'), |
||||
('payment', '0002_delete_purchase'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.CreateModel( |
||||
name='Payment', |
||||
fields=[ |
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||
('amount', models.DecimalField(decimal_places=2, default=0, max_digits=8, verbose_name='Итого')), |
||||
('status', models.PositiveSmallIntegerField(choices=[(0, 'regular'), (1, 'goodwill'), (2, 'negative'), (200, 'risk under review'), (201, 'risk reviewed accepted'), (202, 'risk reviewed declined'), (203, 'risk authorization voided'), (12, 'subscription cancelation'), (13, 'subscription expired'), (14, 'subscription payment failed')], verbose_name='Статус платежа')), |
||||
('data', django.contrib.postgres.fields.jsonb.JSONField(default={}, verbose_name='Данные платежа от провайдера')), |
||||
], |
||||
options={ |
||||
'verbose_name': 'Платеж', |
||||
'verbose_name_plural': 'Платежи', |
||||
}, |
||||
), |
||||
migrations.CreateModel( |
||||
name='CoursePayment', |
||||
fields=[ |
||||
('payment_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='payment.Payment')), |
||||
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='course.Course', verbose_name='Курс')), |
||||
], |
||||
options={ |
||||
'verbose_name': 'Платеж за курс', |
||||
'verbose_name_plural': 'Платежи за курсы', |
||||
}, |
||||
bases=('payment.payment',), |
||||
), |
||||
migrations.CreateModel( |
||||
name='SchoolPayment', |
||||
fields=[ |
||||
('payment_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='payment.Payment')), |
||||
('weekdays', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=7, verbose_name='Дни недели')), |
||||
('date_start', models.DateField(verbose_name='Дата начала подписки')), |
||||
('date_end', models.DateField(verbose_name='Дата окончания подписки')), |
||||
], |
||||
options={ |
||||
'verbose_name': 'Платеж за курс', |
||||
'verbose_name_plural': 'Платежи за курсы', |
||||
}, |
||||
bases=('payment.payment',), |
||||
), |
||||
migrations.AddField( |
||||
model_name='payment', |
||||
name='polymorphic_ctype', |
||||
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_payment.payment_set+', to='contenttypes.ContentType'), |
||||
), |
||||
migrations.AddField( |
||||
model_name='payment', |
||||
name='user', |
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Пользователь'), |
||||
), |
||||
] |
||||
@ -0,0 +1,32 @@ |
||||
# Generated by Django 2.0.2 on 2018-02-21 10:22 |
||||
|
||||
from django.conf import settings |
||||
from django.db import migrations, models |
||||
import django.db.models.deletion |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL), |
||||
('payment', '0003_auto_20180220_1912'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.CreateModel( |
||||
name='AuthorBalance', |
||||
fields=[ |
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||
('type', models.PositiveSmallIntegerField(choices=[(0, 'In'), (1, 'Out')], default=0, verbose_name='Тип')), |
||||
('amount', models.DecimalField(decimal_places=2, default=0, max_digits=8, verbose_name='Итого')), |
||||
('commission', models.DecimalField(decimal_places=2, default=0, max_digits=8, verbose_name='Комиссия')), |
||||
('status', models.PositiveSmallIntegerField(choices=[(0, 'Pending'), (1, 'Accepted'), (2, 'Declined')], default=0, verbose_name='Статус')), |
||||
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Автор')), |
||||
('payment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='payment.Payment', verbose_name='Платёж')), |
||||
], |
||||
), |
||||
migrations.AlterModelOptions( |
||||
name='schoolpayment', |
||||
options={'verbose_name': 'Платеж за школу', 'verbose_name_plural': 'Платежи за школу'}, |
||||
), |
||||
] |
||||
@ -0,0 +1,25 @@ |
||||
# Generated by Django 2.0.2 on 2018-02-21 11:20 |
||||
|
||||
from django.conf import settings |
||||
from django.db import migrations, models |
||||
import django.db.models.deletion |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
('payment', '0004_auto_20180221_1022'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.AlterField( |
||||
model_name='authorbalance', |
||||
name='author', |
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Автор'), |
||||
), |
||||
migrations.AlterField( |
||||
model_name='authorbalance', |
||||
name='payment', |
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='payment.Payment', verbose_name='Платёж'), |
||||
), |
||||
] |
||||
@ -0,0 +1,29 @@ |
||||
# Generated by Django 2.0.2 on 2018-02-21 11:26 |
||||
|
||||
import django.contrib.postgres.fields.jsonb |
||||
from django.db import migrations, models |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
('payment', '0005_auto_20180221_1120'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.AlterField( |
||||
model_name='payment', |
||||
name='amount', |
||||
field=models.DecimalField(decimal_places=2, default=0, editable=False, max_digits=8, verbose_name='Итого'), |
||||
), |
||||
migrations.AlterField( |
||||
model_name='payment', |
||||
name='data', |
||||
field=django.contrib.postgres.fields.jsonb.JSONField(default={}, editable=False, verbose_name='Данные платежа от провайдера'), |
||||
), |
||||
migrations.AlterField( |
||||
model_name='payment', |
||||
name='status', |
||||
field=models.PositiveSmallIntegerField(choices=[(0, 'regular'), (1, 'goodwill'), (2, 'negative'), (200, 'risk under review'), (201, 'risk reviewed accepted'), (202, 'risk reviewed declined'), (203, 'risk authorization voided'), (12, 'subscription cancelation'), (13, 'subscription expired'), (14, 'subscription payment failed')], editable=False, null=True, verbose_name='Статус платежа'), |
||||
), |
||||
] |
||||
@ -0,0 +1,24 @@ |
||||
# Generated by Django 2.0.2 on 2018-02-21 12:58 |
||||
|
||||
from django.conf import settings |
||||
from django.db import migrations, models |
||||
import django.db.models.deletion |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
('payment', '0006_auto_20180221_1126'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.AlterModelOptions( |
||||
name='authorbalance', |
||||
options={'verbose_name': 'Баланс', 'verbose_name_plural': 'Балансы'}, |
||||
), |
||||
migrations.AlterField( |
||||
model_name='authorbalance', |
||||
name='author', |
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='balances', to=settings.AUTH_USER_MODEL, verbose_name='Автор'), |
||||
), |
||||
] |
||||
@ -0,0 +1,20 @@ |
||||
# Generated by Django 2.0.2 on 2018-02-21 13:35 |
||||
|
||||
from django.conf import settings |
||||
from django.db import migrations, models |
||||
import django.db.models.deletion |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
('payment', '0007_auto_20180221_1258'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.AlterField( |
||||
model_name='payment', |
||||
name='user', |
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to=settings.AUTH_USER_MODEL, verbose_name='Пользователь'), |
||||
), |
||||
] |
||||
@ -0,0 +1,19 @@ |
||||
# Generated by Django 2.0.2 on 2018-02-22 09:55 |
||||
|
||||
from django.db import migrations, models |
||||
import django.db.models.deletion |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
('payment', '0008_auto_20180221_1335'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.AlterField( |
||||
model_name='coursepayment', |
||||
name='course', |
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='course.Course', verbose_name='Курс'), |
||||
), |
||||
] |
||||
@ -0,0 +1,23 @@ |
||||
# Generated by Django 2.0.2 on 2018-02-27 09:33 |
||||
|
||||
from django.db import migrations, models |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
('payment', '0009_auto_20180222_0955'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.AlterField( |
||||
model_name='schoolpayment', |
||||
name='date_end', |
||||
field=models.DateField(blank=True, null=True, verbose_name='Дата окончания подписки'), |
||||
), |
||||
migrations.AlterField( |
||||
model_name='schoolpayment', |
||||
name='date_start', |
||||
field=models.DateField(blank=True, null=True, verbose_name='Дата начала подписки'), |
||||
), |
||||
] |
||||
@ -1,15 +1,112 @@ |
||||
from django.db import models |
||||
from django.contrib.auth import get_user_model |
||||
from django.contrib.postgres.fields import ArrayField, JSONField |
||||
|
||||
from constance import config |
||||
from paymentwall import Pingback |
||||
from polymorphic.models import PolymorphicModel |
||||
|
||||
class Purchase(models.Model): |
||||
COMPLETE = 'COMPLETE' |
||||
CHARGEBACK = 'CHARGEBACK' |
||||
REFUNDED = 'REFUNDED' |
||||
ERROR = 'ERROR' |
||||
PENDING = 'PENDING' |
||||
from apps.course.models import Course |
||||
from apps.school.models import SchoolSchedule |
||||
|
||||
transaction_id = models.PositiveIntegerField() |
||||
status = models.CharField(max_length=50) |
||||
product_id = models.PositiveIntegerField() |
||||
created_at = models.DateTimeField(auto_now_add=True) |
||||
updated_at = models.DateTimeField(auto_now=True) |
||||
User = get_user_model() |
||||
|
||||
|
||||
class AuthorBalance(models.Model): |
||||
IN = 0 |
||||
OUT = 1 |
||||
TYPE_CHOICES = ( |
||||
(IN, 'In'), |
||||
(OUT, 'Out'), |
||||
) |
||||
PENDING = 0 |
||||
ACCEPTED = 1 |
||||
DECLINED = 2 |
||||
STATUS_CHOICES = ( |
||||
(PENDING, 'Pending'), |
||||
(ACCEPTED, 'Accepted'), |
||||
(DECLINED, 'Declined'), |
||||
) |
||||
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='Автор', |
||||
null=True, blank=True, related_name='balances') |
||||
type = models.PositiveSmallIntegerField('Тип', choices=TYPE_CHOICES, default=0) |
||||
amount = models.DecimalField('Итого', max_digits=8, decimal_places=2, default=0) |
||||
commission = models.DecimalField('Комиссия', max_digits=8, decimal_places=2, default=0) |
||||
status = models.PositiveSmallIntegerField('Статус', choices=STATUS_CHOICES, default=0) |
||||
payment = models.OneToOneField('Payment', on_delete=models.CASCADE, null=True, blank=True, verbose_name='Платёж') |
||||
|
||||
class Meta: |
||||
verbose_name = 'Баланс' |
||||
verbose_name_plural = 'Балансы' |
||||
|
||||
|
||||
class Payment(PolymorphicModel): |
||||
PW_STATUS_CHOICES = ( |
||||
(Pingback.PINGBACK_TYPE_REGULAR, 'regular',), |
||||
(Pingback.PINGBACK_TYPE_GOODWILL, 'goodwill',), |
||||
(Pingback.PINGBACK_TYPE_NEGATIVE, 'negative',), |
||||
(Pingback.PINGBACK_TYPE_RISK_UNDER_REVIEW, 'risk under review',), |
||||
(Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED, 'risk reviewed accepted',), |
||||
(Pingback.PINGBACK_TYPE_RISK_REVIEWED_DECLINED, 'risk reviewed declined',), |
||||
(Pingback.PINGBACK_TYPE_RISK_AUTHORIZATION_VOIDED, 'risk authorization voided',), |
||||
(Pingback.PINGBACK_TYPE_SUBSCRIPTION_CANCELLATION, 'subscription cancelation',), |
||||
(Pingback.PINGBACK_TYPE_SUBSCRIPTION_EXPIRED, 'subscription expired',), |
||||
(Pingback.PINGBACK_TYPE_SUBSCRIPTION_PAYMENT_FAILED, 'subscription payment failed',), |
||||
) |
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='Пользователь', related_name='payments') |
||||
amount = models.DecimalField('Итого', max_digits=8, decimal_places=2, default=0, editable=False) |
||||
status = models.PositiveSmallIntegerField('Статус платежа', choices=PW_STATUS_CHOICES, null=True, editable=False) |
||||
data = JSONField('Данные платежа от провайдера', default={}, editable=False) |
||||
|
||||
class Meta: |
||||
verbose_name = 'Платеж' |
||||
verbose_name_plural = 'Платежи' |
||||
|
||||
def calc_commission(self): |
||||
return self.amount * config.SERVICE_COMMISSION / 100 |
||||
|
||||
|
||||
class CoursePayment(Payment): |
||||
course = models.ForeignKey(Course, on_delete=models.CASCADE, verbose_name='Курс', related_name='payments') |
||||
|
||||
class Meta: |
||||
verbose_name = 'Платеж за курс' |
||||
verbose_name_plural = 'Платежи за курсы' |
||||
|
||||
def save(self, *args, **kwargs): |
||||
self.amount = self.course.price |
||||
super().save(*args, **kwargs) |
||||
author_balance = getattr(self, 'authorbalance', None) |
||||
if not author_balance: |
||||
AuthorBalance.objects.create( |
||||
author=self.course.author, |
||||
amount=self.amount, |
||||
payment=self, |
||||
commission=self.calc_commission(), |
||||
) |
||||
else: |
||||
author_balance.amount = self.amount |
||||
author_balance.commission = self.calc_commission() |
||||
author_balance.save() |
||||
|
||||
|
||||
class SchoolPayment(Payment): |
||||
weekdays = ArrayField(models.IntegerField(), size=7, verbose_name='Дни недели') |
||||
date_start = models.DateField('Дата начала подписки', null=True, blank=True) |
||||
date_end = models.DateField('Дата окончания подписки', null=True, blank=True) |
||||
|
||||
class Meta: |
||||
verbose_name = 'Платеж за школу' |
||||
verbose_name_plural = 'Платежи за школу' |
||||
|
||||
def save(self, *args, **kwargs): |
||||
aggregate = SchoolSchedule.objects.filter( |
||||
weekday__in=self.weekdays, |
||||
).aggregate( |
||||
models.Sum('month_price'), |
||||
models.Sum('day_discount'), |
||||
) |
||||
month_price_sum = aggregate.get('month_price__sum', 0) |
||||
day_discount_sum = aggregate.get('day_discount__sum', 0) if len(self.weekdays) == 7 else 0 |
||||
self.amount = month_price_sum - day_discount_sum |
||||
super().save(*args, **kwargs) |
||||
|
||||
@ -0,0 +1,12 @@ |
||||
{% extends "templates/lilcity/index.html" %} {% load static %} {% block content %} |
||||
<div class="section"> |
||||
<div class="section__center center center_xs"> |
||||
<div class="done"> |
||||
<div class="done__title title">Произошла ошибка!</div> |
||||
<div class="done__foot"> |
||||
<a class="done__btn btn btn_md btn_stroke" href="/">ПЕРЕЙТИ К ГЛАВНОЙ</a> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
{% endblock content %} |
||||
@ -0,0 +1,12 @@ |
||||
{% extends "templates/lilcity/index.html" %} {% load static %} {% block content %} |
||||
<div class="section"> |
||||
<div class="section__center center center_xs"> |
||||
<div class="done"> |
||||
<div class="done__title title">Вы успешно приобрели курс!</div> |
||||
<div class="done__foot"> |
||||
<a class="done__btn btn btn_md btn_stroke" href="/">ПЕРЕЙТИ К ГЛАВНОЙ</a> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
{% endblock content %} |
||||
@ -0,0 +1,9 @@ |
||||
{% extends "templates/lilcity/index.html" %} {% load static %} {% block content %} |
||||
<div class="section"> |
||||
<div class="section__center center"> |
||||
{% autoescape off %} |
||||
{{ widget }} |
||||
{% endautoescape %} |
||||
</div> |
||||
</div> |
||||
{% endblock content %} |
||||
@ -0,0 +1,18 @@ |
||||
# Generated by Django 2.0.2 on 2018-02-21 11:20 |
||||
|
||||
from django.db import migrations, models |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
('school', '0003_auto_20180221_0901'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.AlterField( |
||||
model_name='schoolschedule', |
||||
name='weekday', |
||||
field=models.PositiveSmallIntegerField(choices=[(1, 'понедельник'), (2, 'вторник'), (3, 'среда'), (4, 'четверг'), (5, 'пятница'), (6, 'суббота'), (7, 'воскресенье')], unique=True, verbose_name='День недели'), |
||||
), |
||||
] |
||||
@ -0,0 +1,18 @@ |
||||
# Generated by Django 2.0.2 on 2018-02-22 10:00 |
||||
|
||||
from django.db import migrations, models |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
('school', '0004_auto_20180221_1120'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.AddField( |
||||
model_name='schoolschedule', |
||||
name='day_discount', |
||||
field=models.DecimalField(decimal_places=2, default=0, max_digits=8, verbose_name='Скидка, в валюте'), |
||||
), |
||||
] |
||||
Loading…
Reference in new issue