def test_activate_bcol_change_to_pad(session):
    """Test Activate account."""
    # Create a pending account first, then call the job
    account = factory_create_pad_account(
        auth_account_id='1', payment_method=PaymentMethod.DRAWDOWN.value)
    CreateAccountTask.create_accounts()
    account = PaymentAccount.find_by_id(account.id)
    cfs_account: CfsAccount = CfsAccount.find_effective_by_account_id(
        account.id)
    assert cfs_account.status == CfsAccountStatus.PENDING_PAD_ACTIVATION.value, 'Created account has pending pad status'
    assert account.payment_method == PaymentMethod.DRAWDOWN.value

    ActivatePadAccountTask.activate_pad_accounts()
    cfs_account: CfsAccount = CfsAccount.find_effective_by_account_id(
        account.id)
    assert cfs_account.status == CfsAccountStatus.PENDING_PAD_ACTIVATION.value, \
        'Same day Job runs and shouldnt change anything.'
    account = PaymentAccount.find_by_id(account.id)
    assert account.payment_method == PaymentMethod.DRAWDOWN.value

    time_delay = current_app.config['PAD_CONFIRMATION_PERIOD_IN_DAYS']
    with freeze_time(datetime.today() + timedelta(days=time_delay, minutes=1)):
        ActivatePadAccountTask.activate_pad_accounts()
        assert cfs_account.status == CfsAccountStatus.ACTIVE.value, \
            'After the confirmation period is over , status should be active'
        account = PaymentAccount.find_by_id(account.id)
        assert account.payment_method == PaymentMethod.PAD.value
Beispiel #2
0
def test_get_statement_report_for_empty_invoices(session):
    """Assert that the get statement report works for statement with no invoices."""
    bcol_account = factory_premium_payment_account()
    bcol_account.save()

    payment = factory_payment()
    payment.save()
    i = factory_invoice(payment_account=bcol_account)
    i.save()
    factory_invoice_reference(i.id).save()
    factory_payment_line_item(invoice_id=i.id, fee_schedule_id=1).save()

    settings_model = factory_statement_settings(
        payment_account_id=bcol_account.id,
        frequency=StatementFrequency.DAILY.value)
    statement_model = factory_statement(
        payment_account_id=bcol_account.id,
        frequency=StatementFrequency.DAILY.value,
        statement_settings_id=settings_model.id)

    payment_account = PaymentAccount.find_by_id(bcol_account.id)
    statements = StatementService.find_by_account_id(
        payment_account.auth_account_id, page=1, limit=10)
    assert statements is not None

    report_response, report_name = StatementService.get_statement_report(
        statement_id=statement_model.id,
        content_type='application/pdf',
        auth=get_auth_premium_user())
    assert report_response is not None
Beispiel #3
0
def test_statement_find_by_account(session):
    """Assert that the statement settings by id works."""
    bcol_account = factory_premium_payment_account()
    bcol_account.save()

    payment = factory_payment()
    payment.save()
    i = factory_invoice(payment_account=bcol_account)
    i.save()
    factory_invoice_reference(i.id).save()

    settings_model = factory_statement_settings(
        payment_account_id=bcol_account.id,
        frequency=StatementFrequency.DAILY.value)
    statement_model = factory_statement(
        payment_account_id=bcol_account.id,
        frequency=StatementFrequency.DAILY.value,
        statement_settings_id=settings_model.id)
    factory_statement_invoices(statement_id=statement_model.id,
                               invoice_id=i.id)

    payment_account = PaymentAccount.find_by_id(bcol_account.id)
    statements = StatementService.find_by_account_id(
        payment_account.auth_account_id, page=1, limit=10)
    assert statements is not None
    assert statements.get('total') == 1
Beispiel #4
0
    def create_accounts(cls):  # pylint: disable=too-many-locals
        """Find all pending accounts to be created in CFS.

        Steps:
        1. Find all pending CFS accounts.
        2. Create CFS accounts.
        3. Publish a message to the queue if successful.
        """
        # Pass payment method if offline account creation has be restricted based on payment method.
        pending_accounts: List[
            CfsAccountModel] = CfsAccountModel.find_all_pending_accounts()
        current_app.logger.info(
            f'Found {len(pending_accounts)} CFS Accounts to be created.')
        if len(pending_accounts) == 0:
            return

        auth_token = get_token()

        for pending_account in pending_accounts:
            # Find the payment account and create the pay system instance.
            pay_account: PaymentAccountModel = PaymentAccountModel.find_by_id(
                pending_account.account_id)
            if pay_account.payment_method in (PaymentMethod.CASH.value,
                                              PaymentMethod.CHEQUE.value):
                routing_slip.create_cfs_account(pending_account, pay_account)
            else:
                cls._create_cfs_account(pending_account, pay_account,
                                        auth_token)
Beispiel #5
0
def test_update_statement_daily_to_daily(session):
    """Assert that going from daily to daily creates a new record."""
    bcol_account = factory_premium_payment_account()
    bcol_account.save()

    payment = factory_payment()
    payment.save()
    i = factory_invoice(payment_account=bcol_account)
    i.save()
    factory_invoice_reference(i.id).save()
    factory_statement_settings(payment_account_id=bcol_account.id,
                               frequency=StatementFrequency.DAILY.value)

    # update to weekly
    payment_account = PaymentAccount.find_by_id(bcol_account.id)
    statement_settings = StatementSettingsService.update_statement_settings(payment_account.auth_account_id,
                                                                            StatementFrequency.DAILY.value)
    assert statement_settings is not None
    assert statement_settings.get('frequency') == StatementFrequency.DAILY.value
    assert statement_settings.get('to_date') is None

    # daily to daily - assert daily should start by tomorow
    assert statement_settings.get('from_date') == (datetime.today() + timedelta(days=1)).strftime('%Y-%m-%d')

    # daily to daily - assert current active one is stil daily ending today
    current_statement_settings = StatementSettingsModel.find_active_settings(payment_account.auth_account_id,
                                                                             datetime.today())
    assert current_statement_settings is not None
    assert current_statement_settings.frequency == StatementFrequency.DAILY.value
    assert current_statement_settings.to_date == datetime.today().date()
Beispiel #6
0
def test_get_monthly_statement_report_as_csv(session, client, jwt, app):
    """Assert that the monthly statement report is returning response."""
    # Create a payment account and statement details, then get all statements for the account
    token = jwt.create_jwt(get_claims(), token_header)
    headers = {
        'Authorization': f'Bearer {token}',
        'content-type': 'application/json',
        'Accept': ContentType.CSV.value
    }

    rv = client.post('/api/v1/payment-requests',
                     data=json.dumps(
                         get_payment_request(business_identifier='CP0002000')),
                     headers=headers)

    payment: Payment = Payment.find_by_id(rv.json.get('id'))
    bcol_account: BcolPaymentAccount = BcolPaymentAccount.find_by_id(
        payment.invoices[0].bcol_account_id)
    pay_account: PaymentAccount = PaymentAccount.find_by_id(
        bcol_account.account_id)

    settings_model = factory_statement_settings(
        payment_account_id=pay_account.id,
        frequency=StatementFrequency.DAILY.value)
    statement_model = factory_statement(
        payment_account_id=pay_account.id,
        frequency=StatementFrequency.DAILY.value,
        statement_settings_id=settings_model.id)
    factory_statement_invoices(statement_id=statement_model.id,
                               invoice_id=payment.invoices[0].id)

    rv = client.get(
        f'/api/v1/accounts/{pay_account.auth_account_id}/statements/{statement_model.id}',
        headers=headers)
    assert rv.status_code == 200
def test_update_pad_account(session):
    """Test update account."""
    # Create a pending account first, then call the job
    account = factory_create_pad_account(auth_account_id='2')
    CreateAccountTask.create_accounts()

    account = PaymentAccount.find_by_id(account.id)
    cfs_account = CfsAccount.find_effective_by_account_id(account.id)

    assert cfs_account.payment_instrument_number

    # Now update the account.
    new_payment_details = {
        'bankInstitutionNumber': '111',
        'bankTransitNumber': '222',
        'bankAccountNumber': '3333333333'
    }
    PadService().update_account(name='Test', cfs_account=cfs_account, payment_info=new_payment_details)
    cfs_account = CfsAccount.find_by_id(cfs_account.id)

    # Run the job again
    CreateAccountTask.create_accounts()

    updated_cfs_account: CfsAccount = CfsAccount.find_effective_by_account_id(account.id)
    assert updated_cfs_account.id != cfs_account.id
    assert updated_cfs_account.bank_account_number == new_payment_details.get('bankAccountNumber')
    assert updated_cfs_account.bank_branch_number == new_payment_details.get('bankTransitNumber')
    assert updated_cfs_account.bank_number == new_payment_details.get('bankInstitutionNumber')

    assert cfs_account.status == CfsAccountStatus.INACTIVE.value
    assert updated_cfs_account.status == CfsAccountStatus.PENDING_PAD_ACTIVATION.value
    assert updated_cfs_account.payment_instrument_number
    def _apply_routing_slips_to_pending_invoices(cls, routing_slip: RoutingSlipModel) -> float:
        """Apply the routing slips again, when routing slip is linked to an NSF parent."""
        current_app.logger.info(f'Starting NSF recovery process for {routing_slip.number}')
        routing_slip_payment_account: PaymentAccountModel = PaymentAccountModel.find_by_id(
            routing_slip.payment_account_id)

        # apply invoice to the active CFS_ACCOUNT which will be the parent routing slip
        active_cfs_account = CfsAccountModel.find_effective_by_account_id(routing_slip_payment_account.id)

        invoices: List[InvoiceModel] = db.session.query(InvoiceModel) \
            .filter(InvoiceModel.routing_slip == routing_slip.number,
                    InvoiceModel.invoice_status_code.in_([InvoiceStatus.CREATED.value, InvoiceStatus.APPROVED.value])) \
            .all()
        current_app.logger.info(f'Found {len(invoices)} to apply receipt')
        applied_amount = 0
        for inv in invoices:
            inv_ref: InvoiceReferenceModel = InvoiceReferenceModel.find_reference_by_invoice_id_and_status(
                inv.id, InvoiceReferenceStatus.ACTIVE.value
            )
            cls.apply_routing_slips_to_invoice(
                routing_slip_payment_account, active_cfs_account, routing_slip, inv, inv_ref.invoice_number
            )

            # IF invoice balance is zero, then update records.
            if CFSService.get_invoice(cfs_account=active_cfs_account, inv_number=inv_ref.invoice_number) \
                    .get('amount_due') == 0:
                applied_amount += inv.total
                inv_ref.status_code = InvoiceReferenceStatus.COMPLETED.value
                inv.invoice_status_code = InvoiceStatus.PAID.value

        return applied_amount
    def _notify_for_ob(cls):  # pylint: disable=too-many-locals
        """Notify for online banking.

        1) Find the accounts with pending invoices
        2) get total remaining for that account

        """
        unpaid_status = (InvoiceStatus.SETTLEMENT_SCHEDULED.value,
                         InvoiceStatus.PARTIAL.value,
                         InvoiceStatus.CREATED.value)
        notification_date = datetime.today() - timedelta(
            days=current_app.config.get('NOTIFY_AFTER_DAYS'))
        # Get distinct accounts with pending invoices for that exact day
        notification_pending_accounts = db.session.query(
            InvoiceModel.payment_account_id
        ).distinct().filter(
            and_(
                InvoiceModel.invoice_status_code.in_(unpaid_status),
                InvoiceModel.payment_method_code ==
                PaymentMethod.ONLINE_BANKING.value,
                # cast is used to get the exact match stripping the timestamp from date
                cast(InvoiceModel.created_on,
                     Date) == notification_date.date())).all()
        current_app.logger.debug(
            f'Found {len(notification_pending_accounts)} invoices to notify admins.'
        )
        for payment_account in notification_pending_accounts:
            try:
                payment_account_id = payment_account[0]
                total = db.session.query(
                    func.sum(InvoiceModel.total).label('total')
                ).filter(
                    and_(
                        InvoiceModel.invoice_status_code.in_(unpaid_status),
                        InvoiceModel.payment_account_id == payment_account_id,
                        InvoiceModel.payment_method_code ==
                        PaymentMethod.ONLINE_BANKING.value)).group_by(
                            InvoiceModel.payment_account_id).all()
                pay_account: PaymentAccountModel = \
                    PaymentAccountModel.find_by_id(payment_account_id)

                cfs_account = CfsAccountModel.find_effective_by_account_id(
                    payment_account_id)

                # emit account mailer event
                addition_params_to_mailer = {
                    'transactionAmount': total[0][0],
                    'cfsAccountId': cfs_account.cfs_account,
                    'authAccountId': pay_account.auth_account_id,
                }
                mailer.publish_mailer_events('ob.outstandingInvoice',
                                             pay_account,
                                             addition_params_to_mailer)
            except Exception as e:  # NOQA # pylint: disable=broad-except
                capture_message(
                    f'Error on notifying mailer  OB Pending invoice: account id={pay_account.id}, '
                    f'auth account : {pay_account.auth_account_id}, ERROR : {str(e)}',
                    level='error')
                current_app.logger.error(e)
Beispiel #10
0
    def find_by_id(cls, account_id: int):
        """Find pay account by id."""
        current_app.logger.debug('<find_by_id')

        account = PaymentAccount()
        account._dao = PaymentAccountModel.find_by_id(account_id)  # pylint: disable=protected-access

        current_app.logger.debug('>find_by_id')
        return account
    def _cancel_rs_invoices(cls):
        """Cancel routing slip invoices in CFS."""
        invoices: List[InvoiceModel] = InvoiceModel.query \
            .filter(InvoiceModel.payment_method_code == PaymentMethod.INTERNAL.value) \
            .filter(InvoiceModel.invoice_status_code == InvoiceStatus.REFUND_REQUESTED.value) \
            .filter(InvoiceModel.routing_slip is not None) \
            .order_by(InvoiceModel.created_on.asc()).all()

        current_app.logger.info(
            f'Found {len(invoices)} to be cancelled in CFS.')
        for invoice in invoices:
            # call unapply rcpts
            # adjust invoice to zero
            current_app.logger.debug(f'Calling the invoice {invoice.id}')
            routing_slip = RoutingSlipModel.find_by_number(
                invoice.routing_slip)
            routing_slip_payment_account: PaymentAccountModel = PaymentAccountModel.find_by_id(
                routing_slip.payment_account_id)
            cfs_account: CfsAccountModel = CfsAccountModel.find_effective_by_account_id(
                routing_slip_payment_account.id)
            # Find COMPLETED invoice reference; as unapply has to be done only if invoice is created and applied in CFS.
            invoice_reference = InvoiceReferenceModel. \
                find_reference_by_invoice_id_and_status(invoice.id, status_code=InvoiceReferenceStatus.COMPLETED.value)
            if invoice_reference:
                current_app.logger.debug(
                    f'Found invoice reference - {invoice_reference.invoice_number}'
                )
                try:
                    # find receipts against the invoice and unapply
                    # apply receipt now
                    receipts: List[
                        ReceiptModel] = ReceiptModel.find_all_receipts_for_invoice(
                            invoice_id=invoice.id)
                    for receipt in receipts:
                        CFSService.unapply_receipt(
                            cfs_account, receipt.receipt_number,
                            invoice_reference.invoice_number)

                    adjustment_negative_amount = -invoice.total
                    CFSService.adjust_invoice(
                        cfs_account=cfs_account,
                        inv_number=invoice_reference.invoice_number,
                        amount=adjustment_negative_amount)

                except Exception as e:  # NOQA # pylint: disable=broad-except
                    capture_message(
                        f'Error on canelling Routing Slip invoice: invoice id={invoice.id}, '
                        f'routing slip : {routing_slip.id}, ERROR : {str(e)}',
                        level='error')
                    current_app.logger.error(e)
                    # TODO stop execution ? what should be the invoice stats ; should we set it to error or retry?
                    continue

                invoice_reference.status_code = InvoiceReferenceStatus.CANCELLED.value

            invoice.invoice_status_code = InvoiceStatus.REFUNDED.value
            invoice.save()
    def find_by_id(account_id: int):
        """Find account by id."""
        account_dao = PaymentAccountModel.find_by_id(account_id)
        if not account_dao:
            raise BusinessException(Error.PAY009)

        account = PaymentAccount()
        account._dao = account_dao  # pylint: disable=protected-access

        current_app.logger.debug('>find_by_id')
        return account
def test_create_pad_account(session):
    """Test create account."""
    # Create a pending account first, then call the job
    account = factory_create_pad_account(auth_account_id='1')
    CreateAccountTask.create_accounts()
    account = PaymentAccount.find_by_id(account.id)
    cfs_account: CfsAccount = CfsAccount.find_effective_by_account_id(account.id)
    assert cfs_account.status == CfsAccountStatus.PENDING_PAD_ACTIVATION.value
    assert cfs_account.bank_account_number
    assert cfs_account.cfs_party
    assert cfs_account.cfs_site
    assert cfs_account.cfs_account
    assert cfs_account.payment_instrument_number
Beispiel #14
0
def test_create_online_banking_account(session):
    """Test create account."""
    # Create a pending account first, then call the job
    account = factory_create_online_banking_account(auth_account_id='2')
    CreateAccountTask.create_accounts()
    account = PaymentAccount.find_by_id(account.id)
    cfs_account = CfsAccount.find_effective_by_account_id(account.id)
    assert cfs_account.status == CfsAccountStatus.ACTIVE.value
    assert not cfs_account.bank_account_number
    assert cfs_account.cfs_party
    assert cfs_account.cfs_site
    assert cfs_account.cfs_account
    assert cfs_account.payment_instrument_number is None
def test_create_pad_account_no_confirmation_period(session):
    """Test create account.Arbitrary scenario when there is no confirmation period."""
    # Create a pending account first, then call the job
    account = factory_create_pad_account(auth_account_id='1', confirmation_period=0)
    CreateAccountTask.create_accounts()
    account = PaymentAccount.find_by_id(account.id)
    cfs_account: CfsAccount = CfsAccount.find_effective_by_account_id(account.id)
    assert cfs_account.status == CfsAccountStatus.ACTIVE.value
    assert cfs_account.bank_account_number
    assert cfs_account.cfs_party
    assert cfs_account.cfs_site
    assert cfs_account.cfs_account
    assert cfs_account.payment_instrument_number
def test_update_online_banking_account(session):
    """Test update account."""
    # Create a pending account first, then call the job
    account = factory_create_online_banking_account(auth_account_id='2')
    CreateAccountTask.create_accounts()
    account = PaymentAccount.find_by_id(account.id)
    cfs_account = CfsAccount.find_effective_by_account_id(account.id)

    # Update account, which shouldn't change any details
    OnlineBankingService().update_account(name='Test', cfs_account=cfs_account, payment_info=None)
    updated_cfs_account = CfsAccount.find_effective_by_account_id(account.id)

    assert updated_cfs_account.status == CfsAccountStatus.ACTIVE.value
    assert cfs_account.id == updated_cfs_account.id
Beispiel #17
0
 def _publish_refund_to_mailer(invoice: InvoiceModel):
     """Construct message and send to mailer queue."""
     from .payment_transaction import publish_response  # pylint:disable=import-outside-toplevel,cyclic-import
     receipt: ReceiptModel = ReceiptModel.find_by_invoice_id_and_receipt_number(
         invoice_id=invoice.id)
     invoice_ref: InvoiceReferenceModel = InvoiceReferenceModel.find_reference_by_invoice_id_and_status(
         invoice_id=invoice.id,
         status_code=InvoiceReferenceStatus.COMPLETED.value)
     payment_transaction: PaymentTransactionModel = PaymentTransactionModel.find_recent_completed_by_invoice_id(
         invoice_id=invoice.id)
     message_type: str = f'bc.registry.payment.{invoice.payment_method_code.lower()}.refundRequest'
     transaction_date_time = receipt.receipt_date if invoice.payment_method_code == PaymentMethod.DRAWDOWN.value \
         else payment_transaction.transaction_end_time
     filing_description = ''
     for line_item in invoice.payment_line_items:
         if filing_description:
             filing_description += ','
         filing_description += line_item.description
     q_payload = dict(
         specversion='1.x-wip',
         type=message_type,
         source=
         f'https://api.pay.bcregistry.gov.bc.ca/v1/invoices/{invoice.id}',
         id=invoice.id,
         datacontenttype='application/json',
         data=dict(identifier=invoice.business_identifier,
                   orderNumber=receipt.receipt_number,
                   transactionDateTime=get_local_formatted_date_time(
                       transaction_date_time),
                   transactionAmount=receipt.receipt_amount,
                   transactionId=invoice_ref.invoice_number,
                   refundDate=get_local_formatted_date_time(
                       datetime.now(), '%Y%m%d'),
                   filingDescription=filing_description))
     if invoice.payment_method_code == PaymentMethod.DRAWDOWN.value:
         payment_account: PaymentAccountModel = PaymentAccountModel.find_by_id(
             invoice.payment_account_id)
         q_payload['data'].update(
             dict(bcolAccount=invoice.bcol_account,
                  bcolUser=payment_account.bcol_user_id))
     current_app.logger.debug(
         f'Publishing payment refund request to mailer for {invoice.id} : {q_payload}'
     )
     publish_response(
         payload=q_payload,
         client_name=current_app.config.get('NATS_MAILER_CLIENT_NAME'),
         subject=current_app.config.get('NATS_MAILER_SUBJECT'))
Beispiel #18
0
def test_statement_settings_find_by_account(session):
    """Assert that the statement settings by id works."""
    bcol_account = factory_premium_payment_account()
    bcol_account.save()

    payment = factory_payment()
    payment.save()
    i = factory_invoice(payment_account=bcol_account)
    i.save()
    factory_invoice_reference(i.id).save()
    factory_statement_settings(payment_account_id=bcol_account.id,
                               frequency=StatementFrequency.DAILY.value)

    payment_account = PaymentAccount.find_by_id(bcol_account.id)
    statement_settings = StatementSettingsService.find_by_account_id(payment_account.auth_account_id)
    assert statement_settings is not None
    assert statement_settings.get('current_frequency').get('frequency') == StatementFrequency.DAILY.value
Beispiel #19
0
def test_link_rs(session):
    """Test link routing slip."""
    child_rs_number = '1234'
    parent_rs_number = '89799'
    factory_routing_slip_account(number=child_rs_number,
                                 status=CfsAccountStatus.ACTIVE.value)
    factory_routing_slip_account(number=parent_rs_number,
                                 status=CfsAccountStatus.ACTIVE.value)
    child_rs = RoutingSlipModel.find_by_number(child_rs_number)
    parent_rs = RoutingSlipModel.find_by_number(parent_rs_number)
    # Do Link
    child_rs.status = RoutingSlipStatus.LINKED.value
    child_rs.parent_number = parent_rs.number
    child_rs.save()
    payment_account: PaymentAccountModel = PaymentAccountModel.find_by_id(
        child_rs.payment_account_id)

    cfs_account: CfsAccountModel = CfsAccountModel.find_effective_by_account_id(
        payment_account.id)

    with patch('pay_api.services.CFSService.reverse_rs_receipt_in_cfs'
               ) as mock_cfs_reverse:
        with patch('pay_api.services.CFSService.create_cfs_receipt'
                   ) as mock_create_cfs:
            RoutingSlipTask.link_routing_slips()
            mock_cfs_reverse.assert_called()
            mock_cfs_reverse.assert_called_with(cfs_account, child_rs.number)
            mock_create_cfs.assert_called()

    # child_rs = RoutingSlipModel.find_by_number(child_rs_number)
    # parent_rs = RoutingSlipModel.find_by_number(parent_rs_number)
    # PS This has changed, no longer updating child rs payment account with parent.
    # assert child_rs.payment_account_id == parent_rs.payment_account_id
    cfs_account: CfsAccountModel = CfsAccountModel.find_by_id(cfs_account.id)
    assert cfs_account.status == CfsAccountStatus.INACTIVE.value

    # make sure next invocation doesnt fetch any records
    with patch('pay_api.services.CFSService.reverse_rs_receipt_in_cfs'
               ) as mock_cfs_reverse:
        with patch('pay_api.services.CFSService.create_cfs_receipt'
                   ) as mock_create_cfs:
            RoutingSlipTask.link_routing_slips()
            mock_cfs_reverse.assert_not_called()
            mock_create_cfs.assert_not_called()
def test_update_statement_monthly(session):
    """Assert that the statement settings by id works."""
    bcol_account = factory_premium_payment_account()
    bcol_account.save()

    payment = factory_payment()
    payment.save()
    i = factory_invoice(payment_account=bcol_account)
    i.save()
    factory_invoice_reference(i.id).save()
    factory_statement_settings(payment_account_id=bcol_account.id,
                               frequency=StatementFrequency.MONTHLY.value)

    # update to weekly
    payment_account = PaymentAccount.find_by_id(bcol_account.id)
    statement_settings = StatementSettingsService.update_statement_settings(
        payment_account.auth_account_id, StatementFrequency.WEEKLY.value)
    assert statement_settings is not None
    assert statement_settings.get(
        'frequency') == StatementFrequency.WEEKLY.value
    assert statement_settings.get('to_date') is None

    # monthly to weekly - assert weekly should start by next week first day
    end_of_month_date = get_first_and_last_dates_of_month(
        datetime.today().month,
        datetime.today().year)[1]
    assert statement_settings.get('from_date') == (
        end_of_month_date + timedelta(days=1)).strftime(DT_SHORT_FORMAT)

    # monthly to weekly - assert current active one is stil monthly ending end of the week
    current_statement_settings = StatementSettingsModel.find_active_settings(
        payment_account.auth_account_id, datetime.today())
    assert current_statement_settings is not None
    assert current_statement_settings.frequency == StatementFrequency.MONTHLY.value
    assert current_statement_settings.to_date == end_of_month_date.date()

    # travel to next week and see whats active
    with freeze_time(end_of_month_date + timedelta(days=2)):
        next_week_statement_settings = StatementSettingsModel.find_active_settings(
            payment_account.auth_account_id, datetime.today())
        assert next_week_statement_settings is not None
        assert next_week_statement_settings.frequency == StatementFrequency.WEEKLY.value
        assert next_week_statement_settings.to_date is None
Beispiel #21
0
    def adjust_routing_slips(cls):
        """Adjust routing slips.

        Steps:
        1. Adjust routing slip receipts for any Write off routing slips.
        2. Adjust routing slip receipts for any Refund approved routing slips.
        """
        current_app.logger.info('<<adjust_routing_slips')
        adjust_statuses = [RoutingSlipStatus.REFUND_AUTHORIZED.value, RoutingSlipStatus.WRITE_OFF_AUTHORIZED.value]
        # For any pending refund/write off balance should be more than $0
        routing_slips = db.session.query(RoutingSlipModel) \
            .filter(RoutingSlipModel.status.in_(adjust_statuses), RoutingSlipModel.remaining_amount > 0).all()
        current_app.logger.info(f'Found {len(routing_slips)} to write off or refund authorized.')
        for routing_slip in routing_slips:
            try:
                # 1.Adjust the routing slip and it's child routing slips for the remaining balance.
                current_app.logger.debug(f'Adjusting routing slip {routing_slip.number}')
                payment_account: PaymentAccountModel = PaymentAccountModel.find_by_id(routing_slip.payment_account_id)
                cfs_account: CfsAccountModel = CfsAccountModel.find_effective_by_account_id(payment_account.id)

                # reverse routing slip receipt
                # Find all child routing slip and reverse it, as all linked routing slips are also considered as NSF.
                child_routing_slips: List[RoutingSlipModel] = RoutingSlipModel.find_children(routing_slip.number)
                for rs in (routing_slip, *child_routing_slips):
                    receipt_number = rs.number
                    is_refund = routing_slip.status == RoutingSlipStatus.REFUND_AUTHORIZED.value
                    if rs.parent_number:
                        receipt_number = f'{receipt_number}L'
                    # Adjust the receipt to zero in CFS
                    CFSService.adjust_receipt_to_zero(cfs_account, receipt_number, is_refund)

                routing_slip.refund_amount = routing_slip.remaining_amount
                routing_slip.remaining_amount = 0
                routing_slip.save()

            except Exception as e:  # NOQA # pylint: disable=broad-except
                capture_message(
                    f'Error on Adjusting Routing Slip for :={routing_slip.number}, '
                    f'routing slip : {routing_slip.id}, ERROR : {str(e)}', level='error')
                current_app.logger.error(e)
                continue
Beispiel #22
0
def _sync_credit_records():
    """Sync credit records with CFS."""
    # 1. Get all credit records with balance > 0
    # 2. If it's on account receipt call receipt endpoint and calculate balance.
    # 3. If it's credit memo, call credit memo endpoint and calculate balance.
    # 4. Roll up the credits to credit field in payment_account.
    active_credits: List[CreditModel] = db.session.query(CreditModel).filter(
        CreditModel.remaining_amount > 0).all()
    logger.info('Found %s credit records', len(active_credits))
    account_ids: List[int] = []
    for credit in active_credits:
        account_ids.append(credit.account_id)
        cfs_account: CfsAccountModel = CfsAccountModel.find_effective_by_account_id(
            credit.account_id)
        if credit.is_credit_memo:
            credit_memo = CFSService.get_cms(cfs_account=cfs_account,
                                             cms_number=credit.cfs_identifier)
            credit.remaining_amount = abs(float(credit_memo.get('amount_due')))
        else:
            receipt = CFSService.get_receipt(
                cfs_account=cfs_account, receipt_number=credit.cfs_identifier)
            receipt_amount = float(receipt.get('receipt_amount'))
            applied_amount: float = 0
            for invoice in receipt.get('invoices', []):
                applied_amount += float(invoice.get('amount_applied'))
            credit.remaining_amount = receipt_amount - applied_amount

        credit.save()

    # Roll up the credits and add up to credit in payment_account.
    for account_id in set(account_ids):
        account_credits: List[CreditModel] = db.session.query(
            CreditModel).filter(CreditModel.remaining_amount > 0).filter(
                CreditModel.account_id == account_id).all()
        credit_total: float = 0
        for account_credit in account_credits:
            credit_total += account_credit.remaining_amount
        pay_account: PaymentAccountModel = PaymentAccountModel.find_by_id(
            account_id)
        pay_account.credit = credit_total
        pay_account.save()
Beispiel #23
0
def test_create_pad_account_system_error(session):
    """Test create account."""
    # Create a pending account first, then call the job
    account = factory_create_pad_account(auth_account_id='1')
    cfs_account: CfsAccount = CfsAccount.find_effective_by_account_id(
        account.id)
    assert cfs_account.status == CfsAccountStatus.PENDING.value
    mock_response = requests.models.Response()
    mock_response.headers['CAS-Returned-Messages'] = '[CFS Down]'
    mock_response.status_code = 404

    side_effect = HTTPError(response=mock_response)
    with patch.object(mailer, 'publish_mailer_events') as mock_mailer:
        with patch('pay_api.services.CFSService.create_cfs_account',
                   side_effect=side_effect):
            CreateAccountTask.create_accounts()
            mock_mailer.assert_not_called()

    account = PaymentAccount.find_by_id(account.id)
    cfs_account: CfsAccount = CfsAccount.find_by_id(cfs_account.id)
    assert cfs_account.status == CfsAccountStatus.PENDING.value
Beispiel #24
0
def test_get_daily_statements_verify_order(session, client, jwt, app):
    """Assert that the default statement setting is daily."""
    # Create a payment account and statement details, then get all statements for the account

    token = jwt.create_jwt(get_claims(), token_header)
    headers = {
        'Authorization': f'Bearer {token}',
        'content-type': 'application/json'
    }

    rv = client.post('/api/v1/payment-requests',
                     data=json.dumps(
                         get_payment_request(business_identifier='CP0002000')),
                     headers=headers)

    invoice: Invoice = Invoice.find_by_id(rv.json.get('id'))
    pay_account: PaymentAccount = PaymentAccount.find_by_id(
        invoice.payment_account_id)

    settings_model = factory_statement_settings(
        payment_account_id=pay_account.id,
        frequency=StatementFrequency.DAILY.value)
    factory_statement(payment_account_id=pay_account.id,
                      frequency=StatementFrequency.DAILY.value,
                      statement_settings_id=settings_model.id)
    factory_statement(payment_account_id=pay_account.id,
                      frequency=StatementFrequency.WEEKLY.value,
                      statement_settings_id=settings_model.id)

    rv = client.get(
        f'/api/v1/accounts/{pay_account.auth_account_id}/statements',
        headers=headers)
    assert rv.status_code == 200
    assert rv.json.get('total') == 2
    # should come in the order latest first
    assert rv.json.get('items')[0].get(
        'frequency') == StatementFrequency.WEEKLY.value
    assert rv.json.get('items')[1].get(
        'frequency') == StatementFrequency.DAILY.value
Beispiel #25
0
    def activate_pad_accounts(cls):
        """Find all accounts with pending account activation status.

        Steps:
        1. Find all accounts with pending PAD account activation status.
        2. Activate them.
        """
        pending_pad_activation_accounts: List[
            CfsAccountModel] = CfsAccountModel.find_all_accounts_with_status(
                status=CfsAccountStatus.PENDING_PAD_ACTIVATION.value)
        current_app.logger.info(
            f'Found {len(pending_pad_activation_accounts)} CFS Accounts to be pending PAD activation.'
        )
        if len(pending_pad_activation_accounts) == 0:
            return

        for pending_account in pending_pad_activation_accounts:
            pay_account: PaymentAccountModel = PaymentAccountModel.find_by_id(
                pending_account.account_id)

            # check is still in the pad activation period
            is_activation_period_over = pay_account.pad_activation_date < datetime.now(
            )
            current_app.logger.info(
                f'Account {pay_account.id} ready for activation:{is_activation_period_over}'
            )

            if is_activation_period_over:
                pending_account.status = CfsAccountStatus.ACTIVE.value
                pending_account.save()
                # If account was in BCOL , change it to PAD
                if pay_account.payment_method != PaymentMethod.PAD.value:
                    pay_account.payment_method = PaymentMethod.PAD.value
                    pay_account.save()
                mailer.publish_mailer_events('confirmationPeriodOver',
                                             pay_account)
Beispiel #26
0
    def _refund_and_create_credit_memo(invoice: InvoiceModel):
        # Create credit memo in CFS if the invoice status is PAID.
        # Don't do anything is the status is APPROVED.
        current_app.logger.info(
            f'Creating credit memo for invoice : {invoice.id}, {invoice.invoice_status_code}'
        )
        if invoice.invoice_status_code == InvoiceStatus.APPROVED.value \
                and InvoiceReferenceModel.find_reference_by_invoice_id_and_status(
                    invoice.id, InvoiceReferenceStatus.ACTIVE.value) is None:
            return

        cfs_account: CfsAccountModel = CfsAccountModel.find_effective_by_account_id(
            invoice.payment_account_id)
        line_items: List[PaymentLineItemModel] = []
        for line_item in invoice.payment_line_items:
            line_items.append(PaymentLineItemModel.find_by_id(line_item.id))

        cms_response = CFSService.create_cms(line_items=line_items,
                                             cfs_account=cfs_account)
        # TODO Create a payment record for this to show up on transactions, when the ticket comes.
        # Create a credit with CM identifier as CMs are not reported in payment interface file
        # until invoice is applied.
        CreditModel(cfs_identifier=cms_response.get('credit_memo_number'),
                    is_credit_memo=True,
                    amount=invoice.total,
                    remaining_amount=invoice.total,
                    account_id=invoice.payment_account_id).flush()

        # Add up the credit amount and update payment account table.
        payment_account: PaymentAccountModel = PaymentAccountModel.find_by_id(
            invoice.payment_account_id)
        payment_account.credit = (payment_account.credit or 0) + invoice.total
        current_app.logger.info(
            f'Updating credit amount to  {payment_account.credit} for account {payment_account.auth_account_id}'
        )
        payment_account.flush()
    def _create_rs_invoices(cls):  # pylint: disable=too-many-locals
        """Create RS invoices in to CFS system."""
        # Find all pending routing slips.

        # find all routing slip invoices [cash or cheque]
        # create invoices in csf
        # do the receipt apply
        invoices: List[InvoiceModel] = db.session.query(InvoiceModel) \
            .join(RoutingSlipModel, RoutingSlipModel.number == InvoiceModel.routing_slip) \
            .join(CfsAccountModel, CfsAccountModel.account_id == RoutingSlipModel.payment_account_id) \
            .filter(InvoiceModel.payment_method_code == PaymentMethod.INTERNAL.value) \
            .filter(InvoiceModel.invoice_status_code == InvoiceStatus.APPROVED.value) \
            .filter(CfsAccountModel.status.in_([CfsAccountStatus.ACTIVE.value, CfsAccountStatus.FREEZE.value])) \
            .filter(InvoiceModel.routing_slip is not None) \
            .order_by(InvoiceModel.created_on.asc()).all()

        current_app.logger.info(f'Found {len(invoices)} to be created in CFS.')

        for invoice in invoices:
            # Create a CFS invoice
            current_app.logger.debug(
                f'Creating cfs invoice for invoice {invoice.id}')
            routing_slip = RoutingSlipModel.find_by_number(
                invoice.routing_slip)
            # If routing slip is not found in Pay-DB, assume legacy RS and move on to next one.
            if not routing_slip:
                continue

            routing_slip_payment_account: PaymentAccountModel = PaymentAccountModel.find_by_id(
                routing_slip.payment_account_id)

            # apply invoice to the active CFS_ACCOUNT which will be the parent routing slip
            active_cfs_account = CfsAccountModel.find_effective_by_account_id(
                routing_slip_payment_account.id)

            invoice_response = CFSService.create_account_invoice(
                transaction_number=invoice.id,
                line_items=invoice.payment_line_items,
                cfs_account=active_cfs_account)
            invoice_number = invoice_response.json().get(
                'invoice_number', None)

            current_app.logger.info(
                f'invoice_number  {invoice_number}  created in CFS.')

            has_error_in_apply_receipt = RoutingSlipTask.apply_routing_slips_to_invoice(
                routing_slip_payment_account, active_cfs_account, routing_slip,
                invoice, invoice_number)

            if has_error_in_apply_receipt:
                # move on to next invoice
                continue

            invoice_reference: InvoiceReference = InvoiceReference.create(
                invoice.id, invoice_number,
                invoice_response.json().get('pbc_ref_number', None))

            current_app.logger.debug('>create_invoice')
            # leave the status as PAID
            invoice_reference.status_code = InvoiceReferenceStatus.COMPLETED.value
            invoice.invoice_status_code = InvoiceStatus.PAID.value
            invoice.paid = invoice.total

            Payment.create(payment_method=PaymentMethod.INTERNAL.value,
                           payment_system=PaymentSystem.INTERNAL.value,
                           payment_status=PaymentStatus.COMPLETED.value,
                           invoice_number=invoice_reference.invoice_number,
                           invoice_amount=invoice.total,
                           payment_account_id=invoice.payment_account_id)
            invoice.save()
Beispiel #28
0
    def link_routing_slips(cls):
        """Create invoice in CFS.

        Steps:
        1. Find all pending rs with pending status.
        1. Notify mailer
        """
        routing_slips = db.session.query(RoutingSlipModel) \
            .join(PaymentAccountModel, PaymentAccountModel.id == RoutingSlipModel.payment_account_id) \
            .join(CfsAccountModel, CfsAccountModel.account_id == PaymentAccountModel.id) \
            .filter(RoutingSlipModel.status == RoutingSlipStatus.LINKED.value) \
            .filter(CfsAccountModel.status == CfsAccountStatus.ACTIVE.value).all()

        for routing_slip in routing_slips:
            # 1.reverse the child routing slip
            # 2.create receipt to the parent
            # 3.change the payment account of child to parent
            # 4. change the status

            try:
                current_app.logger.debug(f'Linking Routing Slip: {routing_slip.number}')
                payment_account: PaymentAccountModel = PaymentAccountModel.find_by_id(
                    routing_slip.payment_account_id)
                cfs_account: CfsAccountModel = CfsAccountModel.find_effective_by_account_id(
                    payment_account.id)

                # reverse routing slip receipt
                CFSService.reverse_rs_receipt_in_cfs(cfs_account, routing_slip.number)
                cfs_account.status = CfsAccountStatus.INACTIVE.value

                # apply receipt to parent cfs account
                parent_rs: RoutingSlipModel = RoutingSlipModel.find_by_number(routing_slip.parent_number)
                parent_payment_account: PaymentAccountModel = PaymentAccountModel.find_by_id(
                    parent_rs.payment_account_id)
                parent_cfs_account: CfsAccountModel = CfsAccountModel.find_effective_by_account_id(
                    parent_payment_account.id)
                # For linked routing slip receipts, append 'L' to the number to avoid duplicate error
                receipt_number = f'{routing_slip.number}L'
                CFSService.create_cfs_receipt(cfs_account=parent_cfs_account,
                                              rcpt_number=receipt_number,
                                              rcpt_date=routing_slip.routing_slip_date.strftime('%Y-%m-%d'),
                                              amount=routing_slip.total,
                                              payment_method=parent_payment_account.payment_method,
                                              access_token=CFSService.get_fas_token().json().get('access_token'))

                # Add to the list if parent is NSF, to apply the receipts.
                if parent_rs.status == RoutingSlipStatus.NSF.value:
                    total_invoice_amount = cls._apply_routing_slips_to_pending_invoices(parent_rs)
                    current_app.logger.debug(f'Total Invoice Amount : {total_invoice_amount}')
                    # Update the parent routing slip status to ACTIVE
                    parent_rs.status = RoutingSlipStatus.ACTIVE.value
                    # linking routing slip balance is transferred ,so use the total
                    parent_rs.remaining_amount = float(routing_slip.total) - total_invoice_amount

                routing_slip.save()

            except Exception as e:  # NOQA # pylint: disable=broad-except
                capture_message(
                    f'Error on Linking Routing Slip number:={routing_slip.number}, '
                    f'routing slip : {routing_slip.id}, ERROR : {str(e)}', level='error')
                current_app.logger.error(e)
                continue
Beispiel #29
0
    def process_nsf(cls):
        """Process NSF routing slips.

        Steps:
        1. Find all routing slips with NSF status.
        2. Reverse the receipt for the NSF routing slips.
        3. Add an invoice for NSF fees.
        """
        routing_slips: List[RoutingSlipModel] = db.session.query(RoutingSlipModel) \
            .join(PaymentAccountModel, PaymentAccountModel.id == RoutingSlipModel.payment_account_id) \
            .join(CfsAccountModel, CfsAccountModel.account_id == PaymentAccountModel.id) \
            .filter(RoutingSlipModel.status == RoutingSlipStatus.NSF.value) \
            .filter(CfsAccountModel.status == CfsAccountStatus.ACTIVE.value).all()

        current_app.logger.info(f'Found {len(routing_slips)} to process NSF.')
        for routing_slip in routing_slips:
            # 1. Reverse the routing slip receipt.
            # 2. Reverse all the child receipts.
            # 3. Change the CFS Account status to FREEZE.
            try:
                current_app.logger.debug(f'Reverse receipt {routing_slip.number}')
                payment_account: PaymentAccountModel = PaymentAccountModel.find_by_id(routing_slip.payment_account_id)
                cfs_account: CfsAccountModel = CfsAccountModel.find_effective_by_account_id(payment_account.id)

                # Find all child routing slip and reverse it, as all linked routing slips are also considered as NSF.
                child_routing_slips: List[RoutingSlipModel] = RoutingSlipModel.find_children(routing_slip.number)
                for rs in (routing_slip, *child_routing_slips):
                    receipt_number = rs.number
                    if rs.parent_number:
                        receipt_number = f'{receipt_number}L'
                    CFSService.reverse_rs_receipt_in_cfs(cfs_account, receipt_number, is_nsf=True)

                    for payment in db.session.query(PaymentModel) \
                            .filter(PaymentModel.receipt_number == receipt_number).all():
                        payment.payment_status_code = PaymentStatus.FAILED.value

                # Update the CFS Account status to FREEZE.
                cfs_account.status = CfsAccountStatus.FREEZE.value

                # Update all invoice status to CREATED.
                invoices: List[InvoiceModel] = db.session.query(InvoiceModel) \
                    .filter(InvoiceModel.routing_slip == routing_slip.number) \
                    .filter(InvoiceModel.invoice_status_code == InvoiceStatus.PAID.value) \
                    .all()
                for inv in invoices:
                    # Reset the statuses
                    inv.invoice_status_code = InvoiceStatus.CREATED.value
                    inv_ref = InvoiceReferenceModel.find_reference_by_invoice_id_and_status(
                        inv.id, InvoiceReferenceStatus.COMPLETED.value
                    )
                    inv_ref.status_code = InvoiceReferenceStatus.ACTIVE.value
                    # Delete receipts as receipts are reversed in CFS.
                    for receipt in ReceiptModel.find_all_receipts_for_invoice(inv.id):
                        db.session.delete(receipt)

                inv = cls._create_nsf_invoice(cfs_account, routing_slip.number, payment_account)
                # Reduce the NSF fee from remaining amount.
                routing_slip.remaining_amount = float(routing_slip.remaining_amount) - inv.total
                routing_slip.save()

            except Exception as e:  # NOQA # pylint: disable=broad-except
                capture_message(
                    f'Error on Processing NSF for :={routing_slip.number}, '
                    f'routing slip : {routing_slip.id}, ERROR : {str(e)}', level='error')
                current_app.logger.error(e)
                continue
Beispiel #30
0
    def create_invoice_pdf(identifier: int, **kwargs) -> Tuple:
        """Find invoice by id."""
        invoice_dao: InvoiceModel = InvoiceModel.find_by_id(identifier)

        if not invoice_dao:
            raise BusinessException(Error.INVALID_INVOICE_ID)

        payment_account: PaymentAccountModel = PaymentAccountModel.find_by_id(
            invoice_dao.payment_account_id)
        cfs_account: CfsAccountModel = CfsAccountModel.find_by_id(
            invoice_dao.cfs_account_id)
        org_response = OAuthService.get(
            current_app.config.get('AUTH_API_ENDPOINT') +
            f'orgs/{payment_account.auth_account_id}',
            kwargs['user'].bearer_token, AuthHeaderType.BEARER,
            ContentType.JSON).json()
        org_contact_response = OAuthService.get(
            current_app.config.get('AUTH_API_ENDPOINT') +
            f'orgs/{payment_account.auth_account_id}/contacts',
            kwargs['user'].bearer_token, AuthHeaderType.BEARER,
            ContentType.JSON).json()

        org_contact = org_contact_response.get(
            'contacts')[0] if org_contact_response.get('contacts',
                                                       None) else {}

        invoice_number: str = invoice_dao.references[0].invoice_number if invoice_dao.references \
            else generate_transaction_number(invoice_dao.id)

        filing_types: List[Dict[str, str]] = []
        for line_item in invoice_dao.payment_line_items:
            business_identifier = invoice_dao.business_identifier \
                if not invoice_dao.business_identifier.startswith('T') \
                else ''
            filing_types.append({
                'folioNumber':
                invoice_dao.folio_number,
                'description':
                line_item.description,
                'businessIdentifier':
                business_identifier,
                'createdOn':
                get_local_formatted_date(invoice_dao.created_on),
                'filingTypeCode':
                line_item.fee_schedule.filing_type_code,
                'fee':
                line_item.total,
                'gst':
                line_item.gst,
                'serviceFees':
                line_item.service_fees,
                'total':
                line_item.total + line_item.service_fees
            })

        template_vars: Dict[str, any] = {
            'invoiceNumber': invoice_number,
            'createdOn': get_local_formatted_date(invoice_dao.created_on),
            'accountNumber': cfs_account.cfs_account if cfs_account else None,
            'total': invoice_dao.total,
            'gst': 0,
            'serviceFees': invoice_dao.service_fees,
            'fees': invoice_dao.total - invoice_dao.service_fees,
            'filingTypes': filing_types,
            'accountContact': {
                'name': org_response.get('name'),
                'city': org_contact.get('city', None),
                'country': org_contact.get('country', None),
                'postalCode': org_contact.get('postalCode', None),
                'region': org_contact.get('region', None),
                'street': org_contact.get('street', None),
                'streetAdditional': org_contact.get('streetAdditional', None)
            }
        }

        invoice_pdf_dict = {
            'templateName': 'invoice',
            'reportName': invoice_number,
            'templateVars': template_vars
        }
        current_app.logger.info('Invoice PDF Dict %s', invoice_pdf_dict)

        pdf_response = OAuthService.post(
            current_app.config.get('REPORT_API_BASE_URL'),
            kwargs['user'].bearer_token, AuthHeaderType.BEARER,
            ContentType.JSON, invoice_pdf_dict)
        current_app.logger.debug('<OAuthService responded to receipt.py')

        return pdf_response, invoice_pdf_dict.get('reportName')