Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# This file is for unifying the coding style for different editors and IDEs.
# More information at http://EditorConfig.org
root = true

[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
max_line_length = 120
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false

[*.py]
include_trailing_comma = True
indent_size = 4
max_line_length = 120
multi_line_output = 5
not_skip = __init__.py
sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER

[*.yml,*.yaml]
indent_size = 2

[Makefile]
indent_style = tab
36 changes: 36 additions & 0 deletions .github/workflows/terraformed-claude.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# This workflow is managed by terraform-github
# Source: https://github.com/carta/terraform-github/blob/main/carta/tf/workflow_files.tf
# DO NOT EDIT THIS FILE DIRECTLY - changes will be overwritten by Terraform
#
# To modify workflow behavior, update the reusable workflow at:
# https://github.com/carta/.github/blob/main/.github/workflows/claude-code-assistant.yml

name: Claude Assistant

on:
pull_request:
types: [opened, synchronize, ready_for_review]
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
# NOTE: pull_request_review is intentionally excluded. Claude's own review
# submissions re-trigger this workflow, creating "skipped" ghost runs that
# overwrite the real check status. @claude in review comments is still
# supported via pull_request_review_comment above.
issues:
types: [opened, assigned]
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to review (optional - auto-detects from branch if not provided)'
required: false
type: number

jobs:
claude:
uses: carta/.github/.github/workflows/claude-code-assistant.yml@main
# secrets: inherit is GitHub's recommended pattern for reusable workflows
# The reusable workflow can only access secrets that exist in this repo
# See: https://docs.github.com/en/actions/using-workflows/reusing-workflows#using-inputs-and-secrets-in-a-reusable-workflow
secrets: inherit
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@ pip-log.txt
#Vim swp
*.swp
.idea
.*
.DS_Store
62 changes: 62 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,65 @@ This returns the following NACHA file:
9000001000001000000040037014587000000015000000000002213
9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999


Below is another example of what happens if the validation fails for one entry.

.. code:: python


from ach.builder import AchFile

settings = {
'immediate_dest' : '123456789', # Your bank's routing number
'immediate_org' : '123456789', # Bank assigned routing number
'immediate_dest_name' : 'YOUR BANK',
'immediate_org_name' : 'YOUR COMPANY',
'company_id' : '1234567890', #tax number
}

ach_file = AchFile('B',settings) #file Id mod

entries = [
{
'type' : '27',
'routing_number' : '********', # invalid
'account_number' : '********', # invalid
'amount' : '150.00',
'name' : 'Billy Holiday',
},
{
'type' : '22',
'routing_number' : '123232318',
'account_number' : '123123123',
'amount' : '12.13',
'name' : 'Rachel Welch',
},
]

print(ach_file.add_batch('PPD', entries, credits=True, debits=True))

This prints the following information:

::

[({'routing_number': '********', 'amount': '150.00', 'type': '27', 'account_number': '********', 'name': 'Billy Holiday'}, AchError('field needs to be numeric characters only',))]

Here is the ach file with the skipped entry.

.. code:: python

print ach_file.render_to_string()

::

101 123456780 1234567802008071448B094101YOUR BANK YOUR COMPANY
5200YOUR COMPANY 1234567890PPDPAYROLL 200808 1123456780000001
622123232318123123123 0000001213 RACHEL WELCH 0123456780000001
820000000100123232310000000000000000000012131234567890 123456780000001
9000001000001000000010012323231000000000000000000001213
9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
134 changes: 75 additions & 59 deletions ach/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@
from datetime import datetime, timedelta

from .data_types import (
Header, FileControl, BatchHeader,
BatchControl, EntryDetail, AddendaRecord
Header,
FileControl,
BatchHeader,
BatchControl,
EntryDetail,
AddendaRecord,
)


Expand All @@ -23,14 +27,16 @@ def __init__(self, file_id_mod, settings):

try:
company_name = settings.get(
'company_name',
settings['immediate_org_name'],
"company_name",
settings["immediate_org_name"],
)
self.settings['company_name'] = company_name
self.settings["company_name"] = company_name
self.header = Header(
settings['immediate_dest'],
settings['immediate_org'], file_id_mod,
settings['immediate_dest_name'], settings['immediate_org_name']
settings["immediate_dest"],
settings["immediate_org"],
file_id_mod,
settings["immediate_dest_name"],
settings["immediate_org_name"],
)
except KeyError:
raise Exception(
Expand All @@ -40,9 +46,17 @@ def __init__(self, file_id_mod, settings):

self.batches = list()

def add_batch(self, std_ent_cls_code, batch_entries=None,
credits=True, debits=False, eff_ent_date=None,
company_id=None, entry_desc=None, company_name=None):
def add_batch(
self,
std_ent_cls_code,
batch_entries=None,
credits=True,
debits=False,
eff_ent_date=None,
company_id=None,
entry_desc=None,
company_name=None,
):
"""
Use this to add batches to the file. For valid std_ent_cls_codes see:
http://en.wikipedia.org/wiki/Automated_Clearing_House#SEC_codes
Expand All @@ -60,54 +74,56 @@ def add_batch(self, std_ent_cls_code, batch_entries=None,
eff_ent_date = datetime.today() + timedelta(days=1)

if credits and debits:
serv_cls_code = '200'
serv_cls_code = "200"
elif credits:
serv_cls_code = '220'
serv_cls_code = "220"
elif debits:
serv_cls_code = '225'
serv_cls_code = "225"

batch_header = BatchHeader(
serv_cls_code=serv_cls_code,
batch_id=batch_count,
company_id=company_id or self.settings['company_id'],
company_id=company_id or self.settings["company_id"],
std_ent_cls_code=std_ent_cls_code,
entry_desc=entry_desc,
desc_date='',
eff_ent_date=eff_ent_date.strftime('%y%m%d'), # YYMMDD
orig_stat_code='1',
orig_dfi_id=self.settings['immediate_dest'][:8],
company_name=(company_name or self.settings['company_name'])[:16],
desc_date="",
eff_ent_date=eff_ent_date.strftime("%y%m%d"), # YYMMDD
orig_stat_code="1",
orig_dfi_id=self.settings["immediate_dest"][:8],
company_name=(company_name or self.settings["company_name"])[:16],
)

entries = []
entries, failed_entry_errors = [], []
entry_counter = 1

for record in batch_entries:
try:
entry = EntryDetail(
std_ent_cls_code=std_ent_cls_code,
id_number=record.get("id_number", ""),
)

entry = EntryDetail(
std_ent_cls_code=std_ent_cls_code,
id_number=record.get('id_number', ''),
)

entry.transaction_code = record.get('type')
entry.recv_dfi_id = record.get('routing_number')
entry.transaction_code = record.get("type")
entry.recv_dfi_id = record.get("routing_number")

if len(record['routing_number']) < 9:
entry.calc_check_digit()
else:
entry.check_digit = record['routing_number'][8]
if len(record["routing_number"]) < 9:
entry.calc_check_digit()
else:
entry.check_digit = record["routing_number"][8]

entry.dfi_acnt_num = record['account_number']
entry.amount = int(round(float(record['amount']) * 100))
entry.ind_name = record['name'].upper()[:22]
entry.trace_num = self.settings['immediate_dest'][:8] \
+ entry.validate_numeric_field(entry_counter, 7)
entry.dfi_acnt_num = record["account_number"]
entry.amount = int(round(float(record["amount"]) * 100))
entry.ind_name = record["name"].upper()[:22]
entry.trace_num = self.settings["immediate_dest"][:8] + entry.validate_numeric_field(entry_counter, 7)

entries.append((entry, record.get('addenda', [])))
entry_counter += 1
entries.append((entry, record.get("addenda", [])))
entry_counter += 1
except Exception as e:
failed_entry_errors.append((record, e))

self.batches.append(FileBatch(batch_header, entries))
self.set_control()
return failed_entry_errors

def set_control(self):

Expand All @@ -119,8 +135,12 @@ def set_control(self):
credit_amount = self.get_credit_amount(self.batches)

self.control = FileControl(
batch_count, block_count, entadd_count,
entry_hash, debit_amount, credit_amount
batch_count,
block_count,
entadd_count,
entry_hash,
debit_amount,
credit_amount,
)

def get_block_count(self, batches):
Expand All @@ -135,8 +155,7 @@ def get_lines(self, batches):

entadd_count = self.get_entadd_count(batches)

lines = header_count + control_count + batch_header_count \
+ batch_footer_count + entadd_count
lines = header_count + control_count + batch_header_count + batch_footer_count + entadd_count

return lines

Expand Down Expand Up @@ -174,16 +193,15 @@ def get_credit_amount(self, batches):
credit_amount = 0

for batch in batches:
credit_amount = credit_amount + \
int(batch.batch_control.credit_amount)
credit_amount = credit_amount + int(batch.batch_control.credit_amount)

return credit_amount

def get_nines(self, rows, line_ending):
nines = ''
nines = ""

for i in range(rows):
nines += '9'*94
nines += "9" * 94
if i == rows - 1:
continue
nines += line_ending
Expand All @@ -192,12 +210,12 @@ def get_nines(self, rows, line_ending):

def get_entry_desc(self, std_ent_cls_code):

if std_ent_cls_code == 'PPD':
entry_desc = 'PAYROLL'
elif std_ent_cls_code == 'CCD':
entry_desc = 'DUES'
if std_ent_cls_code == "PPD":
entry_desc = "PAYROLL"
elif std_ent_cls_code == "CCD":
entry_desc = "DUES"
else:
entry_desc = 'OTHER'
entry_desc = "OTHER"

return entry_desc

Expand Down Expand Up @@ -249,7 +267,7 @@ def __init__(self, batch_header, entries):
entadd_count += len(addenda)
self.entries.append(FileEntry(entry, addenda))

#set up batch_control
# set up batch_control

batch_control = BatchControl(self.batch_header.serv_cls_code)

Expand Down Expand Up @@ -282,8 +300,7 @@ def get_debit_amount(self, entries):
debit_amount = 0

for entry in entries:
if str(entry.entry_detail.transaction_code) in \
['27', '37', '28', '38']:
if str(entry.entry_detail.transaction_code) in ["27", "37", "28", "38"]:
debit_amount = debit_amount + int(entry.entry_detail.amount)

return debit_amount
Expand All @@ -292,8 +309,7 @@ def get_credit_amount(self, entries):
credit_amount = 0

for entry in entries:
if str(entry.entry_detail.transaction_code) in \
['22', '32', '23', '33']:
if str(entry.entry_detail.transaction_code) in ["22", "32", "23", "33"]:
credit_amount += int(entry.entry_detail.amount)

return credit_amount
Expand Down Expand Up @@ -336,9 +352,9 @@ def __init__(self, entry_detail, addenda_record=[]):
self.addenda_record.append(
AddendaRecord(
self.entry_detail.std_ent_cls_code,
pmt_rel_info=addenda.get('payment_related_info').upper(),
pmt_rel_info=addenda.get("payment_related_info").upper(),
add_seq_num=index + 1,
ent_det_seq_num=entry_detail.trace_num[-7:]
ent_det_seq_num=entry_detail.trace_num[-7:],
)
)

Expand Down
Loading