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
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
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.'
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
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
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 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 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
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)
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()
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)
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
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
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
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
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()
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)
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
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()
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
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
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()
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
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)