Exemple #1
0
    async def connect(self):
        """Connect the service worker to th3e NATS/STAN Queue.

        Also handles reconnecting when the network has dropped the connection.
        Both the NATS and the STAN clients need to be reinstantiated to work correctly.

        """
        if not self.config:
            logger.error('missing configuration object.')
            raise AttributeError('missing configuration object.')

        logger.info('Connecting...')
        if self.nc:
            try:
                logger.debug('close old NATS client')
                await self.nc.close()
            except asyncio.CancelledError as err:
                logger.debug('closing stale connection err:%s', err)
            finally:
                self.nc = None

        self.nc = NATS()
        self.sc = STAN()

        nats_connection_options = {
            **self.config.NATS_CONNECTION_OPTIONS,
            **{
                'loop': self._loop,
                'error_cb': error_cb
            },
            **self.nats_connection_options
        }

        stan_connection_options = {
            **self.config.STAN_CONNECTION_OPTIONS,
            **{
                'nats': self.nc,
                'conn_lost_cb': self._stan_conn_lost_cb,
                'loop': self._loop
            },
            **self.stan_connection_options
        }

        subscription_options = {
            **self.config.SUBSCRIPTION_OPTIONS,
            **{
                'cb': self.cb_handler
            },
            **self.subscription_options
        }

        await self.nc.connect(**nats_connection_options)
        await self.sc.connect(**stan_connection_options)
        await self.sc.subscribe(**subscription_options)

        logger.info(
            'Subscribe the callback: %s to the queue: %s.',
            subscription_options.get('cb').__name__
            if subscription_options.get('cb') else 'no_call_back',
            subscription_options.get('queue'))
Exemple #2
0
async def publish_email_message(
        qsm: QueueServiceManager,  # pylint: disable=redefined-outer-name
        cloud_event_msg: dict):
    """Publish the email message onto the NATS emailer subject."""
    logger.debug('publish to queue, subject:%s, event:%s',
                 APP_CONFIG.EMAIL_PUBLISH_OPTIONS['subject'], cloud_event_msg)
    await qsm.service.publish(
        subject=APP_CONFIG.EMAIL_PUBLISH_OPTIONS['subject'],
        msg=cloud_event_msg)
Exemple #3
0
 async def close(self):
     """Close the stream and nats connections."""
     try:
         await self.sc.close()
         await self.nc.close()
     except Exception as err:  # pylint: disable=broad-except # noqa B902
         # catch all errors to log out when closing the service.
         logger.debug('error when closing the streams: %s',
                      err,
                      stack_info=True)
Exemple #4
0
async def cb_subscription_handler(msg: nats.aio.client.Msg):
    """Use Callback to process Queue Msg objects.

    This is the callback handler that gets called when a message is placed on the queue.
    If an exception is thrown and not handled, the message is not marked as consumed
    on the queue. It eventually times out and another worker can grab it.

    In some cases we want to consume the message and capture our failure on Sentry
    to be handled manually by staff.

    This call MUST BE IDEMPOTENT and unroll any partial changes in failures.
    """
    try:
        logger.info('Received raw message seq:%s, data=  %s', msg.sequence, msg.data.decode())
        if not (pay_msg := extract_message(msg)):
            capture_message('Queue Error: no message on queue', level='error')
            logger.debug('Queue Error: no message on queue')
        else:
Exemple #5
0
async def process_payment(pay_msg: dict, flask_app: Flask):
    """Render the payment status."""
    if not flask_app or not pay_msg:
        raise QueueException('Flask App or token not available.')

    with flask_app.app_context():
        logger.debug('entering process payment: %s', pay_msg)

        # capture_message(f'Queue Issue: Unable to find payment.id={payment_id} to place on email queue')
        # return

        if pay_msg.get('paymentToken', {}).get('statusCode') == PaymentState.TRANSACTION_FAILED.value:
            # TODO: The customer has cancelled out of paying, so we could note this better
            # technically the payment for this service is still pending
            logger.debug('Failed transaction on queue:%s', pay_msg)
            return

        complete_payment_status = [PaymentState.COMPLETED.value, PaymentState.APPROVED.value]
        if pay_msg.get('paymentToken', {}).get('statusCode') in complete_payment_status:
            logger.debug('COMPLETED transaction on queue: %s', pay_msg)

            if payment_token := pay_msg.get('paymentToken', {}).get('id'):
                if payment := Payment.find_by_payment_token(payment_token):

                    if update_payment := await update_payment_record(payment):
                        payment = update_payment
                    
                    await furnish_receipt_message(qsm, payment)
                    
                else:
Exemple #6
0
async def cb_subscription_handler(msg: nats.aio.client.Msg):
    """Use Callback to process Queue Msg objects.

    This is the callback handler that gets called when a message is placed on the queue.
    If an exception is thrown and not handled, the message is not marked as consumed
    on the queue. It eventually times out and another worker can grab it.

    In some cases we want to consume the message and capture our failure on Sentry
    to be handled manually by staff.
    """
    try:
        logger.info('Received raw message seq:%s, data=  %s', msg.sequence,
                    msg.data.decode())
        nr_state_change_msg = json.loads(msg.data.decode('utf-8'))
        logger.debug('Extracted nr event msg: %s', nr_state_change_msg)

        if is_processable(nr_state_change_msg):
            logger.debug('Begin process_nr_state_change for nr_event_msg: %s',
                         nr_state_change_msg)
            await process_names_event_message(nr_state_change_msg, FLASK_APP)
            logger.debug(
                'Completed process_nr_state_change for nr_event_msg: %s',
                nr_state_change_msg)
        else:
            # Skip processing of message as it isn't a message type this queue listener processes
            logger.debug(
                'Skipping processing of nr event message as message type is not supported: %s',
                nr_state_change_msg)
    except OperationalError as err:  # message goes back on the queue
        logger.error('Queue Blocked - Database Issue: %s',
                     json.dumps(nr_state_change_msg),
                     exc_info=True)
        raise err  # We don't want to handle the error, as a DB down would drain the queue
    except (RequestException,
            NewConnectionError) as err:  # message goes back on the queue
        logger.error('Queue Blocked - HTTP Connection Issue: %s',
                     json.dumps(nr_state_change_msg),
                     exc_info=True)
        raise err  # We don't want to handle the error, as a http connection error would drain the queue
    except (QueueException, KeyError, Exception):  # pylint: disable=broad-except # noqa B902
        # Catch Exception so that any error is still caught and the message is removed from the queue
        capture_message('Queue Error:' + json.dumps(nr_state_change_msg),
                        level='error')
        logger.error('Queue Error: %s',
                     json.dumps(nr_state_change_msg),
                     exc_info=True)
Exemple #7
0
async def furnish_receipt_message(qsm: QueueServiceManager, payment: Payment):  # pylint: disable=redefined-outer-name
    """Send receipt info to the mail queue if it hasn't yet been done."""
    if payment.furnished == 'Y':
        logger.debug(
            'Queue Issue: Duplicate, already furnished receipt for payment.id=%s',
            payment.id)
        capture_message(
            f'Queue Issue: Duplicate, already furnished receipt for payment.id={payment.id}'
        )
        return

    nr = None

    logger.debug('Start of the furnishing of receipt for payment record:%s',
                 payment.as_dict())
    try:
        payment.furnished = True
        payment.save_to_db()
    except Exception as err:  # noqa: B902; bare exception to catch all
        raise Exception('Unable to alter payment record.') from err

    try:
        nr = RequestDAO.find_by_id(payment.nrId)
        cloud_event_msg = create_cloud_event_msg(
            msg_id=str(uuid.uuid4()),
            msg_type='bc.registry.names.request',
            source=f'/requests/{nr.nrNum}',
            time=datetime.utcfromtimestamp(
                time.time()).replace(tzinfo=timezone.utc).isoformat(),
            identifier=nr.nrNum,
            json_data_body={
                'request': {
                    'header': {
                        'nrNum': nr.nrNum
                    },
                    'paymentToken': payment.payment_token,
                    'statusCode': nr.stateCd
                }
            })
        logger.debug('About to publish email for payment.id=%s', payment.id)
        await publish_email_message(qsm, cloud_event_msg)
    except Exception as err:  # noqa: B902; bare exception to catch all
        payment.furnished = False
        payment.save_to_db()
        logger.debug('Reset payment furnish status payment.id= %s', payment.id)
        raise QueueException(f'Unable to furnish NR info. {err}') from err
Exemple #8
0
async def process_names_event_message(msg: dict, flask_app: Flask):
    """Update solr accordingly based on incoming nr state changes."""
    if not flask_app or not msg:
        raise QueueException('Flask App or msg not available.')

    with flask_app.app_context():
        logger.debug('entering processing of nr event msg: %s', msg)
        request_state_change = msg.get('data').get('request', None)

        if request_state_change:
            new_state = request_state_change.get('newState')
            if new_state in ('APPROVED', 'CONDITIONAL'):
                process_names_add(request_state_change)
                process_possible_conflicts_add(request_state_change)
            elif new_state in ('CANCELLED', 'RESET', 'CONSUMED'):
                process_names_delete(request_state_change)
                process_possible_conflicts_delete(request_state_change)
            else:
                logger.debug(
                    'no names processing required for request state change message %s',
                    msg)
        else:
            logger.debug('skipping - no matching state change message %s', msg)
Exemple #9
0
async def update_payment_record(payment: Payment) -> Optional[Payment]:
    """Update the payment record in the database.

    Alter the NR state as required based on the payment action.
    Payment NR Action - One of [COMPLETE, UPGRADE, REAPPLY]
    COMPLETE - set NR to DRAFT IFF nr.state == PENDING_PAYMENT
    UPGRADE - set the nr.priority to True/'Y'
    REAPPLY - add REAPPLY_EXTENSION to expiry date of NR IFF it hasn't expired
    """
    if payment.payment_completion_date:
        msg = f'Queue Issue: Duplicate, payment already processed for payment.id={payment.id}'
        logger.debug(msg)
        capture_message(msg)
        return None

    payment_action = payment.payment_action
    nr = RequestDAO.find_by_id(payment.nrId)
    if payment_action == Payment.PaymentActions.CREATE.value:  # pylint: disable=R1705
        if nr.stateCd == State.PENDING_PAYMENT:
            nr.stateCd = State.DRAFT
            payment.payment_completion_date = datetime.utcnow()
            payment.payment_status_code = State.COMPLETED

            nr.save_to_db()
            payment.save_to_db()
        return payment

    elif payment_action == Payment.PaymentActions.UPGRADE.value:
        if nr.stateCd == State.PENDING_PAYMENT:
            msg = f'Queue Issue: Upgrading a non-DRAFT NR for payment.id={payment.id}'
            logger.debug(msg)
            capture_message(msg)
            raise QueueException(msg)

        nr.priorityCd = 'Y'
        nr.priorityDate = datetime.utcnow()
        payment.payment_completion_date = datetime.utcnow()
        payment.payment_status_code = State.COMPLETED

        nr.save_to_db()
        payment.save_to_db()
        return payment

    elif payment_action == Payment.PaymentActions.REAPPLY.value:
        if nr.stateCd != State.APPROVED \
                and nr.expirationDate + timedelta(hours=NAME_REQUEST_EXTENSION_PAD_HOURS) < datetime.utcnow():
            msg = f'Queue Issue: Failed attempt to extend NR for payment.id={payment.id} '\
                'nr.state{nr.stateCd}, nr.expires:{nr.expirationDate}'
            logger.debug(msg)
            capture_message(msg)
            raise QueueException(msg)
        nr.expirationDate = nr.expirationDate + timedelta(days=NAME_REQUEST_LIFESPAN_DAYS)
        payment.payment_completion_date = datetime.utcnow()
        payment.payment_status_code = State.COMPLETED

        nr.save_to_db()
        payment.save_to_db()
        return payment

    msg = f'Queue Issue: Unknown action:{payment_action} for payment.id={payment.id}'
    logger.debug(msg)
    capture_message(msg)
    raise QueueException(f'Unknown action:{payment_action} for payment.id={payment.id}')
Exemple #10
0
            return

        complete_payment_status = [PaymentState.COMPLETED.value, PaymentState.APPROVED.value]
        if pay_msg.get('paymentToken', {}).get('statusCode') in complete_payment_status:
            logger.debug('COMPLETED transaction on queue: %s', pay_msg)

            if payment_token := pay_msg.get('paymentToken', {}).get('id'):
                if payment := Payment.find_by_payment_token(payment_token):

                    if update_payment := await update_payment_record(payment):
                        payment = update_payment
                    
                    await furnish_receipt_message(qsm, payment)
                    
                else:
                    logger.debug('Queue Error: Unable to find payment record for :%s', pay_msg)
                    capture_message(f'Queue Error: Unable to find payment record for :{pay_msg}', level='error')
            else:
                logger.debug('Queue Error: Missing id :%s', pay_msg)
                capture_message(f'Queue Error: Missing id :{pay_msg}', level='error')

            return

        # if we're here and haven't been able to action it,
        # then we've received an unknown token
        # Capture it to the log and remove it rom the queue
        logger.error('Unknown payment token given: %s', payment_token)
        capture_message(
            f'Queue Error: Unknown paymentToken:{payment_token}',
            level='error')
Exemple #11
0
def process_delete_from_solr(state_change_msg: dict):  # pylint: disable=too-many-locals, , too-many-branches
    """Process names update via Solr feeder api."""
    logger.debug('names processing: %s', state_change_msg)
    nr_num = state_change_msg.get('nrNum', None)
    nr = RequestDAO.find_by_nr(nr_num)
    send_to_solr_delete(nr)
Exemple #12
0
async def process_payment(pay_msg: dict, flask_app: Flask):
    """Render the payment status."""
    if not flask_app or not pay_msg:
        raise QueueException('Flask App or token not available.')

    with flask_app.app_context():
        logger.debug('entering process payment: %s', pay_msg)

        # capture_message(f'Queue Issue: Unable to find payment.id={payment_id} to place on email queue')
        # return

        if pay_msg.get(
                'paymentToken',
            {}).get('statusCode') == PaymentState.TRANSACTION_FAILED.value:
            # TODO: The customer has cancelled out of paying, so we could note this better
            # technically the payment for this service is still pending
            logger.debug('Failed transaction on queue:%s', pay_msg)
            return

        complete_payment_status = [
            PaymentState.COMPLETED.value, PaymentState.APPROVED.value
        ]
        if pay_msg.get('paymentToken',
                       {}).get('statusCode') in complete_payment_status:  # pylint: disable=R1702
            logger.debug('COMPLETED transaction on queue: %s', pay_msg)

            if payment_token := pay_msg.get('paymentToken', {}).get('id'):
                if payment := Payment.find_by_payment_token(payment_token):

                    if update_payment := await update_payment_record(payment):
                        payment = update_payment
                        # record event
                        nr = RequestDAO.find_by_id(payment.nrId)
                        # TODO: create a namex_pay user for this
                        user = User.find_by_username(
                            'name_request_service_account')
                        EventRecorder.record(
                            user, Event.NAMEX_PAY +
                            f' [payment completed] { payment.payment_action }',
                            nr, nr.json())
                        # try to update NRO otherwise send a sentry msg for OPS
                        if payment.payment_action in \
                                [payment.PaymentActions.UPGRADE.value, payment.PaymentActions.REAPPLY.value]:
                            change_flags = {
                                'is_changed__request': True,
                                'is_changed__previous_request': False,
                                'is_changed__applicant': False,
                                'is_changed__address': False,
                                'is_changed__name1': False,
                                'is_changed__name2': False,
                                'is_changed__name3': False,
                                'is_changed__nwpta_ab': False,
                                'is_changed__nwpta_sk': False,
                                'is_changed__request_state': False,
                                'is_changed_consent': False
                            }
                            warnings = nro.change_nr(nr, change_flags)
                            if warnings:
                                logger.error(
                                    'Queue Error: Unable to update NRO :%s',
                                    warnings)
                                capture_message(
                                    f'Queue Error: Unable to update NRO for {nr} {payment.payment_action} :{warnings}',
                                    level='error')

                    await furnish_receipt_message(qsm, payment)

                else: