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 {}
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)
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))
def __init__(self): self.log = LoggingService()
def __init__(self): self._actions = [] self.log = LoggingService()
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)
def __init__(self): super(ConnectPayrollCompanyEmployeeFrontPageCsvService, self).__init__() self.integration_provider_service = IntegrationProviderService() self.logger = LoggingService() self._state_tax_election_adaptor_factory = ConnectPayrollStateTaxElectionAdaptorFactory()
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
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