-
Notifications
You must be signed in to change notification settings - Fork 4
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
Add PayuProvider.refund #4
Changes from all commits
e771371
8485023
2702eff
e6e66d5
d9b8740
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,4 +10,4 @@ Development Lead | |
Contributors | ||
------------ | ||
|
||
None yet. Why not be the first? | ||
* Radek Holý |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
import hashlib | ||
import json | ||
import logging | ||
import uuid | ||
from decimal import ROUND_HALF_UP, Decimal | ||
from urllib.parse import urljoin | ||
|
||
|
@@ -162,9 +163,11 @@ | |
self.payu_sandbox = kwargs.pop("sandbox", False) | ||
self.payu_base_url = kwargs.pop( | ||
"base_payu_url", | ||
"https://secure.snd.payu.com/" | ||
if self.payu_sandbox | ||
else "https://secure.payu.com/", | ||
( | ||
"https://secure.snd.payu.com/" | ||
if self.payu_sandbox | ||
else "https://secure.payu.com/" | ||
), | ||
) | ||
self.payu_auth_url = kwargs.pop( | ||
"auth_url", urljoin(self.payu_base_url, "/pl/standard/user/oauth/authorize") | ||
|
@@ -175,13 +178,17 @@ | |
self.payu_token_url = kwargs.pop( | ||
"token_url", urljoin(self.payu_api_url, "tokens/") | ||
) | ||
self.payu_api_order_url = urljoin(self.payu_api_url, "orders/") | ||
self.payu_api_orders_url = urljoin(self.payu_api_url, "orders/") | ||
self.payu_api_paymethods_url = urljoin(self.payu_api_url, "paymethods/") | ||
self.payu_widget_branding = kwargs.pop("widget_branding", False) | ||
self.payu_store_card = kwargs.pop("store_card", False) | ||
self.payu_shop_name = kwargs.pop("shop_name", "") | ||
self.grant_type = kwargs.pop("grant_type", "client_credentials") | ||
self.recurring_payments = kwargs.pop("recurring_payments", False) | ||
self.get_refund_description = kwargs.pop("get_refund_description") | ||
self.get_refund_ext_id = kwargs.pop( | ||
"get_refund_ext_id", lambda payment, amount: str(uuid.uuid4()) | ||
) | ||
|
||
# Use card on file paremeter instead of recurring. | ||
# PayU asks CVV2 every time with this setting which can be used for testing purposes. | ||
|
@@ -196,6 +203,9 @@ | |
) | ||
super(PayuProvider, self).__init__(*args, **kwargs) | ||
|
||
def _get_payu_api_order_url(self, order_id): | ||
return urljoin(self.payu_api_orders_url, order_id) | ||
|
||
def get_sig(self, payu_data): | ||
string = "".join( | ||
str(payu_data[key]) for key in sig_sorted_key_list if key in payu_data | ||
|
@@ -401,7 +411,7 @@ | |
payment_processor.pos_id = self.pos_id | ||
json_data = payment_processor.as_json() | ||
response_dict = self.post_request( | ||
self.payu_api_order_url, | ||
self.payu_api_orders_url, | ||
data=json.dumps(json_data), | ||
allow_redirects=False, | ||
) | ||
|
@@ -485,10 +495,7 @@ | |
def reject_order(self, payment): | ||
"Reject order" | ||
|
||
url = urljoin( | ||
self.payu_api_order_url, | ||
payment.transaction_id, | ||
) | ||
url = self._get_payu_api_order_url(payment.transaction_id) | ||
|
||
try: | ||
# If the payment have status WAITING_FOR_CONFIRMATION, it is needed to make two calls of DELETE | ||
|
@@ -547,29 +554,32 @@ | |
print(refunded_price, payment.total) | ||
if data["refund"]["status"] == "FINALIZED": | ||
payment.message += data["refund"]["reasonDescription"] | ||
if refunded_price == payment.total: | ||
if refunded_price == payment.captured_amount: | ||
payment.change_status(PaymentStatus.REFUNDED) | ||
else: | ||
payment.total -= refunded_price | ||
payment.captured_amount -= refunded_price | ||
payment.save() | ||
return HttpResponse("ok", status=200) | ||
else: | ||
raise Exception("Refund was not finelized", data) | ||
else: | ||
status = data["order"]["status"] | ||
status_map = { | ||
"COMPLETED": PaymentStatus.CONFIRMED, | ||
"PENDING": PaymentStatus.INPUT, | ||
"WAITING_FOR_CONFIRMATION": PaymentStatus.INPUT, | ||
"CANCELED": PaymentStatus.REJECTED, | ||
"NEW": PaymentStatus.WAITING, | ||
} | ||
if PaymentStatus.CONFIRMED and "totalAmount" in data["order"]: | ||
status = status_map[data["order"]["status"]] | ||
if ( | ||
status == PaymentStatus.CONFIRMED | ||
and "totalAmount" in data["order"] | ||
): | ||
payment.captured_amount = dequantize_price( | ||
data["order"]["totalAmount"], | ||
data["order"]["currencyCode"], | ||
) | ||
payment.change_status(status_map[status]) | ||
payment.change_status(status) | ||
Comment on lines
-559
to
+582
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess that that |
||
return HttpResponse("ok", status=200) | ||
return HttpResponse("not ok", status=500) | ||
|
||
|
@@ -593,6 +603,90 @@ | |
"request not recognized by django-payments-payu provider", status=500 | ||
) | ||
|
||
def refund(self, payment, amount=None): | ||
request_url = self._get_payu_api_order_url(payment.transaction_id) + "/refunds" | ||
|
||
request_data = { | ||
"refund": { | ||
"currencyCode": payment.currency, | ||
"description": self.get_refund_description( | ||
payment=payment, amount=amount | ||
), | ||
} | ||
} | ||
if amount is not None: | ||
request_data.setdefault("refund", {}).setdefault( | ||
"amount", quantize_price(amount, payment.currency) | ||
) | ||
ext_refund_id = self.get_refund_ext_id(payment=payment, amount=amount) | ||
if ext_refund_id is not None: | ||
request_data.setdefault("refund", {}).setdefault( | ||
"extRefundId", ext_refund_id | ||
) | ||
|
||
response = self.post_request(request_url, data=json.dumps(request_data)) | ||
|
||
payment_extra_data = json.loads(payment.extra_data or "{}") | ||
payment_extra_data_refund_responses = payment_extra_data.setdefault( | ||
"refund_responses", [] | ||
) | ||
payment_extra_data_refund_responses.append(response) | ||
payment.extra_data = json.dumps(payment_extra_data, indent=2) | ||
payment.save() | ||
|
||
try: | ||
refund = response["refund"] | ||
refund_id = refund["refundId"] | ||
except Exception: | ||
refund_id = None | ||
|
||
try: | ||
response_status = dict(response["status"]) | ||
response_status_code = response_status["statusCode"] | ||
except Exception: | ||
raise ValueError( | ||
f"invalid response to refund {refund_id or '???'} of payment {payment.id}: {response}" | ||
) | ||
if response_status_code != "SUCCESS": | ||
raise ValueError( | ||
f"refund {refund_id or '???'} of payment {payment.id} failed: " | ||
f"code={response_status.get('code', '???')}, " | ||
f"statusCode={response_status_code}, " | ||
f"codeLiteral={response_status.get('codeLiteral', '???')}, " | ||
f"statusDesc={response_status.get('statusDesc', '???')}" | ||
) | ||
if refund_id is None: | ||
raise ValueError( | ||
f"invalid response to refund of payment {payment.id}: {response}" | ||
) | ||
|
||
try: | ||
refund_order_id = response["orderId"] | ||
refund_status = refund["status"] | ||
refund_currency = refund["currencyCode"] | ||
refund_amount = dequantize_price(refund["amount"], refund_currency) | ||
except Exception: | ||
raise ValueError( | ||
f"invalid response to refund {refund_id} of payment {payment.id}: {response}" | ||
) | ||
if refund_order_id != payment.transaction_id: | ||
raise NotImplementedError( | ||
f"response of refund {refund_id} of payment {payment.id} containing a different order_id " | ||
f"not supported yet: {refund_order_id}" | ||
) | ||
if refund_status == "CANCELED": | ||
raise ValueError(f"refund {refund_id} of payment {payment.id} canceled") | ||
elif refund_status not in {"PENDING", "FINALIZED"}: | ||
raise ValueError( | ||
f"invalid status of refund {refund_id} of payment {payment.id}" | ||
) | ||
if refund_currency != payment.currency: | ||
raise NotImplementedError( | ||
f"refund {refund_id} of payment {payment.id} in different currency not supported yet: " | ||
f"{refund_currency}" | ||
) | ||
return refund_amount | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't like that we return the amount before the refund actually happens but this is how PayPal refunds work too so I decided to be consistent... |
||
|
||
|
||
class PaymentProcessor(object): | ||
"Payment processor" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that this is more aligned with https://github.com/jazzband/django-payments/blob/1222e0ab5ec51b525f5d0033833a67ae702df296/payments/models.py#L222 and https://github.com/jazzband/django-payments/blob/1222e0ab5ec51b525f5d0033833a67ae702df296/payments/paypal/__init__.py#L314
Although, the whole expected behavior of
Payment.refund
is probably still not clear: jazzband/django-payments#399