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.
- counterparty/__init__.py +29 -0
- counterparty/extraction/__init__.py +0 -0
- counterparty/extraction/clean.py +124 -0
- counterparty/extraction/extract_payer_payee.py +160 -0
- counterparty/extraction/infer_counterparty.py +17 -0
- counterparty/key_engine/__init__.py +0 -0
- counterparty/key_engine/canonical_keys.json +326 -0
- counterparty/key_engine/key_detector.py +335 -0
- counterparty/key_engine/keys.py +332 -0
- counterparty/parsers/LAT_AM/LAT_AM_Entry.py +91 -0
- counterparty/parsers/LAT_AM/__init__.py +0 -0
- counterparty/parsers/LAT_AM/pattern1.py +169 -0
- counterparty/parsers/LAT_AM/pattern10.py +76 -0
- counterparty/parsers/LAT_AM/pattern11.py +76 -0
- counterparty/parsers/LAT_AM/pattern12.py +99 -0
- counterparty/parsers/LAT_AM/pattern2.py +102 -0
- counterparty/parsers/LAT_AM/pattern3.py +75 -0
- counterparty/parsers/LAT_AM/pattern4.py +128 -0
- counterparty/parsers/LAT_AM/pattern5.py +54 -0
- counterparty/parsers/LAT_AM/pattern6.py +141 -0
- counterparty/parsers/LAT_AM/pattern7.py +116 -0
- counterparty/parsers/LAT_AM/pattern8.py +134 -0
- counterparty/parsers/LAT_AM/pattern9.py +86 -0
- counterparty/parsers/__init__.py +0 -0
- counterparty/parsers/ach/__init__.py +0 -0
- counterparty/parsers/ach/ach_parser.py +190 -0
- counterparty/parsers/avidpay/__init__.py +0 -0
- counterparty/parsers/avidpay/avidp_check_parser.py +82 -0
- counterparty/parsers/avidpay/avidp_gen_parser.py +59 -0
- counterparty/parsers/directdebit/__init__.py +0 -0
- counterparty/parsers/directdebit/directdeb.py +80 -0
- counterparty/parsers/disbursement/__init__.py +0 -0
- counterparty/parsers/disbursement/disb_parser.py +72 -0
- counterparty/parsers/fundsTransfer/__init__.py +0 -0
- counterparty/parsers/fundsTransfer/fundsTrans_parser.py +80 -0
- counterparty/parsers/generic/__init__.py +0 -0
- counterparty/parsers/generic/all_parser.py +91 -0
- counterparty/parsers/merchref/__init__.py +0 -0
- counterparty/parsers/merchref/merch_ref_parser.py +47 -0
- counterparty/parsers/misc/__init__.py +0 -0
- counterparty/parsers/misc/cardp.py +61 -0
- counterparty/parsers/misc/invo.py +78 -0
- counterparty/parsers/misc/webt.py +55 -0
- counterparty/parsers/paypal/__init__.py +0 -0
- counterparty/parsers/paypal/paypal.py +118 -0
- counterparty/parsers/processor_eft/__init__.py +0 -0
- counterparty/parsers/processor_eft/peft.py +110 -0
- counterparty/parsers/remittance/__init__.py +0 -0
- counterparty/parsers/remittance/remi.py +79 -0
- counterparty/parsers/swift/__init__.py +0 -0
- counterparty/parsers/swift/swift_parser.py +97 -0
- counterparty/parsers/vendorpay/__init__.py +0 -0
- counterparty/parsers/vendorpay/vp_parser.py +54 -0
- counterparty/parsers/vendorpymt/__init__.py +0 -0
- counterparty/parsers/vendorpymt/vpymt_parser.py +132 -0
- counterparty/parsers/wire/__init__.py +0 -0
- counterparty/parsers/wire/wire_parser.py +137 -0
- counterparty/route.py +116 -0
- counterparty/routines.py +72 -0
- counterparty/util.py +40 -0
- counterparty-0.1.6.dist-info/METADATA +9 -0
- counterparty-0.1.6.dist-info/RECORD +64 -0
- counterparty-0.1.6.dist-info/WHEEL +5 -0
- 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
|