def check_invoice_status(invoice_token): headers = _create_headers() endpoint = MPOWER_ENDPOINT_CHECK_INVOICE_STATUS.format(invoice_token) try: response = requests.get(endpoint, headers=headers) decoded_response = response.json() if decoded_response['response_code'] != MPOWER_RESPONSE_SUCCESS: message = 'ERROR - MPOWER (check_invoice_status for token {}): transaction not found' raise MPowerException if decoded_response['status'] != MPOWER_STATUS_COMPLETED: message = 'ERROR - MPOWER (check_invoice_status for token {}): transaction not completed' raise MPowerException return True except (RequestException, KeyError, ValueError) as e: message = 'ERROR - MPOWER (check_invoice_status for token {}): {}' log_error(message.format(invoice_token, repr(e))) except MPowerException: log_error(message.format(invoice_token)) return False
def end_previous_pricing(): try: previous_pricing = Pricing.objects.get(end__isnull=True) previous_pricing.end = timezone.now() previous_pricing.save() except ObjectDoesNotExist: log_error('ERROR - Failed to end previous pricing.')
def _send_message(mobile_number, content): headers = {'content-type': 'application/json', 'Host': 'api.smsgh.com'} payload = { 'From': 'Kitiwa', 'To': mobile_number, 'Content': content, 'RegisteredDelivery': 'true' } try: response = requests.post(SMSGH_SEND_MESSAGE, data=json.dumps(payload), headers=headers, auth=HTTPBasicAuth(SMSGH_CLIENT_ID, SMSGH_CLIENT_SECRET)) decoded_response = response.json() response_status = decoded_response['Status'] message_id = decoded_response['MessageId'] except KeyError: message = 'ERROR - SMSGH: Failed to send message to {}. '\ 'Status: {}. Message: {}.' log_error(message.format(mobile_number, response_status, content)) message_id = 'N/A' except (RequestException, ValueError) as e: message = 'ERROR - SMSGH: Failed to send message to {}.({}).' log_error(message.format(mobile_number, repr(e))) message_id = response_status = 'N/A' return response_status, message_id
def check_balance(): payload = { 'api_id': SMSGH_CLIENT_ID, 'user': SMSGH_USER, 'password': SMSGH_PASSWORD } try: response = requests.get(SMSGH_CHECK_BALANCE, params=payload) balance = float(re.search(r'\d+.\d+', response.text).group(0)) except (RequestException, IndexError) as e: message = 'ERROR - SMSGH: Failed to check balance ({}).' log_error(message.format(repr(e))) return return balance
def send_mail_to_admins(subject, body): sg = sendgrid.SendGridClient(SENDGRID_USERNAME, SENDGRID_PASSWORD) recipients = User.objects.filter(is_staff=True) mails = [m.email for m in recipients] message = sendgrid.Mail() message.add_to(mails) message.set_from(SENDGRID_EMAIL_FROM) message.set_subject(subject) message.set_text(body) try: sg.send(message) except sendgrid.SendGridError as e: log_error('ERROR - Sendgrid: Failed to send mail to admins ({})'.format(e)) pass
def send_mail_to_admins(subject, body): sg = sendgrid.SendGridClient(SENDGRID_USERNAME, SENDGRID_PASSWORD) recipients = User.objects.filter(is_staff=True) mails = [m.email for m in recipients] message = sendgrid.Mail() message.add_to(mails) message.set_from(SENDGRID_EMAIL_FROM) message.set_subject(subject) message.set_text(body) try: sg.send(message) except sendgrid.SendGridError as e: log_error( 'ERROR - Sendgrid: Failed to send mail to admins ({})'.format(e)) pass
def opr_token_request(amount, mpower_phone_number, invoice_desc='', store_name='Kitiwa'): # convert to local phone number format mpower_phone_number = '0{}'.format(mpower_phone_number[4::]) payload = { 'invoice_data': { 'invoice': { 'total_amount': amount, 'description': invoice_desc }, 'store': { 'name': store_name } }, 'opr_data': { 'account_alias': mpower_phone_number } } headers = _create_headers() try: response = requests.post(MPOWER_ENDPOINT_OPR_TOKEN_REQUEST, data=json.dumps(payload), headers=headers) decoded_response = response.json() response_code = decoded_response['response_code'] response_text = decoded_response['response_text'] opr_token = decoded_response['token'] invoice_token = decoded_response['invoice_token'] except KeyError as e: opr_token = invoice_token = 'N/A' except RequestException as e: message = 'ERROR - MPOWER (opr_token_request for no: {}): {}' log_error(message.format(mpower_phone_number, repr(e))) response_code = opr_token = invoice_token = 'N/A' response_text = repr(e) return response_code, response_text, opr_token, invoice_token
def opr_charge_action(opr_token, confirm_token): headers = _create_headers() payload = {'token': opr_token, 'confirm_token': confirm_token} try: response = requests.post(MPOWER_ENDPOINT_OPR_TOKEN_CHARGE, data=json.dumps(payload), headers=headers) decoded_response = response.json() response_code = decoded_response['response_code'] response_text = decoded_response['response_text'] except (RequestException, KeyError) as e: message = 'ERROR - MPOWER (opr_charge_action for token {}): {}' log_error(message.format(opr_token, repr(e))) response_code = "N/A" response_text = repr(e) return response_code, response_text
def get_bitstamp_exchange_rate(): try: get_rate_call = requests.get(s.BITSTAMP_API_TICKER) if get_rate_call.status_code == 200: try: return get_rate_call.json().get('ask') except AttributeError: log_error('ERROR - BITSTAMP: Ask rate not present in 200 response') return None else: log_error('ERROR - BITSTAMP: Call returned response code: ' + str(get_rate_call.status_code)) return None except requests.RequestException as e: log_error('ERROR - BLOCKCHAIN: Call gave a request exception ' + repr(e)) return None
def post(self, request): params = self.get_params(request) if None in params.values(): log_error('ERROR - BITSTAMP (' + self.url + '): Missing nonce/signature in params') return Response({'error': 'Invalid data received'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) try: r = requests.post(self.url, data=params) if r.status_code == 200: return Response(r.json()) else: log_error('ERROR - BITSTAMP: Call returned response code: ' + str(r.status_code)) return Response(r.json(), status=status.HTTP_500_INTERNAL_SERVER_ERROR) except requests.RequestException as e: log_error('ERROR - BITSTAMP: Call gave a request exception ' + repr(e)) return Response({'error': 'Unable to retrieve balance (request exception)'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def get_balance(request): password = request.POST.get('password', None) if password is None: return Response({'error': 'Password required'}, status=status.HTTP_400_BAD_REQUEST) try: r = requests.get(s.BLOCKCHAIN_API_BALANCE, params={'password': password}) if r.status_code == 200: try: btc = decimal.Decimal(r.json().get('balance')) / s.ONE_SATOSHI except AttributeError: log_error( 'ERROR - BLOCKCHAIN (get_balance): "balance" not present in 200 response' ) return Response(r.json(), status=status.HTTP_403_FORBIDDEN) rate = get_blockchain_exchange_rate() if rate is not None: usd = btc * decimal.Decimal(rate) return Response({ 'btc': btc, 'usd': '{0:.2f}'.format(usd), 'rate': rate }) else: return Response({ 'btc': btc, 'usd': 'Unable to retrieve rate', 'rate': 'Unable to retrieve rate' }) else: log_error( 'ERROR - BLOCKCHAIN (get_balance): Call returned response code: ' + str(r.status_code)) return Response( {'error': 'Unable to retrieve balance (non-200 response)'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) except requests.RequestException as e: log_error( 'ERROR - BLOCKCHAIN (get_balance): Call gave a request exception ' + repr(e)) return Response( {'error': 'Unable to retrieve balance (request exception)'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def get_bitstamp_exchange_rate(): try: get_rate_call = requests.get(s.BITSTAMP_API_TICKER) if get_rate_call.status_code == 200: try: return get_rate_call.json().get('ask') except AttributeError: log_error( 'ERROR - BITSTAMP: Ask rate not present in 200 response') return None else: log_error('ERROR - BITSTAMP: Call returned response code: ' + str(get_rate_call.status_code)) return None except requests.RequestException as e: log_error('ERROR - BLOCKCHAIN: Call gave a request exception ' + repr(e)) return None
def get_blockchain_exchange_rate(): try: get_rate_call = requests.get(s.BLOCKCHAIN_TICKER) if get_rate_call.status_code == 200: try: return get_rate_call.json().get('USD').get('buy') except AttributeError: log_error( 'ERROR - BLOCKCHAIN (get_blockchain_exchange_rate): USD[buy] not present in 200 response' ) return None else: log_error( 'ERROR - BLOCKCHAIN (get_blockchain_exchange_rate): Call returned response code: ' + str(get_rate_call.status_code)) return None except requests.RequestException as e: log_error( 'ERROR - BLOCKCHAIN (get_blockchain_exchange_rate): Call gave a request exception ' + repr(e)) return None
def post(self, request): params = self.get_params(request) if None in params.values(): log_error('ERROR - BITSTAMP (' + self.url + '): Missing nonce/signature in params') return Response({'error': 'Invalid data received'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) try: r = requests.post(self.url, data=params) if r.status_code == 200: return Response(r.json()) else: log_error('ERROR - BITSTAMP: Call returned response code: ' + str(r.status_code)) return Response(r.json(), status=status.HTTP_500_INTERNAL_SERVER_ERROR) except requests.RequestException as e: log_error('ERROR - BITSTAMP: Call gave a request exception ' + repr(e)) return Response( {'error': 'Unable to retrieve balance (request exception)'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def get_blockchain_exchange_rate(): try: get_rate_call = requests.get(s.BLOCKCHAIN_TICKER) if get_rate_call.status_code == 200: try: return get_rate_call.json().get('USD').get('buy') except AttributeError: log_error( 'ERROR - BLOCKCHAIN (get_blockchain_exchange_rate): USD[buy] not present in 200 response' ) return None else: log_error( 'ERROR - BLOCKCHAIN (get_blockchain_exchange_rate): Call returned response code: ' + str(get_rate_call.status_code) ) return None except requests.RequestException as e: log_error( 'ERROR - BLOCKCHAIN (get_blockchain_exchange_rate): Call gave a request exception ' + repr(e) ) return None
def get_balance(request): password = request.POST.get('password', None) if password is None: return Response({'error': 'Password required'}, status=status.HTTP_400_BAD_REQUEST) try: r = requests.get(s.BLOCKCHAIN_API_BALANCE, params={'password': password}) if r.status_code == 200: try: btc = decimal.Decimal(r.json().get('balance')) / s.ONE_SATOSHI except AttributeError: log_error( 'ERROR - BLOCKCHAIN (get_balance): "balance" not present in 200 response' ) return Response(r.json(), status=status.HTTP_403_FORBIDDEN) rate = get_blockchain_exchange_rate() if rate is not None: usd = btc * decimal.Decimal(rate) return Response({'btc': btc, 'usd': '{0:.2f}'.format(usd), 'rate': rate}) else: return Response({'btc': btc, 'usd': 'Unable to retrieve rate', 'rate': 'Unable to retrieve rate'}) else: log_error( 'ERROR - BLOCKCHAIN (get_balance): Call returned response code: ' + str(r.status_code) ) return Response({'error': 'Unable to retrieve balance (non-200 response)'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) except requests.RequestException as e: log_error( 'ERROR - BLOCKCHAIN (get_balance): Call gave a request exception ' + repr(e) ) return Response({'error': 'Unable to retrieve balance (request exception)'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def get_usd_ghs(request): try: r = requests.get(s.OPEN_EXCHANGE_RATE_API_URL) if r.status_code == 200: try: rate = r.json().get('rates').get('GHS') return Response({'rate': rate}) except AttributeError: log_error( 'ERROR - FOREX (get_usd_ghs): rates[GHS] not present in 200 response' ) return Response(r.json(), status=status.HTTP_500_INTERNAL_SERVER_ERROR) else: log_error( 'ERROR - FOREX (get_usd_ghs): Call returned response code: ' + str(r.status_code) ) return Response(r.json(), status=r.status_code) except requests.RequestException as e: log_error( 'ERROR - FOREX (get_usd_ghs): Call gave a request exception ' + repr(e) ) return Response(r.json(), status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def backend_callback(request): try: invoice = request.DATA.get('invoice') transaction_id = request.DATA.get('transaction_id') transaction_reference = request.DATA.get('transaction_reference') notification_private_key = request.DATA.get('notification_private_key') merchant_key = request.DATA.get('merchant_key') amount = float(request.DATA.get('amount')) transaction_datetime = request.DATA.get('transaction_datetime') # find transaction transaction = Transaction.objects.get(transaction_uuid=invoice) if transaction.state != Transaction.INIT: message = 'ERROR - PAGA (backend): request refers to transaction {} in state {}. {}' message = message.format(transaction.id, transaction.state, request.DATA) raise PagaException # validate merchant key if merchant_key != PAGA_MERCHANT_KEY: message = 'ERROR - PAGA (backend): request with invalid merchant key ({}) for transaction {}. {}' message = message.format(merchant_key, transaction.id, request.DATA) raise PagaException # validate private key if notification_private_key != PAGA_PRIVATE_KEY: message = 'ERROR - PAGA (backend): request with invalid private key ({}) for transaction {}. {}' message = message.format(notification_private_key, transaction.id, request.DATA) raise PagaException # double check amount if amount != transaction.amount_ngn: message = 'ERROR - PAGA (backend): amount for transaction {} does not match database value (db: {}, paga: {}). {}' message = message.format(transaction.id, transaction.amount_ngn, amount, request.DATA) raise PagaException # create PagaPayment paga_payment = PagaPayment( transaction=transaction, paga_transaction_reference=transaction_reference, paga_transaction_id=transaction_id, paga_processed_at=transaction_datetime, status='SUCCESS') # update transaction and paga payment (all-or-nothing) with dbtransaction.atomic(): transaction.set_paid() paga_payment.save() sendgrid_mail.notify_admins_paid() return Response({'detail': 'Success'}) except PagaException as e: log_error(message) except Transaction.DoesNotExist as e: message = 'ERROR - PAGA (backend): no transaction found for uuid {}, {}. {}' log_error(message.format(invoice, e, request.DATA)) except (KeyError, TypeError, ValueError) as e: message = 'ERROR - PAGA (backend): received invalid payment notification, {}, {}' log_error(message.format(e, request.DATA)) return Response({'detail': 'Error'}, status.HTTP_400_BAD_REQUEST)
def user_callback(request): try: paga_status = request.DATA.get('status') merchant_key = request.DATA.get('key') transaction_id = request.DATA.get('transaction_id') process_code = request.DATA.get('process_code') invoice = request.DATA.get('invoice') kitiwa_reference = request.GET.get('reference', 'error') # could be used for double checking the value # total = request.DATA.get('total') # not needed for now # fee = request.DATA.get('fee') # test = request.DATA.get('test') # message = request.DATA.get('message') # exchangeRate = request.DATA.get('exchange_rate') # reference_number = request.DATA.get('reference_number') # currency = request.DATA.get('currency') # reference = request.DATA.get('reference') # customer_account = request.DATA.get('customer_account') http_prefix = 'https://' if ENV == ENV_LOCAL: http_prefix = 'http://' # incomplete request if (paga_status is None or merchant_key is None or transaction_id is None or process_code is None or invoice is None): raise ValueError # incorrect merchant key if merchant_key != PAGA_MERCHANT_KEY and paga_status != 'ERROR_AUTHENTICATION': raise PagaException # return redirect(http_prefix + ENV_SITE_MAPPING[ENV][SITE_USER] + '/#!/failed?error=merchantkey') # successful payment if paga_status == 'SUCCESS': return redirect(http_prefix + ENV_SITE_MAPPING[ENV][SITE_USER] + '/#!/thanks?reference=' + kitiwa_reference + '&pagaTransactionId=' + transaction_id) # failed payment else: # in case of erros during authentication with paga, response contains mostly blanks and cannot be persisted if paga_status != 'ERROR_AUTHENTICATION': # TODO: put this in messaging queue transaction = Transaction.objects.get(transaction_uuid=invoice, state=Transaction.INIT) paga_payment = PagaPayment( transaction=transaction, paga_transaction_reference=process_code, paga_transaction_id=transaction_id, status=paga_status) with dbtransaction.atomic(): transaction.set_declined() paga_payment.save() return redirect(http_prefix + ENV_SITE_MAPPING[ENV][SITE_USER] + '/#!/failed?reference=' + kitiwa_reference + '&status=' + paga_status) except (TypeError, ValueError) as e: message = 'ERROR - PAGA (user redirect): received invalid payment notification, {}, {}' log_error(message.format(e, request.DATA)) except Transaction.DoesNotExist as e: message = 'ERROR - PAGA (user redirect): no transaction in state INIT found for uuid {}, {}. {}' log_error(message.format(invoice, e, request.DATA)) except PagaException: message = 'ERROR - PAGA (user redirect): request with invalid merchant key ({}) for transaction {}. {}' log_error(message.format(merchant_key, transaction_id, request.DATA)) # TODO: better to put a redirect here as well? # return redirect(http_prefix + ENV_SITE_MAPPING[ENV][SITE_USER] + '/#!/failed?error=unknown') return Response({'detail': 'Error'}, status.HTTP_400_BAD_REQUEST)
def process_transactions(ids, password1, password2): # TODO: security concerns sending pw1, pw2? # TODO: need to make this a transaction for atomicity or isolation? try: transactions = Transaction.objects.filter(id__in=ids) except Transaction.DoesNotExist: log_error('ERROR - ACCEPT: Invalid ID') return try: # Verify payment with payment provider for t in transactions: if not t.verify_payment(): raise AcceptException( 'ERROR - ACCEPT: Transaction {} could not be verified as paid'.format(t.id) ) # Make sure that there are enough credits in smsgh account to send out confirmation sms smsgh_balance = smsgh.check_balance() if smsgh_balance is None: raise AcceptException('ERROR - ACCEPT: Failed to query smsgh balance') elif smsgh_balance < len(transactions): raise AcceptException('ERROR - ACCEPT: Not enough credit on SMSGH account') # USD-BTC CONVERSION # Get latest exchange rate rate = get_blockchain_exchange_rate() if rate is None: raise AcceptException('ERROR - ACCEPT: Failed to retrieve exchange rate') # Update amount_btc based on latest exchange rate for t in transactions: t.update_btc(rate) # Combine transactions with same wallet address combined_transactions = utils.consolidate_transactions(transactions) # Prepare request and send recipients = utils.create_recipients_string(combined_transactions) btc_transfer_request = requests.get(BLOCKCHAIN_API_SENDMANY, params={ 'password': password1, 'second_password': password2, 'recipients': recipients, 'note': BITCOIN_NOTE }) if btc_transfer_request.json().get('error'): raise AcceptException( 'ERROR - ACCEPT (btc transfer request to blockchain): {}' .format(btc_transfer_request.json()) ) transactions.update(state=Transaction.PROCESSED, processed_at=timezone.now()) except AcceptException as e: log_error(e) transactions.update(state=Transaction.PAID) return repr(e) except requests.RequestException as e: message = 'ERROR - ACCEPT (btc transfer request to blockchain): {}'.format(repr(e)) log_error(message) transactions.update(state=Transaction.PAID) return message combined_sms_confirm = utils.consolidate_notification_sms(transactions) # send out confirmation SMS for number, reference_numbers in combined_sms_confirm.iteritems(): response_status, message_id = smsgh.send_message_confirm( mobile_number=number, reference_numbers=reference_numbers ) for t in transactions.filter(notification_phone_number=number): t.update_after_sms_notification( response_status, message_id ) return 'SUCCESS'
def process_transactions(ids, password1, password2): # TODO: security concerns sending pw1, pw2? # TODO: need to make this a transaction for atomicity or isolation? try: transactions = Transaction.objects.filter(id__in=ids) except Transaction.DoesNotExist: log_error('ERROR - ACCEPT: Invalid ID') return try: # Verify payment with payment provider for t in transactions: if not t.verify_payment(): raise AcceptException( 'ERROR - ACCEPT: Transaction {} could not be verified as paid' .format(t.id)) # Make sure that there are enough credits in smsgh account to send out confirmation sms smsgh_balance = smsgh.check_balance() if smsgh_balance is None: raise AcceptException( 'ERROR - ACCEPT: Failed to query smsgh balance') elif smsgh_balance < len(transactions): raise AcceptException( 'ERROR - ACCEPT: Not enough credit on SMSGH account') # USD-BTC CONVERSION # Get latest exchange rate rate = get_blockchain_exchange_rate() if rate is None: raise AcceptException( 'ERROR - ACCEPT: Failed to retrieve exchange rate') # Update amount_btc based on latest exchange rate for t in transactions: t.update_btc(rate) # Combine transactions with same wallet address combined_transactions = utils.consolidate_transactions(transactions) # Prepare request and send recipients = utils.create_recipients_string(combined_transactions) btc_transfer_request = requests.get(BLOCKCHAIN_API_SENDMANY, params={ 'password': password1, 'second_password': password2, 'recipients': recipients, 'note': BITCOIN_NOTE }) if btc_transfer_request.json().get('error'): raise AcceptException( 'ERROR - ACCEPT (btc transfer request to blockchain): {}'. format(btc_transfer_request.json())) transactions.update(state=Transaction.PROCESSED, processed_at=timezone.now()) except AcceptException as e: log_error(e) transactions.update(state=Transaction.PAID) return repr(e) except requests.RequestException as e: message = 'ERROR - ACCEPT (btc transfer request to blockchain): {}'.format( repr(e)) log_error(message) transactions.update(state=Transaction.PAID) return message combined_sms_confirm = utils.consolidate_notification_sms(transactions) # send out confirmation SMS for number, reference_numbers in combined_sms_confirm.iteritems(): response_status, message_id = smsgh.send_message_confirm( mobile_number=number, reference_numbers=reference_numbers) for t in transactions.filter(notification_phone_number=number): t.update_after_sms_notification(response_status, message_id) return 'SUCCESS'