Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
OpenEdx
edx-platform
Commits
d9f35a4e
Commit
d9f35a4e
authored
Dec 19, 2017
by
Albert St. Aubin
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
PR Updates 2
parent
6c03534f
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
94 additions
and
68 deletions
+94
-68
common/djangoapps/entitlements/api/v1/tests/test_views.py
+1
-1
common/djangoapps/entitlements/api/v1/views.py
+66
-55
lms/djangoapps/commerce/utils.py
+27
-12
No files found.
common/djangoapps/entitlements/api/v1/tests/test_views.py
View file @
d9f35a4e
...
@@ -555,7 +555,7 @@ class EntitlementEnrollmentViewSetTest(ModuleStoreTestCase):
...
@@ -555,7 +555,7 @@ class EntitlementEnrollmentViewSetTest(ModuleStoreTestCase):
revoke_url
,
revoke_url
,
content_type
=
'application/json'
,
content_type
=
'application/json'
,
)
)
assert
response
.
status_code
==
409
assert
response
.
status_code
==
500
course_entitlement
.
refresh_from_db
()
course_entitlement
.
refresh_from_db
()
assert
CourseEnrollment
.
is_enrolled
(
self
.
user
,
self
.
course
.
id
)
assert
CourseEnrollment
.
is_enrolled
(
self
.
user
,
self
.
course
.
id
)
...
...
common/djangoapps/entitlements/api/v1/views.py
View file @
d9f35a4e
import
logging
import
logging
from
django.db
import
transaction
from
django.db
import
IntegrityError
,
transaction
from
django.utils
import
timezone
from
django.utils
import
timezone
from
django_filters.rest_framework
import
DjangoFilterBackend
from
django_filters.rest_framework
import
DjangoFilterBackend
from
edx_rest_framework_extensions.authentication
import
JwtAuthentication
from
edx_rest_framework_extensions.authentication
import
JwtAuthentication
...
@@ -23,6 +23,58 @@ from student.models import CourseEnrollmentException, AlreadyEnrolledError
...
@@ -23,6 +23,58 @@ from student.models import CourseEnrollmentException, AlreadyEnrolledError
log
=
logging
.
getLogger
(
__name__
)
log
=
logging
.
getLogger
(
__name__
)
def
_unenroll_entitlement
(
course_entitlement
,
course_run_key
):
"""
Internal method to handle the details of Unenrolling a User in a Course Run.
"""
CourseEnrollment
.
unenroll
(
course_entitlement
.
user
,
course_run_key
,
skip_refund
=
True
)
course_entitlement
.
set_enrollment
(
None
)
@transaction.atomic
def
_process_revoke_and_unenroll_entitlement
(
course_entitlement
,
is_refund
=
False
):
"""
Process the revoke of the Course Entitlement and refund if needed
Arguments:
course_entitlement: Course Entitlement Object
is_refund (bool): True if a refund should be processed
Exceptions:
IntegrityError if there is an issue that should reverse the database changes
"""
if
course_entitlement
.
expired_at
is
None
:
course_entitlement
.
expired_at
=
timezone
.
now
()
log
.
info
(
'Set expired_at to [
%
s] for course entitlement [
%
s]'
,
course_entitlement
.
expired_at
,
course_entitlement
.
uuid
)
course_entitlement
.
save
()
if
course_entitlement
.
enrollment_course_run
is
not
None
:
course_id
=
course_entitlement
.
enrollment_course_run
.
course_id
_unenroll_entitlement
(
course_entitlement
,
course_id
)
log
.
info
(
'Unenrolled user [
%
s] from course run [
%
s] as part of revocation of course entitlement [
%
s]'
,
course_entitlement
.
user
.
username
,
course_id
,
course_entitlement
.
uuid
)
if
is_refund
:
refund_successful
=
refund_entitlement
(
course_entitlement
=
course_entitlement
)
if
not
refund_successful
:
# This state is achieved in most cases by a failure in the ecommerce service to process the refund.
log
.
warn
(
'Entitlement Refund failed for Course Entitlement [
%
s], alert User'
,
str
(
course_entitlement
.
uuid
)
)
# Force Transaction reset with an Integrity error exception, this will revert all previous transactions
raise
IntegrityError
class
EntitlementViewSet
(
viewsets
.
ModelViewSet
):
class
EntitlementViewSet
(
viewsets
.
ModelViewSet
):
ENTITLEMENT_UUID4_REGEX
=
'[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}'
ENTITLEMENT_UUID4_REGEX
=
'[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}'
...
@@ -93,35 +145,7 @@ class EntitlementViewSet(viewsets.ModelViewSet):
...
@@ -93,35 +145,7 @@ class EntitlementViewSet(viewsets.ModelViewSet):
'Entitlement Revoke requested for Course Entitlement[
%
s]'
,
'Entitlement Revoke requested for Course Entitlement[
%
s]'
,
str
(
instance
.
uuid
)
str
(
instance
.
uuid
)
)
)
process_revoke_and_unenroll_entitlement
(
instance
)
_process_revoke_and_unenroll_entitlement
(
instance
)
@transaction.atomic
def
process_revoke_and_unenroll_entitlement
(
course_entitlement
):
save_model
=
False
if
course_entitlement
.
expired_at
is
None
:
course_entitlement
.
expired_at
=
timezone
.
now
()
log
.
info
(
'Set expired_at to [
%
s] for course entitlement [
%
s]'
,
course_entitlement
.
expired_at
,
course_entitlement
.
uuid
)
save_model
=
True
if
course_entitlement
.
enrollment_course_run
is
not
None
:
CourseEnrollment
.
unenroll
(
user
=
course_entitlement
.
user
,
course_id
=
course_entitlement
.
enrollment_course_run
.
course_id
,
skip_refund
=
True
)
enrollment
=
course_entitlement
.
enrollment_course_run
course_entitlement
.
enrollment_course_run
=
None
save_model
=
True
log
.
info
(
'Unenrolled user [
%
s] from course run [
%
s] as part of revocation of course entitlement [
%
s]'
,
course_entitlement
.
user
.
username
,
enrollment
.
course_id
,
course_entitlement
.
uuid
)
if
save_model
:
course_entitlement
.
save
()
class
EntitlementEnrollmentViewSet
(
viewsets
.
GenericViewSet
):
class
EntitlementEnrollmentViewSet
(
viewsets
.
GenericViewSet
):
...
@@ -183,13 +207,6 @@ class EntitlementEnrollmentViewSet(viewsets.GenericViewSet):
...
@@ -183,13 +207,6 @@ class EntitlementEnrollmentViewSet(viewsets.GenericViewSet):
entitlement
.
set_enrollment
(
enrollment
)
entitlement
.
set_enrollment
(
enrollment
)
return
None
return
None
def
_unenroll_entitlement
(
self
,
entitlement
,
course_run_key
,
user
):
"""
Internal method to handle the details of Unenrolling a User in a Course Run.
"""
CourseEnrollment
.
unenroll
(
user
,
course_run_key
,
skip_refund
=
True
)
entitlement
.
set_enrollment
(
None
)
def
create
(
self
,
request
,
uuid
):
def
create
(
self
,
request
,
uuid
):
"""
"""
On POST this method will be called and will handle enrolling a user in the
On POST this method will be called and will handle enrolling a user in the
...
@@ -247,10 +264,9 @@ class EntitlementEnrollmentViewSet(viewsets.GenericViewSet):
...
@@ -247,10 +264,9 @@ class EntitlementEnrollmentViewSet(viewsets.GenericViewSet):
if
response
:
if
response
:
return
response
return
response
elif
entitlement
.
enrollment_course_run
.
course_id
!=
course_run_id
:
elif
entitlement
.
enrollment_course_run
.
course_id
!=
course_run_id
:
self
.
_unenroll_entitlement
(
_unenroll_entitlement
(
entitlement
=
entitlement
,
course_
entitlement
=
entitlement
,
course_run_key
=
entitlement
.
enrollment_course_run
.
course_id
,
course_run_key
=
entitlement
.
enrollment_course_run
.
course_id
,
user
=
request
.
user
)
)
response
=
self
.
_enroll_entitlement
(
response
=
self
.
_enroll_entitlement
(
entitlement
=
entitlement
,
entitlement
=
entitlement
,
...
@@ -291,28 +307,23 @@ class EntitlementEnrollmentViewSet(viewsets.GenericViewSet):
...
@@ -291,28 +307,23 @@ class EntitlementEnrollmentViewSet(viewsets.GenericViewSet):
'Entitlement Refund requested for Course Entitlement[
%
s]'
,
'Entitlement Refund requested for Course Entitlement[
%
s]'
,
str
(
entitlement
.
uuid
)
str
(
entitlement
.
uuid
)
)
)
refund_successful
=
refund_entitlement
(
course_entitlement
=
entitlement
)
try
:
if
refund_successful
:
_process_revoke_and_unenroll_entitlement
(
course_entitlement
=
entitlement
,
is_refund
=
True
)
process_revoke_and_unenroll_entitlement
(
course_entitlement
=
entitlement
)
except
IntegrityError
:
else
:
# This state is reached when there was a failure in revoke and refund process resulting
# This state is achieved in most cases by a failure in the ecommerce service to process the refund.
# in a reversion of DB changes
log
.
warn
(
'Entitlement Refund failed for Course Entitlement [
%
s], alert User'
,
str
(
entitlement
.
uuid
)
)
return
Response
(
return
Response
(
status
=
status
.
HTTP_
409_CONFLICT
,
status
=
status
.
HTTP_
500_INTERNAL_SERVER_ERROR
,
data
=
{
data
=
{
'message'
:
'Entitlement re
fund failed due to refund process failure or conflict
'
'message'
:
'Entitlement re
voke and refund failed due to refund internal process failure
'
})
})
elif
not
is_refund
:
elif
not
is_refund
:
if
entitlement
.
enrollment_course_run
is
not
None
:
if
entitlement
.
enrollment_course_run
is
not
None
:
self
.
_unenroll_entitlement
(
_unenroll_entitlement
(
entitlement
=
entitlement
,
course_
entitlement
=
entitlement
,
course_run_key
=
entitlement
.
enrollment_course_run
.
course_id
,
course_run_key
=
entitlement
.
enrollment_course_run
.
course_id
,
user
=
request
.
user
)
)
else
:
else
:
log
.
info
(
log
.
info
(
...
...
lms/djangoapps/commerce/utils.py
View file @
d9f35a4e
...
@@ -181,7 +181,7 @@ def refund_entitlement(course_entitlement):
...
@@ -181,7 +181,7 @@ def refund_entitlement(course_entitlement):
)
)
else
:
else
:
log
.
info
(
'No refund opened for user [
%
s], course entitlement [
%
s]'
,
enrollee
.
id
,
entitlement_uuid
)
log
.
info
(
'No refund opened for user [
%
s], course entitlement [
%
s]'
,
enrollee
.
id
,
entitlement_uuid
)
return
Tru
e
return
Fals
e
def
refund_seat
(
course_enrollment
):
def
refund_seat
(
course_enrollment
):
...
@@ -227,7 +227,8 @@ def refund_seat(course_enrollment):
...
@@ -227,7 +227,8 @@ def refund_seat(course_enrollment):
def
_process_refund
(
refund_ids
,
api_client
,
course_product
):
def
_process_refund
(
refund_ids
,
api_client
,
course_product
):
"""
"""
Helper method to process a refund for a given course_product
Helper method to process a refund for a given course_product. This method assumes that the User has already
been unenrolled.
Returns:
Returns:
bool: True if the refund process was successful, False if there are any Errors that are not handled
bool: True if the refund process was successful, False if there are any Errors that are not handled
...
@@ -239,9 +240,9 @@ def _process_refund(refund_ids, api_client, course_product):
...
@@ -239,9 +240,9 @@ def _process_refund(refund_ids, api_client, course_product):
for
refund_id
in
refund_ids
:
for
refund_id
in
refund_ids
:
try
:
try
:
# NOTE:
Approve payment only because the user has already been unenrolled. Additionally, this
# NOTE:
The following assumes that the user has already been unenrolled.
#
ensures we don't tie up an additional web worker when the E-Commerce Service tries to unenroll
#
We are then able to approve payment. Additionally, this ensures we don't tie up an
#
the learner
#
additional web worker when the E-Commerce Service tries to unenroll the learner.
api_client
.
refunds
(
refund_id
)
.
process
.
put
({
'action'
:
'approve_payment_only'
})
api_client
.
refunds
(
refund_id
)
.
process
.
put
({
'action'
:
'approve_payment_only'
})
log
.
info
(
'Refund [
%
d] successfully approved.'
,
refund_id
)
log
.
info
(
'Refund [
%
d] successfully approved.'
,
refund_id
)
except
:
# pylint: disable=bare-except
except
:
# pylint: disable=bare-except
...
@@ -275,7 +276,7 @@ def _process_refund(refund_ids, api_client, course_product):
...
@@ -275,7 +276,7 @@ def _process_refund(refund_ids, api_client, course_product):
return
False
return
False
else
:
else
:
try
:
try
:
_send_refund_notification
(
course_product
,
refunds_requiring_approval
)
return
_send_refund_notification
(
course_product
,
refunds_requiring_approval
)
except
:
# pylint: disable=bare-except
except
:
# pylint: disable=bare-except
# Unable to send notification to Support, do not break as this method is used by Signals
# Unable to send notification to Support, do not break as this method is used by Signals
log
.
warning
(
'Could not send support notification for refund.'
,
exc_info
=
True
)
log
.
warning
(
'Could not send support notification for refund.'
,
exc_info
=
True
)
...
@@ -284,7 +285,13 @@ def _process_refund(refund_ids, api_client, course_product):
...
@@ -284,7 +285,13 @@ def _process_refund(refund_ids, api_client, course_product):
def
_send_refund_notification
(
course_product
,
refund_ids
):
def
_send_refund_notification
(
course_product
,
refund_ids
):
""" Notify the support team of the refund request. """
"""
Notify the support team of the refund request.
Returns:
bool: True if we are able to send the notification. In this case that means we were able to create
a ZenDesk ticket
"""
tags
=
[
'auto_refund'
]
tags
=
[
'auto_refund'
]
...
@@ -292,11 +299,13 @@ def _send_refund_notification(course_product, refund_ids):
...
@@ -292,11 +299,13 @@ def _send_refund_notification(course_product, refund_ids):
# this is not presently supported with the external service.
# this is not presently supported with the external service.
raise
NotImplementedError
(
"Unable to send refund processing emails to support teams."
)
raise
NotImplementedError
(
"Unable to send refund processing emails to support teams."
)
# Build the information for the ZenDesk ticket
student
=
course_product
.
user
student
=
course_product
.
user
subject
=
_
(
"[Refund] User-Requested Refund"
)
subject
=
_
(
"[Refund] User-Requested Refund"
)
body
=
_generate_refund_notification_body
(
student
,
refund_ids
)
body
=
_generate_refund_notification_body
(
student
,
refund_ids
)
requester_name
=
student
.
profile
.
name
or
student
.
username
requester_name
=
student
.
profile
.
name
or
student
.
username
create_zendesk_ticket
(
requester_name
,
student
.
email
,
subject
,
body
,
tags
)
return
create_zendesk_ticket
(
requester_name
,
student
.
email
,
subject
,
body
,
tags
)
def
_generate_refund_notification_body
(
student
,
refund_ids
):
# pylint: disable=invalid-name
def
_generate_refund_notification_body
(
student
,
refund_ids
):
# pylint: disable=invalid-name
...
@@ -317,10 +326,15 @@ def _generate_refund_notification_body(student, refund_ids): # pylint: disable=
...
@@ -317,10 +326,15 @@ def _generate_refund_notification_body(student, refund_ids): # pylint: disable=
def
create_zendesk_ticket
(
requester_name
,
requester_email
,
subject
,
body
,
tags
=
None
):
def
create_zendesk_ticket
(
requester_name
,
requester_email
,
subject
,
body
,
tags
=
None
):
""" Create a Zendesk ticket via API. """
"""
Create a Zendesk ticket via API.
Returns:
bool: False if we are unable to create the ticket for any reason
"""
if
not
(
settings
.
ZENDESK_URL
and
settings
.
ZENDESK_USER
and
settings
.
ZENDESK_API_KEY
):
if
not
(
settings
.
ZENDESK_URL
and
settings
.
ZENDESK_USER
and
settings
.
ZENDESK_API_KEY
):
log
.
debug
(
'Zendesk is not configured. Cannot create a ticket.'
)
log
.
error
(
'Zendesk is not configured. Cannot create a ticket.'
)
return
return
False
# Copy the tags to avoid modifying the original list.
# Copy the tags to avoid modifying the original list.
tags
=
list
(
tags
or
[])
tags
=
list
(
tags
or
[])
...
@@ -356,8 +370,9 @@ def create_zendesk_ticket(requester_name, requester_email, subject, body, tags=N
...
@@ -356,8 +370,9 @@ def create_zendesk_ticket(requester_name, requester_email, subject, body, tags=N
# Check for HTTP codes other than 201 (Created)
# Check for HTTP codes other than 201 (Created)
if
response
.
status_code
!=
201
:
if
response
.
status_code
!=
201
:
log
.
error
(
'Failed to create ticket. Status: [
%
d], Body: [
%
s]'
,
response
.
status_code
,
response
.
content
)
log
.
error
(
'Failed to create ticket. Status: [
%
d], Body: [
%
s]'
,
response
.
status_code
,
response
.
content
)
return
False
else
:
else
:
log
.
debug
(
'Successfully created ticket.'
)
log
.
debug
(
'Successfully created ticket.'
)
except
Exception
:
# pylint: disable=broad-except
except
Exception
:
# pylint: disable=broad-except
log
.
exception
(
'Failed to create ticket.'
)
log
.
exception
(
'Failed to create ticket.'
)
return
return
False
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment