Skip to content

Commit

Permalink
Merge pull request #75 from danesjenovdan/main
Browse files Browse the repository at this point in the history
Pulling refs/heads/main into k8s
  • Loading branch information
tomazkunst authored Dec 19, 2024
2 parents 73f637e + bd321f1 commit ecb85f3
Show file tree
Hide file tree
Showing 34 changed files with 965 additions and 542 deletions.
1 change: 1 addition & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
212e4daa70ba507bee4bc765d9052288307ecad2
32 changes: 32 additions & 0 deletions .github/workflows/check_formatting.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Check formatting
on:
push:
branches:
- "main"
pull_request:
branches:
- "k8s"
jobs:
check_python_formatting:
name: Check Python formatting
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install dependencies
run: |
pip install black isort
- name: Check formatting with black
run: |
black . --check --diff
- name: Check formatting with isort
run: |
isort . --check --diff --profile black
19 changes: 19 additions & 0 deletions .github/workflows/pr.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Automatic PRs
on:
push:
branches:
- main
jobs:
pull-requests:
name: Automatic PRs
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v3

- name: pull-request-k8s
uses: repo-sync/pull-request@v2
with:
destination_branch: "k8s"
github_token: ${{ secrets.GITHUB_TOKEN }}
pr_title: "Pulling ${{ github.ref }} into k8s"
154 changes: 93 additions & 61 deletions novdan_api/api/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,151 +9,175 @@
from django.utils.translation import gettext_lazy as _
from rangefilter.filters import DateTimeRangeFilter

from .models import (Subscription, SubscriptionTimeRange, Transaction, User,
Wallet)
from .models import Subscription, SubscriptionTimeRange, Transaction, User, Wallet
from .serializers import UserSerializer
from .utils import (USER_PAYMENT_AMOUNT, VALID_PAYOUT_USERNAMES,
calculate_receivers_percentage, get_start_of_month,
get_start_of_next_month)
from .utils import (
USER_PAYMENT_AMOUNT,
VALID_PAYOUT_USERNAMES,
calculate_receivers_percentage,
get_start_of_month,
get_start_of_next_month,
)


class CustomUserAdmin(UserAdmin):
fieldsets = UserAdmin.fieldsets + (
(None, {'fields': ('url', 'customer_id')}),
)
add_fieldsets = UserAdmin.add_fieldsets + (
(None, {'fields': ('url',)}),
)
search_fields = UserAdmin.search_fields + ('customer_id',)
fieldsets = UserAdmin.fieldsets + ((None, {"fields": ("url", "customer_id")}),)
add_fieldsets = UserAdmin.add_fieldsets + ((None, {"fields": ("url",)}),)
search_fields = UserAdmin.search_fields + ("customer_id",)


admin.site.register(User, CustomUserAdmin)


@admin.register(Wallet)
class WalletAdmin(admin.ModelAdmin):
list_display = ('id', 'user', 'amount')
list_filter = ('user__is_staff',)
search_fields = ('id', 'user__username')
list_display = ("id", "user", "amount")
list_filter = ("user__is_staff",)
search_fields = ("id", "user__username")


class IsSubscriptionPayedFilter(admin.SimpleListFilter):
title = 'is payed'
parameter_name = 'is_payed'
title = "is payed"
parameter_name = "is_payed"

def lookups(self, request, model_admin):
return (
('1', _('Yes')),
('0', _('No')),
("1", _("Yes")),
("0", _("No")),
)

def queryset(self, request, queryset):
if self.value() == '1':
if self.value() == "1":
return queryset.payed()
if self.value() == '0':
if self.value() == "0":
return queryset.exclude(id__in=queryset.payed())
return queryset


class IsSubscriptionCanceledFilter(admin.SimpleListFilter):
title = 'is canceled'
parameter_name = 'is_canceled'
title = "is canceled"
parameter_name = "is_canceled"

def lookups(self, request, model_admin):
return (
('1', _('Yes')),
('0', _('No')),
("1", _("Yes")),
("0", _("No")),
)

def queryset(self, request, queryset):
if self.value() == '1':
if self.value() == "1":
return queryset.canceled()
if self.value() == '0':
if self.value() == "0":
return queryset.exclude(id__in=queryset.canceled())
return queryset


class SubscriptionTimeRangeStackedInline(admin.StackedInline):
model = SubscriptionTimeRange
fields = ('created_at', 'starts_at', 'ends_at', 'canceled_at', 'payed_at', 'payment_token')
readonly_fields = ('created_at',)
fields = (
"created_at",
"starts_at",
"ends_at",
"canceled_at",
"payed_at",
"payment_token",
)
readonly_fields = ("created_at",)
extra = 0
ordering = ('-ends_at',)
ordering = ("-ends_at",)


@admin.register(Subscription)
class SubscriptionAdmin(admin.ModelAdmin):
list_display = ('id', 'user', 'is_payed', 'is_canceled')
list_filter = ('user__is_staff', IsSubscriptionPayedFilter, IsSubscriptionCanceledFilter)
search_fields = ('id', 'user__username', 'user__customer_id', 'time_range__payment_token')
list_display = ("id", "user", "is_payed", "is_canceled")
list_filter = (
"user__is_staff",
IsSubscriptionPayedFilter,
IsSubscriptionCanceledFilter,
)
search_fields = (
"id",
"user__username",
"user__customer_id",
"time_range__payment_token",
)
inlines = [SubscriptionTimeRangeStackedInline]

def is_payed(self, obj):
return obj.time_ranges.current().payed().exists()

def is_canceled(self, obj):
last_time_range = obj.time_ranges.order_by('-ends_at').first()
last_time_range = obj.time_ranges.order_by("-ends_at").first()
if last_time_range:
return last_time_range.canceled_at is not None
return False


@admin.register(Transaction)
class TransactionAdmin(admin.ModelAdmin):
list_display = ('id', 'from_wallet', 'to_wallet', 'amount')
list_filter = (('created_at', DateTimeRangeFilter),)
search_fields = ('from_wallet__id', 'to_wallet__id', 'amount')
readonly_fields = ('created_at',)
fields = ('created_at', 'from_wallet', 'to_wallet', 'amount')
list_display = ("id", "from_wallet", "to_wallet", "amount")
list_filter = (("created_at", DateTimeRangeFilter),)
search_fields = ("from_wallet__id", "to_wallet__id", "amount")
readonly_fields = ("created_at",)
fields = ("created_at", "from_wallet", "to_wallet", "amount")

def get_urls(self):
return [
path('calculate_monthly_split/', self.calculate_monthly_split),
path("calculate_monthly_split/", self.calculate_monthly_split),
] + super().get_urls()

def calculate_monthly_split(self, request):
date_string = request.GET.get('date')
date_string = request.GET.get("date")
time = timezone.now()
if date_string:
time = timezone.make_aware(datetime.strptime(date_string, '%Y-%m-%d'))
time = timezone.make_aware(datetime.strptime(date_string, "%Y-%m-%d"))

# calculate simple global tokens received percentages --------------------------------------
sum_, percentages = calculate_receivers_percentage(None, time)

context_split = {
'sum': sum_,
'percentages': sorted(percentages, key=lambda x: x['amount'], reverse=True),
"sum": sum_,
"percentages": sorted(percentages, key=lambda x: x["amount"], reverse=True),
}
# ------------------------------------------------------------------------------------------

# calculate payout split percentages for whole `time.month`
# however only for valid payments: from start of `time.month + 1` until 2nd day of `time.month + 1` at 23:59
payment_time_end = get_start_of_next_month(time) + timezone.timedelta(days=2) - timezone.timedelta(seconds=1)
payment_time_end = (
get_start_of_next_month(time)
+ timezone.timedelta(days=2)
- timezone.timedelta(seconds=1)
)

# if we are still waiting for all payments to come through, just return the cutoff date
if timezone.now() < payment_time_end:
context_payout = {
'payment_time_end': payment_time_end,
"payment_time_end": payment_time_end,
}

# if we are past the cutoff date for payments, calculate payouts
else:
# initialize an object to store total payout amounts for valid payout users
total_payout_amounts = {
username: {
'user': UserSerializer(User.objects.get(username=username)).data,
'amount': 0,
"user": UserSerializer(User.objects.get(username=username)).data,
"amount": 0,
}
for username in VALID_PAYOUT_USERNAMES
}

# TODO replace payed with paid everywhere
payed_subscriptions = Subscription.objects.payed(payment_time_end).distinct('id')
payed_subscriptions = Subscription.objects.payed(payment_time_end).distinct(
"id"
)

subscription_count = 0

for subscription in payed_subscriptions:
# skip payments after cutoff date
time_range = subscription.time_ranges.current(payment_time_end).payed().first()
time_range = (
subscription.time_ranges.current(payment_time_end).payed().first()
)
if time_range and time_range.payed_at > payment_time_end:
continue

Expand All @@ -167,24 +191,32 @@ def calculate_monthly_split(self, request):
for username in VALID_PAYOUT_USERNAMES:
amount = USER_PAYMENT_AMOUNT / len(VALID_PAYOUT_USERNAMES)

total_payout_amounts[username]['amount'] += amount
total_payout_amounts[username]["amount"] += amount

# otherwise split by usage
else:
for percentage in sub_percentages:
username = percentage['user']['username']
assert username in VALID_PAYOUT_USERNAMES, f"Tried to pay out to user '{username}' but they are not included in valid payout usernames!"
username = percentage["user"]["username"]
assert (
username in VALID_PAYOUT_USERNAMES
), f"Tried to pay out to user '{username}' but they are not included in valid payout usernames!"

amount = USER_PAYMENT_AMOUNT * percentage['percentage']
amount = USER_PAYMENT_AMOUNT * percentage["percentage"]

total_payout_amounts[username]['amount'] += amount
total_payout_amounts[username]["amount"] += amount

context_payout = {
'payout_amounts': sorted(list(total_payout_amounts.values()), key=lambda x: x['amount'], reverse=True),
'payout_sum': sum(map(lambda x: x['amount'], list(total_payout_amounts.values()))),
'payment_count': subscription_count,
'payment_amount': USER_PAYMENT_AMOUNT,
'payment_sum': subscription_count * USER_PAYMENT_AMOUNT,
"payout_amounts": sorted(
list(total_payout_amounts.values()),
key=lambda x: x["amount"],
reverse=True,
),
"payout_sum": sum(
map(lambda x: x["amount"], list(total_payout_amounts.values()))
),
"payment_count": subscription_count,
"payment_amount": USER_PAYMENT_AMOUNT,
"payment_sum": subscription_count * USER_PAYMENT_AMOUNT,
}

# ------------------------------------------------------------------------------------------
Expand All @@ -200,6 +232,6 @@ def calculate_monthly_split(self, request):

return TemplateResponse(
request,
'admin/api/transaction/calculate_monthly_split.html',
"admin/api/transaction/calculate_monthly_split.html",
context,
)
4 changes: 2 additions & 2 deletions novdan_api/api/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@


class ApiConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'api'
default_auto_field = "django.db.models.BigAutoField"
name = "api"

def ready(self):
from . import signals
18 changes: 9 additions & 9 deletions novdan_api/api/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,28 @@

class ActiveSubscriptionExists(APIException):
status_code = status.HTTP_409_CONFLICT
default_detail = 'Active subscription already exists.'
default_code = 'active_subscription_exists'
default_detail = "Active subscription already exists."
default_code = "active_subscription_exists"


class NoActiveSubscription(APIException):
status_code = status.HTTP_409_CONFLICT
default_detail = 'No active subscription.'
default_code = 'no_active_subscription'
default_detail = "No active subscription."
default_code = "no_active_subscription"


class LowBalance(APIException):
status_code = status.HTTP_409_CONFLICT
default_detail = 'Wallet balance too low.'
default_code = 'low_balance'
default_detail = "Wallet balance too low."
default_code = "low_balance"


def invalid_receiver_error():
return Response(
{
'id': 'InvalidReceiverError',
'message': 'Invalid receiver ID',
"id": "InvalidReceiverError",
"message": "Invalid receiver ID",
},
status=status.HTTP_404_NOT_FOUND,
content_type='application/spsp4+json',
content_type="application/spsp4+json",
)
Loading

0 comments on commit ecb85f3

Please sign in to comment.