def notify_successful_loan_disbursal_transaction(loan_account, transaction_amount, transaction_charge, mpesa_transaction_id): logger.debug('notify_successful_loan_disbursal_transaction') logger.debug({ 'mpesa_transaction_id': mpesa_transaction_id, 'transaction_type': LoanTransactionType.LOAN_DISBURSAL, 'transaction_amount': transaction_amount, 'transaction_charge': transaction_charge, 'loan_account': loan_account.account_id, 'platform': 'mpesa' }) response = queue.send_sqs_message( "loan_transactions_processor", json.dumps({ 'loan_account_id': loan_account.account_id, 'transaction': { 'transaction_type': LoanTransactionType.LOAN_DISBURSAL, 'amount': transaction_amount, 'payment_platform': { 'fee': transaction_charge, 'type': PaymentPlatform.MPESA, 'transaction_id': mpesa_transaction_id } } })) logger.debug(response)
def notify_successful_loan_repayment_transaction(loan_account, repayment_amount, mpesa_transaction_id): logger.debug('notify_successful_loan_repayment_transaction') logger.debug({ 'mpesa_transaction_id': mpesa_transaction_id, 'transaction_type': LoanTransactionType.LOAN_REPAYMENT, 'loan_account': loan_account.account_id, 'repayment_amount': repayment_amount, 'platform': 'mpesa' }) response = queue.send_sqs_message( "loan_transactions_processor", json.dumps({ 'loan_account_id': loan_account.account_id, 'transaction': { 'transaction_type': LoanTransactionType.LOAN_REPAYMENT, 'repayment_amount': repayment_amount, 'payment_platform': { 'type': PaymentPlatform.MPESA, 'transaction_id': mpesa_transaction_id } } })) logger.debug(response)
def notify_loan_repayment_request(loan_account, repayment_amount): logger.info( f"notify_loan_repayment_request({loan_account.account_id}, {repayment_amount})" ) time_now = timezone.now() loan_transaction = LoanTransaction.objects.create( transaction_type=LoanTransactionType.LOAN_REPAYMENT, loan_account=loan_account, initiated_at=time_now, status=LoanTransactionStatus.PENDING_PROCESSING, amount=repayment_amount) logger.info( dict(transaction_type="LOAN_REPAYMENT", loan_account=loan_account, initiated_at=time_now, status="PENDING_PROCESSING", amount=repayment_amount)) response = queue.send_sqs_message( "loan_repayments", json.dumps({ 'loan_transaction_id': loan_transaction.transaction_id, 'loan_account_id': loan_account.account_id, 'repayment_amount': repayment_amount })) logger.debug(response) return loan_transaction
def create_dead_letter_queue(queue_url, dead_letter_queue_arn): # Create SQS client sqs = boto3.client('sqs') redrive_policy = { 'deadLetterTargetArn': dead_letter_queue_arn, 'maxReceiveCount': '10' } # Configure queue to send messages to dead letter queue sqs.set_queue_attributes( QueueUrl=queue_url, Attributes={'RedrivePolicy': json.dumps(redrive_policy)})
def extract_body(request, *args, **kwargs): try: data = json.loads(request.body) except Exception: data = {} logger.error(json.dumps( dict(message="Error decoding data submitted.", args=args, kwargs=kwargs)), exc_info=True) return data
def process_balance_check_result(data, mpesa_transaction): logger.debug(f"process_balance_check_result({data}, {mpesa_transaction})") with db_transaction.atomic(): cleaned = clean_balance_response(data) logger.debug(cleaned) logger.debug(mpesa_transaction.sender_account) if cleaned.get('result_code') != ResultCode.success: return ResultCode.internal_failure balance = MpesaAccountBalance.objects\ .get( request_id=mpesa_transaction.transaction_id, mpesa_account=mpesa_transaction.sender_account) mpesa_transaction.response_payload = balance.response_payload = json.dumps(cleaned) mpesa_transaction.save() balance.balance_updated_at = timezone.now() balance.status = 1 balance.save() balance_items = cleaned.get('balances') or [] if not balance_items: logger.debug("Balance items empty") return ResultCode.internal_failure logger.debug(f"balance_items={balance_items}") linked_bank_accounts = balance\ .mpesa_account\ .linked_bank_accounts\ .all() for linked_bank_account in linked_bank_accounts: bank_account = linked_bank_account.bank_account logger.debug(f"bank_account={bank_account}") bank_account.current_balance = 0 bank_account.balance_items = balance_items for _balance in balance_items: bank_account.current_balance += _balance['balance'] bank_account.save() return ResultCode.success
def test_get_user_is_accessible_via_token_successfully(self): response = self.client.get(reverse( 'users:get_user', kwargs={'user_account_id': self.user.user_account.account_id}), HTTP_AUTHORIZATION=self.HTTP_AUTHORIZATION) self.assertTrue(response.status_code, HTTPStatus.OK.value) self.assertEqual( response.json(), json.loads( json.dumps({ 'success': True, 'user_account': self.user_account.as_dict() })))
def notify_loan_application(loan_profile, loan_amount): logger.info( f"notify_loan_application({loan_profile.profile_id}, {loan_amount})") loan_application = LoanApplication.objects.create( loan_profile=loan_profile, applied_at=timezone.now(), payment_platform=PaymentPlatform.MPESA, amount=D(loan_amount)) response = queue.send_sqs_message( "loan_requests", json.dumps({'loan_application_id': loan_application.application_id})) logger.debug(response) return loan_application
def notify_c2b_mpesa_express_response(mpesa_transaction, personal_mpesa_account, business_paybill_account, amount): logger.info("Notifying c2b mpesa express response") # ----------- payload = { 'business_mpesa_account_id': business_paybill_account.account_id, 'personal_mpesa_account_id': personal_mpesa_account.account_id, 'mpesa_transaction_id': mpesa_transaction.transaction_id, 'transaction_type': 'c2b', 'amount': amount } logger.debug(payload) # ----------- return queue.send_sqs_message('mpesa_c2b_requests', json.dumps(payload))
def notify_b2c_mpesa_response(mpesa_transaction, personal_mpesa_account, business_paybill_account, transaction_amount, transaction_charge): logger.info("Notifying b2c response") # ----------- payload = { 'business_mpesa_account_id': business_paybill_account.account_id, 'personal_mpesa_account_id': personal_mpesa_account.account_id, 'mpesa_transaction_id': mpesa_transaction.transaction_id, 'transaction_type': 'b2c', 'transaction_amount': transaction_amount, 'transaction_charge': transaction_charge } logger.debug(payload) # ----------- return queue.send_sqs_message('mpesa_b2c_requests', json.dumps(payload))
def notify_verify_mpesa_account_request(loan_account): logger.info( f"notify_verify_mpesa_account_request({loan_account.account_id})") time_now = timezone.now() logger.info( dict(transaction_type="MPESA_VERIFICATION", initiated_at=time_now, status="PENDING_PROCESSING", amount=settings.MPESA_VERIFICATION_AMOUNT)) response = queue.send_sqs_message( "mpesa_verification_request", json.dumps({ 'loan_transaction_id': loan_transaction.transaction_id, 'loan_account_id': loan_account.account_id, 'repayment_amount': repayment_amount })) logger.debug(response) return loan_transaction
def b2c_transact(env="sandbox", live_url='https://api.safaricom.co.ke', app_key=None, app_secret=None, initiator_name=None, security_credential=None, command_id=None, amount=None, party_a=None, party_b=None, remarks=None, account_reference=None, queue_timeout_url=None, result_url=None, occassion=None, timeout=settings.MPESA_REQUESTS_TIMEOUT_SECONDS): logger.info("Sending cash for %s" % app_key) logger.info( json.dumps({ "env": env, "amount": amount, "party_a": party_a, "party_b": party_b, })) return B2C(live_url=live_url, env=env, app_key=app_key, app_secret=app_secret, timeout=timeout).transact( initiator_name=initiator_name, security_credential=security_credential, command_id=command_id, amount=amount, party_a=party_a, party_b=party_b, remarks=remarks, queue_timeout_url=queue_timeout_url, result_url=result_url, occassion=occassion)
def get_encrypted_text(plain_text, function_name=None): c = boto3.client( 'lambda', aws_access_key_id=settings.ENCRYPTION_INVOKER_ACCESS_KEY_ID, aws_secret_access_key=settings.ENCRYPTION_INVOKER_SECRET_ACCESS_KEY, region_name="us-east-1") logger.info("Boto client %s prepared" % c) try: logger.debug("Invoking lambda with access key id %s" % settings.ENCRYPTION_INVOKER_ACCESS_KEY_ID) response = c.invoke(FunctionName=function_name, InvocationType='RequestResponse', Payload=json.dumps({"text": plain_text})) logger.info("Response returned %s" % response) payload = json.loads(response['Payload'].read()) return payload['encrypted'] except (TypeError, KeyError) as e: logger.exception(e) return
def test_mpesa_b2c_works(self, b2c_transact, authenticate, get_encrypted_text): expected_mpesa_token_response = { "access_token": "JZIAHHsd43435jjKSu238007K98SO", "expires_in": "3599" } b2c_transact.return_value = { "ConversationID": "AG_20180326_00005ca7f7c21d608166", "OriginatorConversationID": "12363-1328499-6", "ResponseCode": "0", "ResponseDescription": "Accept the service request successfully." } get_encrypted_text.return_value = { "encrypted": "%s==" % ("ahh2iza7823kasksa" * 7) } authenticate.return_value = expected_mpesa_token_response response = self.client.post(reverse_lazy( 'mpesa:mpesa_api_authenticate', kwargs={ 'organization_id': self.organization.organization_id, }), data={'short_code': '263801'}, HTTP_AUTHORIZATION=self.HTTP_AUTHORIZATION) auth_response = response.json() self.assertTrue(auth_response, expected_mpesa_token_response) self.assertTrue(response.status_code, 200) response = self.client.post(reverse_lazy( 'mpesa:mpesa_b2c_transact', kwargs={ 'organization_id': self.organization.organization_id, }), { 'short_code': '263801', 'phone_number': '254703255755', 'amount': '200.00', 'notes': 'Loan to John' }, HTTP_AUTHORIZATION=self.HTTP_AUTHORIZATION) b2c_response = response.json() transaction = MpesaTransaction.objects.last() self.assertEqual( { 'success': True, 'message': 'B2C Transaction successful', 'transaction_id': transaction.third_party_transaction_id }, b2c_response) self.assertTrue(response.status_code, 200) request_payload = transaction.request_payload self.assertTrue('conversation_id' in request_payload) self.assertTrue('originator_conversation_id' in request_payload) self.assertTrue('response_code' in request_payload) self.assertTrue(request_payload['response_description'] == 'Accept the service request successfully.') #simulate a webhook from M-Pesa; does not require token as its csrf token exempted response = self.client.post(reverse_lazy( 'mpesa:mpesa_b2c_result_url', kwargs={ 'organization_id': self.organization.organization_id, 'reference': transaction.third_party_transaction_id }), json.dumps(B2C_RESPONSE), content_type="application/json") #refresh the model from the db transaction.refresh_from_db() self.assertEquals( { 'transaction_id': 'LHL41AHJ6G', 'transaction_recipient_full_name': 'John Doe', 'transaction_recipient_first_name': 'John', 'transaction_recipient_middle_name': '', 'transaction_recipient_last_name': 'Doe', 'transaction_recipient_phone': '254703267860', 'transaction_amount': 100, 'transaction_completed_at': '21.08.2017 12:01:59', 'b2c_utility_balance': 98834.0, 'b2c_working_account_balance': 100000.0, 'kyc_is_registered_mpesa_user': '******', 'conversation_id': 'AG_20170821_000049448b24712383de', 'originator_conversation_id': '14593-80515-2', 'result_code': 0, 'result_description': 'The service request has been accepted successfully.', }, transaction.response_payload)
def test_mpesa_c2b_works(self, c2b_register_url, authenticate, get_encrypted_text): c2b_register_url.return_value = { "ConversationID": "", "OriginatorCoversationID": "", "ResponseDescription": "success" } authenticate.return_value = { "access_token": "JZIAHHsd43435jjKSu238007K98SO", "expires_in": "3599" } get_encrypted_text.return_value = { "encrypted": "%s==" % ("ahh2iza7823kasksa" * 7) } response = self.client.post(reverse_lazy( 'mpesa:mpesa_api_authenticate', kwargs={ 'organization_id': self.organization.organization_id, }), {'short_code': '263801'}, HTTP_AUTHORIZATION=self.HTTP_AUTHORIZATION) auth_response = response.json() self.assertTrue('authentication_token' in auth_response) self.assertTrue(response.status_code, 200) response = self.client.post( reverse_lazy('mpesa:mpesa_c2b_register_urls', kwargs={ 'organization_id': self.organization.organization_id, }), { "short_code": self.paybill_c2b_account.identifier, }, HTTP_AUTHORIZATION=self.HTTP_AUTHORIZATION) self.assertTrue(response.json()['success']) mpesa_registered_url_reference = response.json( )['mpesa_registered_url'] mpesa_registered_url = MpesaAccountRegisteredURL.objects.get( reference=mpesa_registered_url_reference) self.assertEquals( mpesa_registered_url.validation_url, get_absolute_url("mpesa:mpesa_c2b_validation_url", kwargs={ "organization_id": self.organization.organization_id, "reference": mpesa_registered_url_reference })) self.assertEquals( mpesa_registered_url.confirmation_url, get_absolute_url("mpesa:mpesa_c2b_confirmation_url", kwargs={ "organization_id": self.organization.organization_id, "reference": mpesa_registered_url_reference })) #simulate a webhooks from M-Pesa response = self.client.post(reverse_lazy( 'mpesa:mpesa_c2b_validation_url', kwargs={ 'organization_id': self.organization.organization_id, 'reference': mpesa_registered_url.reference }), json.dumps({ "TransactionType": "", "TransID": "LHG31AA5TX", "TransTime": "20190816190243", "TransAmount": "200.00", "BusinessShortCode": self.paybill_c2b_account.identifier, "BillRefNumber": "account", "InvoiceNumber": "", "OrgAccountBalance": "", "ThirdPartyTransID": "", "MSISDN": self.mpesa_account.identifier, "FirstName": self.mpesa_account.first_name, "MiddleName": "", "LastName": self.mpesa_account.last_name }), content_type="application/json") self.assertEqual(response.json(), { 'ResultCode': ResultCode.success, 'ResultDesc': 'Accepted' }) transaction = MpesaTransaction.objects.last() transaction.refresh_from_db() self.assertEqual(transaction.result_code, None) response = self.client.post(reverse_lazy( 'mpesa:mpesa_c2b_confirmation_url', kwargs={ 'organization_id': self.organization.organization_id, 'reference': mpesa_registered_url_reference }), json.dumps({ "TransID": "LHG31AA5TX", "TransactionType": "", "TransTime": "20190816190243", "TransAmount": "200.00", "BusinessShortCode": self.paybill_c2b_account.identifier, "BillRefNumber": "account", "InvoiceNumber": "", "OrgAccountBalance": "", "ThirdPartyTransID": "", "MSISDN": self.mpesa_account.identifier, "FirstName": self.mpesa_account.first_name, "MiddleName": "", "LastName": self.mpesa_account.last_name }), content_type="application/json") self.assertEqual(response.json(), { 'ResultCode': ResultCode.success, 'ResultDesc': 'Accepted' }) transaction.refresh_from_db() self.assertEqual(transaction.result_code, ResultCode.success)
} }, 'formatters': { 'default': { # exact format is not important, this is the minimum information 'format': '%(asctime)s %(name)-12s %(levelname)-8s %(message)s', }, 'django.server': DEFAULT_LOGGING['formatters']['django.server'], 'json_formatter': { 'format': json.dumps({ "name": "%(name)-12s", "level": "%(levelname)-8s", "time": "%(asctime)s", "level_name": "%(levelname)s", "module": "%(module)s", "date": "%(asctime)s", "data": "%(message)s", "line_no": "%(lineno)d", "path": "%(pathname)s" }), 'class': 'pythonjsonlogger.jsonlogger.JsonFormatter', 'datefmt': "%Y-%m-%d %H:%M:%S", }, 'main_formatter': { 'format': '%(levelname)s:%(name)s: %(message)s ' '(%(asctime)s; %(filename)s:%(lineno)d)', 'datefmt': "%Y-%m-%d %H:%M:%S", },
def test_mpesa_user_can_register_and_borrow(self, b2c_transact, authenticate, get_encrypted_text, mpesa_express_stk_push): b2c_transact.return_value = { "ConversationID": "AG_20180326_00005ca7f7c21d608166", "OriginatorConversationID": "12363-1328499-6", "ResponseCode": "0", "ResponseDescription": "Accept the service request successfully." } authenticate.return_value = { "access_token": "JZIAHHsd43435jjKSu238007K98SO", "expires_in": "3599" } get_encrypted_text.return_value = { "encrypted": "%s==" % ("ahh2iza7823kasksa" * 7) } mpesa_express_stk_push.return_value = { "MerchantRequestID": "21605-295434-4", "CheckoutRequestID": "ws_CO_04112017184930742", "ResponseCode": "0", "ResponseDescription": "Success. Request accepted for processing", "CustomerMessage": "Success. Request accepted for processing" } #------------- Setup # 3. Now, borrow the amount response = self.client.post(reverse_lazy( 'mpesa:mpesa_express_c2b_push', kwargs={'organization_id': self.organization.organization_id}), data={ 'short_code': '263801', 'phone_number': self.user.username, 'amount': 320, 'notes': 'My notes' }, HTTP_AUTHORIZATION=self.HTTP_AUTHORIZATION) transaction = MpesaTransaction.objects.last() self.assertEqual( response.json(), json.loads( json.dumps({ 'success': True, 'transaction': transaction.as_dict() }))) # 3. Simulate callback from M-Pesa response = self.client.post(reverse_lazy( 'mpesa:mpesa_c2b_stk_push_callback_url', kwargs={ 'organization_id': self.organization.organization_id, 'reference': transaction.third_party_transaction_id }), json.dumps(C2B_STK_MPESA_EXPRESS_RESPONSE), content_type="application/json") self.assertEqual(response.json(), { 'ResultCode': 0, 'ResultDesc': 'Accepted' })