Skip to content

Commit

Permalink
marks canceled balance as paid or write-off
Browse files Browse the repository at this point in the history
This commit removes the order `Transaction` from `offline_payment`.
The order must be created as a separate step (ex: through the paylater API).
  • Loading branch information
smirolo committed Jan 14, 2025
1 parent 32644cd commit 3ce5797
Show file tree
Hide file tree
Showing 4 changed files with 232 additions and 104 deletions.
14 changes: 13 additions & 1 deletion saas/api/billing.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (c) 2024, DjaoDjin inc.
# Copyright (c) 2025, DjaoDjin inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
Expand Down Expand Up @@ -35,6 +35,7 @@
from rest_framework import response as http

from ..backends import ProcessorError
from ..cart import session_cart_to_database
from ..compat import gettext_lazy as _, is_authenticated, reverse, StringIO
from ..docs import extend_schema, OpenApiResponse
from ..filters import DateRangeFilter, OrderingFilter, SearchFilter
Expand Down Expand Up @@ -526,9 +527,20 @@ def post(self, request, *args, **kwargs):
"state": "created"
}
"""
# We are not getting here without an authenticated user. It is time
# to store the cart into the database.
# Implementation note: we are not running `session_cart_to_database`
# in `dispatch` because we don't want it to run on OPTIONS API calls.
session_cart_to_database(self.request)
return self.create(request, *args, **kwargs)

def get(self, request, *args, **kwargs):
# We are not getting here without an authenticated user. It is time
# to store the cart into the database.
# Implementation note: we are not running `session_cart_to_database`
# in `dispatch` because we don't want it to run on OPTIONS API calls.
session_cart_to_database(self.request)

provider = self.invoicables_provider
resp_data = {
'processor_info':
Expand Down
11 changes: 11 additions & 0 deletions saas/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1535,6 +1535,17 @@ class QueryParamActiveSerializer(NoModelSerializer):
default=None, allow_null=True)


class QueryParamCancelBalanceSerializer(NoModelSerializer):

claim_code = serializers.CharField(required=False,
help_text=_("Claim code for payment to mark as paid or write-off"))
amount = serializers.IntegerField(required=False, min_value=1,
help_text=_("Amount to mark as paid or write-off. Min value is 1."))
paid = serializers.BooleanField(required=False,
help_text=_("When true, the cancelation was recovered offline,"\
" else it is a write-off"))


class QueryParamCartItemSerializer(NoModelSerializer):

plan = PlanRelatedField(required=False, allow_null=True,
Expand Down
117 changes: 111 additions & 6 deletions saas/api/transactions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (c) 2023, DjaoDjin inc.
# Copyright (c) 2025, DjaoDjin inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
Expand Down Expand Up @@ -32,14 +32,14 @@

from .serializers import (CartItemCreateSerializer,
CreateOfflineTransactionSerializer, OfflineTransactionSerializer,
TransactionSerializer)
from ..compat import gettext_lazy as _
QueryParamCancelBalanceSerializer, TransactionSerializer)
from ..compat import gettext_lazy as _, six
from ..decorators import _valid_manager
from ..docs import extend_schema, OpenApiResponse
from ..filters import DateRangeFilter, OrderingFilter, SearchFilter
from ..mixins import OrganizationMixin, ProviderMixin, DateRangeContextMixin
from ..models import (get_broker, record_use_charge, sum_orig_amount,
Subscription, Transaction, Plan)
Charge, Plan, Subscription, Transaction)
from ..backends import ProcessorError
from ..pagination import (BalancePagination, StatementBalancePagination,
TotalPagination)
Expand Down Expand Up @@ -849,6 +849,7 @@ def create(self, request, *args, **kwargs):
resp, status=status.HTTP_201_CREATED if resp else status.HTTP_200_OK)


@extend_schema(parameters=[QueryParamCancelBalanceSerializer])
def delete(self, request, *args, **kwargs):
"""
Cancels a balance due
Expand All @@ -869,10 +870,114 @@ def delete(self, request, *args, **kwargs):
"""
return self.destroy(request, *args, **kwargs)

def destroy(self, request, *args, **kwargs): #pylint:disable=unused-argument
def destroy(self, request, *args, **kwargs):
#pylint:disable=unused-argument,too-many-nested-blocks,too-many-locals
if not _valid_manager(request, [get_broker()]):
# XXX temporary workaround to provide GET balance API
# to subscribers and providers.
raise PermissionDenied()
self.organization.create_cancel_transactions(user=request.user)

at_time = datetime_or_now()
query_serializer = QueryParamCancelBalanceSerializer(
data=self.request.query_params)
query_serializer.is_valid(raise_exception=True)
claim_code = query_serializer.validated_data.get('claim_code')
amount = query_serializer.validated_data.get('amount')
paid = query_serializer.validated_data.get('paid', False)

if claim_code:
# If we have a payment claim_code, it is relatively easy.
# We mark invoiced items as paid or written-off until we reach
# the amount passed as a query parameter, or the total amount
# of the charge if no amount was passed.
charge = generics.get_object_or_404(
Charge.objects.filter(customer=self.organization),
claim_code=claim_code)
charge.retrieve()
if paid:
if amount:
for item in charge.charge_items.order_by('id'):
cancel_amount = min(item.available_amount, amount)
if cancel_amount > 0:
Transaction.objects.offline_payment(
item.subscription, cancel_amount,
payment_event_id=claim_code,
created_at=at_time, user=request.user)
amount -= cancel_amount
else:
for item in charge.charge_items.order_by('id'):
cancel_amount = item.available_amount
if cancel_amount > 0:
Transaction.objects.offline_payment(
item.subscription, cancel_amount,
payment_event_id=claim_code,
created_at=at_time, user=request.user)
else:
if amount:
for item in charge.charge_items.order_by('id'):
cancel_amount = min(item.available_amount, amount)
if cancel_amount > 0:
self.organization.create_cancel_transactions(
item.subscription, cancel_amount,
dest_unit=item.subscription.plan.unit,
created_at=at_time, user=request.user)
amount -= cancel_amount
else:
for item in charge.charge_items.order_by('id'):
cancel_amount = item.available_amount
if cancel_amount > 0:
self.organization.create_cancel_transactions(
item.subscription, cancel_amount,
dest_unit=item.subscription.plan.unit,
created_at=at_time, user=request.user)
else:
# If we do not have a payment claim_code, we iterate through
# the current balances.
balances = Transaction.objects.get_statement_balances(
self, until=at_time)
if paid:
if amount:
for sub_event_id, balance in six.iteritems(balances):
subscription = Subscription.objects.get_by_event_id(
sub_event_id)
for dest_unit, avail_amount in six.iteritems(balance):
cancel_amount = min(avail_amount, amount)
if cancel_amount > 0:
Transaction.objects.offline_payment(
subscription, cancel_amount,
created_at=at_time, user=request.user)
amount -= cancel_amount
else:
for sub_event_id, balance in six.iteritems(balances):
subscription = Subscription.objects.get_by_event_id(
sub_event_id)
for dest_unit, cancel_amount in six.iteritems(balance):
if cancel_amount > 0:
Transaction.objects.offline_payment(
subscription, cancel_amount,
created_at=at_time, user=request.user)
else:
if amount:
for sub_event_id, balance in six.iteritems(balances):
subscription = Subscription.objects.get_by_event_id(
sub_event_id)
for dest_unit, avail_amount in six.iteritems(balance):
cancel_amount = min(avail_amount, amount)
if cancel_amount > 0:
self.organization.create_cancel_transactions(
subscription, cancel_amount,
dest_unit=dest_unit,
created_at=at_time, user=request.user)
amount -= cancel_amount
else:
for sub_event_id, balance in six.iteritems(balances):
subscription = Subscription.objects.get_by_event_id(
sub_event_id)
for dest_unit, cancel_amount in six.iteritems(balance):
if cancel_amount > 0:
self.organization.create_cancel_transactions(
subscription, cancel_amount,
dest_unit=dest_unit,
created_at=at_time, user=request.user)

return http.Response(status=status.HTTP_204_NO_CONTENT)
Loading

0 comments on commit 3ce5797

Please sign in to comment.