예제 #1
0
def test_receipt_adjustments(session, rs_status):
    """Test routing slip adjustments."""
    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,
                                 total=10,
                                 remaining_amount=10)
    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()

    parent_rs.status = rs_status

    # Test exception path first.
    with patch('pay_api.services.CFSService.adjust_receipt_to_zero') as mock:
        mock.side_effect = Exception('ERROR!')
        RoutingSlipTask.adjust_routing_slips()

    parent_rs = RoutingSlipModel.find_by_number(parent_rs.number)
    assert parent_rs.remaining_amount == 10

    with patch('pay_api.services.CFSService.adjust_receipt_to_zero'):
        RoutingSlipTask.adjust_routing_slips()

    parent_rs = RoutingSlipModel.find_by_number(parent_rs.number)
    assert parent_rs.remaining_amount == 0
예제 #2
0
def test_internal_rs_back_active(session, public_user_mock):
    """12033 - Scenario 2.

    Routing slip is complete and a transaction is cancelled
    the balance is restored - Should move back to Active
    """
    payment_response = PaymentService.create_invoice(
        get_routing_slip_payment_request(), get_auth_staff())
    account_model = PaymentAccount.find_by_auth_account_id(get_auth_staff().get('account').get('id'))
    account_id = account_model.id
    assert account_id is not None
    assert payment_response.get('id') is not None

    rs_number = '123456789'
    rs = factory_routing_slip(number=rs_number, payment_account_id=account_id, remaining_amount=50.00)
    rs.save()

    # Create another invoice with a routing slip.
    invoice = PaymentService.create_invoice(get_routing_slip_payment_request(), get_auth_staff())
    account_model = PaymentAccount.find_by_auth_account_id(get_auth_staff().get('account').get('id'))

    assert account_id == account_model.id

    rs = RoutingSlipModel.find_by_number(rs_number)
    assert rs.remaining_amount == 0.0
    assert rs.status == RoutingSlipStatus.COMPLETE.name

    invoice = Invoice.find_by_id(invoice['id'])
    InternalPayService().process_cfs_refund(invoice)

    assert rs.status == RoutingSlipStatus.ACTIVE.name
예제 #3
0
def test_routing_slip_status_to_nsf_attempt(client, jwt, app):
    """12033 - Scenario 4.

    Routing slip in Completed,
    user attempts to move it into another status, can only set to NSF.
    """
    token = jwt.create_jwt(get_claims(roles=[Role.FAS_CREATE.value, Role.FAS_LINK.value,
                                             Role.FAS_SEARCH.value, Role.FAS_EDIT.value]),
                           token_header)
    headers = {'Authorization': f'Bearer {token}', 'content-type': 'application/json'}
    child = get_routing_slip_request('438607657')
    client.post('/api/v1/fas/routing-slips', data=json.dumps(child), headers=headers)

    rs_model = RoutingSlip.find_by_number('438607657')
    rs_model.status = RoutingSlipStatus.COMPLETE.value
    rs_model.commit()

    # Active shouldn't work.
    rv = client.patch(f"/api/v1/fas/routing-slips/{child.get('number')}?action={PatchActions.UPDATE_STATUS.value}",
                      data=json.dumps({'status': RoutingSlipStatus.ACTIVE.value}), headers=headers)
    assert rv.status_code == 400

    # NSF should work.
    rv = client.patch(f"/api/v1/fas/routing-slips/{child.get('number')}?action={PatchActions.UPDATE_STATUS.value}",
                      data=json.dumps({'status': RoutingSlipStatus.NSF.value}), headers=headers)
    assert rv.status_code == 200, 'status changed successfully.'
예제 #4
0
def test_routing_slip_link_attempt(client, jwt, app):
    """12033 - Scenario 3.

    Routing slip is Completed, attempt to be linked.
    Linking shouldn't be allowed and explaining that completed routing
    slip cannot be involved in linking.
    """
    token = jwt.create_jwt(get_claims(roles=[Role.FAS_CREATE.value, Role.FAS_LINK.value, Role.FAS_SEARCH.value]),
                           token_header)
    headers = {'Authorization': f'Bearer {token}', 'content-type': 'application/json'}
    child = get_routing_slip_request('438607657')
    parent1 = get_routing_slip_request('355336710')
    client.post('/api/v1/fas/routing-slips', data=json.dumps(child), headers=headers)
    client.post('/api/v1/fas/routing-slips', data=json.dumps(parent1), headers=headers)

    rs_model = RoutingSlip.find_by_number('438607657')
    rs_model.status = RoutingSlipStatus.COMPLETE.value
    rs_model.commit()

    data = {'childRoutingSlipNumber': f"{child.get('number')}", 'parentRoutingSlipNumber': f"{parent1.get('number')}"}
    rv = client.post('/api/v1/fas/routing-slips/links', data=json.dumps(data), headers=headers)
    assert rv.json.get('type') == 'RS_IN_INVALID_STATUS'
    assert rv.status_code == 400

    # Try the reverse:
    data = {'childRoutingSlipNumber': f"{parent1.get('number')}", 'parentRoutingSlipNumber': f"{child.get('number')}"}
    rv = client.post('/api/v1/fas/routing-slips/links', data=json.dumps(data), headers=headers)
    assert rv.json.get('type') == 'RS_IN_INVALID_STATUS'
    assert rv.status_code == 400
예제 #5
0
def test_create_payment_record_with_internal_pay(session, public_user_mock):
    """Assert that the payment records are created."""
    # Create invoice without routing slip.
    payment_response = PaymentService.create_invoice(
        get_routing_slip_payment_request(), get_auth_staff())
    account_model = PaymentAccount.find_by_auth_account_id(get_auth_staff().get('account').get('id'))
    account_id = account_model.id
    assert account_id is not None
    assert payment_response.get('id') is not None

    rs_number = '123456789'
    rs = factory_routing_slip(number=rs_number, payment_account_id=account_id, remaining_amount=50.00)
    rs.save()

    # Create another invoice with a routing slip.
    PaymentService.create_invoice(get_routing_slip_payment_request(), get_auth_staff())
    account_model = PaymentAccount.find_by_auth_account_id(get_auth_staff().get('account').get('id'))

    assert account_id == account_model.id

    rs = RoutingSlipModel.find_by_number(rs_number)
    assert rs.remaining_amount == 0.0
    """12033 - Scenario 1.

    Manual transaction reduces RS to 0.00
    Routing slip status becomes Completed
    """
    assert rs.status == RoutingSlipStatus.COMPLETE.name
예제 #6
0
    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()
예제 #7
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()
예제 #8
0
 def find_by_number(cls, rs_number: str) -> Dict[str, any]:
     """Find by routing slip number."""
     routing_slip_dict: Dict[str, any] = None
     routing_slip: RoutingSlipModel = RoutingSlipModel.find_by_number(rs_number)
     if routing_slip:
         routing_slip_schema = RoutingSlipSchema()
         routing_slip_dict = routing_slip_schema.dump(routing_slip)
         routing_slip_dict['allowedStatuses'] = RoutingSlipStatusTransitionService. \
             get_possible_transitions(routing_slip)
     return routing_slip_dict
예제 #9
0
    def do_link(cls, rs_number: str, parent_rs_number: str) -> Dict[str, any]:
        """Link routing slip to parent routing slip."""
        routing_slip: RoutingSlipModel = RoutingSlipModel.find_by_number(rs_number)
        parent_routing_slip: RoutingSlipModel = RoutingSlipModel.find_by_number(parent_rs_number)
        if routing_slip is None or parent_routing_slip is None:
            raise BusinessException(Error.FAS_INVALID_ROUTING_SLIP_NUMBER)

        # do validations if its linkable
        RoutingSlip._validate_linking(routing_slip=routing_slip, parent_rs_slip=parent_routing_slip)

        routing_slip.parent_number = parent_routing_slip.number
        routing_slip.status = RoutingSlipStatus.LINKED.value

        # transfer the amount to parent.
        # we keep the total amount as such and transfer only the remaining amount.
        parent_routing_slip.remaining_amount += routing_slip.remaining_amount
        routing_slip.remaining_amount = 0

        routing_slip.commit()
        return cls.find_by_number(rs_number)
예제 #10
0
def test_routing_slip_refunds(session, monkeypatch):
    """Test Routing slip refund job.

    Steps:
    1) Create a routing slip with remaining_amount and status REFUND_AUTHORIZED
    2) Run the job and assert status
    """
    rs_1 = 'RS0000001'
    factory_routing_slip_account(
        number=rs_1,
        status=CfsAccountStatus.ACTIVE.value,
        total=100,
        remaining_amount=0,
        auth_account_id='1234',
        routing_slip_status=RoutingSlipStatus.REFUND_AUTHORIZED.value,
        refund_amount=100)

    routing_slip = RoutingSlip.find_by_number(rs_1)
    factory_refund(
        routing_slip.id, {
            'name': 'TEST',
            'mailingAddress': {
                'city': 'Victoria',
                'region': 'BC',
                'street': '655 Douglas St',
                'country': 'CA',
                'postalCode': 'V8V 0B6',
                'streetAdditional': ''
            }
        })
    with patch('pysftp.Connection.put') as mock_upload:
        ApRoutingSlipRefundTask.create_ap_file()
        mock_upload.assert_called()

    routing_slip = RoutingSlip.find_by_number(rs_1)
    assert routing_slip.status == RoutingSlipStatus.REFUND_UPLOADED.value

    # Run again and assert nothing is uploaded
    with patch('pysftp.Connection.put') as mock_upload:
        ApRoutingSlipRefundTask.create_ap_file()
        mock_upload.assert_not_called()
예제 #11
0
 def create_invoice(self, payment_account: PaymentAccount,
                    line_items: [PaymentLineItem], invoice: Invoice,
                    **kwargs) -> InvoiceReference:
     """Return a static invoice number."""
     routing_slip = None
     is_zero_dollar_invoice = get_quantized(invoice.total) == 0
     invoice_reference: InvoiceReference = None
     if routing_slip_number := invoice.routing_slip:
         current_app.logger.info(
             f'Routing slip number {routing_slip_number}, for invoice {invoice.id}'
         )
         routing_slip = RoutingSlipModel.find_by_number(routing_slip_number)
         InternalPayService._validate_routing_slip(routing_slip, invoice)
예제 #12
0
    def get_links(cls, rs_number: str) -> Dict[str, any]:
        """Find dependents/links of a routing slips."""
        links: Dict[str, any] = None
        routing_slip: RoutingSlipModel = RoutingSlipModel.find_by_number(rs_number)
        if routing_slip:
            routing_slip_schema = RoutingSlipSchema()
            children = RoutingSlipModel.find_children(rs_number)
            links = {
                'parent': routing_slip_schema.dump(routing_slip.parent),
                'children': routing_slip_schema.dump(children, many=True)
            }

        return links
예제 #13
0
def test_routing_slip_find_creation(session):
    """Assert a routing slip is stored.

    Start with a blank database.
    """
    payment_account = factory_payment_account()
    payment_account.save()

    rs = factory_routing_slip(payment_account_id=payment_account.id)
    rs.save()
    assert rs.id is not None

    routing_slip = RoutingSlip()
    assert routing_slip.find_by_number(rs.number) is not None
예제 #14
0
    def find_all_comments_for_a_routingslip(cls, routing_slip_number: str):
        """Find comments for a routing slip."""
        current_app.logger.debug('<Comment.get.service')
        routing_slip: RoutingSlipModel = RoutingSlipModel.find_by_number(
            routing_slip_number)

        if routing_slip is None:
            raise BusinessException(Error.FAS_INVALID_ROUTING_SLIP_NUMBER)

        comments_dao = CommentModel.find_all_comments_for_a_routingslip(
            routing_slip.number)
        comments = CommentSchema().dump(comments_dao, many=True)
        data = {'comments': comments}

        current_app.logger.debug('>Comment.get.service')
        return data
예제 #15
0
def test_routing_slip_usd_creation(session):
    """Assert a routing slip is stored with total_usd column.

    Start with a blank database.
    """
    payment_account = factory_payment_account()
    payment_account.save()

    rs = factory_routing_slip_usd(payment_account_id=payment_account.id,
                                  total_usd=50)
    rs.save()
    assert rs.id is not None
    assert rs.total_usd == 50

    routing_slip = RoutingSlip()
    assert routing_slip.find_by_number(rs.number) is not None
예제 #16
0
    def create(cls, comment_value: str, rs_number: str):
        """Create routing slip comment."""
        current_app.logger.debug('<Comment.create.service')
        routing_slip: RoutingSlipModel = RoutingSlipModel.find_by_number(
            number=rs_number)
        if routing_slip is None:
            raise BusinessException(Error.FAS_INVALID_ROUTING_SLIP_NUMBER)

        # Create a routing slip comment record.
        comment_service = Comment()
        comment_service._dao = CommentModel(comment=comment_value,
                                            routing_slip_number=rs_number)
        comment_service.flush()

        comment_service.commit()
        current_app.logger.debug('>Comment.create.service')
        return comment_service.asdict()
예제 #17
0
 def create_routing_slip_refund(cls, routing_slip_number: str,
                                request: Dict[str, str],
                                **kwargs) -> Dict[str, str]:
     """Create Routing slip refund."""
     current_app.logger.debug('<create Routing slip  refund')
     #
     # check if routing slip exists
     # validate user role -> update status of routing slip
     # check refunds table
     #   if Yes ; update the data [only with whatever is in payload]
     #   if not ; create new entry
     # call cfs
     rs_model = RoutingSlipModel.find_by_number(routing_slip_number)
     if not rs_model:
         raise BusinessException(Error.RS_DOESNT_EXIST)
     reason = get_str_by_path(request, 'reason')
     if (refund_status := get_str_by_path(request, 'status')) is None:
         raise BusinessException(Error.INVALID_REQUEST)
예제 #18
0
async def _process_ap_feedback(group_batches) -> bool:  # pylint:disable=too-many-locals
    """Process AP Feedback contents."""
    has_errors = False
    for group_batch in group_batches:
        ejv_file: Optional[EjvFileModel] = None
        for line in group_batch.splitlines():
            # For all these indexes refer the sharepoint docs refer : https://github.com/bcgov/entity/issues/6226
            is_batch_group: bool = line[2:4] == 'BG'
            is_batch_header: bool = line[2:4] == 'BH'
            is_ap_header: bool = line[2:4] == 'IH'
            if is_batch_group:
                batch_number = int(line[15:24])
                ejv_file = EjvFileModel.find_by_id(batch_number)
            elif is_batch_header:
                return_code = line[7:11]
                return_message = line[11:161]
                ejv_file.disbursement_status_code = _get_disbursement_status(
                    return_code)
                ejv_file.message = return_message
                if ejv_file.disbursement_status_code == DisbursementStatus.ERRORED.value:
                    has_errors = True
            elif is_ap_header:
                routing_slip_number = line[19:69].strip()
                routing_slip: RoutingSlipModel = RoutingSlipModel.find_by_number(
                    routing_slip_number)
                ap_header_return_code = line[414:418]
                ap_header_error_message = line[418:568]
                if _get_disbursement_status(
                        ap_header_return_code
                ) == DisbursementStatus.ERRORED.value:
                    has_errors = True
                    routing_slip.status = RoutingSlipStatus.REFUND_REJECTED.value
                    capture_message(
                        f'Refund failed for {routing_slip_number}, reason : {ap_header_error_message}',
                        level='error')
                else:
                    routing_slip.status = RoutingSlipStatus.REFUND_COMPLETED.value

    db.session.commit()
    return has_errors
예제 #19
0
    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()
예제 #20
0
    def process_cfs_refund(self, invoice: InvoiceModel):
        """Process refund in CFS."""
        # TODO handle Routing Slip refund CAS integration here
        if invoice.total == 0:
            raise BusinessException(Error.NO_FEE_REFUND)

        # find if its a routing slip refund
        # if yes -> check existing one or legacy
        # if yes , add money back to rs after refunding

        if (routing_slip_number := invoice.routing_slip) is None:
            raise BusinessException(Error.INVALID_REQUEST)
        if invoice.total == 0:
            raise BusinessException(Error.NO_FEE_REFUND)
        if not (routing_slip :=
                RoutingSlipModel.find_by_number(routing_slip_number)):
            raise BusinessException(Error.ROUTING_SLIP_REFUND)
        current_app.logger.info(
            f'Processing refund for {invoice.id}, on routing slip {routing_slip.number}'
        )
        payment: PaymentModel = PaymentModel.find_payment_for_invoice(
            invoice.id)
        if payment:
            payment.payment_status_code = PaymentStatus.REFUNDED.value
            payment.flush()
        routing_slip.remaining_amount += get_quantized(invoice.total)
        # Move routing slip back to active on refund.
        if routing_slip.status == RoutingSlipStatus.COMPLETE.value:
            routing_slip.status = RoutingSlipStatus.ACTIVE.value
        routing_slip.flush()
        invoice.invoice_status_code = InvoiceStatus.REFUND_REQUESTED.value
예제 #21
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
예제 #22
0
def test_process_nsf(session):
    """Test process NSF."""
    # 1. Link 2 child routing slips with parent.
    # 2. Mark the parent as NSF and run job.
    child_1 = '123456789'
    child_2 = '987654321'
    parent = '111111111'
    factory_routing_slip_account(number=child_1,
                                 status=CfsAccountStatus.ACTIVE.value,
                                 total=10)
    factory_routing_slip_account(number=child_2,
                                 status=CfsAccountStatus.ACTIVE.value,
                                 total=10)
    pay_account = factory_routing_slip_account(
        number=parent, status=CfsAccountStatus.ACTIVE.value, total=10)

    child_1_rs = RoutingSlipModel.find_by_number(child_1)
    child_2_rs = RoutingSlipModel.find_by_number(child_2)
    parent_rs = RoutingSlipModel.find_by_number(parent)

    # Do Link
    for child in (child_2_rs, child_1_rs):
        child.status = RoutingSlipStatus.LINKED.value
        child.parent_number = parent_rs.number
        child.save()

    RoutingSlipTask.link_routing_slips()

    # Now mark the parent as NSF
    parent_rs.remaining_amount = -30
    parent_rs.status = RoutingSlipStatus.NSF.value
    parent_rs.save()

    # Create an invoice record against this routing slip.
    invoice = factory_invoice(payment_account=pay_account,
                              total=30,
                              status_code=InvoiceStatus.PAID.value,
                              payment_method_code=PaymentMethod.INTERNAL.value,
                              routing_slip=parent_rs.number)

    fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type(
        'CP', 'OTANN')
    line = factory_payment_line_item(
        invoice.id, fee_schedule_id=fee_schedule.fee_schedule_id)
    line.save()

    # Create a distribution for NSF -> As this is a manual step once in each env.
    nsf_fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type(
        'BCR', 'NSF')
    distribution = factory_distribution('NSF')
    factory_distribution_link(distribution.distribution_code_id,
                              nsf_fee_schedule.fee_schedule_id)

    # Create invoice
    factory_invoice_reference(
        invoice.id, status_code=InvoiceReferenceStatus.COMPLETED.value)

    # Create receipts for the invoices
    factory_receipt(invoice.id, parent_rs.number)
    factory_receipt(invoice.id, child_1_rs.number)
    factory_receipt(invoice.id, child_2_rs.number)

    with patch('pay_api.services.CFSService.reverse_rs_receipt_in_cfs'
               ) as mock_cfs_reverse:
        RoutingSlipTask.process_nsf()
        mock_cfs_reverse.assert_called()

    # Assert the records.
    invoice: InvoiceModel = InvoiceModel.find_by_id(invoice.id)
    assert invoice.invoice_status_code == InvoiceStatus.CREATED.value
    assert InvoiceReferenceModel.find_reference_by_invoice_id_and_status(
        invoice.id, status_code=InvoiceReferenceStatus.ACTIVE.value)
    assert not ReceiptModel.find_all_receipts_for_invoice(invoice.id)
    assert float(
        RoutingSlipModel.find_by_number(
            parent_rs.number).remaining_amount) == -60  # Including NSF Fee

    with patch('pay_api.services.CFSService.reverse_rs_receipt_in_cfs'
               ) as mock_cfs_reverse_2:
        RoutingSlipTask.process_nsf()
        mock_cfs_reverse_2.assert_not_called()
예제 #23
0
def test_link_to_nsf_rs(session):
    """Test routing slip with NSF as parent."""
    child_rs_number = '1234'
    parent_rs_number = '89799'
    factory_routing_slip_account(number=child_rs_number,
                                 status=CfsAccountStatus.ACTIVE.value)
    pay_account = 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()

    # Run link process
    RoutingSlipTask.link_routing_slips()

    # Create an invoice for the routing slip
    # Create an invoice record against this routing slip.
    invoice = factory_invoice(payment_account=pay_account,
                              total=30,
                              status_code=InvoiceStatus.PAID.value,
                              payment_method_code=PaymentMethod.INTERNAL.value,
                              routing_slip=parent_rs.number)

    fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type(
        'CP', 'OTANN')
    line = factory_payment_line_item(
        invoice.id, fee_schedule_id=fee_schedule.fee_schedule_id)
    line.save()

    # Create a distribution for NSF -> As this is a manual step once in each env.
    nsf_fee_schedule = FeeScheduleModel.find_by_filing_type_and_corp_type(
        'BCR', 'NSF')
    distribution = factory_distribution('NSF')
    factory_distribution_link(distribution.distribution_code_id,
                              nsf_fee_schedule.fee_schedule_id)

    # Create invoice reference
    factory_invoice_reference(
        invoice.id, status_code=InvoiceReferenceStatus.COMPLETED.value)

    # Create receipts for the invoices
    factory_receipt(invoice.id, parent_rs.number)
    factory_receipt(invoice.id, child_rs.number)

    # Mark parent as NSF
    parent_rs.status = RoutingSlipStatus.NSF.value
    RoutingSlipTask.process_nsf()

    # Now create another RS and link it to the NSF RS, and assert status
    child_rs_2_number = '8888'
    factory_routing_slip_account(number=child_rs_2_number,
                                 status=CfsAccountStatus.ACTIVE.value)
    child_2_rs = RoutingSlipModel.find_by_number(child_rs_2_number)
    child_2_rs.status = RoutingSlipStatus.LINKED.value
    child_2_rs.parent_number = parent_rs.number
    child_2_rs.save()

    # Run link process
    with patch('pay_api.services.CFSService.reverse_rs_receipt_in_cfs'):
        RoutingSlipTask.link_routing_slips()

    # Now the invoice status should be PAID as RS has recovered.
    assert InvoiceModel.find_by_id(
        invoice.id).invoice_status_code == InvoiceStatus.PAID.value
    # Parent Routing slip status should be ACTIVE now
    assert RoutingSlipModel.find_by_number(
        parent_rs.number).status == RoutingSlipStatus.ACTIVE.value
예제 #24
0
class RoutingSlip:  # pylint: disable=too-many-instance-attributes, too-many-public-methods
    """Service to manage Routing slip related operations."""

    def __init__(self):
        """Initialize the service."""
        self.__dao = None
        self._id: int = None
        self._number: str = None
        self._payment_account_id: int = None
        self._status_code: str = None
        self._total: Decimal = None
        self._remaining_amount: Decimal = None
        self._total_usd: Decimal = None

    @property
    def _dao(self):
        if not self.__dao:
            self.__dao = RoutingSlipModel()
        return self.__dao

    @_dao.setter
    def _dao(self, value):
        self.__dao: RoutingSlipModel = value
        self.id: int = self._dao.id
        self.number: str = self._dao.number
        self.status_code: str = self._dao.status_code
        self.payment_account_id: int = self._dao.payment_account_id
        self.total: Decimal = self._dao.total
        self.remaining_amount: Decimal = self._dao.remaining_amount
        self._total_usd: Decimal = self._dao.total_usd

    @property
    def id(self):
        """Return the _id."""
        return self._id

    @id.setter
    def id(self, value: int):
        """Set the id."""
        self._id = value
        self._dao.id = value

    @property
    def number(self):
        """Return the number."""
        return self._number

    @number.setter
    def number(self, value: str):
        """Set the number."""
        self._number = value
        self._dao.number = value

    @property
    def status_code(self):
        """Return the status_code."""
        return self._status_code

    @status_code.setter
    def status_code(self, value: str):
        """Set the status_code."""
        self._status_code = value
        self._dao.status_code = value

    @property
    def payment_account_id(self):
        """Return the payment_account_id."""
        return self._payment_account_id

    @payment_account_id.setter
    def payment_account_id(self, value: int):
        """Set the payment_account_id."""
        self._payment_account_id = value
        self._dao.payment_account_id = value

    @property
    def total(self):
        """Return the total."""
        return self._total

    @total.setter
    def total(self, value: Decimal):
        """Set the total."""
        self._total = value
        self._dao.total = value

    @property
    def remaining_amount(self):
        """Return the remaining_amount."""
        return self._remaining_amount

    @remaining_amount.setter
    def remaining_amount(self, value: Decimal):
        """Set the amount."""
        self._remaining_amount = value
        self._dao.remaining_amount = value

    @property
    def total_usd(self):
        """Return the usd total."""
        return self._total_usd

    @total_usd.setter
    def total_usd(self, value: Decimal):
        """Set the usd total."""
        self._total_usd = value
        self._dao.total_usd = value

    def commit(self):
        """Save the information to the DB."""
        return self._dao.commit()

    def flush(self):
        """Save the information to the DB."""
        return self._dao.flush()

    def rollback(self):
        """Rollback."""
        return self._dao.rollback()

    def save(self):
        """Save the information to the DB."""
        return self._dao.save()

    def asdict(self) -> Dict[str]:
        """Return the routing slip as a python dict."""
        routing_slip_schema = RoutingSlipSchema()
        d = routing_slip_schema.dump(self._dao)
        return d

    @classmethod
    def search(cls, search_filter: Dict, page: int, limit: int, return_all: bool = False):
        """Search for routing slip."""
        routing_slips, total = RoutingSlipModel.search(search_filter, page, limit, return_all)
        data = {
            'total': total,
            'page': page,
            'limit': limit,
            # We need these fields, to populate the UI.
            'items': RoutingSlipSchema(only=('number',
                                             'payments.receipt_number',
                                             'payment_account.name',
                                             'created_name',
                                             'routing_slip_date',
                                             'status',
                                             'invoices.business_identifier',
                                             'payments.cheque_receipt_number',
                                             'remaining_amount',
                                             'total',
                                             'invoices.corp_type_code',
                                             'payments.payment_method_code',
                                             'payments.payment_status_code',
                                             'payment_account.payment_method'
                                             )
                                       ).dump(routing_slips, many=True)
        }

        return data

    @classmethod
    @user_context
    def create_daily_reports(cls, date: str, **kwargs):
        """Create and return daily report for the day provided."""
        routing_slips: List[RoutingSlipModel] = RoutingSlipModel.search(
            dict(
                dateFilter=dict(
                    endDate=date,
                    startDate=date,
                    target='created_on'
                )
            ),
            page=1, limit=0, return_all=True
        )[0]

        total: float = 0
        no_of_cash: int = 0
        no_of_cheque: int = 0
        total_cash_usd: float = 0
        total_cheque_usd: float = 0
        total_cash_cad: float = 0
        total_cheque_cad: float = 0
        # TODO Only CAD supported now, so just add up the total.
        for routing_slip in routing_slips:
            total += float(routing_slip.total)
            if routing_slip.payment_account.payment_method == PaymentMethod.CASH.value:
                no_of_cash += 1
                # TODO check if the payment is CAD or USD.
                total_cash_cad += float(routing_slip.total)
                if routing_slip.total_usd is not None:
                    total_cash_usd += float(routing_slip.total_usd)
            else:
                no_of_cheque += 1
                total_cheque_cad += float(routing_slip.total)
                if routing_slip.total_usd is not None:
                    total_cheque_usd += float(routing_slip.total_usd)

        report_dict = dict(
            templateName='routing_slip_report',
            reportName=f'Routing-Slip-Daily-Report-{date}',
            templateVars=dict(
                day=date,
                reportDay=str(get_local_time(datetime.now())),
                total=total,
                numberOfCashReceipts=no_of_cash,
                numberOfChequeReceipts=no_of_cheque,
                totalCashInUsd=total_cash_usd,
                totalChequeInUsd=total_cheque_usd,
                totalCashInCad=total_cash_cad,
                totalChequeInCad=total_cheque_cad
            )
        )

        pdf_response = OAuthService.post(current_app.config.get('REPORT_API_BASE_URL'),
                                         kwargs['user'].bearer_token, AuthHeaderType.BEARER,
                                         ContentType.JSON, report_dict)

        return pdf_response, report_dict.get('reportName')

    @classmethod
    def validate_and_find_by_number(cls, rs_number: str) -> Dict[str, any]:
        """Validate digits before finding by routing slip number."""
        if not current_app.config.get('ALLOW_LEGACY_ROUTING_SLIPS'):
            RoutingSlip._validate_routing_slip_number_digits(rs_number)
        return cls.find_by_number(rs_number)

    @classmethod
    def find_by_number(cls, rs_number: str) -> Dict[str, any]:
        """Find by routing slip number."""
        routing_slip_dict: Dict[str, any] = None
        routing_slip: RoutingSlipModel = RoutingSlipModel.find_by_number(rs_number)
        if routing_slip:
            routing_slip_schema = RoutingSlipSchema()
            routing_slip_dict = routing_slip_schema.dump(routing_slip)
            routing_slip_dict['allowedStatuses'] = RoutingSlipStatusTransitionService. \
                get_possible_transitions(routing_slip)
        return routing_slip_dict

    @classmethod
    def get_links(cls, rs_number: str) -> Dict[str, any]:
        """Find dependents/links of a routing slips."""
        links: Dict[str, any] = None
        routing_slip: RoutingSlipModel = RoutingSlipModel.find_by_number(rs_number)
        if routing_slip:
            routing_slip_schema = RoutingSlipSchema()
            children = RoutingSlipModel.find_children(rs_number)
            links = {
                'parent': routing_slip_schema.dump(routing_slip.parent),
                'children': routing_slip_schema.dump(children, many=True)
            }

        return links

    @classmethod
    @user_context
    def create(cls, request_json: Dict[str, any], **kwargs):
        """Search for routing slip."""
        # 1. Create customer profile in CFS and store it in payment_account and cfs_accounts
        # 2. Create receipt in CFS
        # 3. Create routing slip and payment records.

        rs_number = request_json.get('number')
        # Validate Routing slip digits and if slip number is unique.
        if cls.validate_and_find_by_number(rs_number):
            raise BusinessException(Error.FAS_INVALID_ROUTING_SLIP_NUMBER)

        payment_methods: [str] = [payment.get('paymentMethod') for payment in request_json.get('payments')]
        # all the payment should have the same payment method
        if len(set(payment_methods)) != 1:
            raise BusinessException(Error.FAS_INVALID_PAYMENT_METHOD)

        # If payment method is cheque and then there is no payment date then raise error
        if payment_methods[0] == PaymentMethod.CHEQUE.value:
            for payment in request_json.get('payments'):
                if payment.get('paymentDate') is None:
                    raise BusinessException(Error.INVALID_REQUEST)

        pay_account: PaymentAccountModel = PaymentAccountModel(
            name=request_json.get('paymentAccount').get('accountName'),
            payment_method=payment_methods[0],
        ).flush()

        CfsAccountModel(
            account_id=pay_account.id,
            status=CfsAccountStatus.PENDING.value
        ).flush()

        total = get_quantized(sum(float(payment.get('paidAmount')) for payment in request_json.get('payments')))

        # Calculate Total USD
        total_usd = get_quantized(sum(float(payment.get('paidUsdAmount', 0))
                                      for payment in request_json.get('payments')))

        # Create a routing slip record.
        routing_slip: RoutingSlipModel = RoutingSlipModel(
            number=rs_number,
            payment_account_id=pay_account.id,
            status=RoutingSlipStatus.ACTIVE.value,
            total=total,
            remaining_amount=total,
            routing_slip_date=string_to_date(request_json.get('routingSlipDate')),
            total_usd=total_usd
        ).flush()

        for payment in request_json.get('payments'):
            PaymentModel(
                payment_system_code=PaymentSystem.FAS.value,
                payment_account_id=pay_account.id,
                payment_method_code=payment.get('paymentMethod'),
                payment_status_code=PaymentStatus.COMPLETED.value,
                receipt_number=rs_number,
                cheque_receipt_number=payment.get('chequeReceiptNumber'),
                is_routing_slip=True,
                paid_amount=payment.get('paidAmount'),
                payment_date=string_to_date(payment.get('paymentDate')) if payment.get('paymentDate') else None,
                created_by=kwargs['user'].user_name,
                paid_usd_amount=payment.get('paidUsdAmount', None)
            ).flush()

        routing_slip.commit()
        return cls.find_by_number(rs_number)

    @classmethod
    def do_link(cls, rs_number: str, parent_rs_number: str) -> Dict[str, any]:
        """Link routing slip to parent routing slip."""
        routing_slip: RoutingSlipModel = RoutingSlipModel.find_by_number(rs_number)
        parent_routing_slip: RoutingSlipModel = RoutingSlipModel.find_by_number(parent_rs_number)
        if routing_slip is None or parent_routing_slip is None:
            raise BusinessException(Error.FAS_INVALID_ROUTING_SLIP_NUMBER)

        # do validations if its linkable
        RoutingSlip._validate_linking(routing_slip=routing_slip, parent_rs_slip=parent_routing_slip)

        routing_slip.parent_number = parent_routing_slip.number
        routing_slip.status = RoutingSlipStatus.LINKED.value

        # transfer the amount to parent.
        # we keep the total amount as such and transfer only the remaining amount.
        parent_routing_slip.remaining_amount += routing_slip.remaining_amount
        routing_slip.remaining_amount = 0

        routing_slip.commit()
        return cls.find_by_number(rs_number)

    @classmethod
    @user_context
    def update(cls, rs_number: str, action: str, request_json: Dict[str, any], **kwargs) -> Dict[str, any]:
        """Update routing slip."""
        user: UserContext = kwargs['user']
        if (patch_action := PatchActions.from_value(action)) is None:
            raise BusinessException(Error.PATCH_INVALID_ACTION)

        routing_slip: RoutingSlipModel = RoutingSlipModel.find_by_number(rs_number)
        if routing_slip is None:
            raise BusinessException(Error.FAS_INVALID_ROUTING_SLIP_NUMBER)

        if patch_action == PatchActions.UPDATE_STATUS:
            # Update the remaining amount as negative total of sum of all totals for that routing slip.
            status = request_json.get('status')
            RoutingSlipStatusTransitionService.validate_possible_transitions(routing_slip, status)
            status = RoutingSlipStatusTransitionService.get_actual_status(status)

            # Our routing_slips job will create an invoice (under transactions in the UI).
            if status == RoutingSlipStatus.NSF.value:
                total_paid_to_reverse: float = 0
                for rs in (routing_slip, *RoutingSlipModel.find_children(routing_slip.number)):
                    total_paid_to_reverse += rs.total
                routing_slip.remaining_amount += -total_paid_to_reverse
            elif status in (RoutingSlipStatus.WRITE_OFF_AUTHORIZED.value, RoutingSlipStatus.REFUND_AUTHORIZED.value) \
                    and not user.has_role(Role.FAS_REFUND_APPROVER.value):
                abort(403)

            routing_slip.status = status

        routing_slip.save()
        return cls.find_by_number(rs_number)