Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reconciliation workers #421

Merged
merged 11 commits into from
Nov 1, 2023
Merged
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ coverage.xml
/.python-version

prueba-cert.pem

htmlcov/
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ newrelic==6.2.0.156
pandas==1.2.4
python-hosts==1.0.1
sentry-sdk==1.14.0
stpmex==3.11.2
stpmex==3.13.1
importlib-metadata==4.13.0
44 changes: 11 additions & 33 deletions speid/commands/spei.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,21 @@
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.helpers.transaction_helper import (
process_incoming_transaction,
stp_model_to_dict,
)
from speid.models import Event, Transaction
from speid.models.transaction import (
REFUNDS_PAYMENTS_TYPES,
STP_VALID_DEPOSITS_STATUSES,
)
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():
Expand Down Expand Up @@ -87,11 +85,11 @@ def reconciliate_deposits(
# 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:
if recibida.tipoPago in REFUNDS_PAYMENTS_TYPES:
no_procesadas.append(recibida.claveRastreo)
continue

if recibida.estado not in ESTADOS_DEPOSITOS_VALIDOS:
if recibida.estado not in STP_VALID_DEPOSITS_STATUSES:
no_procesadas.append(recibida.claveRastreo)
continue

Expand All @@ -105,27 +103,7 @@ def reconciliate_deposits(
# 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,
)
stp_request = stp_model_to_dict(recibida)
click.echo(f'Depósito procesado: {recibida.claveRastreo}')
process_incoming_transaction(stp_request)
else:
Expand Down
13 changes: 13 additions & 0 deletions speid/exc.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from dataclasses import dataclass


class OrderNotFoundException(ReferenceError):
pass

Expand All @@ -16,3 +19,13 @@ class ScheduleError(Exception):
"""

pass


@dataclass
class TransactionNeedManualReviewError(Exception):
"""
when a person should review the transaction status manually
"""

speid_id: str
error: str
23 changes: 23 additions & 0 deletions speid/helpers/transaction_helper.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
from typing import Dict

from mongoengine import NotUniqueError
from sentry_sdk import capture_exception, capture_message
Expand Down Expand Up @@ -40,3 +41,25 @@ def process_incoming_transaction(incoming_transaction: dict) -> dict:
transaction.save()
capture_exception(e)
return r


def stp_model_to_dict(model) -> Dict:
return dict(
Clave=model.idEF,
FechaOperacion=model.fechaOperacion.strftime('%Y%m%d'),
InstitucionOrdenante=model.institucionContraparte,
InstitucionBeneficiaria=model.institucionOperante,
ClaveRastreo=model.claveRastreo,
Monto=model.monto,
NombreOrdenante=model.nombreOrdenante,
TipoCuentaOrdenante=model.tipoCuentaOrdenante,
CuentaOrdenante=model.cuentaOrdenante,
RFCCurpOrdenante=model.rfcCurpOrdenante,
NombreBeneficiario=model.nombreBeneficiario,
TipoCuentaBeneficiario=model.tipoCuentaBeneficiario,
CuentaBeneficiario=model.cuentaBeneficiario,
RFCCurpBeneficiario=getattr(model, 'rfcCurpBeneficiario', 'NA'),
ConceptoPago=model.conceptoPago,
ReferenciaNumerica=model.referenciaNumerica,
Empresa=model.empresa,
)
99 changes: 64 additions & 35 deletions speid/models/transaction.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import datetime as dt
import os
from datetime import datetime
from enum import Enum
from typing import Optional

Expand All @@ -16,12 +16,12 @@
)
from sentry_sdk import capture_exception
from stpmex.business_days import get_next_business_day
from stpmex.exc import NoEntityFound, StpmexException
from stpmex.exc import EmptyResultsError, StpmexException
from stpmex.resources import Orden
from stpmex.types import Estado as STPEstado

from speid import STP_EMPRESA
from speid.exc import MalformedOrderException
from speid.exc import MalformedOrderException, TransactionNeedManualReviewError
from speid.helpers import callback_helper
from speid.processors import stpmex_client
from speid.types import Estado, EventType, TipoTransaccion
Expand All @@ -42,17 +42,32 @@
os.getenv('SKIP_VALIDATION_PRIOR_SEND_ORDER', 'false').lower() == 'true'
)

STP_FAILED_STATUSES = [
STP_FAILED_TRANSFERS_STATUSES = {
STPEstado.traspaso_cancelado,
STPEstado.cancelada,
STPEstado.cancelada_adapter,
STPEstado.cancelada_rechazada,
]
STPEstado.devuelta,
}

STP_SUCCEDED_TRANSFERS_STATUSES = {
STPEstado.liquidada,
STPEstado.traspaso_liquidado,
}


STP_VALID_DEPOSITS_STATUSES = {
STPEstado.confirmada,
STPEstado.liquidada,
STPEstado.traspaso_liquidado,
}

REFUNDS_PAYMENTS_TYPES = {0, 16, 17, 18, 23, 24}


@handler(signals.pre_save)
def pre_save_transaction(sender, document):
date = document.fecha_operacion or datetime.today()
date = document.fecha_operacion or dt.datetime.today()
document.compound_key = (
f'{document.clave_rastreo}:{date.strftime("%Y%m%d")}'
)
Expand Down Expand Up @@ -130,6 +145,18 @@ class Transaction(Document, BaseModel):
]
}

@property
def created_at_cdmx(self) -> dt.datetime:
utc_created_at = self.created_at.replace(tzinfo=pytz.utc)
return utc_created_at.astimezone(pytz.timezone('America/Mexico_City'))

@property
def created_at_fecha_operacion(self) -> dt.date:
# STP doesn't return `fecha_operacion` on withdrawal creation, but we
# can calculate it.
assert self.tipo is TipoTransaccion.retiro
return get_next_business_day(self.created_at_cdmx)

def set_state(self, state: Estado):
from ..tasks.transactions import send_transaction_status

Expand Down Expand Up @@ -162,39 +189,41 @@ def is_valid_account(self) -> bool:
pass
return is_valid

def fetch_stp_status(self) -> Optional[STPEstado]:
# checa status en stp
estado = None
try:
stp_order = stpmex_client.ordenes.consulta_clave_rastreo(
claveRastreo=self.clave_rastreo,
institucionOperante=self.institucion_ordenante,
fechaOperacion=get_next_business_day(self.created_at),
)
estado = stp_order.estado
except NoEntityFound:
...
return estado

def is_current_working_day(self) -> bool:
# checks if transaction was made in the current working day
local = self.created_at.replace(tzinfo=pytz.utc)
local = local.astimezone(pytz.timezone('America/Mexico_City'))
return get_next_business_day(local) == datetime.utcnow().date()

def fail_if_not_found_stp(self) -> None:
# if transaction is not found in stp, or has a failed status,
# return to origin. Only checking for curent working day
if not self.is_current_working_day():
return
def fetch_stp_status(self) -> STPEstado:
stp_order = stpmex_client.ordenes_v2.consulta_clave_rastreo_enviada(
clave_rastreo=self.clave_rastreo,
fecha_operacion=self.created_at_fecha_operacion,
)
return stp_order.estado

def update_stp_status(self) -> None:
try:
estado = self.fetch_stp_status()
status: Optional[STPEstado] = self.fetch_stp_status()
except EmptyResultsError:
status = None
except StpmexException as ex:
capture_exception(ex)
return

if not status:
raise TransactionNeedManualReviewError(
self.speid_id,
f'Can not retrieve transaction stp_id: {self.stp_id}',
)
elif status in STP_FAILED_TRANSFERS_STATUSES:
self.set_state(Estado.failed)
self.save()
elif status in STP_SUCCEDED_TRANSFERS_STATUSES:
self.set_state(Estado.succeeded)
self.save()
elif status is STPEstado.autorizada:
return
else:
if not estado or estado in STP_FAILED_STATUSES:
self.set_state(Estado.failed)
self.save()
# Cualquier otro caso se debe revisar manualmente y aplicar
# el fix correspondiente
raise TransactionNeedManualReviewError(
self.speid_id, f'Unhandled stp status: {status}'
)

def create_order(self) -> Orden:
# Validate account has already been created
Expand Down
58 changes: 38 additions & 20 deletions speid/tasks/orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
MalformedOrderException,
ResendSuccessOrderException,
ScheduleError,
TransactionNeedManualReviewError,
)
from speid.helpers.task_helpers import time_in_range
from speid.models import Event, Transaction
Expand Down Expand Up @@ -52,7 +53,11 @@ def retry_timeout(attempts: int) -> int:
def send_order(self, order_val: dict):
try:
execute(order_val)
except (MalformedOrderException, ResendSuccessOrderException) as exc:
except (
MalformedOrderException,
ResendSuccessOrderException,
TransactionNeedManualReviewError,
) as exc:
capture_exception(exc)
except ScheduleError:
self.retry(countdown=STP_COUNTDOWN)
Expand Down Expand Up @@ -93,30 +98,43 @@ def execute(order_val: dict):
transaction.save()
pass
except AssertionError:
# Se hace un reenvío del estado de la transferencia
# transaction.set_state(Estado.succeeded)
# Para evitar que se vuelva a mandar o regresar se manda la excepción
raise ResendSuccessOrderException()

# Estas validaciones aplican para transferencias existentes que
# pudieron haber fallado o han sido enviadas a STP
if transaction.estado in [Estado.failed, Estado.error]:
transaction.set_state(Estado.failed)
return

# Revisa el estado de una transferencia si ya tiene asignado stp_id o ha
# pasado más de 2 hrs.
now = datetime.utcnow()
if transaction.stp_id or (now - transaction.created_at) > timedelta(
hours=2
):
transaction.update_stp_status()
return

# A partir de aquí son validaciones para transferencias nuevas
if transaction.monto > MAX_AMOUNT:
transaction.events.append(Event(type=EventType.error))
transaction.save()
raise MalformedOrderException()

now = datetime.utcnow()
# Return transaction after 2 hours of creation
if (now - transaction.created_at) > timedelta(hours=2):
transaction.fail_if_not_found_stp()
else:
try:
transaction.create_order()
except (
AccountDoesNotExist,
BankCodeClabeMismatch,
InvalidAccountType,
InvalidAmount,
InvalidInstitution,
InvalidTrackingKey,
PldRejected,
ValidationError,
):
transaction.set_state(Estado.failed)
transaction.save()
try:
transaction.create_order()
except (
AccountDoesNotExist,
BankCodeClabeMismatch,
InvalidAccountType,
InvalidAmount,
InvalidInstitution,
InvalidTrackingKey,
PldRejected,
ValidationError,
):
transaction.set_state(Estado.failed)
transaction.save()
Loading
Loading