Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow for the cancellation of a previously confirmed/sent disbursement #491

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions mtp_api/apps/disbursement/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
('REJECTED', 'rejected', _('Rejected')),
('CONFIRMED', 'confirmed', _('Confirmed')),
('SENT', 'sent', _('Sent')),
('CANCELLED', 'cancelled', _('Cancelled')),
)


Expand All @@ -17,6 +18,7 @@
('PRECONFIRMED', 'preconfirmed', _('Pre-confirmed')),
('CONFIRMED', 'confirmed', _('Confirmed')),
('SENT', 'sent', _('Sent')),
('CANCELLED', 'cancelled', _('Cancelled')),
)

DISBURSEMENT_METHOD = Choices(
Expand Down
7 changes: 6 additions & 1 deletion mtp_api/apps/disbursement/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def get_queryset(self):
def update_resolution(self, queryset, disbursement_ids, resolution, user):
to_update = queryset.filter(
pk__in=disbursement_ids,
resolution__in=[self.model.get_permitted_state(resolution), resolution]
resolution__in=self.model.get_permitted_state(resolution) + [resolution]
).select_for_update()

ids_to_update = [c.id for c in to_update]
Expand All @@ -60,6 +60,8 @@ def update_resolution(self, queryset, disbursement_ids, resolution, user):
Log.objects.disbursements_confirmed(to_update, user)
elif resolution == DISBURSEMENT_RESOLUTION.SENT:
Log.objects.disbursements_sent(to_update, user)
elif resolution == DISBURSEMENT_RESOLUTION.CANCELLED:
Log.objects.disbursements_cancelled(to_update, user)

if resolution == DISBURSEMENT_RESOLUTION.CONFIRMED:
to_update.update(
Expand Down Expand Up @@ -103,3 +105,6 @@ def disbursements_confirmed(self, disbursements, by_user):

def disbursements_sent(self, disbursements, by_user):
self._log_action(LOG_ACTIONS.SENT, disbursements, by_user)

def disbursements_cancelled(self, disbursements, by_user):
self._log_action(LOG_ACTIONS.CANCELLED, disbursements, by_user)
23 changes: 23 additions & 0 deletions mtp_api/apps/disbursement/migrations/0019_auto_20190115_1430.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 2.0.8 on 2019-01-15 14:30

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('disbursement', '0018_auto_20181002_1429'),
]

operations = [
migrations.AlterField(
model_name='disbursement',
name='resolution',
field=models.CharField(choices=[('pending', 'Pending'), ('rejected', 'Rejected'), ('preconfirmed', 'Pre-confirmed'), ('confirmed', 'Confirmed'), ('sent', 'Sent'), ('cancelled', 'Cancelled')], db_index=True, default='pending', max_length=50),
),
migrations.AlterField(
model_name='log',
name='action',
field=models.CharField(choices=[('created', 'Created'), ('edited', 'Edited'), ('rejected', 'Rejected'), ('confirmed', 'Confirmed'), ('sent', 'Sent'), ('cancelled', 'Cancelled')], max_length=50),
),
]
29 changes: 23 additions & 6 deletions mtp_api/apps/disbursement/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from .managers import DisbursementManager, DisbursementQuerySet, LogManager
from .signals import (
disbursement_confirmed, disbursement_created, disbursement_rejected,
disbursement_sent, disbursement_edited
disbursement_sent, disbursement_edited, disbursement_cancelled
)
from prison.models import Prison
from transaction.utils import format_amount
Expand Down Expand Up @@ -71,13 +71,15 @@ class Meta:
@staticmethod
def get_permitted_state(new_resolution):
if new_resolution == DISBURSEMENT_RESOLUTION.SENT:
return DISBURSEMENT_RESOLUTION.CONFIRMED
return [DISBURSEMENT_RESOLUTION.CONFIRMED]
elif new_resolution == DISBURSEMENT_RESOLUTION.CONFIRMED:
return DISBURSEMENT_RESOLUTION.PRECONFIRMED
return [DISBURSEMENT_RESOLUTION.PRECONFIRMED]
elif new_resolution == DISBURSEMENT_RESOLUTION.PENDING:
return DISBURSEMENT_RESOLUTION.PRECONFIRMED
return [DISBURSEMENT_RESOLUTION.PRECONFIRMED]
elif new_resolution == DISBURSEMENT_RESOLUTION.CANCELLED:
return [DISBURSEMENT_RESOLUTION.CONFIRMED, DISBURSEMENT_RESOLUTION.SENT]
else:
return DISBURSEMENT_RESOLUTION.PENDING
return [DISBURSEMENT_RESOLUTION.PENDING]

def __str__(self):
return 'Disbursement {id}, {amount} {prisoner} > {recipient}, {status}'.format(
Expand All @@ -89,7 +91,7 @@ def __str__(self):
)

def resolution_permitted(self, new_resolution):
return self.resolution == self.get_permitted_state(new_resolution)
return self.resolution in self.get_permitted_state(new_resolution)

@property
def recipient_name(self):
Expand Down Expand Up @@ -148,6 +150,16 @@ def send(self, by_user):
disbursement_sent.send(
sender=Disbursement, disbursement=self, by_user=by_user)

def cancel(self, by_user):
if self.resolution == DISBURSEMENT_RESOLUTION.CANCELLED:
return
if not self.resolution_permitted(DISBURSEMENT_RESOLUTION.CANCELLED):
raise InvalidDisbursementStateException([self.id])
self.resolution = DISBURSEMENT_RESOLUTION.CANCELLED
self.save()
disbursement_cancelled.send(
sender=Disbursement, disbursement=self, by_user=by_user)

def _generate_invoice_number(self):
return 'PMD%s' % (settings.INVOICE_NUMBER_BASE + self.id)

Expand Down Expand Up @@ -220,3 +232,8 @@ def disbursement_confirmed_receiver(sender, disbursement, by_user, **kwargs):
@receiver(disbursement_sent)
def disbursement_sent_receiver(sender, disbursement, by_user, **kwargs):
Log.objects.disbursements_sent([disbursement], by_user)


@receiver(disbursement_cancelled)
def disbursement_cancelled_receiver(sender, disbursement, by_user, **kwargs):
Log.objects.disbursements_cancelled([disbursement], by_user)
1 change: 1 addition & 0 deletions mtp_api/apps/disbursement/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
disbursement_rejected = Signal(providing_args=['disbursement', 'by_user'])
disbursement_confirmed = Signal(providing_args=['disbursement', 'by_user'])
disbursement_sent = Signal(providing_args=['disbursement', 'by_user'])
disbursement_cancelled = Signal(providing_args=['disbursement', 'by_user'])
84 changes: 83 additions & 1 deletion mtp_api/apps/disbursement/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,7 @@ def test_cannot_send_unconfirmed_disbursement(self):
method=DISBURSEMENT_METHOD.BANK_TRANSFER,
recipient_first_name='Sam',
recipient_last_name='Hall',
resolution=DISBURSEMENT_RESOLUTION.REJECTED,
)

response = self.client.post(
Expand All @@ -603,6 +604,87 @@ def test_cannot_send_unconfirmed_disbursement(self):
disbursements = Disbursement.objects.all()
self.assertEqual(disbursements.count(), 2)
self.assertEqual(disbursements[0].resolution, DISBURSEMENT_RESOLUTION.PENDING)
self.assertEqual(disbursements[1].resolution, DISBURSEMENT_RESOLUTION.PENDING)
self.assertEqual(disbursements[1].resolution, DISBURSEMENT_RESOLUTION.REJECTED)

self.assertEqual(Log.objects.all().count(), 0)

def test_cancel_disbursements(self):
user = self.bank_admins[0]

disbursement1 = Disbursement.objects.create(
amount=1000,
prisoner_number='A1234BC',
prison=Prison.objects.get(pk='IXB'),
method=DISBURSEMENT_METHOD.BANK_TRANSFER,
recipient_first_name='Sam',
recipient_last_name='Hall',
resolution=DISBURSEMENT_RESOLUTION.CONFIRMED
)
disbursement2 = Disbursement.objects.create(
amount=1000,
prisoner_number='A1234BD',
prison=Prison.objects.get(pk='INP'),
method=DISBURSEMENT_METHOD.BANK_TRANSFER,
recipient_first_name='Sam',
recipient_last_name='Hall',
resolution=DISBURSEMENT_RESOLUTION.SENT
)

response = self.client.post(
reverse('disbursement-cancel'),
data={'disbursement_ids': [disbursement1.id, disbursement2.id]},
format='json',
HTTP_AUTHORIZATION=self.get_http_authorization_for_user(user)
)

self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)

disbursements = Disbursement.objects.all()
self.assertEqual(disbursements.count(), 2)
self.assertEqual(disbursements[0].resolution, DISBURSEMENT_RESOLUTION.CANCELLED)
self.assertEqual(disbursements[1].resolution, DISBURSEMENT_RESOLUTION.CANCELLED)

logs = Log.objects.all()
self.assertEqual(logs[0].disbursement, disbursements[0])
self.assertEqual(logs[0].user, user)
self.assertEqual(logs[0].action, LOG_ACTIONS.CANCELLED)
self.assertEqual(logs[1].disbursement, disbursements[1])
self.assertEqual(logs[1].user, user)
self.assertEqual(logs[1].action, LOG_ACTIONS.CANCELLED)

def test_cannot_cancel_unconfirmed_disbursement(self):
user = self.bank_admins[0]

disbursement1 = Disbursement.objects.create(
amount=1000,
prisoner_number='A1234BC',
prison=Prison.objects.get(pk='IXB'),
method=DISBURSEMENT_METHOD.BANK_TRANSFER,
recipient_first_name='Sam',
recipient_last_name='Hall',
)
disbursement2 = Disbursement.objects.create(
amount=1000,
prisoner_number='A1234BD',
prison=Prison.objects.get(pk='INP'),
method=DISBURSEMENT_METHOD.BANK_TRANSFER,
recipient_first_name='Sam',
recipient_last_name='Hall',
resolution=DISBURSEMENT_RESOLUTION.REJECTED,
)

response = self.client.post(
reverse('disbursement-cancel'),
data={'disbursement_ids': [disbursement1.id, disbursement2.id]},
format='json',
HTTP_AUTHORIZATION=self.get_http_authorization_for_user(user)
)

self.assertEqual(response.status_code, status.HTTP_409_CONFLICT)

disbursements = Disbursement.objects.all()
self.assertEqual(disbursements.count(), 2)
self.assertEqual(disbursements[0].resolution, DISBURSEMENT_RESOLUTION.PENDING)
self.assertEqual(disbursements[1].resolution, DISBURSEMENT_RESOLUTION.REJECTED)

self.assertEqual(Log.objects.all().count(), 0)
2 changes: 2 additions & 0 deletions mtp_api/apps/disbursement/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,7 @@
views.ConfirmDisbursementsView.as_view(), name='disbursement-confirm'),
url(r'^disbursements/actions/send/$',
views.SendDisbursementsView.as_view(), name='disbursement-send'),
url(r'^disbursements/actions/cancel/$',
views.CancelDisbursementsView.as_view(), name='disbursement-cancel'),
url(r'^', include(router.urls)),
]
14 changes: 12 additions & 2 deletions mtp_api/apps/disbursement/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ class DisbursementView(
filter_class = DisbursementFilter
filter_backends = (DjangoFilterBackend, SafeOrderingFilter)
ordering_fields = ('created', 'amount', 'resolution', 'method', 'recipient_name',
'prisoner_number', 'prisoner_name')
'prisoner_number', 'prisoner_name', 'log__created')
permission_classes = (
IsAuthenticated, ActionsBasedViewPermissions, get_client_permissions_class(
CASHBOOK_OAUTH_CLIENT_ID, NOMS_OPS_OAUTH_CLIENT_ID,
Expand Down Expand Up @@ -184,6 +184,14 @@ class SendDisbursementsView(ResolveDisbursementsView):
)


class CancelDisbursementsView(ResolveDisbursementsView):
resolution = DISBURSEMENT_RESOLUTION.CANCELLED

permission_classes = (
IsAuthenticated, ActionsBasedViewPermissions, BankAdminClientIDPermissions
)


class ConfirmDisbursementsView(DisbursementViewMixin, APIView):
serializer_class = DisbursementConfirmationSerializer
action = 'update'
Expand Down Expand Up @@ -241,7 +249,9 @@ class CommentView(
serializer_class = CommentSerializer

permission_classes = (
IsAuthenticated, ActionsBasedViewPermissions, CashbookClientIDPermissions
IsAuthenticated, ActionsBasedViewPermissions, get_client_permissions_class(
CASHBOOK_OAUTH_CLIENT_ID, BANK_ADMIN_OAUTH_CLIENT_ID
)
)

def get_serializer(self, *args, **kwargs):
Expand Down
3 changes: 2 additions & 1 deletion mtp_api/apps/mtp_auth/fixtures/initial_groups.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@
"permissions": [
["view_disbursement", "disbursement", "disbursement"],
["change_disbursement", "disbursement", "disbursement"],
["add_filedownload", "core", "filedownload"]
["add_filedownload", "core", "filedownload"],
["add_comment", "disbursement", "comment"]
]
}
}
Expand Down