diff --git a/.gitignore b/.gitignore index a9387a91..bee4090c 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ docker-build /.mypy_cache coverage.xml /.python-version + +prueba-cert.pem diff --git a/requirements-dev.txt b/requirements-dev.txt index 016d0529..7bd8c648 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,3 +11,4 @@ pytest-vcr==1.0.* coveralls==3.0.* wheel==0.40.0 freezegun==1.2.2 +requests-mock==1.9.* \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 3ae937ae..faafd01b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ blinker==1.4 boto3==1.16.* celery==5.2.3 -cepmex==0.2.0 +cepmex==0.2.2 Flask==2.2.5 flask-mongoengine==1.0.0 luhnmod10==1.0.2 diff --git a/speid/commands/spei.py b/speid/commands/spei.py index 374d47db..984fad6c 100644 --- a/speid/commands/spei.py +++ b/speid/commands/spei.py @@ -1,11 +1,26 @@ +import datetime as dt + import click +import pytz from mongoengine import DoesNotExist +from stpmex.business_days import get_next_business_day +from stpmex.types import Estado as StpEstado from speid import app from speid.helpers.callback_helper import set_status_transaction +from speid.helpers.transaction_helper import process_incoming_transaction from speid.models import Event, Transaction +from speid.processors import stpmex_client from speid.types import Estado, EventType +ESTADOS_DEPOSITOS_VALIDOS = { + StpEstado.confirmada, + StpEstado.liquidada, + StpEstado.traspaso_liquidado, +} + +TIPOS_PAGO_DEVOLUCION = {0, 16, 17, 18, 23, 24} + @app.cli.group('speid') def speid_group(): @@ -48,5 +63,77 @@ def re_execute_transactions(speid_id): transaction.create_order() +@speid_group.command('reconciliate-deposits') +@click.argument('fecha_operacion', type=click.DateTime()) +@click.argument('claves_rastreo', type=str) +def reconciliate_deposits( + fecha_operacion: dt.datetime, claves_rastreo: str +) -> None: + + claves_rastreo_filter = set(claves_rastreo.split(',')) + mex_query_date = dt.datetime.utcnow().astimezone( + pytz.timezone('America/Mexico_City') + ) + + if fecha_operacion.date() < get_next_business_day(mex_query_date): + recibidas = stpmex_client.ordenes.consulta_recibidas(fecha_operacion) + else: + recibidas = stpmex_client.ordenes.consulta_recibidas() + + no_procesadas = [] + for recibida in recibidas: + if recibida.claveRastreo not in claves_rastreo_filter: + no_procesadas.append(recibida.claveRastreo) + continue + # Se ignora los tipos pago devolución debido a que + # el estado de estas operaciones se envían + # al webhook `POST /orden_events` + if recibida.tipoPago in TIPOS_PAGO_DEVOLUCION: + no_procesadas.append(recibida.claveRastreo) + continue + + if recibida.estado not in ESTADOS_DEPOSITOS_VALIDOS: + no_procesadas.append(recibida.claveRastreo) + continue + + try: + Transaction.objects.get( + clave_rastreo=recibida.claveRastreo, + fecha_operacion=recibida.fechaOperacion, + ) + except DoesNotExist: + # Para reutilizar la lógica actual para abonar depósitos se + # hace una conversión del modelo de respuesta de + # la función `consulta_recibidas` al modelo del evento que envía + # STP por el webhook en `POST /ordenes` + stp_request = dict( + Clave=recibida.idEF, + FechaOperacion=recibida.fechaOperacion.strftime('%Y%m%d'), + InstitucionOrdenante=recibida.institucionContraparte, + InstitucionBeneficiaria=recibida.institucionOperante, + ClaveRastreo=recibida.claveRastreo, + Monto=recibida.monto, + NombreOrdenante=recibida.nombreOrdenante, + TipoCuentaOrdenante=recibida.tipoCuentaOrdenante, + CuentaOrdenante=recibida.cuentaOrdenante, + RFCCurpOrdenante=recibida.rfcCurpOrdenante, + NombreBeneficiario=recibida.nombreBeneficiario, + TipoCuentaBeneficiario=recibida.tipoCuentaBeneficiario, + CuentaBeneficiario=recibida.cuentaBeneficiario, + RFCCurpBeneficiario=getattr( + recibida, 'rfcCurpBeneficiario', 'NA' + ), + ConceptoPago=recibida.conceptoPago, + ReferenciaNumerica=recibida.referenciaNumerica, + Empresa=recibida.empresa, + ) + click.echo(f'Depósito procesado: {recibida.claveRastreo}') + process_incoming_transaction(stp_request) + else: + no_procesadas.append(recibida.claveRastreo) + + click.echo(f'No procesadas: {no_procesadas}') + + if __name__ == "__main__": re_execute_transactions() # pragma: no cover diff --git a/tests/commands/deposits_20230825.json b/tests/commands/deposits_20230825.json new file mode 100644 index 00000000..a25e6c36 --- /dev/null +++ b/tests/commands/deposits_20230825.json @@ -0,0 +1,123 @@ +{ + "resultado": { + "lst": [ + { + "claveRastreo": "058-25/08/2023/25-300CPWF924", + "conceptoPago": "Transferencia", + "cuentaBeneficiario": "646180157000000004", + "cuentaOrdenante": "058597000027474792", + "empresa": "TAMIZI", + "estado": "CCO", + "fechaOperacion": "20230825", + "horaServidorBanxico": "18:02:31", + "idEF": 34922, + "institucionContraparte": 40058, + "institucionOperante": 90646, + "iva": 0.0, + "medioEntrega": 1, + "monto": 40.0, + "nombreBeneficiario": "No disponible", + "nombreCEP": "Juan Perez", + "nombreOrdenante": "Juan Perez", + "prioridad": 0, + "referenciaNumerica": 240823, + "rfcCEP": "XAXX010101000", + "rfcCurpOrdenante": "ND", + "sello": "abc", + "tipoCuentaBeneficiario": 40, + "tipoCuentaOrdenante": 40, + "tipoPago": 1, + "topologia": "V", + "tsCaptura": 1692921752078, + "tsEntrega": 608, + "tsLiquidacion": 1692921752078 + }, + { + "claveRastreo": "HSBC438541", + "conceptoPago": "Juan Perez", + "cuentaBeneficiario": "646180157000000004", + "cuentaOrdenante": "021180065652373317", + "empresa": "TAMIZI", + "estado": "D", + "fechaOperacion": "20230825", + "horaServidorBanxico": "14:47:19", + "idEF": 31507, + "institucionContraparte": 40021, + "institucionOperante": 90646, + "iva": 0.0, + "medioEntrega": 1, + "monto": 6000.0, + "nombreBeneficiario": "Transferencia Express", + "nombreOrdenante": "Juan Perez", + "prioridad": 0, + "referenciaNumerica": 250823, + "rfcCurpOrdenante": "XAXX010101000", + "tipoCuentaBeneficiario": 40, + "tipoCuentaOrdenante": 40, + "tipoPago": 1, + "topologia": "V", + "tsCaptura": 1692996439524, + "tsDevolucion": 3654, + "tsLiquidacion": 1692996439524 + }, + { + "clavePago": "", + "claveRastreo": "AMU0016929218440013495354", + "conceptoPago": "Juan Perez", + "conceptoPago2": "", + "cuentaBeneficiario": "646180157000000004", + "cuentaBeneficiario2": "", + "cuentaOrdenante": "646731258610319776", + "empresa": "TAMIZI", + "estado": "TLQ", + "fechaOperacion": "20230825", + "idEF": 35966, + "institucionContraparte": 90646, + "institucionOperante": 90646, + "medioEntrega": 3, + "monto": 2900.0, + "nombreBeneficiario": "Juan Perez", + "nombreBeneficiario2": "", + "nombreCEP": "CUENCA TECNOLOGIA FINANCIERA SA DE CV", + "nombreOrdenante": "Juan Perez", + "prioridad": 0, + "referenciaCobranza": "", + "referenciaNumerica": 8244561, + "rfcCEP": "XAXX010101000", + "rfcCurpBeneficiario": "ND", + "rfcCurpBeneficiario2": "", + "rfcCurpOrdenante": "XAXX010101000", + "sello": "abc", + "tipoCuentaBeneficiario": 40, + "tipoCuentaOrdenante": 40, + "tipoPago": 1, + "topologia": "V", + "tsCaptura": 1692921845110, + "tsDevolucion": 0, + "tsEntrega": 0, + "tsLiquidacion": 1692921845110 + }, + { + "causaDevolucion": 3, + "claveRastreo": "CUENCA224487850225", + "claveRastreoDevolucion": "CUENCA224487850225", + "cuentaBeneficiario": "646180157000000004", + "empresa": "TAMIZI", + "estado": "LQ", + "fechaOperacion": "20230825", + "horaServidorBanxico": "18:23:56", + "idEF": 59746, + "institucionContraparte": 40002, + "institucionOperante": 90646, + "medioEntrega": 1, + "monto": 679.58, + "prioridad": 1, + "tipoPago": 0, + "topologia": "V", + "tsCaptura": 1692923037042, + "tsDevolucion": 582.142, + "tsLiquidacion": 1692923037042 + } + ] + } +} diff --git a/tests/commands/deposits_20230828.json b/tests/commands/deposits_20230828.json new file mode 100644 index 00000000..741bf575 --- /dev/null +++ b/tests/commands/deposits_20230828.json @@ -0,0 +1,202 @@ +{ + "resultado": { + "lst": [ + { + "claveRastreo": "20202347256237160030011294", + "conceptoPago": "HONORARIOS ASIMILADOS", + "cuentaBeneficiario": "646180157000000004", + "cuentaOrdenante": "001180228001000108", + "empresa": "TAMIZI", + "estado": "CCO", + "fechaOperacion": "20230828", + "horaServidorBanxico": "15:36:26", + "idEF": 79113, + "institucionContraparte": 2001, + "institucionOperante": 90646, + "iva": 0.0, + "medioEntrega": 1, + "monto": 22833.38, + "nombreBeneficiario": "Juan Perez", + "nombreCEP": "Juan Perez", + "nombreOrdenante": "TESORERIA DE LA FEDERACION", + "prioridad": 0, + "referenciaCobranza": "20202347256237160030011294", + "referenciaNumerica": 7174381, + "rfcCEP": "XAXX010101000", + "rfcCurpBeneficiario": "XAXX010101000", + "rfcCurpOrdenante": "XAXX010101000", + "sello": "abc", + "tipoCuentaBeneficiario": 40, + "tipoCuentaOrdenante": 40, + "tipoPago": 1, + "topologia": "V", + "tsCaptura": 1693008093163, + "tsEntrega": 2717955, + "tsLiquidacion": 1693008093163 + }, + { + "claveRastreo": "MBAN01002308280073291391", + "conceptoPago": "Cambio", + "cuentaBeneficiario": "646180157000000004", + "cuentaOrdenante": "012180015260519399", + "empresa": "TAMIZI", + "estado": "D", + "fechaOperacion": "20230828", + "horaServidorBanxico": "18:16:07", + "idEF": 89820, + "institucionContraparte": 40012, + "institucionOperante": 90646, + "medioEntrega": 1, + "monto": 2.0, + "nombreBeneficiario": "Juan Perez", + "nombreOrdenante": "Juan Perez", + "prioridad": 0, + "referenciaNumerica": 2508230, + "rfcCurpOrdenante": "XAXX010101000", + "tipoCuentaBeneficiario": 40, + "tipoCuentaOrdenante": 40, + "tipoPago": 1, + "topologia": "V", + "tsCaptura": 1693008968344, + "tsDevolucion": 3373, + "tsLiquidacion": 1693008968344 + }, + { + "claveRastreo": "MBAN17002308280000779197", + "claveRastreoDevolucion": "CUENCA89434278781", + "cuentaBeneficiario": "646180157000000004", + "empresa": "TAMIZI", + "estado": "LQ", + "fechaOperacion": "20230828", + "horaServidorBanxico": "17:08:08", + "idEF": 640033816, + "institucionContraparte": 40012, + "institucionOperante": 90646, + "medioEntrega": 1, + "monto": 400.0, + "prioridad": 0, + "tipoPago": 17, + "topologia": "V", + "tsCaptura": 1693091289111, + "tsDevolucion": 1851561, + "tsLiquidacion": 1693091289111 + }, + { + "clavePago": "", + "claveRastreo": "KUESKI64e64e1376295", + "conceptoPago": "Validacion de cuenta", + "conceptoPago2": "", + "cuentaBeneficiario": "646180157000000004", + "cuentaBeneficiario2": "", + "cuentaOrdenante": "646180109701000005", + "empresa": "TAMIZI", + "estado": "TLQ", + "fechaOperacion": "20230828", + "idEF": 79288, + "institucionContraparte": 90646, + "institucionOperante": 90646, + "medioEntrega": 3, + "monto": 0.01, + "nombreBeneficiario": "Juan Perez", + "nombreBeneficiario2": "", + "nombreCEP": "CUENCA TECNOLOGIA FINANCIERA SA DE CV", + "nombreOrdenante": "KUESKI SAPI DE CV SOFOM ENR", + "prioridad": 0, + "referenciaCobranza": "", + "referenciaNumerica": 3398543, + "rfcCEP": "XAXX010101000", + "rfcCurpBeneficiario": "XAXX010101000", + "rfcCurpBeneficiario2": "", + "rfcCurpOrdenante": "XAXX010101000", + "sello": "abc", + "tipoCuentaBeneficiario": 40, + "tipoCuentaOrdenante": 40, + "tipoPago": 1, + "topologia": "V", + "tsCaptura": 1693008124332, + "tsDevolucion": 0, + "tsEntrega": 0, + "tsLiquidacion": 1693008129010 + }, + { + "causaDevolucion": 14, + "claveRastreo": "CUENCA04700787624", + "claveRastreoDevolucion": "CUENCA04700787624", + "cuentaBeneficiario": "646180157000000004", + "empresa": "TAMIZI", + "estado": "LQ", + "fechaOperacion": "20230828", + "horaServidorBanxico": "18:05:11", + "idEF": 81194, + "institucionContraparte": 40021, + "institucionOperante": 90646, + "medioEntrega": 1, + "monto": 0.01, + "prioridad": 1, + "tipoPago": 0, + "topologia": "V", + "tsCaptura": 1693008312059, + "tsDevolucion": 3280, + "tsLiquidacion": 1693008312059 + }, + { + "claveRastreo": "230828070497534251I", + "conceptoPago": "Cobro con CoDi Banxico", + "cuentaBeneficiario": "646180157000000004", + "cuentaOrdenante": "127180001202673464", + "empresa": "TAMIZI", + "estado": "CCO", + "fechaOperacion": "20230828", + "horaServidorBanxico": "18:32:46", + "idEF": 639020119, + "institucionContraparte": 40127, + "institucionOperante": 90646, + "medioEntrega": 1, + "monto": 95.0, + "nombreBeneficiario": "Juan Perez", + "nombreCEP": "Juan Perez", + "nombreOrdenante": "Juan Perez", + "prioridad": 0, + "referenciaNumerica": 747375, + "rfcCEP": "XAXX010101000", + "rfcCurpBeneficiario": "ND", + "rfcCurpOrdenante": "XAXX010101000", + "sello": "abc", + "tipoCuentaBeneficiario": 40, + "tipoCuentaOrdenante": 40, + "tipoPago": 19, + "topologia": "V", + "tsCaptura": 1693009966359, + "tsEntrega": 174, + "tsLiquidacion": 1693009966359 + }, + { + "claveRastreo": "230828070497193104I", + "conceptoPago": "Juan Perez", + "cuentaBeneficiario": "646180157000000004", + "cuentaOrdenante": "127180016550268998", + "empresa": "TAMIZI", + "estado": "LQ", + "fechaOperacion": "20230828", + "horaServidorBanxico": "18:05:18", + "idEF": 81279, + "institucionContraparte": 40127, + "institucionOperante": 90646, + "iva": 0.0, + "medioEntrega": 1, + "monto": 150.0, + "nombreBeneficiario": "Juan Perez", + "nombreOrdenante": "Juan Perez", + "prioridad": 0, + "referenciaNumerica": 416737, + "rfcCurpOrdenante": "XAXX010101000", + "tipoCuentaBeneficiario": 40, + "tipoCuentaOrdenante": 40, + "tipoPago": 1, + "topologia": "V", + "tsCaptura": 1693008318789, + "tsLiquidacion": 1693008318789 + } + ] + } +} diff --git a/tests/commands/test_spei.py b/tests/commands/test_spei.py index b554361c..70ecaa8e 100644 --- a/tests/commands/test_spei.py +++ b/tests/commands/test_spei.py @@ -1,8 +1,18 @@ +import datetime as dt +import json + import pytest +import requests_mock +from freezegun import freeze_time -from speid.commands.spei import speid_group +from speid.commands.spei import ( + ESTADOS_DEPOSITOS_VALIDOS, + TIPOS_PAGO_DEVOLUCION, + speid_group, +) from speid.models import Transaction from speid.types import Estado, EventType +from speid.validations import StpTransaction @pytest.fixture @@ -99,3 +109,165 @@ def test_re_execute_transaction_not_found( assert transaction.estado is Estado.created assert type(result.exception) is ValueError + + +@freeze_time("2023-08-26 01:00:00") # 2023-08-25 19:00 UTC-6 +@pytest.mark.usefixtures('mock_callback_queue') +def test_reconciliate_deposits_historic(runner): + """ + Esta prueba simula obtener depósitos de días históricos, es decir, de + depósitos que llegaron en días operativos anteriores al día operativo + en curso + """ + + fecha_operacion = dt.date(2023, 8, 25) + + initial_deposits_count = Transaction.objects( + tipo='deposito', fecha_operacion=fecha_operacion + ).count() + + assert initial_deposits_count == 0 + + with open('tests/commands/deposits_20230825.json') as f: + stp_response = json.loads(f.read()) + deposits = stp_response['resultado']['lst'] + + with requests_mock.mock() as m: + m.post('/speiws/rest/ordenPago/consOrdenesFech', json=stp_response) + + runner.invoke( + speid_group, + [ + 'reconciliate-deposits', + fecha_operacion.strftime('%Y-%m-%d'), + deposits[0]['claveRastreo'], + ], + ) + + deposits_db = Transaction.objects( + tipo='deposito', fecha_operacion=fecha_operacion + ).all() + + assert len(deposits_db) == 1 + Transaction.drop_collection() + + +@freeze_time("2023-08-27") # 2023-08-26 18:00 UTC-6 +@pytest.mark.usefixtures('mock_callback_queue') +def test_reconciliate_deposits_current_fecha_operacion(runner): + """ + Esta prueba simula obtener depósitos del día operativo en curso + """ + fecha_operacion = dt.date(2023, 8, 28) + + initial_deposits_count = Transaction.objects( + tipo='deposito', fecha_operacion=fecha_operacion + ).count() + + assert initial_deposits_count == 0 + + with open('tests/commands/deposits_20230828.json') as f: + stp_response = json.loads(f.read()) + deposits = stp_response['resultado']['lst'] + + claves_rastreo = ','.join(d['claveRastreo'] for d in deposits) + devolucion = next(d for d in deposits if d['estado'] == 'D') + valid_deposits = [ + d + for d in deposits + if d['estado'] in ESTADOS_DEPOSITOS_VALIDOS + and d['tipoPago'] not in TIPOS_PAGO_DEVOLUCION + ] + + with requests_mock.mock() as m: + m.post('/speiws/rest/ordenPago/consOrdenesFech', json=stp_response) + + runner.invoke( + speid_group, + [ + 'reconciliate-deposits', + fecha_operacion.strftime('%Y-%m-%d'), + claves_rastreo, + ], + ) + + deposits_db = Transaction.objects( + tipo='deposito', fecha_operacion=fecha_operacion + ).all() + + assert len(deposits_db) == len(valid_deposits) + assert not any( + d.clave_rastreo == devolucion['claveRastreo'] for d in deposits_db + ) + Transaction.drop_collection() + + +@freeze_time("2023-08-26 01:00:00") # 2023-08-25 19:00 UTC-6 +@pytest.mark.usefixtures('mock_callback_queue') +def test_reconciliate_deposits_ignores_duplicated(runner): + """ + Esta prueba simula obtener depósitos de días históricos. Ignora depósitos + que ya existen en speid + """ + fecha_operacion = dt.date(2023, 8, 25) + + initial_deposits_count = Transaction.objects( + tipo='deposito', fecha_operacion=fecha_operacion + ).count() + + assert initial_deposits_count == 0 + + with open('tests/commands/deposits_20230825.json') as f: + stp_response = json.loads(f.read()) + deposits = stp_response['resultado']['lst'] + + deposit = deposits[0] + claves_rastreo = ','.join(d['claveRastreo'] for d in deposits) + external_tx = StpTransaction( # type: ignore + Clave=deposit['idEF'], + FechaOperacion=deposit['fechaOperacion'], + InstitucionOrdenante=deposit['institucionContraparte'], + InstitucionBeneficiaria=deposit['institucionOperante'], + ClaveRastreo=deposit['claveRastreo'], + Monto=deposit['monto'], + NombreOrdenante=deposit['nombreOrdenante'], + TipoCuentaOrdenante=deposit['tipoCuentaOrdenante'], + CuentaOrdenante=deposit['cuentaOrdenante'], + RFCCurpOrdenante=deposit['rfcCurpOrdenante'], + NombreBeneficiario=deposit['nombreBeneficiario'], + TipoCuentaBeneficiario=deposit['tipoCuentaBeneficiario'], + CuentaBeneficiario=deposit['cuentaBeneficiario'], + RFCCurpBeneficiario='NA', + ConceptoPago=deposit['conceptoPago'], + ReferenciaNumerica=deposit['referenciaNumerica'], + Empresa=deposit['empresa'], + ) + transaction = external_tx.transform() + transaction.estado = Estado.succeeded + transaction.save() + + valid_deposits = [ + d + for d in deposits + if d['estado'] in ESTADOS_DEPOSITOS_VALIDOS + and d['tipoPago'] not in TIPOS_PAGO_DEVOLUCION + ] + + with requests_mock.mock() as m: + m.post('/speiws/rest/ordenPago/consOrdenesFech', json=stp_response) + + runner.invoke( + speid_group, + [ + 'reconciliate-deposits', + fecha_operacion.strftime('%Y-%m-%d'), + claves_rastreo, + ], + ) + + deposits_db = Transaction.objects( + tipo='deposito', fecha_operacion=fecha_operacion + ).all() + + assert len(deposits_db) == len(valid_deposits) + Transaction.drop_collection()