event.stopPropagation()}
>
From 7772196b65f403215a79ac161a413f82a0833a43 Mon Sep 17 00:00:00 2001
From: Dana Khaing
Date: Sat, 23 May 2026 03:27:06 +0100
Subject: [PATCH 5/6] Restore totals when returning CSO to TCSO
Co-authored-by: Khant Zayar
---
flowbit-backend/core/models.py | 42 +++++++++++++++++++-------
flowbit-backend/core/serializers.py | 13 ++------
flowbit-backend/core/tests.py | 46 +++++++++++++++++++++++++++++
flowbit-backend/core/views.py | 31 ++-----------------
4 files changed, 82 insertions(+), 50 deletions(-)
diff --git a/flowbit-backend/core/models.py b/flowbit-backend/core/models.py
index 43c0b4c..45ddd19 100644
--- a/flowbit-backend/core/models.py
+++ b/flowbit-backend/core/models.py
@@ -5,7 +5,8 @@
from django.db import models, transaction
from django.contrib.auth.models import User
from django.contrib.auth.hashers import check_password, identify_hasher, make_password
-from django.db.models import Q, Sum
+from django.db.models import Case, DecimalField, F, Q, Sum, When
+from django.db.models.functions import Coalesce
from decimal import Decimal
from django.core.exceptions import ValidationError
from django.utils import timezone
@@ -1106,17 +1107,8 @@ def total_amount(self):
transaction__is_refunded=False,
status=Overflow.STATUS_REFUNDED,
).aggregate(total=Sum('refund_amount'))['total'] or Decimal('0.00')
- returned_overflow_total = Overflow.objects.filter(
- transaction__ticket=self,
- transaction__is_refunded=False,
- status=Overflow.STATUS_TCSO,
- refunded_at__isnull=False,
- resolution_type=Overflow.RESOLUTION_REFUND_OVERFLOW,
- ).aggregate(total=Sum('refund_amount'))['total'] or Decimal('0.00')
- active_total = visible_total - _from_allocation_basis_amount(
- refunded_overflow_total + returned_overflow_total
- )
+ active_total = visible_total - _from_allocation_basis_amount(refunded_overflow_total)
if active_total < Decimal('0.00'):
return Decimal('0.00')
return active_total
@@ -1946,6 +1938,32 @@ def _reduce_transaction_total_for_overflow_refund(overflow):
transaction_obj.ticket.refresh_refund_state()
+def _restore_transaction_total_from_active_amounts(transaction_obj):
+ if transaction_obj is None:
+ return
+
+ allocated_total = transaction_obj.allocations.aggregate(
+ total=Sum('amount')
+ )['total'] or Decimal('0.00')
+ active_overflow_total = transaction_obj.overflows.exclude(
+ status=Overflow.STATUS_REFUNDED
+ ).aggregate(
+ total=Sum(
+ Case(
+ When(status=Overflow.STATUS_TCSO, then=F('excess_amount')),
+ default=Coalesce(F('amount_to_approve'), F('excess_amount')),
+ output_field=DecimalField(max_digits=12, decimal_places=2),
+ )
+ )
+ )['total'] or Decimal('0.00')
+
+ restored_total = _from_allocation_basis_amount(allocated_total + active_overflow_total)
+ transaction_obj.total_amount = restored_total
+ transaction_obj.save(update_fields=['total_amount'])
+ if transaction_obj.ticket_id:
+ transaction_obj.ticket.refresh_refund_state()
+
+
def _move_approved_overflow_to_overkill(
overflow,
helper_name,
@@ -2078,6 +2096,8 @@ def refund_overflow(overflow, helper_name, resolution_type, refunded_at=None, cs
'resolution_type',
])
overflow.collaborators.clear()
+ if overflow.transaction_id:
+ _restore_transaction_total_from_active_amounts(overflow.transaction)
if period:
_retry_pending_overflows(period, overflow.transaction.identifier)
return overflow
diff --git a/flowbit-backend/core/serializers.py b/flowbit-backend/core/serializers.py
index d2ab98d..de9b30e 100644
--- a/flowbit-backend/core/serializers.py
+++ b/flowbit-backend/core/serializers.py
@@ -361,7 +361,6 @@ def _get_overflows(self, obj):
def get_total_amount(self, obj):
visible_total = Decimal('0.00')
refunded_overflow_total = Decimal('0.00')
- returned_overflow_total = Decimal('0.00')
transactions = self._get_transactions(obj)
for transaction in transactions:
@@ -375,16 +374,8 @@ def get_total_amount(self, obj):
continue
if overflow.status == Overflow.STATUS_REFUNDED:
refunded_overflow_total += overflow.refund_amount or Decimal('0.00')
- elif (
- overflow.status == Overflow.STATUS_TCSO
- and overflow.refunded_at is not None
- and overflow.resolution_type == Overflow.RESOLUTION_REFUND_OVERFLOW
- ):
- returned_overflow_total += overflow.refund_amount or Decimal('0.00')
-
- active_total = visible_total - _from_allocation_basis_amount(
- refunded_overflow_total + returned_overflow_total
- )
+
+ active_total = visible_total - _from_allocation_basis_amount(refunded_overflow_total)
if active_total < Decimal('0.00'):
return Decimal('0.00')
return active_total
diff --git a/flowbit-backend/core/tests.py b/flowbit-backend/core/tests.py
index a9b481b..e715367 100644
--- a/flowbit-backend/core/tests.py
+++ b/flowbit-backend/core/tests.py
@@ -3827,6 +3827,52 @@ def test_ticket_refund_with_cso_can_change_back_to_tcso_without_reducing_total(s
self.assertEqual(refund_transaction.total_amount, Decimal('400.00'))
self.assertEqual(refund_ticket.total_amount, Decimal('400.00'))
+ detail_response = self.client.get(f'/api/tickets/{refund_ticket.ticket_number}/')
+ self.assertEqual(detail_response.status_code, status.HTTP_200_OK)
+ self.assertEqual(Decimal(detail_response.data['total_amount']), Decimal('400.00'))
+
+ def test_return_to_tcso_restores_transaction_total_if_old_bug_already_reduced_it(self):
+ refund_ticket = Ticket.objects.create(
+ customer_name='Ticket CSO Restore',
+ created_by=self.approver,
+ )
+ refund_transaction = Transaction.objects.create(
+ ticket=refund_ticket,
+ identifier=self.second_identifier,
+ total_amount=Decimal('400.00'),
+ created_by=self.approver,
+ )
+ overflow = Overflow.objects.get(transaction=refund_transaction, status=Overflow.STATUS_TCSO)
+ self.client.post(
+ f'/api/overflows/{overflow.id}/approve/',
+ {
+ 'amount_to_approve': '300.00',
+ 'collaborator_ids': [self.collaborator.id],
+ },
+ format='json',
+ )
+
+ refund_transaction.total_amount = Decimal('160.00')
+ refund_transaction.save(update_fields=['total_amount'])
+
+ response = self.client.post(
+ f'/api/tickets/{refund_ticket.ticket_number}/refund/',
+ {
+ 'action': 'refund_ticket',
+ 'admin_override_code': 'override-123',
+ 'cso_refund_mode': 'return_to_tcso',
+ },
+ format='json',
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ refund_transaction.refresh_from_db()
+ refund_ticket.refresh_from_db()
+ detail_response = self.client.get(f'/api/tickets/{refund_ticket.ticket_number}/')
+ self.assertEqual(detail_response.status_code, status.HTTP_200_OK)
+ self.assertEqual(refund_transaction.total_amount, Decimal('400.00'))
+ self.assertEqual(Decimal(detail_response.data['total_amount']), Decimal('400.00'))
+
def test_ticket_transaction_refund_with_cso_can_refund_spill_over_into_overkill(self):
refund_ticket = Ticket.objects.create(
customer_name='Transaction CSO Refund',
diff --git a/flowbit-backend/core/views.py b/flowbit-backend/core/views.py
index 0d43406..256265c 100644
--- a/flowbit-backend/core/views.py
+++ b/flowbit-backend/core/views.py
@@ -53,7 +53,6 @@
LedgerAllocation,
IdentifierCapacityAdjustment,
IdentifierLedgerFreeze,
- _is_returned_pending_overflow,
_announce_pending_overflows,
_notify_remaining_overkill_for_lucky_draw,
_retry_pending_overflows,
@@ -5323,16 +5322,6 @@ def get_queryset(self):
total=Sum('refund_amount'),
).values('total')[:1]
- returned_overflow_total_subquery = Overflow.objects.filter(
- transaction__ticket=OuterRef('pk'),
- transaction__is_refunded=False,
- status=Overflow.STATUS_TCSO,
- refunded_at__isnull=False,
- resolution_type=Overflow.RESOLUTION_REFUND_OVERFLOW,
- ).values('transaction__ticket').annotate(
- total=Sum('refund_amount'),
- ).values('total')[:1]
-
queryset = queryset.annotate(
ticket_transaction_count=Count('transactions', distinct=True),
active_spill_over_count_annotated=Count(
@@ -5360,11 +5349,6 @@ def get_queryset(self):
Value(Decimal('0.00')),
output_field=DecimalField(max_digits=14, decimal_places=2),
),
- returned_overflow_total_annotated=Coalesce(
- Subquery(returned_overflow_total_subquery),
- Value(Decimal('0.00')),
- output_field=DecimalField(max_digits=14, decimal_places=2),
- ),
).prefetch_related(ticket_transactions).distinct()
queryset = queryset.annotate(
@@ -5372,7 +5356,7 @@ def get_queryset(self):
Value(Decimal('0.00')),
ExpressionWrapper(
F('visible_total_amount_annotated') - ExpressionWrapper(
- (F('refunded_overflow_total_annotated') + F('returned_overflow_total_annotated')) / Value(Decimal('1.25')),
+ F('refunded_overflow_total_annotated') / Value(Decimal('1.25')),
output_field=DecimalField(max_digits=14, decimal_places=2),
),
output_field=DecimalField(max_digits=14, decimal_places=2),
@@ -5719,21 +5703,12 @@ def _ticket_visible_total(ticket):
def _transaction_visible_request_amount(transaction_obj):
refunded_overflow_total = Decimal('0.00')
- returned_overflow_total = Decimal('0.00')
for overflow in transaction_obj.overflows.all():
if overflow.status == Overflow.STATUS_REFUNDED:
refunded_overflow_total += overflow.refund_amount or Decimal('0.00')
- elif (
- overflow.status == Overflow.STATUS_TCSO
- and overflow.refunded_at is not None
- and overflow.resolution_type == Overflow.RESOLUTION_REFUND_OVERFLOW
- ):
- returned_overflow_total += overflow.refund_amount or Decimal('0.00')
- active_total = transaction_obj.total_amount - _from_allocation_basis_amount(
- refunded_overflow_total + returned_overflow_total
- )
+ active_total = transaction_obj.total_amount - _from_allocation_basis_amount(refunded_overflow_total)
if active_total < Decimal('0.00'):
return Decimal('0.00')
return active_total
@@ -5751,7 +5726,7 @@ def _ticket_visible_line_amount(transaction_obj):
active_overflows = [
overflow
for overflow in transaction_obj.overflows.all()
- if overflow.status != Overflow.STATUS_REFUNDED and not _is_returned_pending_overflow(overflow)
+ if overflow.status != Overflow.STATUS_REFUNDED
]
overflow_total = sum(
(
From 5923636e63a64f2d745df84de54cb797c189ce0f Mon Sep 17 00:00:00 2001
From: Dana Khaing
Date: Sat, 23 May 2026 11:13:17 +0100
Subject: [PATCH 6/6] Separate ticket refund confirmation modal
Co-authored-by: Khant Zayar
---
.../tickets/ticket-refund-modal.tsx | 198 +++++++++---------
1 file changed, 100 insertions(+), 98 deletions(-)
diff --git a/flowbit-frontend/src/components/tickets/ticket-refund-modal.tsx b/flowbit-frontend/src/components/tickets/ticket-refund-modal.tsx
index 95e4ee1..f6305fc 100644
--- a/flowbit-frontend/src/components/tickets/ticket-refund-modal.tsx
+++ b/flowbit-frontend/src/components/tickets/ticket-refund-modal.tsx
@@ -117,14 +117,15 @@ export function TicketRefundModal({
}
return (
-
+ <>
event.stopPropagation()}
+ className="fixed inset-0 z-50 flex items-center justify-center bg-stone-950/30 px-4"
+ onClick={closeModal}
>
+
event.stopPropagation()}
+ >
Refund
@@ -270,106 +271,107 @@ export function TicketRefundModal({
) : null}
-
-
- Close
-
+
+
+ Close
+
+
+
- {confirmAction ? (
+ {confirmAction ? (
+
setConfirmAction(null)}
+ >
setConfirmAction(null)}
+ className="max-h-[90vh] w-full max-w-lg overflow-y-auto rounded-[24px] border border-stone-900/8 bg-white p-5 shadow-[0_18px_48px_rgba(24,24,24,0.18)]"
+ onClick={(event) => event.stopPropagation()}
>
-
event.stopPropagation()}
- >
-
- Confirmation
-
-
- Confirm refund
-
-
- {confirmAction.label}. This action will reverse the selected ticket records.
-
- {confirmAction.requiresCsoChoice ? (
-
-
- Approved spill-over handling
-
-
setCsoRefundMode("return_to_tcso")}
- >
-
-
Change back to TCSO
-
- Keep the transaction amount unchanged and move approved spill-over back to pending.
-
-
-
-
setCsoRefundMode("refund_spill_over")}
- >
-
-
Refund spill-over
-
- Move approved spill-over into overkill and reduce the active transaction amount.
-
-
-
-
- ) : null}
- {requireOverrideCode ? (
-
-
- Admin override code
-
- onCodeChange(event.target.value)}
- placeholder="Enter override code"
- disabled={Boolean(busyAction)}
- />
-
- ) : null}
-
-
setConfirmAction(null)}
- disabled={Boolean(busyAction)}
+
+ Confirmation
+
+
+ Confirm refund
+
+
+ {confirmAction.label}. This action will reverse the selected ticket records.
+
+ {confirmAction.requiresCsoChoice ? (
+
+
+ Approved spill-over handling
+
+
setCsoRefundMode("return_to_tcso")}
>
- Cancel
-
-
+ Change back to TCSO
+
+ Keep the transaction amount unchanged and move approved spill-over back to pending.
+
+
+
+
setCsoRefundMode("refund_spill_over")}
>
- Confirm refund
-
+
+
Refund spill-over
+
+ Move approved spill-over into overkill and reduce the active transaction amount.
+
+
+
+ ) : null}
+ {requireOverrideCode ? (
+
+
+ Admin override code
+
+ onCodeChange(event.target.value)}
+ placeholder="Enter override code"
+ disabled={Boolean(busyAction)}
+ />
+
+ ) : null}
+
+ setConfirmAction(null)}
+ disabled={Boolean(busyAction)}
+ >
+ Cancel
+
+
+ Confirm refund
+
- ) : null}
-
-
+
+ ) : null}
+ >
);
}