Example #1
0
    def __enter__(self) -> "AbstractSystemRunner":
        """
        Supports running a system object directly as a context manager.

        The system is run with the SingleThreadedRunner.
        """
        from eventsourcing.system.runner import SingleThreadedRunner

        if self.runner:
            raise ProgrammingError("System is already running: {}".format(self.runner))

        runner = SingleThreadedRunner(
            system=self,
            use_direct_query_if_available=self.use_direct_query_if_available,
        )
        runner.start()
        self.runner = weakref.ref(runner)
        return runner
class TestCargoShippingExample(TestCase):
    def setUp(self) -> None:
        self.runner = SingleThreadedRunner(
            system=System(BookingApplication),
            infrastructure_class=SQLAlchemyApplication,
            setup_tables=True,
        )
        self.runner.start()
        self.client = LocalClient(self.runner)

    def tearDown(self) -> None:
        self.runner.close()

    def test_admin_can_book_new_cargo(self) -> None:
        arrival_deadline = datetime.now() + timedelta(weeks=3)

        cargo_id = self.client.book_new_cargo(
            origin="NLRTM", destination="USDAL", arrival_deadline=arrival_deadline
        )

        cargo_details = self.client.get_cargo_details(cargo_id)
        self.assertTrue(cargo_details["id"])
        self.assertEqual(cargo_details["origin"], "NLRTM")
        self.assertEqual(cargo_details["destination"], "USDAL")

        self.client.change_destination(cargo_id, destination="AUMEL")
        cargo_details = self.client.get_cargo_details(cargo_id)
        self.assertEqual(cargo_details["destination"], "AUMEL")
        self.assertEqual(cargo_details["arrival_deadline"], arrival_deadline)

    def test_scenario_cargo_from_hongkong_to_stockholm(self) -> None:
        # Test setup: A cargo should be shipped from Hongkong to Stockholm,
        # and it should arrive in no more than two weeks.
        origin = "HONGKONG"
        destination = "STOCKHOLM"
        arrival_deadline = datetime.now() + timedelta(weeks=2)

        # Use case 1: booking.

        # A new cargo is booked, and the unique tracking id is assigned to the cargo.
        tracking_id = self.client.book_new_cargo(origin, destination, arrival_deadline)

        # The tracking id can be used to lookup the cargo in the repository.
        # Important: The cargo, and thus the domain model, is responsible for
        # determining the status of the cargo, whether it is on the right track
        # or not and so on. This is core domain logic. Tracking the cargo basically
        # amounts to presenting information extracted from the cargo aggregate in a
        # suitable way.
        cargo_details = self.client.get_cargo_details(tracking_id)
        self.assertEqual(cargo_details["transport_status"], "NOT_RECEIVED")
        self.assertEqual(cargo_details["routing_status"], "NOT_ROUTED")
        self.assertEqual(cargo_details["is_misdirected"], False)
        self.assertEqual(cargo_details["estimated_time_of_arrival"], None)
        self.assertEqual(cargo_details["next_expected_activity"], None)

        # Use case 2: routing.
        #
        # A number of possible routes for this cargo is requested and may be
        # presented to the customer in some way for him/her to choose from.
        # Selection could be affected by things like price and time of delivery,
        # but this test simply uses an arbitrary selection to mimic that process.
        routes_details = self.client.request_possible_routes_for_cargo(tracking_id)
        route_details = select_preferred_itinerary(routes_details)

        # The cargo is then assigned to the selected route, described by an itinerary.
        self.client.assign_route(tracking_id, route_details)

        cargo_details = self.client.get_cargo_details(tracking_id)
        self.assertEqual(cargo_details["transport_status"], "NOT_RECEIVED")
        self.assertEqual(cargo_details["routing_status"], "ROUTED")
        self.assertEqual(cargo_details["is_misdirected"], False)
        self.assertTrue(cargo_details["estimated_time_of_arrival"])
        self.assertEqual(
            cargo_details["next_expected_activity"], ("RECEIVE", "HONGKONG")
        )

        # Use case 3: handling

        # A handling event registration attempt will be formed from parsing
        # the data coming in as a handling report either via the web service
        # interface or as an uploaded CSV file. The handling event factory
        # tries to create a HandlingEvent from the attempt, and if the factory
        # decides that this is a plausible handling event, it is stored.
        # If the attempt is invalid, for example if no cargo exists for the
        # specfied tracking id, the attempt is rejected.
        #
        # Handling begins: cargo is received in Hongkong.
        self.client.register_handling_event(tracking_id, None, "HONGKONG", "RECEIVE")
        cargo_details = self.client.get_cargo_details(tracking_id)
        self.assertEqual(cargo_details["transport_status"], "IN_PORT")
        self.assertEqual(cargo_details["last_known_location"], "HONGKONG")
        self.assertEqual(
            cargo_details["next_expected_activity"], ("LOAD", "HONGKONG", "V1")
        )

        # Load onto voyage V1.
        self.client.register_handling_event(tracking_id, "V1", "HONGKONG", "LOAD")
        cargo_details = self.client.get_cargo_details(tracking_id)
        self.assertEqual(cargo_details["current_voyage_number"], "V1")
        self.assertEqual(cargo_details["last_known_location"], "HONGKONG")
        self.assertEqual(cargo_details["transport_status"], "ONBOARD_CARRIER")
        self.assertEqual(
            cargo_details["next_expected_activity"], ("UNLOAD", "NEWYORK", "V1")
        )

        # Incorrectly unload in Tokyo.
        self.client.register_handling_event(tracking_id, "V1", "TOKYO", "UNLOAD")
        cargo_details = self.client.get_cargo_details(tracking_id)
        self.assertEqual(cargo_details["current_voyage_number"], None)
        self.assertEqual(cargo_details["last_known_location"], "TOKYO")
        self.assertEqual(cargo_details["transport_status"], "IN_PORT")
        self.assertEqual(cargo_details["is_misdirected"], True)
        self.assertEqual(cargo_details["next_expected_activity"], None)

        # Reroute.
        routes_details = self.client.request_possible_routes_for_cargo(tracking_id)
        route_details = select_preferred_itinerary(routes_details)
        self.client.assign_route(tracking_id, route_details)

        # Load in Tokyo.
        self.client.register_handling_event(tracking_id, "V3", "TOKYO", "LOAD")
        cargo_details = self.client.get_cargo_details(tracking_id)
        self.assertEqual(cargo_details["current_voyage_number"], "V3")
        self.assertEqual(cargo_details["last_known_location"], "TOKYO")
        self.assertEqual(cargo_details["transport_status"], "ONBOARD_CARRIER")
        self.assertEqual(cargo_details["is_misdirected"], False)
        self.assertEqual(
            cargo_details["next_expected_activity"], ("UNLOAD", "HAMBURG", "V3")
        )

        # Unload in Hamburg.
        self.client.register_handling_event(tracking_id, "V3", "HAMBURG", "UNLOAD")
        cargo_details = self.client.get_cargo_details(tracking_id)
        self.assertEqual(cargo_details["current_voyage_number"], None)
        self.assertEqual(cargo_details["last_known_location"], "HAMBURG")
        self.assertEqual(cargo_details["transport_status"], "IN_PORT")
        self.assertEqual(cargo_details["is_misdirected"], False)
        self.assertEqual(
            cargo_details["next_expected_activity"], ("LOAD", "HAMBURG", "V4")
        )

        # Load in Hamburg
        self.client.register_handling_event(tracking_id, "V4", "HAMBURG", "LOAD")
        cargo_details = self.client.get_cargo_details(tracking_id)
        self.assertEqual(cargo_details["current_voyage_number"], "V4")
        self.assertEqual(cargo_details["last_known_location"], "HAMBURG")
        self.assertEqual(cargo_details["transport_status"], "ONBOARD_CARRIER")
        self.assertEqual(cargo_details["is_misdirected"], False)
        self.assertEqual(
            cargo_details["next_expected_activity"], ("UNLOAD", "STOCKHOLM", "V4")
        )

        # Unload in Stockholm
        self.client.register_handling_event(tracking_id, "V4", "STOCKHOLM", "UNLOAD")
        cargo_details = self.client.get_cargo_details(tracking_id)
        self.assertEqual(cargo_details["current_voyage_number"], None)
        self.assertEqual(cargo_details["last_known_location"], "STOCKHOLM")
        self.assertEqual(cargo_details["transport_status"], "IN_PORT")
        self.assertEqual(cargo_details["is_misdirected"], False)
        self.assertEqual(
            cargo_details["next_expected_activity"], ("CLAIM", "STOCKHOLM")
        )

        # Finally, cargo is claimed in Stockholm.
        self.client.register_handling_event(tracking_id, None, "STOCKHOLM", "CLAIM")
        cargo_details = self.client.get_cargo_details(tracking_id)
        self.assertEqual(cargo_details["current_voyage_number"], None)
        self.assertEqual(cargo_details["last_known_location"], "STOCKHOLM")
        self.assertEqual(cargo_details["transport_status"], "CLAIMED")
        self.assertEqual(cargo_details["is_misdirected"], False)
        self.assertEqual(cargo_details["next_expected_activity"], None)
Example #3
0
class TestBankAccountSystem(TestCase):
    def setUp(self) -> None:
        # Run the system with single threaded runner.
        self.runner = SingleThreadedRunner(
            BankAccountSystem(infrastructure_class=None))
        self.runner.start()
        self.commands: Commands = self.runner.get(Commands)
        self.sagas: Sagas = self.runner.get(Sagas)
        self.accounts: Accounts = self.runner.get(Accounts)

    def tearDown(self) -> None:
        del self.accounts
        del self.sagas
        del self.commands
        self.runner.close()
        del self.runner

    def test_deposit_funds_ok(self):
        # Create an account.
        account_id1 = self.accounts.create_account()

        # Check balance.
        self.assertEqual(self.accounts.get_balance(account_id1),
                         Decimal("0.00"))

        # Deposit funds.
        transaction_id = self.commands.deposit_funds(account_id1,
                                                     Decimal("200.00"))

        # Check saga succeeded.
        self.assertTrue(self.sagas.get_saga(transaction_id).has_succeeded)
        self.assertFalse(self.sagas.get_saga(transaction_id).has_errored)
        self.assertFalse(self.sagas.get_saga(transaction_id).errors)

        # Check balance.
        self.assertEqual(self.accounts.get_balance(account_id1),
                         Decimal("200.00"))

    def test_deposit_funds_error_account_closed(self):
        # Create an account.
        account_id1 = self.accounts.create_account()

        # Close account.
        self.accounts.close_account(account_id1)

        # Deposit funds.
        transaction_id = self.commands.deposit_funds(account_id1,
                                                     Decimal("200.00"))

        # Check saga errored.
        self.assertFalse(self.sagas.get_saga(transaction_id).has_succeeded)
        self.assertTrue(self.sagas.get_saga(transaction_id).has_errored)
        errors = self.sagas.get_saga(transaction_id).errors
        self.assertEqual(len(errors), 1)
        self.assertEqual(errors[0],
                         AccountClosedError({"account_id": account_id1}))

        # Check balance.
        self.assertEqual(self.accounts.get_balance(account_id1),
                         Decimal("0.00"))

    def test_withdraw_funds_ok(self):
        # Create an account and deposit funds.
        account_id1 = self.accounts.create_account()
        self.commands.deposit_funds(account_id1, Decimal("200.00"))

        # Withdraw funds.
        transaction_id = self.commands.withdraw_funds(account_id1,
                                                      Decimal("50.00"))

        # Check balance.
        self.assertEqual(self.accounts.get_balance(account_id1),
                         Decimal("150.00"))

        # Check saga succeeded.
        self.assertTrue(self.sagas.get_saga(transaction_id).has_succeeded)
        self.assertFalse(self.sagas.get_saga(transaction_id).has_errored)
        self.assertFalse(self.sagas.get_saga(transaction_id).errors)

    def test_withdraw_funds_error_insufficient_funds(self):
        # Create an account and deposit funds.
        account_id1 = self.accounts.create_account()
        self.commands.deposit_funds(account_id1, Decimal("200.00"))

        # Fail to withdraw funds - insufficient funds.
        transaction_id = self.commands.withdraw_funds(account_id1,
                                                      Decimal("200.01"))

        # Check saga errored.
        self.assertFalse(self.sagas.get_saga(transaction_id).has_succeeded)
        self.assertTrue(self.sagas.get_saga(transaction_id).has_errored)
        errors = self.sagas.get_saga(transaction_id).errors
        self.assertEqual(len(errors), 1)
        self.assertEqual(errors[0],
                         InsufficientFundsError({"account_id": account_id1}))

        # Check balance.
        self.assertEqual(self.accounts.get_balance(account_id1),
                         Decimal("200.00"))

    def test_withdraw_funds_error_account_closed(self):
        # Create an account and deposit funds.
        account_id1 = self.accounts.create_account()
        self.commands.deposit_funds(account_id1, Decimal("200.00"))

        # Close account.
        self.accounts.close_account(account_id1)

        # Fail to withdraw funds - account closed.
        transaction_id = self.commands.withdraw_funds(
            debit_account_id=account_id1, amount=Decimal("50.00"))

        # Check saga errored.
        self.assertFalse(self.sagas.get_saga(transaction_id).has_succeeded)
        self.assertTrue(self.sagas.get_saga(transaction_id).has_errored)
        errors = self.sagas.get_saga(transaction_id).errors
        self.assertEqual(len(errors), 1)
        self.assertEqual(errors[0],
                         AccountClosedError({"account_id": account_id1}))

        # Check balance.
        self.assertEqual(self.accounts.get_balance(account_id1),
                         Decimal("200.00"))

    def test_transfer_funds_ok(self):
        # Create two accounts and deposit funds.
        account_id1 = self.accounts.create_account()
        account_id2 = self.accounts.create_account()
        self.commands.deposit_funds(account_id1, Decimal("200.00"))

        # Transfer funds.
        transaction_id = self.commands.transfer_funds(
            debit_account_id=account_id1,
            credit_account_id=account_id2,
            amount=Decimal("50.00"),
        )

        # Check saga succeeded.
        self.assertTrue(self.sagas.get_saga(transaction_id).has_succeeded)
        self.assertFalse(self.sagas.get_saga(transaction_id).has_errored)
        self.assertFalse(self.sagas.get_saga(transaction_id).errors)

        # Check balances.
        self.assertEqual(self.accounts.get_balance(account_id1),
                         Decimal("150.00"))
        self.assertEqual(self.accounts.get_balance(account_id2),
                         Decimal("50.00"))

    def test_transfer_funds_error_insufficient_funds(self):
        # Create two accounts and deposit funds.
        account_id1 = self.accounts.create_account()
        account_id2 = self.accounts.create_account()
        self.commands.deposit_funds(account_id1, Decimal("200.00"))

        # Fail to transfer funds - insufficient funds.
        transaction_id = self.commands.transfer_funds(
            debit_account_id=account_id1,
            credit_account_id=account_id2,
            amount=Decimal("1000.00"),
        )

        # Check saga errored.
        self.assertFalse(self.sagas.get_saga(transaction_id).has_succeeded)
        self.assertTrue(self.sagas.get_saga(transaction_id).has_errored)
        errors = self.sagas.get_saga(transaction_id).errors
        self.assertEqual(len(errors), 1)
        self.assertEqual(errors[0],
                         InsufficientFundsError({"account_id": account_id1}))

        # Check balances - should be unchanged.
        self.assertEqual(self.accounts.get_balance(account_id1),
                         Decimal("200.00"))
        self.assertEqual(self.accounts.get_balance(account_id2),
                         Decimal("0.00"))

    def test_transfer_funds_error_debit_account_closed(self):
        # Create two accounts and deposit funds.
        account_id1 = self.accounts.create_account()
        account_id2 = self.accounts.create_account()
        self.commands.deposit_funds(account_id1, Decimal("200.00"))

        # Close account.
        self.accounts.close_account(account_id1)

        # Fail to transfer funds - account closed.
        transaction_id = self.commands.transfer_funds(
            debit_account_id=account_id1,
            credit_account_id=account_id2,
            amount=Decimal("50.00"),
        )

        # Check saga errored.
        self.assertFalse(self.sagas.get_saga(transaction_id).has_succeeded)
        self.assertTrue(self.sagas.get_saga(transaction_id).has_errored)
        errors = self.sagas.get_saga(transaction_id).errors
        self.assertEqual(len(errors), 1)
        self.assertEqual(errors[0],
                         AccountClosedError({"account_id": account_id1}))

        # Check balances - should be unchanged.
        self.assertEqual(self.accounts.get_balance(account_id1),
                         Decimal("200.00"))
        self.assertEqual(self.accounts.get_balance(account_id2),
                         Decimal("0.00"))

    def test_transfer_funds_error_credit_account_closed(self):
        # Create two accounts and deposit funds.
        account_id1 = self.accounts.create_account()
        account_id2 = self.accounts.create_account()
        self.commands.deposit_funds(account_id2, Decimal("200.00"))

        # Close account.
        self.accounts.close_account(account_id1)

        # Fail to transfer funds - account closed.
        transaction_id = self.commands.transfer_funds(
            debit_account_id=account_id2,
            credit_account_id=account_id1,
            amount=Decimal("50.00"),
        )

        # Check saga errored.
        self.assertFalse(self.sagas.get_saga(transaction_id).has_succeeded)
        self.assertTrue(self.sagas.get_saga(transaction_id).has_errored)
        errors = self.sagas.get_saga(transaction_id).errors
        self.assertEqual(len(errors), 1)
        self.assertEqual(errors[0],
                         AccountClosedError({"account_id": account_id1}))

        # Check balances - should be unchanged.
        self.assertEqual(self.accounts.get_balance(account_id1),
                         Decimal("0.00"))
        self.assertEqual(self.accounts.get_balance(account_id2),
                         Decimal("200.00"))

    def test_overdraft_limit(self):
        # Create an account and deposit funds.
        account_id1 = self.accounts.create_account()
        self.commands.deposit_funds(account_id1, Decimal("200.00"))

        # Check overdraft limit.
        self.assertEqual(self.accounts.get_overdraft_limit(account_id1),
                         Decimal("0.00"))

        # Set overdraft limit.
        self.accounts.set_overdraft_limit(account_id=account_id1,
                                          overdraft_limit=Decimal("500.00"))

        # Can't set negative overdraft limit.
        with self.assertRaises(AssertionError):
            self.accounts.set_overdraft_limit(
                account_id=account_id1, overdraft_limit=Decimal("-500.00"))

        # Check overdraft limit.
        self.assertEqual(self.accounts.get_overdraft_limit(account_id1),
                         Decimal("500.00"))

        # Withdraw funds.
        transaction_id = self.commands.withdraw_funds(account_id1,
                                                      Decimal("500.00"))

        # Check saga succeeded.
        self.assertTrue(self.sagas.get_saga(transaction_id).has_succeeded)
        self.assertFalse(self.sagas.get_saga(transaction_id).has_errored)
        self.assertFalse(self.sagas.get_saga(transaction_id).errors)

        # Check balance - should be overdrawn.
        self.assertEqual(self.accounts.get_balance(account_id1),
                         Decimal("-300.00"))

        # Fail to withdraw funds - insufficient funds.
        transaction_id = self.commands.withdraw_funds(account_id1,
                                                      Decimal("200.01"))

        # Check saga errored.
        self.assertFalse(self.sagas.get_saga(transaction_id).has_succeeded)
        self.assertTrue(self.sagas.get_saga(transaction_id).has_errored)
        errors = self.sagas.get_saga(transaction_id).errors
        self.assertEqual(len(errors), 1)
        self.assertEqual(errors[0],
                         InsufficientFundsError({"account_id": account_id1}))

        # Check balance.
        self.assertEqual(self.accounts.get_balance(account_id1),
                         Decimal("-300.00"))

        # Close account.
        self.accounts.close_account(account_id1)

        # Fail to set overdraft limit - account closed.
        with self.assertRaises(AccountClosedError):
            self.accounts.set_overdraft_limit(
                account_id=account_id1, overdraft_limit=Decimal("5000.00"))