Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
ecommerce
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
edx
ecommerce
Commits
0f35b042
Commit
0f35b042
authored
Jun 03, 2015
by
Renzo Lucioni
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #121 from edx/renzo/support-initiated-refunds
Add Refund class method for creating targeted Refunds
parents
ef7bbf87
4965cd3c
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
86 additions
and
38 deletions
+86
-38
ecommerce/extensions/refund/api.py
+2
-16
ecommerce/extensions/refund/models.py
+46
-13
ecommerce/extensions/refund/tests/mixins.py
+14
-9
ecommerce/extensions/refund/tests/test_models.py
+24
-0
No files found.
ecommerce/extensions/refund/api.py
View file @
0f35b042
from
django.conf
import
settings
from
oscar.core.loading
import
get_model
from
oscar.core.loading
import
get_model
from
ecommerce.extensions.fulfillment.status
import
ORDER
from
ecommerce.extensions.fulfillment.status
import
ORDER
from
ecommerce.extensions.refund.status
import
REFUND
,
REFUND_LINE
Refund
=
get_model
(
'refund'
,
'Refund'
)
Refund
=
get_model
(
'refund'
,
'Refund'
)
...
@@ -59,19 +56,8 @@ def create_refunds(orders, course_id):
...
@@ -59,19 +56,8 @@ def create_refunds(orders, course_id):
product__attribute_values__attribute__code
=
'course_key'
,
product__attribute_values__attribute__code
=
'course_key'
,
product__attribute_values__value_text
=
course_id
)
product__attribute_values__value_text
=
course_id
)
# Only create a refund if there are line items to refund.
refund
=
Refund
.
create_with_lines
(
order
,
lines
)
if
lines
:
if
refund
is
not
None
:
total_credit_excl_tax
=
sum
([
line
.
line_price_excl_tax
for
line
in
lines
])
status
=
getattr
(
settings
,
'OSCAR_INITIAL_REFUND_STATUS'
,
REFUND
.
OPEN
)
refund
=
Refund
.
objects
.
create
(
order
=
order
,
user
=
order
.
user
,
status
=
status
,
total_credit_excl_tax
=
total_credit_excl_tax
)
status
=
getattr
(
settings
,
'OSCAR_INITIAL_REFUND_LINE_STATUS'
,
REFUND_LINE
.
OPEN
)
for
line
in
lines
:
RefundLine
.
objects
.
create
(
refund
=
refund
,
order_line
=
line
,
line_credit_excl_tax
=
line
.
line_price_excl_tax
,
quantity
=
line
.
quantity
,
status
=
status
)
refunds
.
append
(
refund
)
refunds
.
append
(
refund
)
return
refunds
return
refunds
ecommerce/extensions/refund/models.py
View file @
0f35b042
...
@@ -14,6 +14,7 @@ from ecommerce.extensions.payment.helpers import get_processor_class_by_name
...
@@ -14,6 +14,7 @@ from ecommerce.extensions.payment.helpers import get_processor_class_by_name
from
ecommerce.extensions.refund.exceptions
import
InvalidStatus
from
ecommerce.extensions.refund.exceptions
import
InvalidStatus
from
ecommerce.extensions.refund.status
import
REFUND
,
REFUND_LINE
from
ecommerce.extensions.refund.status
import
REFUND
,
REFUND_LINE
logger
=
logging
.
getLogger
(
__name__
)
logger
=
logging
.
getLogger
(
__name__
)
post_refund
=
get_class
(
'refund.signals'
,
'post_refund'
)
post_refund
=
get_class
(
'refund.signals'
,
'post_refund'
)
...
@@ -29,13 +30,12 @@ class StatusMixin(object):
...
@@ -29,13 +30,12 @@ class StatusMixin(object):
return
getattr
(
settings
,
self
.
pipeline_setting
)
return
getattr
(
settings
,
self
.
pipeline_setting
)
def
available_statuses
(
self
):
def
available_statuses
(
self
):
"""
Returns all possible statuses that this object can move to.
"""
"""
Returns all possible statuses that this object can move to.
"""
return
self
.
pipeline
.
get
(
self
.
status
,
())
return
self
.
pipeline
.
get
(
self
.
status
,
())
# pylint: disable=access-member-before-definition,attribute-defined-outside-init
# pylint: disable=access-member-before-definition,attribute-defined-outside-init
def
set_status
(
self
,
new_status
):
def
set_status
(
self
,
new_status
):
"""
"""Set a new status for this object.
Set a new status for this object.
If the requested status is not valid, then ``InvalidStatus`` is raised.
If the requested status is not valid, then ``InvalidStatus`` is raised.
"""
"""
...
@@ -68,12 +68,49 @@ class Refund(StatusMixin, TimeStampedModel):
...
@@ -68,12 +68,49 @@ class Refund(StatusMixin, TimeStampedModel):
@classmethod
@classmethod
def
all_statuses
(
cls
):
def
all_statuses
(
cls
):
"""
Returns all possible statuses for a refund.
"""
"""
Returns all possible statuses for a refund.
"""
return
list
(
getattr
(
settings
,
cls
.
pipeline_setting
)
.
keys
())
return
list
(
getattr
(
settings
,
cls
.
pipeline_setting
)
.
keys
())
@classmethod
def
create_with_lines
(
cls
,
order
,
lines
):
"""Given an order and order lines, creates a Refund with corresponding RefundLines.
Only creates RefundLines for unrefunded order lines.
Arguments:
order (order.Order): The order to which the newly-created refund corresponds.
lines (list of order.Line): Order lines to be refunded.
Returns:
None: If no unrefunded order lines have been provided.
Refund: With RefundLines corresponding to each given unrefunded order line.
"""
unrefunded_lines
=
[
line
for
line
in
lines
if
not
line
.
refund_lines
.
exists
()]
if
unrefunded_lines
:
status
=
getattr
(
settings
,
'OSCAR_INITIAL_REFUND_STATUS'
,
REFUND
.
OPEN
)
total_credit_excl_tax
=
sum
([
line
.
line_price_excl_tax
for
line
in
unrefunded_lines
])
refund
=
cls
.
objects
.
create
(
order
=
order
,
user
=
order
.
user
,
status
=
status
,
total_credit_excl_tax
=
total_credit_excl_tax
)
status
=
getattr
(
settings
,
'OSCAR_INITIAL_REFUND_LINE_STATUS'
,
REFUND_LINE
.
OPEN
)
for
line
in
unrefunded_lines
:
RefundLine
.
objects
.
create
(
refund
=
refund
,
order_line
=
line
,
line_credit_excl_tax
=
line
.
line_price_excl_tax
,
quantity
=
line
.
quantity
,
status
=
status
)
return
refund
@property
@property
def
num_items
(
self
):
def
num_items
(
self
):
"""
Returns the number of items in this refund.
"""
"""
Returns the number of items in this refund.
"""
num_items
=
0
num_items
=
0
for
line
in
self
.
lines
.
all
():
for
line
in
self
.
lines
.
all
():
num_items
+=
line
.
quantity
num_items
+=
line
.
quantity
...
@@ -81,20 +118,16 @@ class Refund(StatusMixin, TimeStampedModel):
...
@@ -81,20 +118,16 @@ class Refund(StatusMixin, TimeStampedModel):
@property
@property
def
can_approve
(
self
):
def
can_approve
(
self
):
"""
"""Returns a boolean indicating if this Refund can be approved."""
Returns a boolean indicating if this Refund can be approved.
"""
return
self
.
status
not
in
(
REFUND
.
COMPLETE
,
REFUND
.
DENIED
)
return
self
.
status
not
in
(
REFUND
.
COMPLETE
,
REFUND
.
DENIED
)
@property
@property
def
can_deny
(
self
):
def
can_deny
(
self
):
"""
"""Returns a boolean indicating if this Refund can be denied."""
Returns a boolean indicating if this Refund can be denied.
"""
return
self
.
status
==
settings
.
OSCAR_INITIAL_REFUND_STATUS
return
self
.
status
==
settings
.
OSCAR_INITIAL_REFUND_STATUS
def
_issue_credit
(
self
):
def
_issue_credit
(
self
):
"""
Issue a credit to the purchaser via the payment processor used for the original order.
"""
"""
Issue a credit to the purchaser via the payment processor used for the original order.
"""
# TODO Update this if we ever support multiple payment sources for a single order.
# TODO Update this if we ever support multiple payment sources for a single order.
source
=
self
.
order
.
sources
.
first
()
source
=
self
.
order
.
sources
.
first
()
...
@@ -102,7 +135,7 @@ class Refund(StatusMixin, TimeStampedModel):
...
@@ -102,7 +135,7 @@ class Refund(StatusMixin, TimeStampedModel):
processor
.
issue_credit
(
source
,
self
.
total_credit_excl_tax
,
self
.
currency
)
processor
.
issue_credit
(
source
,
self
.
total_credit_excl_tax
,
self
.
currency
)
def
_revoke_lines
(
self
):
def
_revoke_lines
(
self
):
"""
Revoke fulfillment for the lines in this Refund.
"""
"""
Revoke fulfillment for the lines in this Refund.
"""
if
revoke_fulfillment_for_refund
(
self
):
if
revoke_fulfillment_for_refund
(
self
):
self
.
set_status
(
REFUND
.
COMPLETE
)
self
.
set_status
(
REFUND
.
COMPLETE
)
logger
.
info
(
'Successfully revoked fulfillment for Refund [
%
d]'
,
self
.
id
)
logger
.
info
(
'Successfully revoked fulfillment for Refund [
%
d]'
,
self
.
id
)
...
...
ecommerce/extensions/refund/tests/mixins.py
View file @
0f35b042
...
@@ -26,10 +26,14 @@ class RefundTestMixin(object):
...
@@ -26,10 +26,14 @@ class RefundTestMixin(object):
self
.
honor_product
=
self
.
course
.
add_mode
(
'honor'
,
0
)
self
.
honor_product
=
self
.
course
.
add_mode
(
'honor'
,
0
)
self
.
verified_product
=
self
.
course
.
add_mode
(
'verified'
,
Decimal
(
10.00
),
id_verification_required
=
True
)
self
.
verified_product
=
self
.
course
.
add_mode
(
'verified'
,
Decimal
(
10.00
),
id_verification_required
=
True
)
def
create_order
(
self
,
user
=
None
):
def
create_order
(
self
,
user
=
None
,
multiple_lines
=
False
):
user
=
user
or
self
.
user
user
=
user
or
self
.
user
basket
=
BasketFactory
(
owner
=
user
)
basket
=
BasketFactory
(
owner
=
user
)
basket
.
add_product
(
self
.
verified_product
)
basket
.
add_product
(
self
.
verified_product
)
if
multiple_lines
:
basket
.
add_product
(
self
.
honor_product
)
order
=
create_order
(
basket
=
basket
,
user
=
user
)
order
=
create_order
(
basket
=
basket
,
user
=
user
)
order
.
status
=
ORDER
.
COMPLETE
order
.
status
=
ORDER
.
COMPLETE
order
.
save
()
order
.
save
()
...
@@ -41,14 +45,15 @@ class RefundTestMixin(object):
...
@@ -41,14 +45,15 @@ class RefundTestMixin(object):
self
.
assertEqual
(
refund
.
user
,
order
.
user
)
self
.
assertEqual
(
refund
.
user
,
order
.
user
)
self
.
assertEqual
(
refund
.
status
,
settings
.
OSCAR_INITIAL_REFUND_STATUS
)
self
.
assertEqual
(
refund
.
status
,
settings
.
OSCAR_INITIAL_REFUND_STATUS
)
self
.
assertEqual
(
refund
.
total_credit_excl_tax
,
order
.
total_excl_tax
)
self
.
assertEqual
(
refund
.
total_credit_excl_tax
,
order
.
total_excl_tax
)
self
.
assertEqual
(
refund
.
lines
.
count
(),
1
)
self
.
assertEqual
(
refund
.
lines
.
count
(),
order
.
lines
.
count
())
refund_line
=
refund
.
lines
.
first
()
refund_lines
=
refund
.
lines
.
all
()
line
=
order
.
lines
.
first
()
order_lines
=
order
.
lines
.
all
()
.
order_by
(
'refund_lines'
)
self
.
assertEqual
(
refund_line
.
status
,
settings
.
OSCAR_INITIAL_REFUND_LINE_STATUS
)
for
refund_line
,
order_line
in
zip
(
refund_lines
,
order_lines
):
self
.
assertEqual
(
refund_line
.
order_line
,
line
)
self
.
assertEqual
(
refund_line
.
status
,
settings
.
OSCAR_INITIAL_REFUND_LINE_STATUS
)
self
.
assertEqual
(
refund_line
.
line_credit_excl_tax
,
line
.
line_price_excl_tax
)
self
.
assertEqual
(
refund_line
.
order_line
,
order_line
)
self
.
assertEqual
(
refund_line
.
quantity
,
1
)
self
.
assertEqual
(
refund_line
.
line_credit_excl_tax
,
order_line
.
line_price_excl_tax
)
self
.
assertEqual
(
refund_line
.
quantity
,
order_line
.
quantity
)
def
create_refund
(
self
,
processor_name
=
'cybersource'
):
def
create_refund
(
self
,
processor_name
=
'cybersource'
):
refund
=
RefundFactory
()
refund
=
RefundFactory
()
...
...
ecommerce/extensions/refund/tests/test_models.py
View file @
0f35b042
...
@@ -4,6 +4,7 @@ from django.test import TestCase
...
@@ -4,6 +4,7 @@ from django.test import TestCase
import
mock
import
mock
from
oscar.apps.payment.exceptions
import
PaymentError
from
oscar.apps.payment.exceptions
import
PaymentError
from
oscar.core.loading
import
get_model
,
get_class
from
oscar.core.loading
import
get_model
,
get_class
from
oscar.test.newfactories
import
UserFactory
from
ecommerce.extensions.refund
import
models
from
ecommerce.extensions.refund
import
models
from
ecommerce.extensions.refund.exceptions
import
InvalidStatus
from
ecommerce.extensions.refund.exceptions
import
InvalidStatus
...
@@ -72,6 +73,29 @@ class RefundTests(RefundTestMixin, StatusTestsMixin, TestCase):
...
@@ -72,6 +73,29 @@ class RefundTests(RefundTestMixin, StatusTestsMixin, TestCase):
""" Refund.all_statuses should return all possible statuses for a refund. """
""" Refund.all_statuses should return all possible statuses for a refund. """
self
.
assertEqual
(
Refund
.
all_statuses
(),
self
.
pipeline
.
keys
())
self
.
assertEqual
(
Refund
.
all_statuses
(),
self
.
pipeline
.
keys
())
@ddt.data
(
False
,
True
)
def
test_create_with_lines
(
self
,
multiple_lines
):
"""
Given an order and order lines that have not been refunded, Refund.create_with_lines
should create a Refund with corresponding RefundLines.
"""
order
=
self
.
create_order
(
user
=
UserFactory
(),
multiple_lines
=
multiple_lines
)
refund
=
Refund
.
create_with_lines
(
order
,
list
(
order
.
lines
.
all
()))
self
.
assert_refund_matches_order
(
refund
,
order
)
def
test_create_with_lines_with_existing_refund
(
self
):
"""
Refund.create_with_lines should not create RefundLines for order lines
which have already been refunded.
"""
order
=
self
.
create_order
(
user
=
UserFactory
())
line
=
order
.
lines
.
first
()
RefundLineFactory
(
order_line
=
line
)
refund
=
Refund
.
create_with_lines
(
order
,
[
line
])
self
.
assertEqual
(
refund
,
None
)
@ddt.unpack
@ddt.unpack
@ddt.data
(
@ddt.data
(
(
REFUND
.
OPEN
,
True
),
(
REFUND
.
OPEN
,
True
),
...
...
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