Exemplo n.º 1
0
class TriggerBase(object):
    def __init__(self):
        self._actions = []
        self.log = LoggingService()

    def has_actions(self):
        return len(self._actions) > 0

    def append_action(self, action):
        self._actions.append(action)

    def examine_and_execute_actions(self):
        condition_met = self._examine_condition()

        self.log.info("Trigger {} met condition? {}".format(
            self.__class__.__name__, condition_met))

        if (condition_met):
            for action in self._actions:
                self.log.debug("Action " + type(action).__name__)
                action.execute(self._get_action_data())

    def _examine_condition(self):
        raise NotImplementedError

    def _get_action_data(self):
        return {}
Exemplo n.º 2
0
 def post(self, request, level, format=None):
     if any(level.lower() in l for l in LEVELS):
         log = LoggingService()
         data = request.DATA
         if data:
             log.error(data)
         return Response(status=status.HTTP_201_CREATED)
     else:
         return Response(status=status.HTTP_400_BAD_REQUEST)
Exemplo n.º 3
0
    def __init__(self, original_pdf_stream):
        self._logger = LoggingService()

        self._original_pdf_stream = original_pdf_stream
        self._original_pdf_stream.seek(0)

        # initialize a buffer stream to compose the modification
        # that to be merged later
        self._modification_stream = StringIO.StringIO()
class EventHandlerBase(object):
    _logger = LoggingService()

    def __init__(self, event_class):
        ''' Requires to register the target class of
            the event this is handling
        '''
        self.event_class = event_class

    ''' Handle the event.
        Note: the event here an instance of the event_class
              registered for this handler.
    '''

    def handle(self, event):
        self._logger.info('Begin handling event ...')
        self._logger.info(event)

        try:
            self._internal_handle(event)
            self._logger.info('Finished handling event')
        except Exception as e:
            self._logger.error(traceback.format_exc())
            raise

    def _internal_handle(self, event):
        raise NotImplementedError()
class AwsEventMessagePublisher(AwsEventMessageFacilityBase):
    _logger = LoggingService()
    
    def __init__(self, aws_region=None):
        super(AwsEventMessagePublisher, self).__init__(aws_region)

    def publish_event(self, event_object):
        try:
            self._logger.info('Start publishing event ...')
            self._logger.info(event_object)

            AwsEventMessageUtility.validate_event_object(event_object)

            topic_name = AwsEventMessageUtility.get_sns_topic_name(type(event_object))
            topic_arn = AwsEventMessageUtility.ensure_sns_topic(self._aws_client.sns, topic_name)
            
            if (not topic_arn):
                self._logger.error('Failed to ensure SNS topic with name: "{0}"'.format(topic_name))
                return

            response = self._aws_client.sns.publish(
                TopicArn=topic_arn,
                Message=event_object.serialize())

            if (not AwsEventMessageUtility.is_success_response(response)):
                self._logger.error('Failed to publish event')
                self._logger.info(response)
            else:
                self._logger.info('Successfully published event')

            return response
        except Exception as e:
            self._logger.error('Failed to publish event: {0}'.format(e))
Exemplo n.º 6
0
 def __init__(self):
     self.log = LoggingService()
Exemplo n.º 7
0
 def __init__(self):
     self._actions = []
     self.log = LoggingService()
Exemplo n.º 8
0
class PdfModifier(object):
    def __init__(self, original_pdf_stream):
        self._logger = LoggingService()

        self._original_pdf_stream = original_pdf_stream
        self._original_pdf_stream.seek(0)

        # initialize a buffer stream to compose the modification
        # that to be merged later
        self._modification_stream = StringIO.StringIO()

    ''' Apply specified list of operations to the PDF
        @param modification_operations list of predefined PDF operations
               (concrete implementations of 'PDFOperationBase')
    '''
    def with_modification_operations(self, modification_operations):
        for modification_operation in modification_operations:
            # start a new composer and apply the operation logic
            # polymophically 
            pdf_composer = self._init_pdf_composer()
            modification_operation.write_to_pdf(pdf_composer)
            pdf_composer.save()

        return self

    def with_modifications(self, pdf_composer_callback):
        pdf_composer = self._init_pdf_composer()
        pdf_composer_callback(pdf_composer)
        pdf_composer.save()

        return self

    ''' The output method of the builder, to actually  
    '''
    def build_output_pdf(self, output_stream):

        # Move to the beginning of the StringIO buffer
        # And initialize a PDF file reader to read that in
        # as source asset PDF to be merged into the original 
        self._modification_stream.seek(0)
        source_pdf = PdfFileReader(self._modification_stream)

        # Now read in the destination/original PDF as the merge target
        self._original_pdf_stream.seek(0)
        original_pdf = self._get_pdf_reader(self._original_pdf_stream)

        # Now create the output PDF as the merge result holder
        output_pdf = PdfFileWriter()

        # Enumerate through the list of pages from the original PDF
        #  * Merge the specified page, and
        #  * For other pages, simply add them as is to the new PDF
        for page_index in range(0, original_pdf.numPages):    
            page = original_pdf.getPage(page_index)
            if (page_index < source_pdf.numPages):
                page.mergePage(source_pdf.getPage(page_index))
            output_pdf.addPage(page)

        # If the modification doc has more pages than the original
        # also just append them to the resultant document
        for page_index in range(0, source_pdf.numPages):
            if (page_index >= original_pdf.numPages):
                page = source_pdf.getPage(page_index)
                output_pdf.addPage(page)

        # Finally, write the result PDF to the given output stream
        output_pdf.write(output_stream)

    def get_num_pages_in_original(self):
        self._original_pdf_stream.seek(0)
        original_pdf = self._get_pdf_reader(self._original_pdf_stream)
        return original_pdf.numPages

    def _get_pdf_reader(self, pdf_stream):
        pdf = PdfFileReader(pdf_stream)
        if pdf.isEncrypted:
            result = pdf.decrypt("")
            if (result == 0):
                self._logger.error("Failed to decrypt PDF file.")
                raise ValueError('Failed to decrypt PDF file.')

        return pdf

    def _init_pdf_composer(self):
        self._modification_stream.seek(0)
        return PdfComposer(self._modification_stream)
Exemplo n.º 9
0
 def __init__(self):
     super(ConnectPayrollCompanyEmployeeFrontPageCsvService, self).__init__()
     self.integration_provider_service = IntegrationProviderService()
     self.logger = LoggingService()
     self._state_tax_election_adaptor_factory = ConnectPayrollStateTaxElectionAdaptorFactory()
Exemplo n.º 10
0
class ConnectPayrollCompanyEmployeeFrontPageCsvService(CsvReportServiceBase):

    ########################################################################
    ## Items of further investigations
    ## * [Employement Type] WBM: Only W-2   CP: W-2 and 1099-M
    ## * [W-4 Status] WBM: Married high rate    CP: Head of house hold
    ## * [W-4 withold state] WBM: Company State (correct?). CP: allow more options
    ## * [W-4 additional amount] WBM: additional dollar.  CP: more options
    ## * [I-9 status] WBM does not know F-1/J-1 status
    ## * [Employment Status] Does WBM include terminated employees?
    ## * [New Hire Flag] Does the NewHire flag stands for the same expectation
    ##   on WBM and CP?
    ## * [I-9/W-4 non-new employee] WBM does not have data, what would CP expect?
    ########################################################################

    def __init__(self):
        super(ConnectPayrollCompanyEmployeeFrontPageCsvService, self).__init__()
        self.integration_provider_service = IntegrationProviderService()
        self.logger = LoggingService()
        self._state_tax_election_adaptor_factory = ConnectPayrollStateTaxElectionAdaptorFactory()

    def get_report(self, company_id, outputStream):
        try:
            self.view_model_factory = CompanyReportViewModelFactory(company_id)
            client_id = self._get_client_number(company_id)
            if (not client_id):
                raise ValueError('The company is not properly configured to integrate with Connect Payroll service!')

            self._write_headers()
            self._write_company(company_id, client_id)
            self._save(outputStream)
        except Exception as e:
            self.logger.error('Failed to produce Connect Payroll Employee Front Page export for company "{0}"'.format(company_id))
            self.logger.error(traceback.format_exc())
            raise e

    def _get_client_number(self, company_id):
        return self.integration_provider_service.get_company_integration_provider_external_id(
            company_id,
            INTEGRATION_SERVICE_TYPE_PAYROLL,
            INTEGRATION_PAYROLL_CONNECT_PAYROLL)

    def _write_headers(self):

        # Integration Identifier
        self._write_cell('EmpNumber')
        self._write_cell('Ssn')

        # Organizational Allocation
        self._write_cell('HomeLocation')
        self._write_cell('Division')
        self._write_cell('Department')

        # Employee Name
        self._write_cell('FirstName')
        self._write_cell('MiddleName')
        self._write_cell('LastName')

        # Pay Schedule
        self._write_cell('PayFrequency')
        self._write_cell('ScheduleName')

        # Employee Address
        self._write_cell('AddressLine1')
        self._write_cell('AddressLine2')
        self._write_cell('City')
        self._write_cell('ResidentState')
        self._write_cell('ZipCode')
        self._write_cell('ZipCodeExt')

        # Employment Type
        self._write_cell('EmpType')

        # Federal Tax
        self._write_cell('FedFS')
        self._write_cell('FedExemptions')
        self._write_cell('FedAddl')
        self._write_cell('FedAmt')
        self._write_cell('WorkingIn')
        self._write_cell('WithholdIncomeTaxIn')

        # State Tax
        self._write_cell('StateFS')
        self._write_cell('StateExemptions')
        self._write_cell('StateAddlExemptions')
        self._write_cell('StateAddl')
        self._write_cell('StateAmt')
        self._write_cell('SecondStateFS')
        self._write_cell('SecondStateExemptions')
        self._write_cell('SecondStateAddlExemptions')
        self._write_cell('SecondStateAddl')
        self._write_cell('SecondStateAmt')

        # 1099-M Employee Info
        # [Remark] WBM Non-supported
        self._write_cell('ApplyFedId')

        # Employee HR Info
        self._write_cell('Email')
        self._write_cell('BirthDate')

        # Salary Data
        self._write_cell('Salary')
        self._write_cell('SalaryDate')
        self._write_cell('Rate')
        self._write_cell('RateDate')

        # Other HR Data
        self._write_cell('NewHire')
        self._write_cell('Seasonal')
        self._write_cell('HireDate')
        self._write_cell('Gender')
        self._write_cell('J1F1Visa')
        self._write_cell('VisaDate')
        self._write_cell('Status')
        self._write_cell('StatusDate')
        self._write_cell('StatusReason')

        # Other Tax Data
        # [Remark] WBM Non-supported
        self._write_cell('SsExempt')
        self._write_cell('MedcExempt')
        self._write_cell('FutaExempt')
        self._write_cell('FutaExemptReason')
        self._write_cell('SuiExempt')
        self._write_cell('NonUsAddress')
        self._write_cell('PostalCode1')
        self._write_cell('PostalCode2')
        self._write_cell('PostalCode3')
        self._write_cell('Country')

    def _write_company(self, company_id, client_id):
        user_ids = self._get_all_employee_user_ids_for_company(company_id)

        # For each of them, write out his/her information
        for i in range(len(user_ids)):
            self._write_employee(user_ids[i], company_id, client_id)

    def _write_employee(self, employee_user_id, company_id, client_id):
        employee_data_context = _EmployeeDataContext(employee_user_id, company_id, self.view_model_factory)

        if not employee_data_context.user_completed_onboarding():
            self.logger.warn('Skipping employee "{0}": Onboarding not complete.'.format(employee_user_id))
            return
        if (not employee_data_context.has_complete_data()):
            self.logger.warn('Skipping employee "{0}": Missing necessary information'.format(employee_user_id))
            return

        # now start writing the employee row
        self._next_row()

        self._write_integration_info(employee_data_context)
        self._write_organizational_allocation_info(employee_data_context)
        self._write_employee_name(employee_data_context)
        self._write_employee_pay_schedule(employee_data_context)
        self._write_employee_address(employee_data_context)
        self._write_employment_type(employee_data_context)
        self._write_federal_tax_info(employee_data_context)
        self._write_state_tax_info(employee_data_context)
        self._write_1099M_data(employee_data_context)
        self._write_employee_HR_info(employee_data_context)
        self._write_employee_salary_data(employee_data_context)
        self._write_employee_other_HR_data(employee_data_context)

    def _write_integration_info(self, employee_data_context):
        self._write_cell(employee_data_context.employee_number)
        self._write_cell(employee_data_context.person_info.ssn)

    def _write_organizational_allocation_info(self, employee_data_context):
        # Skippng 'HomeLocation' as it is not used
        self._skip_cells(1)

        employee_profile_info = employee_data_context.employee_profile_info
        
        if (employee_profile_info.division and employee_profile_info.division.code):
            self._write_cell(employee_profile_info.division.code)
        else:
            self._skip_cells(1)

        if (employee_profile_info.department and employee_profile_info.department.code):
            self._write_cell(employee_profile_info.department.code)
        else:
            self._skip_cells(1)

    def _write_employee_name(self, employee_data_context):
        person_info = employee_data_context.person_info

        self._write_cell(person_info.first_name)
        self._write_cell(person_info.middle_name)
        self._write_cell(person_info.last_name)

    def _write_employee_pay_schedule(self, employee_data_context):
        employee_profile_info = employee_data_context.employee_profile_info

        mapped_paycode = self._get_pay_cycle_code(employee_profile_info.pay_cycle)
        if (not mapped_paycode):
            self.logger.warn('[Data Issue] Employee "{0}": has invalid pay-code'.format(employee_data_context.employee_user_id))
            self._skip_cells(1)
        else:
            self._write_cell(mapped_paycode)

        # Skip the 'ScheduleName' field
        self._skip_cells(1)

    def _get_pay_cycle_code(self, pay_cycle):
        if (pay_cycle == PERIOD_WEEKLY):
            return 'Weekly'
        elif(pay_cycle == PERIOD_BIWEEKLY):
            return 'Biweekly'
        elif(pay_cycle == PERIOD_SEMIMONTHLY):
            return 'Semi-Monthly'
        elif(pay_cycle == PERIOD_MONTHLY):
            return 'Monthly'
        else:
            return ''

    def _write_employee_address(self, employee_data_context):
        person_info = employee_data_context.person_info
        self._write_cell(person_info.address1)
        self._write_cell(person_info.address2)
        self._write_cell(person_info.city)
        self._write_cell(person_info.state)

        zip_and_ext = person_info.get_zipcode_and_extension()
        self._write_cell(zip_and_ext[0])
        self._write_cell(zip_and_ext[1])

    def _write_employment_type(self, employee_data_context):
        # [Remark]: We only support W2 employee (?)
        self._write_cell('W2')

    def _write_federal_tax_info(self, employee_data_context):
        w4_info = employee_data_context.w4_info

        if (not w4_info):
            self._skip_cells(6)
        else:
            company_info = employee_data_context.company_info

            status_code = self._get_w4_marriage_status_code(w4_info.marriage_status)

            self._write_cell(status_code)
            self._write_cell(w4_info.total_points)
            self._write_cell('A')
            self._write_cell(w4_info.extra_amount)
            self._write_cell(company_info.state)
            self._write_cell(company_info.state)

    def _get_w4_marriage_status_code(self, marriage_status):
        if (marriage_status == W4_MARRIAGE_STATUS_SINGLE):
            return 'S'
        elif(marriage_status == W4_MARRIAGE_STATUS_MARRIED):
            return 'M'
        elif(marriage_status == W4_MARRIAGE_STATUS_MARRIED_HIGH_SINGLE):
            return 'H'
        else:
            return ''

    def _write_state_tax_info(self, employee_data_context):
        state_tax_info = employee_data_context.state_tax_info

        # This limits how many state elections CP can take and hence
        # we can output
        export_limit = 2
        counter = 0

        if (state_tax_info):
            all_state_elections = state_tax_info.get_all_state_elections()

            for state in all_state_elections:
                if (counter >= export_limit):
                    break
                adaptor = self._state_tax_election_adaptor_factory.get_adaptor(state, all_state_elections[state])
                if (adaptor):
                    self._write_cell(adaptor.get_filing_status())
                    self._write_cell(adaptor.get_total_exemptions())
                    self._write_cell(adaptor.get_additional_exemptions())
                    self._write_cell(adaptor.get_additional_amount_code())
                    self._write_cell(adaptor.get_additional_amount())

                    counter = counter + 1

        # Skip any set of fields that the employee does not have data to fill
        # E.g. secondary state witholding
        self._skip_cells((export_limit - counter) * 5)

    def _write_1099M_data(self, employee_data_context):
        # [Remark]: WBM does not support 1099-M
        self._skip_cells(1)

    def _write_employee_HR_info(self, employee_data_context):
        person_info = employee_data_context.person_info
        self._write_cell(person_info.email)
        self._write_cell(self._get_date_string(person_info.birth_date))

    def _write_employee_salary_data(self, employee_data_context):
        employee_profile_info = employee_data_context.employee_profile_info
        pay_type = employee_profile_info.pay_type
        if (pay_type == PAY_TYPE_HOURLY):
            self._skip_cells(2)
            self._write_cell(self._normalize_decimal_number(employee_profile_info.current_hourly_rate))
            self._write_cell(self._get_date_string(employee_profile_info.compensation_effective_date))
        elif (pay_type == PAY_TYPE_SALARY):
            self._write_cell(self._normalize_decimal_number(employee_profile_info.annual_salary))
            self._write_cell(self._get_date_string(employee_profile_info.compensation_effective_date))
            self._skip_cells(2)
        else:
            self.logger.warn('Skipping data for employee "{0}": Invalid pay type.'.format(employee_data_context.employee_user_id))
            self._skip_cells(4)

    def _write_employee_other_HR_data(self, employee_data_context):
        employee_profile_info = employee_data_context.employee_profile_info
        person_info = employee_data_context.person_info

        self._write_cell(employee_profile_info.new_employee)
        # [Remark]: Seasonal is not supported on WBM and seems to not be used by CP
        self._skip_cells(1)
        self._write_cell(self._get_date_string(employee_profile_info.hire_date))
        self._write_cell(person_info.gender)

        # [Remark]: Skip all J1/F1 Visa 
        self._skip_cells(2)

        code_and_date = self._get_employement_status_code_and_date(employee_profile_info)
        if (not code_and_date):
            self.logger.warn('Skipping data for employee "{0}": Invalid employment status'.format(employee_data_context.employee_user_id))
            self._skip_cells(2)
        else:
            self._write_cell(code_and_date['code'])
            self._write_cell(code_and_date['date'])

        # [Remark]: Skip the status reason
        self._skip_cells(1)

    def _get_employement_status_code_and_date(self, employee_profile_info):
        employment_status = employee_profile_info.employment_status
        if (employment_status == EMPLOYMENT_STATUS_ACTIVE):
            return {
                'code': 'Active',
                'date': self._get_date_string(employee_profile_info.hire_date)
            }
        elif (employment_status == EMPLOYMENT_STATUS_TERMINATED):
            return {
                'code': 'Terminated',
                'date': self._get_date_string(employee_profile_info.end_date)
            }
        else:
            return None

    def _write_other_tax_data(self, employee_data_context):
        # [Remark] WBM Non-supported
        # [Remark] CP does not seem to use 
        self._skip_cells(10)

    def _normalize_decimal_number(self, decimal_number):
        result = decimal_number
        if (decimal_number == 0 or decimal_number):
            result = "{:.2f}".format(float(decimal_number))
        return result
Exemplo n.º 11
0
    class AwsQueuePump(object):
        _logger = LoggingService()

        def __init__(self, aws_client, event_message_handler_class,
                     message_queue_config):
            self._aws_client = aws_client
            self._event_message_handler_class = event_message_handler_class
            self._queue = None
            self._dead_letter_queue = None
            self._message_handler = None
            self._message_queue_config = message_queue_config

        def ensure_queue_setup(self):
            try:
                # Validate the handler class
                self._message_handler = self._event_message_handler_class()
                AwsEventMessageUtility.validate_event_handler_instance(
                    self._message_handler)

                # Ensure setup of the message queue
                queue_name = AwsEventMessageUtility.get_sqs_queue_name(
                    self._message_handler)
                self._queue = AwsEventMessageUtility.ensure_sqs_queue(
                    self._aws_client.sqs, queue_name)

                if (not self._queue):
                    self._logger.error(
                        'Failed to ensure event message queue with name: "{0}"'
                        .format(queue_name))
                    return False
                queue_arn = self._queue.attributes.get('QueueArn')

                # Ensure the SNS topic
                topic_name = AwsEventMessageUtility.get_sns_topic_name(
                    self._message_handler.event_class)
                topic_arn = AwsEventMessageUtility.ensure_sns_topic(
                    self._aws_client.sns, topic_name)

                if (not topic_arn):
                    self._logger.error(
                        'Failed to ensure SNS topic with name: "{0}"'.format(
                            topic_name))
                    return False

                # Ensure subscription, queue -> topic
                response = self._aws_client.sns.subscribe(TopicArn=topic_arn,
                                                          Protocol='sqs',
                                                          Endpoint=queue_arn)

                if (not AwsEventMessageUtility.is_success_response(response)):
                    self._logger.error(
                        'Failed to ensure SQS-SNS subscription: "{0}-{1}"'.
                        format(queue_name, topic_name))
                    return False

                # Set up a policy to allow SNS access to the queue
                if 'Policy' in self._queue.attributes:
                    policy = json.loads(self._queue.attributes['Policy'])
                else:
                    policy = {'Version': '2008-10-17'}

                if 'Statement' not in policy:
                    statement = AwsEventMessageUtility.queue_policy_statement
                    statement['Resource'] = queue_arn
                    statement['Condition']['StringLike'][
                        'aws:SourceArn'] = topic_arn
                    policy['Statement'] = [statement]

                    self._queue.set_attributes(
                        Attributes={'Policy': json.dumps(policy)})

                # Now setup the queue config if specified
                if (self._message_queue_config):
                    self._queue.set_attributes(
                        Attributes=self._message_queue_config.to_dict())

                # Now ensure the setup of the dead-letter queue
                self._ensure_dead_letter_queue(queue_name)

                return True

            except Exception as e:
                self._logger.error(traceback.format_exc())

        ''' This is to ensure the proper setup of dead-letter
            queue.
            For now, this is not yet made highly configurable
            from the consumer side, and below is the hardcoded
            setup:
                * Source queue is 1-1 mapped to a corresponding dead-letter queue
                * The queue configuration for all dead-letter queues use SQS defaults
                * Redrive policy on the source queue is set to allow max 5 recerives
                  of a message before moving it to dead-letter queue

            All of these knobs could be later on exposed on API for consumers
            to control/override, but this is not a priority for now.
        '''

        def _ensure_dead_letter_queue(self, source_queue_name):
            dl_queue_name = source_queue_name + '_DL'
            self._dead_letter_queue = AwsEventMessageUtility.ensure_sqs_queue(
                self._aws_client.sqs, dl_queue_name)

            if (not self._dead_letter_queue):
                self._logger.error(
                    'Failed to ensure deadletter queue for: "{0}"'.format(
                        source_queue_name))
                return

            dl_queue_arn = self._dead_letter_queue.attributes.get('QueueArn')
            redrive_policy = {
                'maxReceiveCount': '5',
                'deadLetterTargetArn': dl_queue_arn
            }
            self._queue.set_attributes(
                Attributes={'RedrivePolicy': json.dumps(redrive_policy)})

        def pump_event_messages(self):
            try:
                messages = self._queue.receive_messages()

                # Process messages
                for message in messages:
                    self._logger.info('Handling event message: {0}'.format(
                        message.body))

                    # Parse out the actual event message from
                    # Boto3 sqs message data structure
                    # And deserialize into the expected event
                    # instance.
                    body = json.loads(message.body)
                    event_message = body.get('Message', '{}')
                    event_instance = self._message_handler.event_class()
                    event_instance.deserialize(event_message)

                    # Delegate to the message handler to handle
                    self._message_handler.handle(event_instance)

                    # Let the queue know that the message is processed
                    message.delete()
            except Exception as e:
                self._logger.error(traceback.format_exc())

        def start_pump_async(self):
            thread.start_new_thread(self._run_pump, ())

        def _run_pump(self):
            while (True):
                self.pump_event_messages()
class CompanyIntegrationProviderDataService(object):
    _logger = LoggingService()

    def __init__(self):
        self.integration_provider_service = IntegrationProviderService()
        self.company_personnel_service = CompanyPersonnelService()

        self._data_service_registry = {}
        for integration_provider_type in INTEGRATION_SERVICE_TYPES:
            self._data_service_registry[integration_provider_type] = {}      

        self._register_data_service_classes()

    def _register_data_service_classes(self):
        # Register all data services
        self._data_service_registry[INTEGRATION_SERVICE_TYPE_PAYROLL][INTEGRATION_PAYROLL_CONNECT_PAYROLL] = ConnectPayrollDataService
        self._data_service_registry[INTEGRATION_SERVICE_TYPE_PAYROLL][INTEGRATION_PAYROLL_ADVANTAGE_PAYROLL] = AdvantagePayrollDataService

    def sync_employee_data_to_remote(self, employee_user_id):
        # Enumerate through all the integration providers associated
        # with the company, identify all registered data services, 
        # and invoke them to sync with the remote
        company_id = self.company_personnel_service.get_company_id_by_employee_user_id(employee_user_id)
        if (not company_id):
            return
        return self._enumerate_company_data_services(company_id, lambda data_service: data_service.sync_employee_data_to_remote(employee_user_id))

    def generate_and_record_external_employee_number(self, employee_user_id):
        company_id = self.company_personnel_service.get_company_id_by_employee_user_id(employee_user_id)
        if (not company_id):
            raise ValueError('The given employee user ID "{0}" is not properly linked to a valid company.'.format(employee_user_id))
        return self._enumerate_company_data_services(company_id, lambda data_service: data_service.generate_and_record_external_employee_number(employee_user_id))

    def _enumerate_company_data_services(self, company_id, data_service_action):
        # Record failed actions and report back to caller
        failed_data_service_records = []

        company_integration_providers = self.integration_provider_service.get_company_integration_providers(company_id)
        for service_type in company_integration_providers:
            company_service_type_provider = company_integration_providers[service_type]
            if (company_service_type_provider):
                provider_name = company_service_type_provider['integration_provider']['name']
                # Now we have the service type and the provider name, check whether
                # we have a registered data service class
                if (service_type in self._data_service_registry):
                    service_type_data_services = self._data_service_registry[service_type]
                    if (provider_name in service_type_data_services):
                        # create an instance of the date service, and invoke the action
                        data_service = service_type_data_services[provider_name]()

                        # Collect the current data service specs into a record instance
                        # for logging and anormaly reporting to upstream
                        service_action_record = IntegrationDataServiceActionRecord(
                            company_id=company_id,
                            service_type=service_type,
                            provider_name=provider_name,
                            service_action=data_service_action
                        )

                        try:
                            data_service_action(data_service)
                            self._logger.error('Successfully executed integration data action')
                            self._logger.info(service_action_record)
                        except Exception as e:
                            self._logger.error('Failed to complete integration data action')
                            self._logger.info(service_action_record)
                            failed_data_service_records.append(service_action_record)
                else:
                    self._logger.warning('Unsupported integration service type encoutered: "{0}"'.format(service_type))

        return failed_data_service_records
class ConnectPayrollDataService(IntegrationProviderDataServiceBase):
    _logger = LoggingService()

    def __init__(self):
        super(ConnectPayrollDataService, self).__init__()
        self.view_model_factory = ReportViewModelFactory()
        self.web_request_service = WebRequestService()
        self.company_personnel_service = CompanyPersonnelService()
        self.integration_provider_service = IntegrationProviderService()

        # Retrieve the api token if available
        setting_service = SystemSettingsService()
        self._cp_api_auth_token = setting_service.get_setting_value_by_name(
            SYSTEM_SETTING_CPAPIAUTHTOKEN)

        # Also construct the API url, if available
        self._cp_api_url = None
        base_uri = setting_service.get_setting_value_by_name(
            SYSTEM_SETTING_CPAPIBASEURI)
        employee_route = setting_service.get_setting_value_by_name(
            SYSTEM_SETTING_CPAPIEMPLOYEEROUTE)
        if (base_uri and employee_route):
            self._cp_api_url = base_uri + employee_route

    def _integration_service_type(self):
        return INTEGRATION_SERVICE_TYPE_PAYROLL

    def _integration_provider_name(self):
        return INTEGRATION_PAYROLL_CONNECT_PAYROLL

    def _internal_generate_and_record_external_employee_number(
            self, employee_user_id):
        # First check whether the said employee already have a number
        # If so, this is an exception state, log it, and skip the operation
        employee_number = self.integration_provider_service.get_employee_integration_provider_external_id(
            employee_user_id, self._integration_service_type(),
            self._integration_provider_name())
        if (employee_number):
            logging.error(
                'Invalid Operation: Try to generate external ID for employee (User ID={0}) already has one!'
                .format(employee_user_id))
            return

        company_id = self.company_personnel_service.get_company_id_by_employee_user_id(
            employee_user_id)
        next_employee_number = self._get_next_external_employee_number(
            company_id)
        # Now save the next usable external employee number to the profile
        # of the specified employee
        self._set_employee_external_id(employee_user_id,
                                       self._integration_service_type(),
                                       self._integration_provider_name(),
                                       next_employee_number)

    def _get_next_external_employee_number(self, company_id):
        return 0

    def _internal_sync_employee_data_to_remote(self, employee_user_id):
        # If the Connect Payroll API's auth token is not specified
        # in the environment, consider this feature to be off, and
        # skip all together.
        if (not self._cp_api_auth_token):
            return

        if (not self._cp_api_url):
            return

        # Also check whether the employee belong to a company with
        # the right setup with the remote system. And also skip if
        # this is not the case
        external_company_id = self._get_cp_client_code_by_employee(
            employee_user_id)
        if (not external_company_id):
            return

        try:
            # Populate the data object from the current state of the employee in WBM system
            # Also apply client(WBM) side validation on the data, based on understanding of
            # documentation from ConnectPay
            employee_data_dto = self._get_employee_data_dto(
                employee_user_id, external_company_id)
            issue_list = self._validate_employee_data_dto(employee_data_dto)

            if (issue_list and len(issue_list) > 0):
                raise RuntimeError(
                    'There are problems collecting complete data required to sync to ConnectPay API for employee "{0}"'
                    .format(employee_user_id), issue_list)

            if (employee_data_dto.payrollId):
                # Already exists in CP system, update
                self._logger.info('Updating Employee CP ID: ' +
                                  employee_data_dto.payrollId)
                self._logger.info(employee_data_dto)
                self._update_employee_data_to_remote(employee_data_dto)
            else:
                # Does not yet exist in CP system, new employee addition, create
                self._logger.info(
                    'Creating new employee record on CP system ...')
                self._logger.info(employee_data_dto)
                payroll_id = self._create_employee_data_to_remote(
                    employee_data_dto)
                self._logger.info(
                    'Created Employee CP ID: {0}'.format(payroll_id))

                # Sync the cp ID from the response
                self._set_employee_external_id(
                    employee_user_id, self._integration_service_type(),
                    self._integration_provider_name(), payroll_id)
        except Exception as e:
            self._logger.error(traceback.format_exc())
            raise

    def _get_employee_data_dto(self, employee_user_id, external_company_id):
        # First populate the CP identifiers
        dto = ConnectPayrollEmployeeDto()
        dto.payrollId = self._get_employee_external_id(
            employee_user_id, self._integration_service_type(),
            self._integration_provider_name())
        dto.companyId = external_company_id

        # Now populate other data
        company_id = self.company_personnel_service.get_company_id_by_employee_user_id(
            employee_user_id)
        company_info = self.view_model_factory.get_company_info(company_id)
        person_info = self.view_model_factory.get_employee_person_info(
            employee_user_id)

        # Employee basic data
        dto.ssn = person_info.ssn
        dto.firstName = person_info.first_name
        dto.lastName = person_info.last_name
        dto.dob = self._get_date_string(person_info.birth_date)
        dto.gender = person_info.gender
        dto.address1 = person_info.address1
        dto.address2 = person_info.address2
        dto.city = person_info.city
        dto.country = person_info.country
        dto.state = person_info.state
        dto.zip = person_info.zipcode
        dto.email = person_info.email
        if (len(person_info.phones) > 0):
            dto.phone = person_info.phones[0]['number']

        # Employment data
        employee_profile_info = self.view_model_factory.get_employee_employment_profile_data(
            employee_user_id, company_info.company_id)

        if (employee_profile_info):
            dto.jobTitle = employee_profile_info.job_title
            dto.fullTime = employee_profile_info.is_full_time_employee()
            dto.hireDate = self._get_date_string(
                employee_profile_info.hire_date)
            dto.originalHireDate = self._get_date_string(
                employee_profile_info.hire_date)
            # [TODO]: Needs specification on employee status values
            dto.employeeStatus = '3'
            dto.terminationDate = self._get_date_string(
                employee_profile_info.end_date)

            # Salary data
            dto.payEffectiveDate = self._get_date_string(
                employee_profile_info.compensation_effective_date)
            dto.annualBaseSalary = self._get_decimal_string(
                employee_profile_info.annual_salary)
            dto.baseHourlyRate = self._get_decimal_string(
                employee_profile_info.current_hourly_rate)
            dto.hoursPerWeek = self._get_decimal_string(
                employee_profile_info.projected_hours_per_week)

        # Other
        employee_i9_info = self.view_model_factory.get_employee_i9_data(
            employee_user_id)
        if (employee_i9_info):
            self.usCitizen = employee_i9_info.citizen_data is not None

        return dto

    def _validate_employee_data_dto(self, employee_data_dto):
        issue_list = []

        # System Data
        _DataValidator(employee_data_dto, 'companyId', issue_list) \
            .with_value_exists_check() \
            .with_value_length_check(6, 6) \
            .validate()

        _DataValidator(employee_data_dto, 'payrollId', issue_list) \
            .with_value_valid_integer_check() \
            .validate()

        # Employee bio data and basic info
        _DataValidator(employee_data_dto, 'ssn', issue_list) \
            .with_value_exists_check() \
            .with_value_length_check(9, 9) \
            .validate()

        _DataValidator(employee_data_dto, 'firstName', issue_list) \
            .with_value_exists_check() \
            .with_value_length_check(1, 20) \
            .validate()

        _DataValidator(employee_data_dto, 'middleName', issue_list) \
            .with_value_length_check(0, 20) \
            .validate()

        _DataValidator(employee_data_dto, 'lastName', issue_list) \
            .with_value_exists_check() \
            .with_value_length_check(1, 20) \
            .validate()

        _DataValidator(employee_data_dto, 'dob', issue_list) \
            .with_value_exists_check() \
            .with_value_valid_datetime_check() \
            .validate()

        _DataValidator(employee_data_dto, 'gender', issue_list) \
            .with_value_exists_check() \
            .with_value_in_list_check(['M', 'F']) \
            .validate()

        _DataValidator(employee_data_dto, 'address1', issue_list) \
            .with_value_length_check(0, 30) \
            .validate()

        _DataValidator(employee_data_dto, 'address2', issue_list) \
            .with_value_length_check(0, 30) \
            .validate()

        _DataValidator(employee_data_dto, 'city', issue_list) \
            .with_value_length_check(0, 28) \
            .validate()

        _DataValidator(employee_data_dto, 'state', issue_list) \
            .with_value_exists_check() \
            .with_value_length_check(2, 2) \
            .validate()

        _DataValidator(employee_data_dto, 'zip', issue_list) \
            .with_value_exists_check() \
            .with_value_length_check(5, 10) \
            .validate()

        _DataValidator(employee_data_dto, 'country', issue_list) \
            .with_value_length_check(0, 30) \
            .validate()

        _DataValidator(employee_data_dto, 'email', issue_list) \
            .with_value_exists_check() \
            .with_value_length_check(1, 250) \
            .validate()

        _DataValidator(employee_data_dto, 'phone', issue_list) \
            .with_value_length_check(0, 14) \
            .validate()

        _DataValidator(employee_data_dto, 'phone', issue_list) \
            .with_value_length_check(0, 14) \
            .validate()

        # Employment data
        _DataValidator(employee_data_dto, 'department', issue_list) \
            .with_value_valid_integer_check() \
            .validate()

        _DataValidator(employee_data_dto, 'division', issue_list) \
            .with_value_valid_integer_check() \
            .validate()

        _DataValidator(employee_data_dto, 'union', issue_list) \
            .with_value_type_boolean_check() \
            .validate()

        _DataValidator(employee_data_dto, 'jobTitle', issue_list) \
            .with_value_length_check(0, 30) \
            .validate()

        _DataValidator(employee_data_dto, 'fullTime', issue_list) \
            .with_value_type_boolean_check() \
            .validate()

        _DataValidator(employee_data_dto, 'seasonal', issue_list) \
            .with_value_type_boolean_check() \
            .validate()

        _DataValidator(employee_data_dto, 'hireDate', issue_list) \
            .with_value_exists_check() \
            .with_value_valid_datetime_check() \
            .validate()

        _DataValidator(employee_data_dto, 'originalHireDate', issue_list) \
            .with_value_valid_datetime_check() \
            .validate()

        _DataValidator(employee_data_dto, 'terminationDate', issue_list) \
            .with_value_valid_datetime_check() \
            .validate()

        self.employeeStatus = None

        # Salary data
        _DataValidator(employee_data_dto, 'payEffectiveDate', issue_list) \
            .with_value_valid_datetime_check() \
            .validate()

        _DataValidator(employee_data_dto, 'annualBaseSalary', issue_list) \
            .with_value_valid_decimal_check() \
            .validate()

        _DataValidator(employee_data_dto, 'baseHourlyRate', issue_list) \
            .with_value_valid_decimal_check() \
            .validate()

        return issue_list

    def _update_employee_data_to_remote(self, employee_data_dto):
        data = employee_data_dto.__dict__

        response = self.web_request_service.put(
            self._cp_api_url,
            data_object=data,
            auth_token=self._cp_api_auth_token)

        response.raise_for_status()

    def _create_employee_data_to_remote(self, employee_data_dto):
        data = employee_data_dto.__dict__

        response = self.web_request_service.post(
            self._cp_api_url,
            data_object=data,
            auth_token=self._cp_api_auth_token)

        response.raise_for_status()

        # Also, we really only expect here the below based on CP API behavior
        # * HTTP 200
        # * body contains the resultant ID created
        # So throw if we receive anything else
        if (response.status_code != 200):
            raise RuntimeError(
                'POST to ConnectPay Employee API resulted in a non-200 status: "{0}"'
                .format(response.status_code), response)

        if (not response.text):
            raise RuntimeError(
                'POST to ConnectPay Employee API resulted in empty body, and hence was not able to receive new employee ID.'
            )

        return response.text

    def _get_cp_client_code_by_employee(self, employee_user_id):
        company_id = self.company_personnel_service.get_company_id_by_employee_user_id(
            employee_user_id)

        if (company_id):
            return self.integration_provider_service.get_company_integration_provider_external_id(
                company_id, self._integration_service_type(),
                self._integration_provider_name())

        return None

    def _get_date_string(self, date):
        if date:
            try:
                return date.isoformat()
            except:
                return None
        else:
            return None

    def _get_decimal_string(self, input_value):
        if isinstance(input_value, decimal.Decimal):
            return str(input_value)

        return input_value