From 8c1e6197d63bf168c93485128017d6d2cc29319b Mon Sep 17 00:00:00 2001 From: Dibik Date: Thu, 9 Nov 2023 12:56:08 +0530 Subject: [PATCH] G2P-1499: Able to send payments without preparing payment batch, Added changes to all payments, Removed CSV creation part, Cycle tree view descending order. --- g2p_payment_cash/models/payment_manager.py | 2 +- .../models/payment_manager.py | 148 ++++++------- .../models/payment_manager.py | 198 ++++++++++-------- .../models/payment_manager.py | 178 ++++++++-------- .../models/managers/payment_manager.py | 130 ++++++------ g2p_programs/views/programs_view.xml | 2 +- 6 files changed, 348 insertions(+), 310 deletions(-) diff --git a/g2p_payment_cash/models/payment_manager.py b/g2p_payment_cash/models/payment_manager.py index d2ab6ffc..2e60c329 100644 --- a/g2p_payment_cash/models/payment_manager.py +++ b/g2p_payment_cash/models/payment_manager.py @@ -49,7 +49,7 @@ class G2PPaymentManagerCash(models.Model): def _send_payments(self, batches): if not batches: message = _("No payment batches to process.") - kind = "danger" # Use a valid type like "danger" for an error message + kind = "warning" else: _logger.info("DEBUG! send_payments Manager: Payment via CASH") for batch in batches: diff --git a/g2p_payment_g2p_connect/models/payment_manager.py b/g2p_payment_g2p_connect/models/payment_manager.py index 3fc76e77..f410dd7b 100644 --- a/g2p_payment_g2p_connect/models/payment_manager.py +++ b/g2p_payment_g2p_connect/models/payment_manager.py @@ -81,77 +81,85 @@ def _onchange_payee_id_type(self): self.payee_prefix = prefix_mapping.get(self.payee_id_type) def _send_payments(self, batches): - _logger.info("DEBUG! send_payments Manager: G2P Connect.") - for batch in batches: - if batch.batch_has_started: - continue - batch.batch_has_started = True - batch_data = { - "signature": 'Signature: namespace="g2p", kidId="{sender_id}|{unique_key_id}|{algorithm}", ' - 'algorithm="ed25519", created="1606970629", expires="1607030629", ' - 'headers="(created) (expires) digest", signature="Base64(signing content)', - "header": { - "version": "1.0.0", - "message_id": "123", - "message_ts": "", - "action": "search", - "sender_id": "spp.example.org", - "sender_uri": "https://spp.example.org/{namespace}/callback/on-search", - "receiver_id": "pymts.example.org", - "total_count": 21800, - "is_msg_encrypted": False, - "meta": {}, - }, - "message": {"transaction_id": batch.name, "disbursements": []}, - } - headers = { - "Content-Type": "application/json", - } - for payment in batch.payment_ids: - batch_data["message"]["disbursements"].append( - { - "reference_id": payment.name, - "payer_fa": self.payer_fa, - "payee_fa": self._get_payee_fa(payment), - "amount": payment.amount_issued, - "scheduled_timestamp": "", - "payer_name": self.payer_name, - "payee_name": payment.partner_id.name, - "note": "string", - "purpose": self.program_id.name, - "instruction": "string", - "currency_code": payment.currency_id.name, - "locale": self.locale, - } - ) - try: - response = requests.post( - self.payment_endpoint_url, json=batch_data, headers=headers - ) - _logger.info("G2P Connect Disbursement response: %s", response.content) - response.raise_for_status() - - # TODO: Do Status check rather than hardcoding + if not batches: + message = _("No payment batches to process.") + kind = "warning" + else: + _logger.info("DEBUG! send_payments Manager: G2P Connect.") + for batch in batches: + if batch.batch_has_started: + continue + batch.batch_has_started = True + batch_data = { + "signature": 'Signature: namespace="g2p", kidId="{sender_id}|{unique_key_id}|{algorithm}", ' + 'algorithm="ed25519", created="1606970629", expires="1607030629", ' + 'headers="(created) (expires) digest", signature="Base64(signing content)', + "header": { + "version": "1.0.0", + "message_id": "123", + "message_ts": "", + "action": "search", + "sender_id": "spp.example.org", + "sender_uri": "https://spp.example.org/{namespace}/callback/on-search", + "receiver_id": "pymts.example.org", + "total_count": 21800, + "is_msg_encrypted": False, + "meta": {}, + }, + "message": {"transaction_id": batch.name, "disbursements": []}, + } + headers = { + "Content-Type": "application/json", + } for payment in batch.payment_ids: - payment.state = "sent" - payment.status = "paid" - payment.amount_paid = payment.amount_issued - payment.payment_datetime = datetime.utcnow() - - except Exception as e: - _logger.error( - "G2P Connect Payment Failed with unknown reason: %s", str(e) - ) - error_msg = "G2P Connect Payment Failed with unknown reason: " + str(e) - self.message_post( - body=error_msg, subject=_("G2P Connect Payment Disbursement") - ) - - # TODO: Compute status of disbursement from API - batch.batch_has_completed = True - - message = _("Payment sent successfully") - kind = "success" + batch_data["message"]["disbursements"].append( + { + "reference_id": payment.name, + "payer_fa": self.payer_fa, + "payee_fa": self._get_payee_fa(payment), + "amount": payment.amount_issued, + "scheduled_timestamp": "", + "payer_name": self.payer_name, + "payee_name": payment.partner_id.name, + "note": "string", + "purpose": self.program_id.name, + "instruction": "string", + "currency_code": payment.currency_id.name, + "locale": self.locale, + } + ) + try: + response = requests.post( + self.payment_endpoint_url, json=batch_data, headers=headers + ) + _logger.info( + "G2P Connect Disbursement response: %s", response.content + ) + response.raise_for_status() + + # TODO: Do Status check rather than hardcoding + for payment in batch.payment_ids: + payment.state = "sent" + payment.status = "paid" + payment.amount_paid = payment.amount_issued + payment.payment_datetime = datetime.utcnow() + + except Exception as e: + _logger.error( + "G2P Connect Payment Failed with unknown reason: %s", str(e) + ) + error_msg = ( + "G2P Connect Payment Failed with unknown reason: " + str(e) + ) + self.message_post( + body=error_msg, subject=_("G2P Connect Payment Disbursement") + ) + + # TODO: Compute status of disbursement from API + batch.batch_has_completed = True + + message = _("Payment sent successfully") + kind = "success" return { "type": "ir.actions.client", "tag": "display_notification", diff --git a/g2p_payment_interop_layer/models/payment_manager.py b/g2p_payment_interop_layer/models/payment_manager.py index e7c4168e..151509f7 100644 --- a/g2p_payment_interop_layer/models/payment_manager.py +++ b/g2p_payment_interop_layer/models/payment_manager.py @@ -67,98 +67,120 @@ class G2PPaymentInteropLayerManager(models.Model): def _send_payments(self, batches): payment_endpoint_url = self.payment_endpoint_url all_paid_counter = 0 - for batch in batches: - if batch.batch_has_started: - continue - else: - batch.batch_has_started = True - - disbursement_id = batch.name - - cycle_id = batch.cycle_id - disbursement_note = ( - f"Program: {cycle_id.program_id.name}. Cycle ID - {cycle_id.name}" - ) - - final_json_request_dict = { - "note": disbursement_note, - "disbursementId": disbursement_id, - "payeeList": [], - } - - for payment_id in batch.payment_ids: - payee_id_type, payee_id_value = self._get_dfsp_id_and_type(payment_id) - payee_item = { - "payeeIdType": payee_id_type, - "payeeIdValue": payee_id_value, - "amount": payment_id.amount_issued, - "currency": payment_id.currency_id.name, - } - final_json_request_dict["payeeList"].append(payee_item) - - # TODO: Add authentication mechanism - try: - res = requests.post(payment_endpoint_url, json=final_json_request_dict) - res.raise_for_status() - jsonResponse = res.json() - _logger.info( - f"Interop Layer Disbursement API: jsonResponse: {jsonResponse}" - ) + if not batches: + message = _("No payment batches to process.") + kind = "warning" + else: + for batch in batches: + if batch.batch_has_started: + continue + else: + batch.batch_has_started = True - except HTTPError as http_err: - _logger.error( - f"Interop Layer Disbursement API: HTTP error occurred: {http_err}. res: {res} - {res.content}" - ) - continue - except Exception as err: - _logger.error( - f"Interop Layer Disbursement API: Other error occurred: {err}. res: {res} - {res.content}" + disbursement_id = batch.name + + cycle_id = batch.cycle_id + disbursement_note = ( + f"Program: {cycle_id.program_id.name}. Cycle ID - {cycle_id.name}" ) - continue - - batch.payment_ids.write({"state": "sent"}) - - paid_counter = 0 - for i, payee_result in enumerate(jsonResponse["payeeResults"]): - if payee_result["status"] == "COMPLETED": - paid_counter += 1 - batch.payment_ids[i].update( - { - "state": "reconciled", - "status": "paid", - "amount_paid": payee_result["amountCredited"], - "payment_datetime": datetime.strptime( - payee_result["timestamp"], "%Y-%m-%dT%H:%M:%S.%fZ" - ), - } + + final_json_request_dict = { + "note": disbursement_note, + "disbursementId": disbursement_id, + "payeeList": [], + } + + for payment_id in batch.payment_ids: + payee_id_type, payee_id_value = self._get_dfsp_id_and_type( + payment_id ) - elif payee_result["status"] in [ - "REJECTED", - "ABORTED", - "ERROR_OCCURRED", - ]: - batch.payment_ids[i].update( - { - "state": "reconciled", - "status": "failed", - } + payee_item = { + "payeeIdType": payee_id_type, + "payeeIdValue": payee_id_value, + "amount": payment_id.amount_issued, + "currency": payment_id.currency_id.name, + } + final_json_request_dict["payeeList"].append(payee_item) + + # TODO: Add authentication mechanism + res = None + try: + res = requests.post( + payment_endpoint_url, json=final_json_request_dict ) - if paid_counter and paid_counter == len(batch.payment_ids): - batch.batch_has_completed = True - all_paid_counter += paid_counter - - total_payments_counter = sum(len(batch.payment_ids) for batch in batches) - if all_paid_counter == total_payments_counter: - message = _(f"{all_paid_counter} Payments sent successfully") - kind = "success" - elif all_paid_counter == 0: - message = _("Failed to sent payments") - kind = "danger" - else: - message = _( - f"{all_paid_counter} Payments sent successfully out of {total_payments_counter}" - ) - kind = "warning" + res.raise_for_status() + jsonResponse = res.json() + _logger.info( + f"Interop Layer Disbursement API: jsonResponse: {jsonResponse}" + ) + + except HTTPError as http_err: + if res is not None: + _logger.error( + "Interop Layer Disbursement API: HTTP error occurred: " + f"{http_err}. res: {res} - {res.content}" + ) + + else: + _logger.error( + f"Interop Layer Disbursement API: HTTP error occurred: {http_err}." + ) + continue + + except Exception as err: + if res is not None: + _logger.error( + f"Interop Layer Disbursement API: Other error occurred: {err}. res: {res} - {res.content}" + ) + else: + _logger.error( + f"Interop Layer Disbursement API: Other error occurred: {err}." + ) + continue + + batch.payment_ids.write({"state": "sent"}) + + paid_counter = 0 + for i, payee_result in enumerate(jsonResponse["payeeResults"]): + if payee_result["status"] == "COMPLETED": + paid_counter += 1 + batch.payment_ids[i].update( + { + "state": "reconciled", + "status": "paid", + "amount_paid": payee_result["amountCredited"], + "payment_datetime": datetime.strptime( + payee_result["timestamp"], "%Y-%m-%dT%H:%M:%S.%fZ" + ), + } + ) + elif payee_result["status"] in [ + "REJECTED", + "ABORTED", + "ERROR_OCCURRED", + ]: + batch.payment_ids[i].update( + { + "state": "reconciled", + "status": "failed", + } + ) + if paid_counter and paid_counter == len(batch.payment_ids): + batch.batch_has_completed = True + all_paid_counter += paid_counter + + total_payments_counter = sum(len(batch.payment_ids) for batch in batches) + if all_paid_counter == total_payments_counter: + message = _(f"{all_paid_counter} Payments sent successfully") + kind = "success" + elif all_paid_counter == 0: + message = _("Failed to sent payments") + kind = "danger" + else: + message = _( + f"{all_paid_counter} Payments sent successfully out of {total_payments_counter}" + ) + kind = "warning" return { "type": "ir.actions.client", diff --git a/g2p_payment_simple_mpesa/models/payment_manager.py b/g2p_payment_simple_mpesa/models/payment_manager.py index cc9c394f..49466fce 100644 --- a/g2p_payment_simple_mpesa/models/payment_manager.py +++ b/g2p_payment_simple_mpesa/models/payment_manager.py @@ -68,96 +68,98 @@ class G2PPaymentManagerSimpleMpesa(models.Model): def _send_payments(self, batches): # Transfer to Simple Mpesa all_paid_counter = 0 - _logger.info("DEBUG! send_payments Manager: Simple Mpesa.") - for batch in batches: - if batch.batch_has_started: - continue + if not batches: + message = _("No payment batches to process.") + kind = "warning" + else: + _logger.info("DEBUG! send_payments Manager: Simple Mpesa.") + for batch in batches: + if batch.batch_has_started: + continue + else: + batch.batch_has_started = True + + try: + data = {"email": self.username, "password": self.password} + headers = {"Content-Type": "application/x-www-form-urlencoded"} + + response = requests.post( + self.auth_endpoint_url, + data=data, + headers=headers, + timeout=self.api_timeout, + ) + response.raise_for_status() + response_data = response.json() + auth_token = response_data.get("token") + + completed_payments = 0 + + for payment in batch.payment_ids: + if payment.state not in ("issued"): + continue + if payment.status == "paid": + continue + payee_id_value = self._get_dfsp_id_and_type(payment) + amount = int(payment.amount_issued) + auth_header = "Bearer " + auth_token + # Define the headers + headers = { + "Authorization": auth_header, + "Content-Type": "application/x-www-form-urlencoded", + } + data = { + "amount": amount, + "accountNo": payee_id_value, + "customerType": self.customer_type, + } + try: + response = requests.post( + self.payment_endpoint_url, + headers=headers, + data=data, + timeout=self.api_timeout, + ) + _logger.info( + "MPesa Payment Transfer response: %s", response.content + ) + response.raise_for_status() + + # TODO: Do Status check rather than hardcoding + payment.state = "sent" + payment.status = "paid" + payment.amount_paid = amount + payment.payment_datetime = datetime.utcnow() + completed_payments += 1 + all_paid_counter += 1 + except Exception as e: + _logger.error("Mpesa Payment Failed with unknown reason", e) + error_msg = "Mpesa Payment Failed during transfer with unknown reason" + self.message_post( + body=error_msg, subject=_("Mpesa Payment Transfer") + ) + + if completed_payments == len(batch.payment_ids): + batch.batch_has_completed = True + + except Exception as e: + _logger.error("Mpesa Payment Failed during authentication", e) + error_msg = "Mpesa Payment Failed during authentication" + self.message_post(body=error_msg, subject=_("Mpesa Payment Auth")) + + total_payments_counter = sum(len(batch.payment_ids) for batch in batches) + + if all_paid_counter == total_payments_counter: + message = _(f"{all_paid_counter} Payments sent successfully") + kind = "success" + elif all_paid_counter == 0: + message = _("Failed to sent payments") + kind = "danger" else: - batch.batch_has_started = True - - try: - data = {"email": self.username, "password": self.password} - headers = {"Content-Type": "application/x-www-form-urlencoded"} - - response = requests.post( - self.auth_endpoint_url, - data=data, - headers=headers, - timeout=self.api_timeout, + message = _( + f"{all_paid_counter} Payments sent successfully out of {total_payments_counter}" ) - response.raise_for_status() - response_data = response.json() - auth_token = response_data.get("token") - - completed_payments = 0 - - for payment in batch.payment_ids: - if payment.state not in ("issued"): - continue - if payment.status == "paid": - continue - payee_id_value = self._get_dfsp_id_and_type(payment) - amount = int(payment.amount_issued) - auth_header = "Bearer " + auth_token - # Define the headers - headers = { - "Authorization": auth_header, - "Content-Type": "application/x-www-form-urlencoded", - } - data = { - "amount": amount, - "accountNo": payee_id_value, - "customerType": self.customer_type, - } - try: - response = requests.post( - self.payment_endpoint_url, - headers=headers, - data=data, - timeout=self.api_timeout, - ) - _logger.info( - "MPesa Payment Transfer response: %s", response.content - ) - response.raise_for_status() - - # TODO: Do Status check rather than hardcoding - payment.state = "sent" - payment.status = "paid" - payment.amount_paid = amount - payment.payment_datetime = datetime.utcnow() - completed_payments += 1 - all_paid_counter += 1 - except Exception as e: - _logger.error("Mpesa Payment Failed with unknown reason", e) - error_msg = ( - "Mpesa Payment Failed during transfer with unknown reason" - ) - self.message_post( - body=error_msg, subject=_("Mpesa Payment Transfer") - ) - - if completed_payments == len(batch.payment_ids): - batch.batch_has_completed = True - - except Exception as e: - _logger.error("Mpesa Payment Failed during authentication", e) - error_msg = "Mpesa Payment Failed during authentication" - self.message_post(body=error_msg, subject=_("Mpesa Payment Auth")) - - total_payments_counter = sum(len(batch.payment_ids) for batch in batches) - - if all_paid_counter == total_payments_counter: - message = _(f"{all_paid_counter} Payments sent successfully") - kind = "success" - elif all_paid_counter == 0: - message = _("Failed to sent payments") - kind = "danger" - else: - message = _( - f"{all_paid_counter} Payments sent successfully out of {total_payments_counter}" - ) - kind = "warning" + kind = "warning" return { "type": "ir.actions.client", diff --git a/g2p_programs/models/managers/payment_manager.py b/g2p_programs/models/managers/payment_manager.py index 116be3e3..e6e43b12 100644 --- a/g2p_programs/models/managers/payment_manager.py +++ b/g2p_programs/models/managers/payment_manager.py @@ -1,8 +1,8 @@ # Part of OpenG2P. See LICENSE file for full copyright and licensing details. -import base64 -import csv +# import base64 +# import csv +# from io import StringIO import logging -from io import StringIO from uuid import uuid4 from odoo import _, api, fields, models @@ -264,66 +264,72 @@ def send_payments(self, batches): def _send_payments(self, batches): # Create a payment list (CSV) # _logger.debug("DEBUG! send_payments Manager: DEFAULT") - for rec in batches: - filename = f"{rec.name}.csv" - data = StringIO() - csv_writer = csv.writer(data, quoting=csv.QUOTE_MINIMAL) - header = [ - "row_number", - "internal_payment_reference", - "account_number", - "beneficiary_name", - "amount", - "currency", - "details_of_payment", - ] - csv_writer.writerow(header) - for row, payment_id in enumerate(rec.payment_ids): - account_number = "" - if payment_id.partner_id.bank_ids: - account_number = payment_id.partner_id.bank_ids[0].iban - details_of_payment = ( - f"{payment_id.program_id.name} - {payment_id.cycle_id.name}" - ) - row = [ - row, - payment_id.name, - account_number, - payment_id.partner_id.name, - payment_id.amount_issued, - payment_id.currency_id.name, - details_of_payment, - ] - csv_writer.writerow(row) - csv_data = base64.encodebytes(bytearray(data.getvalue(), "utf-8")) - # Attach the generated CSV to payment batch - self.env["ir.attachment"].create( - { - "name": filename, - "res_model": "g2p.payment.batch", - "res_id": rec.id, - "type": "binary", - "store_fname": filename, - "mimetype": "text/csv", - "datas": csv_data, - } - ) - # _logger.debug("DEFAULT Payment Manager: data: %s" % csv_data) - message = _("Payment CSV created successfully") - kind = "success" - return { - "type": "ir.actions.client", - "tag": "display_notification", - "params": { - "title": _("Payment"), - "message": message, - "sticky": True, - "type": kind, - "next": { - "type": "ir.actions.act_window_close", + if not batches: + message = _("No payment batches to process.") + kind = "warning" + + return { + "type": "ir.actions.client", + "tag": "display_notification", + "params": { + "title": _("Payment"), + "message": message, + "sticky": True, + "type": kind, + "next": { + "type": "ir.actions.act_window_close", + }, }, - }, - } + } + # TODO: Removed CSV Creation part after confirmation with team for timebeing. + # else: + # for rec in batches: + # filename = f"{rec.name}.csv" + # data = StringIO() + # csv_writer = csv.writer(data, quoting=csv.QUOTE_MINIMAL) + # header = [ + # "row_number", + # "internal_payment_reference", + # "account_number", + # "beneficiary_name", + # "amount", + # "currency", + # "details_of_payment", + # ] + # csv_writer.writerow(header) + # for row, payment_id in enumerate(rec.payment_ids): + # account_number = "" + # if payment_id.partner_id.bank_ids: + # account_number = payment_id.partner_id.bank_ids[0].iban + # details_of_payment = ( + # f"{payment_id.program_id.name} - {payment_id.cycle_id.name}" + # ) + # row = [ + # row, + # payment_id.name, + # account_number, + # payment_id.partner_id.name, + # payment_id.amount_issued, + # payment_id.currency_id.name, + # details_of_payment, + # ] + # csv_writer.writerow(row) + # csv_data = base64.encodebytes(bytearray(data.getvalue(), "utf-8")) + # # Attach the generated CSV to payment batch + # self.env["ir.attachment"].create( + # { + # "name": filename, + # "res_model": "g2p.payment.batch", + # "res_id": rec.id, + # "type": "binary", + # "store_fname": filename, + # "mimetype": "text/csv", + # "datas": csv_data, + # } + # ) + # # _logger.debug("DEFAULT Payment Manager: data: %s" % csv_data) + # message = _("Payment CSV created successfully") + # kind = "success" def _send_payments_async(self, cycle, batches): _logger.debug("Send Payments asynchronously") diff --git a/g2p_programs/views/programs_view.xml b/g2p_programs/views/programs_view.xml index 76e443a8..dd6bdbc5 100644 --- a/g2p_programs/views/programs_view.xml +++ b/g2p_programs/views/programs_view.xml @@ -206,7 +206,7 @@ Part of OpenG2P. See LICENSE file for full copyright and licensing details. context="{'default_program_id':active_id}" readonly="1" > - +