Skip to content
Open
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
Empty file added pycon_web/apps/__init__.py
Empty file.
9 changes: 9 additions & 0 deletions pycon_web/apps/mixin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from django.db import models


class CreateUpdateMixin(models.Model):
created_at = models.DateTimeField(auto_now_add=True, editable=False)
last_updated_at = models.DateTimeField(auto_now=True, editable=False)

class Meta:
abstract = True
Empty file.
15 changes: 15 additions & 0 deletions pycon_web/apps/payments/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from django.contrib import admin

from .models import PaymentMethod, EasyPayPayment


class PaymentMethodAdmin(admin.ModelAdmin):
list_display = ['id', 'code', 'name']


class EasyPayPaymentAdmin(admin.ModelAdmin):
list_display = ['id', 'payment_method']


admin.site.register(PaymentMethod, PaymentMethodAdmin)
admin.site.register(EasyPayPayment, EasyPayPaymentAdmin)
Empty file.
Empty file.
167 changes: 167 additions & 0 deletions pycon_web/apps/payments/api/v1/easypay_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import logging
from enum import Enum

import zeep
from requests import Session
from zeep.transports import Transport


logger = logging.getLogger(__name__)


class EasyPayException(Exception):
def __init__(self, message):
super(EasyPayException, self).__init__(message)


class EasyPayIPNTransactionStatus(Enum):
SUCCESSFUL = 'PAID'
PENDING = 'PENDING'
FAILED = 'FAILED'
EXPIRED = 'EXPIRED'

def is_successful(self):
return self == EasyPayIPNTransactionStatus.SUCCESSFUL

def easypay_payment_status(self):
from pycon_web.apps.payments.models import TRANSACTION_STATUS_SUCCESS, TRANSACTION_STATUS_FAILURE, \
TRANSACTION_STATUS_PENDING
if self == EasyPayIPNTransactionStatus.SUCCESSFUL:
return TRANSACTION_STATUS_SUCCESS
elif self == EasyPayIPNTransactionStatus.PENDING:
return TRANSACTION_STATUS_PENDING
elif self == EasyPayIPNTransactionStatus.FAILED:
return TRANSACTION_STATUS_FAILURE
elif self == EasyPayIPNTransactionStatus.EXPIRED:
return TRANSACTION_STATUS_FAILURE


def easypay_default_wsdl_client(wsdl_url):
session = Session()
session.verify = False
transport = Transport(session=session)
client = zeep.Client(wsdl=wsdl_url, transport=transport, strict=False)
return client


class EasyPayAPIHelper:
def __init__(self, easypay_payment_method):
wsdl_url = easypay_payment_method.get_value_or_none('WSDL_URL')

self.username = easypay_payment_method.get_value_or_none('MERCHANT_USERNAME')
self.password = easypay_payment_method.get_value_or_none('MERCHANT_PASSWORD')
self.storeId = easypay_payment_method.get_value_or_none('STORE_ID')
self.binding_name = easypay_payment_method.get_value_or_none('SOAP_BINDING_NAME')
self.soap_endpoint = easypay_payment_method.get_value_or_none('SOAP_ENDPOINT')

self.transaction_id = None
self.transaction_datetime = None
self.transaction_expiry_datetime = None
self.easy_pay_voucher_code = None

self.client = easypay_default_wsdl_client(wsdl_url)

def initiate_transaction(self, amount, easypay_transaction_type, mobile_number, email_address=None):
initiateTransactionHackService = self.client.create_service(self.binding_name, self.soap_endpoint)

order_id = "Pycon-212" # setting it as constant because order functionality is missing
params_dict = {
'username': self.username,
'password': self.password,
'orderId': order_id,
'storeId': self.storeId,
'transactionAmount': str(amount),
'transactionType': easypay_transaction_type.api_representation(),
'msisdn': mobile_number,
'mobileAccountNo': mobile_number,
'emailAddress': email_address
}

logger.info('Sending request to EasyPay. Params: {}'.format(params_dict))

result = initiateTransactionHackService.initiateTransaction(**params_dict)

"""
Sample Response
<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Body>
<ns3:initiateTransactionResponseType xmlns:ns3="http://dto.transaction.partner.pg.systems.com/" xmlns:ns2="http://dto.common.pg.systems.com/">
<ns2:responseCode>0000</ns2:responseCode>
<orderId>Pycon-2</orderId>
<storeId>2824</storeId>
<paymentToken>592342</paymentToken>
<transactionDateTime>2017-08-18T15:47:25.937+05:00</transactionDateTime>
<paymentTokenExiryDateTime>2017-08-23T15:47:25.922+05:00</paymentTokenExiryDateTime>
</ns3:initiateTransactionResponseType>
</soapenv:Body>
</soapenv:Envelope>
"""

logger.info('Response from EasyPay : {}'.format(str(result)))

self.easy_pay_voucher_code = result['paymentToken']
self.transaction_id = result['transactionId']
self.transaction_datetime = result['transactionDateTime']
self.transaction_expiry_datetime = result['paymentTokenExiryDateTime']

status_code = EasyPayStatusCode(result['responseCode'])

if status_code.is_successful():
return self.easy_pay_voucher_code, self.transaction_id, self.transaction_expiry_datetime

logger.error(result['responseCode'])
raise EasyPayException(status_code.user_facing_message())


class EasyPayStatusCode(Enum):
PAYMENT_SUCCESSFUL = '0000'
SYSTEM_ERROR = '0001'
REQUIRED_FIELD_MISSING = '0002'
INVALID_ORDER_ID = '0003'
MERCHANT_DOES_NOT_EXIST = '0004'
MERCHANT_NOT_ACTIVE = '0005'
STORE_DOES_NOT_EXIST = '0006'
STORE_INACTIVE = '0007'

def user_facing_message(self):
if self == EasyPayStatusCode.PAYMENT_SUCCESSFUL:
return 'Payment Successful.'
elif self == EasyPayStatusCode.SYSTEM_ERROR:
return 'System Error.'
elif self == EasyPayStatusCode.REQUIRED_FIELD_MISSING:
return 'A required field is missing in the request.Please make sure your ' \
'mobile number is correct'
elif self == EasyPayStatusCode.INVALID_ORDER_ID:
return 'Invalid Order ID.'
elif self == EasyPayStatusCode.MERCHANT_DOES_NOT_EXIST:
return 'Merchant does not exist.'
elif self == EasyPayStatusCode.MERCHANT_NOT_ACTIVE:
return 'Merchant not active.'
elif self == EasyPayStatusCode.STORE_DOES_NOT_EXIST:
return 'Store does not exist.'
elif self == EasyPayStatusCode.STORE_INACTIVE:
return 'Store Inactive.'

def is_successful(self):
return self == EasyPayStatusCode.PAYMENT_SUCCESSFUL


class EasyPayPaymentMethod(Enum):
OTC = 0
MA = 1

def api_representation(self):
if self == EasyPayPaymentMethod.OTC:
return 'OTC'
elif self == EasyPayPaymentMethod.MA:
return 'MA'

def label(self):
if self == EasyPayPaymentMethod.OTC:
return 'Over the Counter'
elif self == EasyPayPaymentMethod.MA:
return 'Mobile Account'

def api_success_code(self):
return EasyPayStatusCode.PAYMENT_SUCCESSFUL
17 changes: 17 additions & 0 deletions pycon_web/apps/payments/api/v1/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from rest_framework import serializers

from pycon_web.apps.payments.models import PaymentMethod


class MakePaymentRequestSerializer(serializers.Serializer):
payment_method = serializers.SlugRelatedField(slug_field='code',
queryset=PaymentMethod.objects.all())
mobile_number = serializers.CharField(required=True)
transaction_amount = serializers.IntegerField(required=True)
email = serializers.CharField(required=False, allow_null=True, allow_blank=True)

def validate_mobile_number(self, mobile_number):
if len(mobile_number) != 11:
raise serializers.ValidationError("Invalid Mobile Number")

return mobile_number
7 changes: 7 additions & 0 deletions pycon_web/apps/payments/api/v1/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django.conf.urls import url

from pycon_web.apps.payments.api.v1.views import MakePayment

urlpatterns = [
url(r'^make_payment/?$', MakePayment.as_view(), name='make_payment')
]
72 changes: 72 additions & 0 deletions pycon_web/apps/payments/api/v1/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import logging

from rest_framework import status
from rest_framework.response import Response

logger = logging.getLogger(__name__)


DEFAULT_SUCCESS_MESSAGE = 'Operation successful.'
DEFAULT_FAILURE_MESSAGE = 'Something went wrong.'


def attempt_transaction(payment, email):
transaction_error_message = "Error while attempting transaction"
try:
logger.info('Attempting transactions')
success, message = payment.make_payment(email)

except Exception as e:
logger.error(str(e))

response = failure_response(failure_message=transaction_error_message,
data=get_payment_messages(payment))
return response

if success:

response = success_response(
{'payment_method': get_payment_messages(payment)}, message
)
else:
response = failure_response(
failure_message=message,
data=get_payment_messages(payment)
)

return response


def success_response(data=None, success_message=DEFAULT_SUCCESS_MESSAGE,
status_code=status.HTTP_200_OK):

response_data = {
'success': True,
'message': success_message,
'data': data
}
return Response(response_data, status=status_code)


def failure_response(failure_message=DEFAULT_FAILURE_MESSAGE,
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, data=None):
response_data = {
'status_code': status_code,
'success': False,
'message': failure_message,
'data': data
}
return Response(response_data, status=status_code)


def get_payment_messages(attrs):
payment_method = attrs.payment_method

if payment_method.code == 'EASYPAY_MA':
payment_method_message = "Easypaisa mobile account"
elif payment_method.code == 'EASYPAY_OTC':
payment_method_message = "Easypaisa shop"
else:
payment_method_message = str(attrs.payment_method.code)

return payment_method_message
34 changes: 34 additions & 0 deletions pycon_web/apps/payments/api/v1/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from rest_framework.exceptions import ValidationError
from rest_framework.views import APIView

from pycon_web.apps.payments.api.v1.serializers import MakePaymentRequestSerializer
from pycon_web.apps.payments.api.v1.utils import attempt_transaction, failure_response
from pycon_web.apps.payments.models import EasyPayPayment


class MakePayment(APIView):

def post(self, request):
try:
request_serializer = MakePaymentRequestSerializer(data=request.data)
request_serializer.is_valid(raise_exception=True)
request_data = request_serializer.validated_data

payment_method = request_data['payment_method']
mobile_number = request_data['mobile_number']
if not mobile_number:
raise Exception("Mobile number not found.")

transaction_amount = request_data['transaction_amount']
email = request_data.get('email', "pycon_arbisoft@gmail.com")

if payment_method.code in ['EASYPAY_MA', 'EASYPAY_OTC']:
payment = EasyPayPayment.create(transaction_amount, mobile_number,
payment_method)

response = attempt_transaction(payment, email)

return response

except ValidationError as ve:
return failure_response(failure_message=ve, status_code=400)
64 changes: 64 additions & 0 deletions pycon_web/apps/payments/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.8 on 2018-09-17 12:58
from __future__ import unicode_literals

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

initial = True

dependencies = [
]

operations = [
migrations.CreateModel(
name='Payment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('last_updated_at', models.DateTimeField(auto_now=True)),
('amount', models.FloatField(default=0)),
('description', models.TextField(blank=True, null=True)),
('mobile_number', models.CharField(blank=True, max_length=20, null=True)),
('transaction_status', models.CharField(choices=[(b'success', b'Transaction successful.'), (b'pending', b'Transaction in progress.'), (b'fail', b'Transaction failed.'), (b'initial', b'Transaction initialized.')], default=b'initial', max_length=25)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='PaymentMethod',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('last_updated_at', models.DateTimeField(auto_now=True)),
('name', models.CharField(max_length=100)),
('code', models.CharField(max_length=100, unique=True)),
('merchant_config', models.TextField()),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='EasyPayPayment',
fields=[
('payment_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='payments.Payment')),
('payment_token', models.CharField(blank=True, help_text=b'EasyPay generated token in case of OTC transactions', max_length=50, null=True)),
('transaction_id', models.CharField(blank=True, help_text=b'EasyPay generated Order ID (only for Mobile Account)', max_length=50, null=True)),
('transaction_expiry_datetime', models.DateTimeField(blank=True, null=True)),
],
options={
'abstract': False,
},
bases=('payments.payment',),
),
migrations.AddField(
model_name='payment',
name='payment_method',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='payments.PaymentMethod'),
),
]
Empty file.
Loading