Example #1
0
    def _validate_linking(routing_slip: RoutingSlipModel, parent_rs_slip: RoutingSlipModel) -> None:
        """Validate the linking.

        1). child already has a parent/already linked.
        2). its already a parent.
        3). parent_rs_slip has a parent.ie parent_rs_slip shouldn't already be linked
        4). one of them has transactions
        """
        if RoutingSlip._is_linked_already(routing_slip):
            raise BusinessException(Error.RS_ALREADY_LINKED)

        children = RoutingSlipModel.find_children(routing_slip.number)
        if children and len(children) > 0:
            raise BusinessException(Error.RS_ALREADY_A_PARENT)

        if RoutingSlip._is_linked_already(parent_rs_slip):
            raise BusinessException(Error.RS_PARENT_ALREADY_LINKED)

        # prevent self linking

        if routing_slip.number == parent_rs_slip.number:
            raise BusinessException(Error.RS_CANT_LINK_TO_SAME)

        # has one of these has pending
        if routing_slip.invoices:
            raise BusinessException(Error.RS_CHILD_HAS_TRANSACTIONS)

        # Stop the user from linking NSF. NSF can only be a parent.
        if routing_slip.status == RoutingSlipStatus.NSF.value:
            raise BusinessException(Error.RS_CANT_LINK_NSF)

        RoutingSlip._validate_status(parent_rs_slip, routing_slip)
Example #2
0
    def apply_routing_slips_to_invoice(cls,  # pylint: disable = too-many-arguments, too-many-locals
                                       routing_slip_payment_account: PaymentAccountModel,
                                       active_cfs_account: CfsAccountModel,
                                       parent_routing_slip: RoutingSlipModel,
                                       invoice: InvoiceModel,
                                       invoice_number: str) -> bool:
        """Apply routing slips (receipts in CFS) to invoice."""
        has_errors = False
        child_routing_slips: List[RoutingSlipModel] = RoutingSlipModel.find_children(parent_routing_slip.number)
        # an invoice has to be applied to multiple receipts (incl. all linked RS); apply till the balance is zero
        for routing_slip in (parent_routing_slip, *child_routing_slips):
            try:
                # apply receipt now
                current_app.logger.debug(f'Apply receipt {routing_slip.number} on invoice {invoice_number} '
                                         f'for routing slip {routing_slip.number}')
                receipt_number = routing_slip.number
                # For linked routing slips, new receipt numbers ends with 'L'
                if routing_slip.status == RoutingSlipStatus.LINKED.value:
                    receipt_number = f'{routing_slip.number}L'

                # If balance of receipt is zero, continue to next receipt.
                receipt_balance_before_apply = float(
                    CFSService.get_receipt(active_cfs_account, receipt_number).get('unapplied_amount')
                )
                current_app.logger.debug(f'Current balance on {receipt_number} = {receipt_balance_before_apply}')
                if receipt_balance_before_apply == 0:
                    continue

                current_app.logger.debug(f'Applying receipt {receipt_number} to {invoice_number}')
                receipt_response = CFSService.apply_receipt(active_cfs_account, receipt_number, invoice_number)

                # Create receipt.
                receipt = Receipt()
                receipt.receipt_number = receipt_response.json().get('receipt_number', None)
                receipt_amount = receipt_balance_before_apply - float(receipt_response.json().get('unapplied_amount'))
                receipt.receipt_amount = receipt_amount
                receipt.invoice_id = invoice.id
                receipt.receipt_date = datetime.now()
                receipt.flush()

                invoice_from_cfs = CFSService.get_invoice(active_cfs_account, invoice_number)
                if invoice_from_cfs.get('amount_due') == 0:
                    break

            except Exception as e:  # NOQA # pylint: disable=broad-except
                capture_message(
                    f'Error on creating Routing Slip invoice: account id={routing_slip_payment_account.id}, '
                    f'routing slip : {routing_slip.id}, ERROR : {str(e)}', level='error')
                current_app.logger.error(e)
                has_errors = True
                continue
        return has_errors
Example #3
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
Example #4
0
    def adjust_routing_slips(cls):
        """Adjust routing slips.

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

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

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

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

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

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

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

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

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

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

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

            except Exception as e:  # NOQA # pylint: disable=broad-except
                capture_message(
                    f'Error on Processing NSF for :={routing_slip.number}, '
                    f'routing slip : {routing_slip.id}, ERROR : {str(e)}', level='error')
                current_app.logger.error(e)
                continue
Example #6
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)