counterparty 0.1.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. counterparty/__init__.py +29 -0
  2. counterparty/extraction/__init__.py +0 -0
  3. counterparty/extraction/clean.py +124 -0
  4. counterparty/extraction/extract_payer_payee.py +160 -0
  5. counterparty/extraction/infer_counterparty.py +17 -0
  6. counterparty/key_engine/__init__.py +0 -0
  7. counterparty/key_engine/canonical_keys.json +326 -0
  8. counterparty/key_engine/key_detector.py +335 -0
  9. counterparty/key_engine/keys.py +332 -0
  10. counterparty/parsers/LAT_AM/LAT_AM_Entry.py +91 -0
  11. counterparty/parsers/LAT_AM/__init__.py +0 -0
  12. counterparty/parsers/LAT_AM/pattern1.py +169 -0
  13. counterparty/parsers/LAT_AM/pattern10.py +76 -0
  14. counterparty/parsers/LAT_AM/pattern11.py +76 -0
  15. counterparty/parsers/LAT_AM/pattern12.py +99 -0
  16. counterparty/parsers/LAT_AM/pattern2.py +102 -0
  17. counterparty/parsers/LAT_AM/pattern3.py +75 -0
  18. counterparty/parsers/LAT_AM/pattern4.py +128 -0
  19. counterparty/parsers/LAT_AM/pattern5.py +54 -0
  20. counterparty/parsers/LAT_AM/pattern6.py +141 -0
  21. counterparty/parsers/LAT_AM/pattern7.py +116 -0
  22. counterparty/parsers/LAT_AM/pattern8.py +134 -0
  23. counterparty/parsers/LAT_AM/pattern9.py +86 -0
  24. counterparty/parsers/__init__.py +0 -0
  25. counterparty/parsers/ach/__init__.py +0 -0
  26. counterparty/parsers/ach/ach_parser.py +190 -0
  27. counterparty/parsers/avidpay/__init__.py +0 -0
  28. counterparty/parsers/avidpay/avidp_check_parser.py +82 -0
  29. counterparty/parsers/avidpay/avidp_gen_parser.py +59 -0
  30. counterparty/parsers/directdebit/__init__.py +0 -0
  31. counterparty/parsers/directdebit/directdeb.py +80 -0
  32. counterparty/parsers/disbursement/__init__.py +0 -0
  33. counterparty/parsers/disbursement/disb_parser.py +72 -0
  34. counterparty/parsers/fundsTransfer/__init__.py +0 -0
  35. counterparty/parsers/fundsTransfer/fundsTrans_parser.py +80 -0
  36. counterparty/parsers/generic/__init__.py +0 -0
  37. counterparty/parsers/generic/all_parser.py +91 -0
  38. counterparty/parsers/merchref/__init__.py +0 -0
  39. counterparty/parsers/merchref/merch_ref_parser.py +47 -0
  40. counterparty/parsers/misc/__init__.py +0 -0
  41. counterparty/parsers/misc/cardp.py +61 -0
  42. counterparty/parsers/misc/invo.py +78 -0
  43. counterparty/parsers/misc/webt.py +55 -0
  44. counterparty/parsers/paypal/__init__.py +0 -0
  45. counterparty/parsers/paypal/paypal.py +118 -0
  46. counterparty/parsers/processor_eft/__init__.py +0 -0
  47. counterparty/parsers/processor_eft/peft.py +110 -0
  48. counterparty/parsers/remittance/__init__.py +0 -0
  49. counterparty/parsers/remittance/remi.py +79 -0
  50. counterparty/parsers/swift/__init__.py +0 -0
  51. counterparty/parsers/swift/swift_parser.py +97 -0
  52. counterparty/parsers/vendorpay/__init__.py +0 -0
  53. counterparty/parsers/vendorpay/vp_parser.py +54 -0
  54. counterparty/parsers/vendorpymt/__init__.py +0 -0
  55. counterparty/parsers/vendorpymt/vpymt_parser.py +132 -0
  56. counterparty/parsers/wire/__init__.py +0 -0
  57. counterparty/parsers/wire/wire_parser.py +137 -0
  58. counterparty/route.py +116 -0
  59. counterparty/routines.py +72 -0
  60. counterparty/util.py +40 -0
  61. counterparty-0.1.6.dist-info/METADATA +9 -0
  62. counterparty-0.1.6.dist-info/RECORD +64 -0
  63. counterparty-0.1.6.dist-info/WHEEL +5 -0
  64. counterparty-0.1.6.dist-info/top_level.txt +1 -0
@@ -0,0 +1,128 @@
1
+ import re
2
+ import json
3
+
4
+
5
+ def normalize_narrative(line: str) -> str:
6
+ if not line:
7
+ return ""
8
+ line = line.upper()
9
+ line = re.sub(r"^[,|\\]+", "", line)
10
+ line = re.sub(r"[\\|,]+$", "", line)
11
+ line = re.sub(r"\s+", " ", line)
12
+ return line.strip()
13
+
14
+
15
+ # ========================
16
+ # IDENTIFICATION PATTERNS
17
+ # ========================
18
+ pattern4_qrs = re.compile(
19
+ r"^PIX QRS",
20
+ re.IGNORECASE
21
+ )
22
+
23
+ pattern4_qrcode = re.compile(
24
+ r"PIX.*QR CODE|PIX-RECEB|PIX-ENVIADO|PIX-RECEBIMENTO|PIX DEVOLVID",
25
+ re.IGNORECASE
26
+ )
27
+
28
+
29
+ def is_pattern4(line: str) -> bool:
30
+ if not line:
31
+ return False
32
+ txt = normalize_narrative(line)
33
+ return bool(pattern4_qrs.search(txt) or pattern4_qrcode.search(txt))
34
+
35
+
36
+ # ========================
37
+ # QRS PAYLOAD PARSER
38
+ # ========================
39
+ def parse_qrs_payload(txt: str):
40
+ raw = txt.replace("PIX QRS", "").strip()
41
+ tokens = raw.split()
42
+
43
+ # 1️⃣ counterparty
44
+ name = []
45
+ idx = 0
46
+ for idx, t in enumerate(tokens):
47
+ if re.search(r"[0-9/]", t):
48
+ break
49
+ name.append(t)
50
+ counterparty = " ".join(name).strip() if name else None
51
+
52
+ # 2️⃣ movement date
53
+ movement_date = None
54
+ for t in tokens:
55
+ m = re.match(r"[A-Z]*([0-9]{2})/([0-9]{2})", t)
56
+ if m:
57
+ dd, mm = m.groups()
58
+ movement_date = f"2025-{mm}-{dd}"
59
+ break
60
+
61
+ # 3️⃣ capture the code + the aux
62
+ remainder = tokens[len(name):]
63
+
64
+ codes = [x for x in remainder if not re.search(r"[A-Z]*[0-9]{2}/[0-9]{2}", x)]
65
+
66
+ pix_reference_code = codes[0] if len(codes) >= 1 else None
67
+ pix_aux_code = codes[1] if len(codes) >= 2 else None
68
+
69
+ return counterparty, movement_date, pix_reference_code, pix_aux_code
70
+
71
+
72
+ # ========================
73
+ # MAIN PARSER
74
+ # ========================
75
+ def parse_pattern4(line: str) -> dict | None:
76
+ if not line:
77
+ return None
78
+
79
+ txt = normalize_narrative(line)
80
+
81
+ # ----------------------
82
+ # QRS static pattern
83
+ # ----------------------
84
+ if pattern4_qrs.search(txt):
85
+ counterparty, movement_date, ref, aux = parse_qrs_payload(txt)
86
+ return {
87
+ "ENTITY": counterparty,
88
+ "payment_system": "PIX",
89
+ "direction": None,
90
+ "payment_method": "PIX_QR_STATIC",
91
+ "transaction_type": "INSTANT_PAYMENT",
92
+ "movement_date": movement_date,
93
+ "pix_reference_code": ref,
94
+ "pix_aux_code": aux
95
+ }
96
+
97
+ # ----------------------
98
+ # QR directional or devolvido
99
+ # ----------------------
100
+ if pattern4_qrcode.search(txt):
101
+
102
+ if "DEVOLVID" in txt:
103
+ direction = "REVERSAL"
104
+ elif "RECEB" in txt:
105
+ direction = "INCOMING"
106
+ elif "ENVIADO" in txt:
107
+ direction = "OUTGOING"
108
+ else:
109
+ direction = None
110
+
111
+ payment_method = "QR_CODE" if "QR CODE" in txt else None
112
+
113
+ return {
114
+ "ENTITY": None,
115
+ "payment_system": "PIX",
116
+ "direction": direction,
117
+ "payment_method": payment_method,
118
+ "transaction_type": "INSTANT_PAYMENT",
119
+ "movement_date": None,
120
+ "pix_reference_code": None,
121
+ "pix_aux_code": None
122
+ }
123
+
124
+ return None
125
+
126
+
127
+ if __name__ == "__main__":
128
+ pass
@@ -0,0 +1,54 @@
1
+ import re
2
+ import json
3
+
4
+
5
+ def normalize_narrative(line: str) -> str:
6
+ if not line:
7
+ return ""
8
+ line = line.upper()
9
+ line = re.sub(r"^[,|\\]+", "", line)
10
+ line = re.sub(r"[\\|,]+$", "", line)
11
+ line = re.sub(r"\s+", " ", line)
12
+ return line.strip()
13
+
14
+
15
+ pattern5 = re.compile(
16
+ r"^DB.*RV",
17
+ re.IGNORECASE
18
+ )
19
+
20
+
21
+ def is_pattern5(line: str) -> bool:
22
+ if not line:
23
+ return False
24
+ return bool(pattern5.search(normalize_narrative(line)))
25
+
26
+
27
+ def parse_pattern5(line: str) -> dict | None:
28
+ if not line:
29
+ return None
30
+
31
+ txt = normalize_narrative(line)
32
+
33
+ if not is_pattern5(txt):
34
+ return None
35
+
36
+ # capture last numeric token
37
+ ref = None
38
+ nums = re.findall(r"[0-9]+", txt)
39
+ if nums:
40
+ ref = nums[-1]
41
+
42
+ return {
43
+ "transaction_type": "INTERNAL_REVERSAL",
44
+ "entry_side": "DEBIT",
45
+ "is_reversal": True,
46
+ "ledger_bucket": "CF",
47
+ "posting_scope": "INTERNAL",
48
+ "reference_id": ref,
49
+ "ENTITY": None
50
+ }
51
+
52
+
53
+ if __name__ == "__main__":
54
+ pass
@@ -0,0 +1,141 @@
1
+ import re
2
+ import json
3
+
4
+
5
+ def normalize_narrative(line: str) -> str:
6
+ if not line:
7
+ return ""
8
+ line = line.upper()
9
+ line = re.sub(r"^[,|\\]+", "", line)
10
+ line = re.sub(r"[\\|,]+$", "", line)
11
+ line = re.sub(r"\s+", " ", line)
12
+ return line.strip()
13
+
14
+
15
+ pattern6 = re.compile(
16
+ r"^066|^MOV POS",
17
+ re.IGNORECASE
18
+ )
19
+
20
+
21
+ def is_pattern6(line: str) -> bool:
22
+ if not line:
23
+ return False
24
+ return bool(pattern6.search(normalize_narrative(line)))
25
+
26
+
27
+ def parse_mov_pos(txt: str) -> dict | None:
28
+ """
29
+ Handles: MOV POS IVALEY6380 4747130055
30
+ → merchant: IVALEY
31
+ → merchant_id: 4747130055
32
+ """
33
+
34
+ if not txt.startswith("MOV POS"):
35
+ return None
36
+
37
+ parts = txt.split()
38
+
39
+ # must have at least: MOV POS <merchant+num> <merchant_id>
40
+ if len(parts) < 4:
41
+ return None
42
+
43
+ merchant_token = parts[2]
44
+ merchant_id = None
45
+
46
+ # last token must be merchant id (large numeric)
47
+ tail = parts[-1]
48
+ if re.match(r"^[0-9]{6,}$", tail):
49
+ merchant_id = tail
50
+
51
+ # strip trailing digits from merchant_token
52
+ m = re.match(r"([A-Z]+)", merchant_token)
53
+ merchant = m.group(1) if m else merchant_token
54
+
55
+ return {
56
+ "transaction_type": "CARD_PAYMENT",
57
+ "direction": "OUTGOING",
58
+ "channel": "POS",
59
+ "merchant": merchant,
60
+ "merchant_id": merchant_id,
61
+ "card_type": "DEBIT",
62
+ "card_network": None,
63
+ "is_ecommerce": False,
64
+ "is_domestic": True
65
+ }
66
+
67
+
68
+ def parse_pattern6(line: str) -> dict | None:
69
+ if not line:
70
+ return None
71
+
72
+ txt = normalize_narrative(line)
73
+
74
+ if not is_pattern6(txt):
75
+ return None
76
+
77
+ # MOV POS mode (new feature)
78
+ if txt.startswith("MOV POS"):
79
+ return parse_mov_pos(txt)
80
+
81
+ # ===== existing 066 mode below =====
82
+ bank_txn_code = "066"
83
+ direction = "OUTGOING"
84
+
85
+ # POS indicator (with or without dots)
86
+ pos_match = bool(re.search(r"P\.?O\.?S", txt))
87
+
88
+ # SERVICE PAYMENT POS
89
+ if "PAGO SERVICIO" in txt:
90
+ return {
91
+ "transaction_type": "SERVICE_PAYMENT",
92
+ "direction": direction,
93
+ "channel": "POS",
94
+ "payment_instrument": "CARD",
95
+ "bank_txn_code": bank_txn_code
96
+ }
97
+
98
+ # E-COMMERCE
99
+ if "E-COMMERCE" in txt:
100
+ return {
101
+ "transaction_type": "CARD_PAYMENT",
102
+ "direction": direction,
103
+ "card_type": "DEBIT",
104
+ "channel": "ECOMMERCE",
105
+ "card_network": None,
106
+ "is_ecommerce": True,
107
+ "is_domestic": True,
108
+ "bank_txn_code": bank_txn_code
109
+ }
110
+
111
+ # POS + BANCARD
112
+ if pos_match and "BANCARD" in txt:
113
+ return {
114
+ "transaction_type": "CARD_PAYMENT",
115
+ "direction": direction,
116
+ "card_type": "DEBIT",
117
+ "channel": "POS",
118
+ "card_network": "BANCARD",
119
+ "is_ecommerce": False,
120
+ "is_domestic": True,
121
+ "bank_txn_code": bank_txn_code
122
+ }
123
+
124
+ # generic POS
125
+ if pos_match:
126
+ return {
127
+ "transaction_type": "CARD_PAYMENT",
128
+ "direction": direction,
129
+ "card_type": "DEBIT",
130
+ "channel": "POS",
131
+ "card_network": None,
132
+ "is_ecommerce": False,
133
+ "is_domestic": True,
134
+ "bank_txn_code": bank_txn_code
135
+ }
136
+
137
+ return None
138
+
139
+
140
+ if __name__ == "__main__":
141
+ pass
@@ -0,0 +1,116 @@
1
+ import re
2
+ import json
3
+
4
+
5
+ def normalize_narrative(line: str) -> str:
6
+ if not line:
7
+ return ""
8
+ line = line.upper()
9
+ line = re.sub(r"[\\|,]+", " ", line)
10
+ line = re.sub(r"\s+", " ", line)
11
+ return line.strip()
12
+
13
+
14
+ pattern7 = re.compile(
15
+ r"TRASPASO",
16
+ re.IGNORECASE
17
+ )
18
+
19
+
20
+ def is_pattern7(line: str) -> bool:
21
+ if not line:
22
+ return False
23
+ txt = normalize_narrative(line)
24
+ return "TRASPASO" in txt and ("A CTA" in txt or "A CUENTA" in txt)
25
+
26
+
27
+ def extract_reference_account(txt: str) -> str | None:
28
+ m = re.search(r"A CTA\s*:?\s*([0-9]{8,})", txt)
29
+ return m.group(1) if m else None
30
+
31
+
32
+ def extract_tax_amount(txt: str) -> float | None:
33
+ m = re.search(r"IVA\s+([0-9]+\.[0-9]+)", txt)
34
+ if not m:
35
+ return None
36
+ try:
37
+ return float(m.group(1))
38
+ except:
39
+ return None
40
+
41
+
42
+ def extract_beneficiary_account(txt: str) -> str | None:
43
+ m = re.search(r"A CUENTA\s+([0-9]{8,})", txt)
44
+ return m.group(1) if m else None
45
+
46
+
47
+ def extract_rfc(txt: str) -> str | None:
48
+ m = re.search(r"RFC\.?([A-Z0-9]+)", txt)
49
+ if m:
50
+ return m.group(1)
51
+ m = re.search(r"R\.F\.C\.?([A-Z0-9]+)", txt)
52
+ if m:
53
+ return m.group(1)
54
+ return None
55
+
56
+
57
+ def extract_beneficiary_name(txt: str, acct: str | None) -> str | None:
58
+ if not acct:
59
+ return None
60
+
61
+ idx = txt.find(acct)
62
+ if idx == -1:
63
+ return None
64
+ tail = txt[idx + len(acct):].strip()
65
+
66
+ parts = []
67
+ for t in tail.split():
68
+ if t.startswith(("RFC", "R.F.C", "IVA")):
69
+ break
70
+ if re.match(r"[0-9]{4,}", t):
71
+ break
72
+ parts.append(t)
73
+
74
+ if not parts:
75
+ return None
76
+
77
+ return " ".join(parts).strip()
78
+
79
+
80
+ def infer_direction(txt: str) -> str:
81
+ incoming_markers = ["CR", "ABONO", "ACRED", "RECIBIDO", "INGRESO"]
82
+ for mark in incoming_markers:
83
+ if mark in txt:
84
+ return "INCOMING"
85
+ return "OUTGOING"
86
+
87
+
88
+ def parse_pattern7(line: str) -> dict | None:
89
+ if not line:
90
+ return None
91
+
92
+ txt = normalize_narrative(line)
93
+
94
+ if not is_pattern7(txt):
95
+ return None
96
+
97
+ direction = infer_direction(txt)
98
+ reference_acct = extract_reference_account(txt)
99
+ tax_amount = extract_tax_amount(txt)
100
+ beneficiary_acct = extract_beneficiary_account(txt)
101
+ rfc = extract_rfc(txt)
102
+ name = extract_beneficiary_name(txt, beneficiary_acct)
103
+
104
+ return {
105
+ "transaction_type": "ACCOUNT_TRANSFER",
106
+ "direction": direction,
107
+ "beneficiary_name": name,
108
+ "beneficiary_account": beneficiary_acct,
109
+ "tax_amount": tax_amount,
110
+ "reference_account": reference_acct,
111
+ "rfc": rfc
112
+ }
113
+
114
+
115
+ if __name__ == "__main__":
116
+ pass
@@ -0,0 +1,134 @@
1
+ import re
2
+ import json
3
+
4
+
5
+ def normalize_narrative(line: str) -> str:
6
+ if not line:
7
+ return ""
8
+ line = line.upper()
9
+ line = re.sub(r"[\\|,]+", " ", line)
10
+ line = re.sub(r"\s+", " ", line)
11
+ return line.strip()
12
+
13
+
14
+ pattern8 = re.compile(
15
+ r"(CONTRACARGO|CHARGEBACK)",
16
+ re.IGNORECASE
17
+ )
18
+
19
+
20
+ def is_pattern8(line: str) -> bool:
21
+ if not line:
22
+ return False
23
+ txt = normalize_narrative(line)
24
+ return bool(pattern8.search(txt))
25
+
26
+
27
+ def extract_merchant(txt: str) -> str | None:
28
+ """
29
+ CC D LOCALREST DIDI 08934620 ...
30
+ → merchant = LOCALREST DIDI
31
+ """
32
+ m = re.search(r"CC\s+D\s+(.+?)\s+[0-9]{4,}", txt)
33
+ if not m:
34
+ return None
35
+
36
+ raw = m.group(1).strip()
37
+
38
+ # remove known filler tokens if present
39
+ banned = {"AFIL.", "CONTRACARGO", "APLICADO"}
40
+ parts = [w for w in raw.split() if w not in banned]
41
+
42
+ merchant = " ".join(parts).strip()
43
+ return merchant if merchant else None
44
+
45
+
46
+ def extract_last4(txt: str) -> str | None:
47
+ # TARJ.NO.42686031
48
+ m = re.search(r"TARJ\.NO\.([0-9]{8,})", txt)
49
+ if not m:
50
+ return None
51
+ digits = m.group(1)
52
+ return digits[-4:]
53
+
54
+
55
+ def extract_date(txt: str) -> str | None:
56
+ # F.TRANS.02-10-2025
57
+ m = re.search(r"F\.TRANS\.([0-9]{2})-([0-9]{2})-([0-9]{4})", txt)
58
+ if not m:
59
+ return None
60
+ dd, mm, yyyy = m.groups()
61
+ return f"{yyyy}-{mm}-{dd}"
62
+
63
+
64
+ def extract_auth(txt: str) -> str | None:
65
+ # NO.AUT.379320
66
+ m = re.search(r"NO\.AUT\.([0-9]+)", txt)
67
+ return m.group(1) if m else None
68
+
69
+
70
+ def extract_reason_code(txt: str) -> str | None:
71
+ # RAZON 0068
72
+ m = re.search(r"RAZON\s+([0-9]{4})", txt)
73
+ return m.group(1) if m else None
74
+
75
+
76
+ def extract_reason(txt: str) -> str | None:
77
+ """
78
+ Capture all text after reason code until TARJREFERENCIA
79
+ Example:
80
+ RAZON 0068 VENTA NO RECONOCIDA POR TARJETAHABIENTE TARJREFERENCIA <num>
81
+ """
82
+ m = re.search(
83
+ r"RAZON\s+[0-9]{4}\s+(.+?)\s+TARJREFERENCIA",
84
+ txt
85
+ )
86
+ if m:
87
+ return m.group(1).strip()
88
+
89
+ # fallback: until end
90
+ m = re.search(r"RAZON\s+[0-9]{4}\s+(.+)$", txt)
91
+ if m:
92
+ return m.group(1).strip()
93
+
94
+ return None
95
+
96
+
97
+ def extract_reference(txt: str) -> str | None:
98
+ # TARJREFERENCIA 74518995275208389346204
99
+ m = re.search(r"TARJREFERENCIA\s+([0-9]+)", txt)
100
+ return m.group(1) if m else None
101
+
102
+
103
+ def parse_pattern8(line: str) -> dict | None:
104
+ if not line:
105
+ return None
106
+
107
+ txt = normalize_narrative(line)
108
+
109
+ if not is_pattern8(txt):
110
+ return None
111
+
112
+ merchant = extract_merchant(txt)
113
+ last4 = extract_last4(txt)
114
+ date = extract_date(txt)
115
+ auth = extract_auth(txt)
116
+ reason_code = extract_reason_code(txt)
117
+ reason = extract_reason(txt)
118
+ ref = extract_reference(txt)
119
+
120
+ return {
121
+ "transaction_type": "CARD_CHARGEBACK",
122
+ "direction": "INCOMING",
123
+ "merchant": merchant,
124
+ "card_last_digits": last4,
125
+ "authorization_code": auth,
126
+ "transaction_date": date,
127
+ "chargeback_reason_code": reason_code,
128
+ "chargeback_reason": reason,
129
+ "network_reference": ref
130
+ }
131
+
132
+
133
+ if __name__ == "__main__":
134
+ pass
@@ -0,0 +1,86 @@
1
+ import re
2
+ import json
3
+
4
+
5
+ def normalize_narrative(line: str) -> str:
6
+ if not line:
7
+ return ""
8
+ line = line.upper()
9
+ line = re.sub(r"[\\|,]+", " ", line)
10
+ line = re.sub(r"\s+", " ", line)
11
+ return line.strip()
12
+
13
+
14
+ pattern9 = re.compile(
15
+ r"ACH\s+CREDIT.*CITIDIRECT",
16
+ re.IGNORECASE
17
+ )
18
+
19
+
20
+ def is_pattern9(line: str) -> bool:
21
+ if not line:
22
+ return False
23
+ txt = normalize_narrative(line)
24
+ return bool(pattern9.search(txt))
25
+
26
+
27
+ def extract_our_ref(txt: str) -> str | None:
28
+ m = re.search(r"OUR REF\s*#\s*([0-9]+)", txt)
29
+ return m.group(1) if m else None
30
+
31
+
32
+ def extract_aba(txt: str) -> str | None:
33
+ m = re.search(r"RECEIVING BANK\s*#\s*([0-9]+)", txt)
34
+ return m.group(1) if m else None
35
+
36
+
37
+ def extract_receiver_account(txt: str) -> str | None:
38
+ m = re.search(r"RECEIVER\s*A/C\s*#\s*([0-9]+)", txt)
39
+ return m.group(1) if m else None
40
+
41
+
42
+ def extract_receiver_name(txt: str) -> str | None:
43
+ # RECEIVER; GLAMOUR GOODS CORP.
44
+ m = re.search(r"RECEIVER;\s+(.+?)\s+(ADDENDA|RECEIVING|OUR REF|CREDIT|PT/|$)", txt)
45
+ if m:
46
+ return m.group(1).strip()
47
+ return None
48
+
49
+
50
+ def extract_addenda(txt: str) -> str | None:
51
+ # ADDENDA INFORMATION AMAZON/BTN/535428119865
52
+ m = re.search(r"ADDENDA INFORMATION\s+(.+)$", txt)
53
+ if m:
54
+ return m.group(1).strip()
55
+ return None
56
+
57
+
58
+ def parse_pattern9(line: str) -> dict | None:
59
+ if not line:
60
+ return None
61
+
62
+ txt = normalize_narrative(line)
63
+
64
+ if not is_pattern9(txt):
65
+ return None
66
+
67
+ our_ref = extract_our_ref(txt)
68
+ aba = extract_aba(txt)
69
+ acct = extract_receiver_account(txt)
70
+ name = extract_receiver_name(txt)
71
+ addenda = extract_addenda(txt)
72
+
73
+ return {
74
+ "transaction_type": "ACH_CREDIT",
75
+ "direction": "OUTGOING",
76
+ "originating_platform": "CITIDIRECT",
77
+ "receiving_bank_aba": aba,
78
+ "receiver_account": acct,
79
+ "receiver_name": name,
80
+ "addenda_information": addenda,
81
+ "our_reference": our_ref
82
+ }
83
+
84
+
85
+ if __name__ == "__main__":
86
+ pass
File without changes
File without changes