Revenue

Overview

The project was a webpage that automated the most difficult and tedious journal entries the revenue team had to complete. Additionally, aggregated aging data across the various entities tracked separately in the accounting system. The combined aging data was used to streamline the month end closing process.

Main page

The CORE Group had close to twenty different legal entities each with their own set of books. The customers would make a payment (or multiple payments) to one of the entities. When the accounting department booked the revenue to properly recognize the revenue the payment had to split between the entities.

Features and Value added

Automated Journal Entries

The webpage took an Excel file broken down by location and generated the various intercompany and Customer Invoice entries to clean up the Customer Aging. The webpage was processed by the backend server to pull out the relevant data reducing the fields needed to be entered. The final result was a csv file that was imported into each Company's accounting system.

Aging

The Aging is a report of entries that do not have a corresponding offsetting entry in the books, or they have not been applied. This is a standard Accounting report generated by accounting systems. The complexity for the CORE Group was that there were close to twenty of these reports each could have information related to the other one. The Aging page combined all the entities into a single report to make reconciliation easier.

Development Process

The application was developed and used in production in three weeks. The first phase generated the journal entries only. The second phase looked at the file to autofill the form needed to generate the entry. 10 fields are need to complete the entry after the second phase all, but one of the fields was autofilled. The third phase was adding the aging report and only came about because of discussions with the team to see what pain points they had in their process overall.

The application was built with flask and used sql alchemy to query the MSSQL database. Additionally, the frontend used vue.js for the front end automation and ajax requests to parse the excel file.

Learning

At the time, I was on a Domain Driven Design kick, so I used that to build the webpage. I really should not have because it was not a heavy domain logic problem, so the architecture added unnecessary overhead. I had to do some really weird things, like attach the response of the handlers to the unit of work object, below is an example.

def generate_imports():
    ...
    cmd = commands.CreateJEImports(
        imported_data=imported_data_df.to_dict(orient='records'),
        # ...
    )
    bus.handle(cmd)
    return bus.uow.response  # TODO: see if there is a better way!

In order to support the synchronous nature of the request/response of the webserver the message bus had to have separate methods to return a value.

class MessageBus:
    ...

    def handle_with_return_value(self, message: Message) -> typing.Any:
        """Only handles message...
        Will not handle the events generated
        from the message (Command or Event) parameter
        """
        if isinstance(message, events.Event):
            logger.debug(
                'Handling message of type '
                'Event with result value'
            )
            try:
                handler = self.event_handlers[type(message)]
                return handler(message)  # <-- Return happening here
            except KeyError as e:
                logger.exception(
                    f'Exception handling Event '
                    f'{message} no handler set'
                )
                raise
            except Exception as e:
                logger.exception(
                    f'Exception handling Event with '
                    f'return value {message} error: {e}',
                    exc_info=True
                )
                raise

        if isinstance(message, commands.Command):
            logger.debug(
                'Handling message of type '
                'Command with result value'
            )
            try:
                handler = self.command_handlers[type(message)]
                return handler(message)  # <-- Return happening here
            except KeyError as e:
                logger.exception(
                    f'Exception handling Command '
                    f'{message} no handler set',
                    exc_info=True
                )
                raise
            except Exception as e:
                logger.exception(
                    f'Exception handling Command with '
                    f'return value {message} error: {e}',
                    exc_info=True
                )
                raise

Both of those broke the crux of the Command and Query Responsibility Segregation which was used as the architecture of the codebase.

Extras

Because we could leverage the current process the team used the Excel file that was parsed used tab names to delineate which one to use.