Skip to content

Commit

Permalink
17830 - EFT TDI17 Parser (#1275)
Browse files Browse the repository at this point in the history
* 17830 - EFT TDI17 Parser

* 17830 - PR updates
  • Loading branch information
ochiu authored Oct 6, 2023
1 parent d467e71 commit 90fc03c
Show file tree
Hide file tree
Showing 15 changed files with 1,208 additions and 2 deletions.
2 changes: 1 addition & 1 deletion pay-api/src/pay_api/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@
Development release segment: .devN
"""

__version__ = '1.20.0' # pylint: disable=invalid-name
__version__ = '1.20.1' # pylint: disable=invalid-name
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Copyright © 2023 Province of British Columbia
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Exposes all EFT classes."""

from .eft_header import EFTHeader
from .eft_record import EFTRecord
from .eft_trailer import EFTTrailer
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# Copyright © 2023 Province of British Columbia
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# TDI17 File Specifications
# One header record:
# . Columns Len Purpose
# . 1 - 1 1 Record type (always 1)
# . 2 - 16 15 "CREATION DATE: "
# . 17 - 24 8 File creation date in YYYYMMDD format
# . 25 - 41 17 "CREATION TIME: "
# . 42 - 45 8 File creation time in HHMM format
# . 46 - 69 24 "DEPOSIT DATE(S) FROM: "
# . 70 - 77 8 Starting deposit date in YYYYMMDD format
# . 78 - 89 12 " TO DATE : "
# . 90 - 97 8 Ending deposit date in YYYYMMDD format
#
# Zero or more detail records:
# . Columns Len Purpose
# . 1 - 1 1 Record type (always 2)
# . 2 - 3 2 Ministry code
# . 4 - 7 4 Program code
# . 8 - 15 8 Deposit date in YYYYMMDD format
# . 16 - 20 5 Location ID
# . 21 - 24 4 Deposit time in YYYYMMDD format (optional)
# . 25 - 27 3 Transaction sequence number (optional)
# . 28 - 67 40 Transaction description
# . 68 - 80 13 Deposit amount in the specified currency, in cents
# . 81 - 82 2 Currency (blank = CAD, US = USD)
# . 83 - 95 13 Exchange adjustment amount, in cents
# . 96 - 108 13 Deposit amount in CAD, in cents
# .109 - 112 4 Destination bank number
# .113 - 121 9 Batch number (optional; specified only if posted to GL)
# .122 - 122 1 JV type (I = inter, J = intra; mandatory if JV batch specified)
# .123 - 131 9 JV number (mandatory if JV batch specified)
# .132 - 139 8 Transaction date (optional)
#
# One trailer record:
# . Columns Len Purpose
# . 1 - 1 1 Record type (always 7)
# . 2 - 7 6 Number of details (left zero filled)
# . 8 - 21 14 Total deposit amount, in CAD (left zero filled)
#
# All numbers are right justified and left padded with zeroes.
#
# In a money field, the rightmost character is either blank or a minus sign.

"""This manages the EFT base class."""
import decimal
from datetime import datetime

from reconciliations.eft.eft_enums import EFTConstants
from reconciliations.eft.eft_errors import EFTError
from reconciliations.eft.eft_parse_error import EFTParseError


class EFTBase:
"""Defines the structure of the base class of an EFT record."""

record_type: str # Always 1 for header, 2 for transaction, 7 for trailer
content: str
index: int
errors: [EFTParseError]

def __init__(self, content: str, index: int):
"""Return an EFT Base record."""
self.content = content
self.index = index
self.errors = []

def is_valid_length(self, length: int = EFTConstants.EXPECTED_LINE_LENGTH.value) -> bool:
"""Validate content is the expected length."""
if self.content is None or len(self.content) != length:
return False

return True

def validate_record_type(self, expected_record_type: str) -> bool:
"""Validate if the record type is the expected value."""
if not self.record_type == expected_record_type:
self.add_error(EFTParseError(EFTError.INVALID_RECORD_TYPE, self.index))

def extract_value(self, start_index: int, end_index: int) -> str:
"""Extract and strip content value."""
return self.content[start_index:end_index].strip()

def parse_decimal(self, value: str, error: EFTError) -> decimal:
"""Try to parse decimal value from a string, return None if it fails and add an error."""
try:
# ends with blank or minus sign, handle the minus sign situation
if value.endswith('-'):
value = '-' + value[:-1]

result = decimal.Decimal(str(value))
except (ValueError, TypeError, decimal.InvalidOperation):
result = None
self.add_error(EFTParseError(error))

return result

def parse_int(self, value: str, error: EFTError) -> decimal:
"""Try to parse int value from a string, return None if it fails and add an error."""
try:
result = int(value)
except (ValueError, TypeError):
result = None
self.add_error(EFTParseError(error))

return result

def parse_date(self, date_str: str, error: EFTError) -> decimal:
"""Try to parse date value from a string, return None if it fails and add an error."""
try:
result = datetime.strptime(date_str, EFTConstants.DATE_FORMAT.value)
except (ValueError, TypeError):
result = None
self.add_error(EFTParseError(error))

return result

def parse_datetime(self, datetime_str: str, error: EFTError) -> decimal:
"""Try to parse date time value from a string, return None if it fails and add an error."""
try:
result = datetime.strptime(datetime_str, EFTConstants.DATE_TIME_FORMAT.value)
except (ValueError, TypeError):
result = None
self.add_error(EFTParseError(error))

return result

def add_error(self, error: EFTParseError):
"""Add parse error to error array."""
error.index = self.index
self.errors.append(error)

def has_errors(self) -> bool:
"""Return true if the error array has elements."""
return len(self.errors) > 0

def get_error_messages(self) -> [str]:
"""Return a string array of the error messages."""
return [error.message for error in self.errors]
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Copyright © 2023 Province of British Columbia
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""EFT Enum definitions."""
from enum import Enum


class EFTConstants(Enum):
"""EFT constants."""

# Currency
CURRENCY_CAD = 'CAD'

# Record Type
HEADER_RECORD_TYPE = '1'
TRANSACTION_RECORD_TYPE = '2'
TRAILER_RECORD_TYPE = '7'

# Formats
DATE_TIME_FORMAT = '%Y%m%d%H%M'
DATE_FORMAT = '%Y%m%d'

# Lengths
EXPECTED_LINE_LENGTH = 140
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Copyright © 2023 Province of British Columbia
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""EFT error codes."""

from enum import Enum


class EFTError(Enum):
"""EFT Error Enum."""

INVALID_LINE_LENGTH = 'Invalid EFT file line length.'
INVALID_RECORD_TYPE = 'Invalid Record Type.'
INVALID_CREATION_DATETIME = 'Invalid header creation date time.'
INVALID_DEPOSIT_START_DATE = 'Invalid header deposit start date.'
INVALID_DEPOSIT_END_DATE = 'Invalid header deposit end date.'
INVALID_NUMBER_OF_DETAILS = 'Invalid trailer number of details value.'
INVALID_TOTAL_DEPOSIT_AMOUNT = 'Invalid trailer total deposit amount.'
INVALID_DEPOSIT_AMOUNT = 'Invalid transaction deposit amount.'
INVALID_EXCHANGE_ADJ_AMOUNT = 'Invalid transaction exchange adjustment amount.'
INVALID_DEPOSIT_AMOUNT_CAD = 'Invalid transaction deposit amount CAD.'
INVALID_TRANSACTION_DATE = 'Invalid transaction date.'
INVALID_DEPOSIT_DATETIME = 'Invalid transaction deposit date time'
BCROS_ACCOUNT_NUMBER_REQUIRED = 'BCROS Account number is missing from the transaction description.'
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Copyright © 2023 Province of British Columbia
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""This manages the EFT Header record."""
from datetime import datetime

from reconciliations.eft.eft_base import EFTBase
from reconciliations.eft.eft_enums import EFTConstants
from reconciliations.eft.eft_errors import EFTError
from reconciliations.eft.eft_parse_error import EFTParseError


class EFTHeader(EFTBase):
"""Defines the structure of the header of a received EFT file."""

creation_datetime: datetime
starting_deposit_date: datetime
ending_deposit_date: datetime

def __init__(self, content: str, index: int):
"""Return an EFT Header."""
super().__init__(content, index)
self._process()

def _process(self):
"""Process and validate EFT Header string."""
# Confirm line length is valid, skip if it is not, but add to error array
if not self.is_valid_length():
self.add_error(EFTParseError(EFTError.INVALID_LINE_LENGTH))
return

# Confirm record type is as expected
self.record_type = self.extract_value(0, 1)
self.validate_record_type(EFTConstants.HEADER_RECORD_TYPE.value)

# Confirm valid file creation datetime
self.creation_datetime = self.parse_datetime(self.extract_value(16, 24) + self.extract_value(41, 45),
EFTError.INVALID_CREATION_DATETIME)

# Confirm valid deposit dates
self.starting_deposit_date = self.parse_date(self.extract_value(69, 77), EFTError.INVALID_DEPOSIT_START_DATE)
self.ending_deposit_date = self.parse_date(self.extract_value(89, 97), EFTError.INVALID_DEPOSIT_END_DATE)
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Copyright © 2023 Province of British Columbia
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Defines the structure of EFT Errors."""
from reconciliations.eft.eft_errors import EFTError


class EFTParseError: # pylint: disable=too-few-public-methods
"""Defines the structure of a parse error when parsing an EFT File."""

code: str
message: str
index: int

def __init__(self, eft_error: EFTError, index: int = None) -> object:
"""Return an EFT Parse Error."""
self.code = eft_error.name
self.message = eft_error.value
self.index = index
Loading

0 comments on commit 90fc03c

Please sign in to comment.