Completed
Push — master ( 7042a8...62234f )
by
unknown
01:12
created

TrrFile   B

Complexity

Total Complexity 38

Size/Duplication

Total Lines 202
Duplicated Lines 0 %

Importance

Changes 6
Bugs 0 Features 0
Metric Value
c 6
b 0
f 0
dl 0
loc 202
rs 8.3999
wmc 38

5 Methods

Rating   Name   Duplication   Size   Complexity  
A parse() 0 3 1
A handle() 0 4 1
A __init__() 0 4 1
F parse_line() 0 120 33
A send() 0 5 2
1
'''Parser for Transaction Detail Report files
2
3
See https://www.paypalobjects.com/webstatic/en_US/developer/docs/pdf/PP_LRD_Gen_TransactionDetailReport.pdf
4
'''
5
6
import re
7
8
from process.logging import Logger as log
9
import process.globals
10
import queue.redis_wrap
11
import ppreport
12
13
import civicrm.civicrm
14
import paypal_api
15
16
17
class TrrFile(object):
18
    VERSION = [4, 8]
19
    redis = None
20
    # FIXME: these are version 8 headers, we would fail on multi-part v4 files...
21
    column_headers = [
22
        "Column Type",
23
        "Transaction ID",
24
        "Invoice ID",
25
        "PayPal Reference ID",
26
        "PayPal Reference ID Type",
27
        "Transaction Event Code",
28
        "Transaction Initiation Date",
29
        "Transaction Completion Date",
30
        "Transaction  Debit or Credit",
31
        "Gross Transaction Amount",
32
        "Gross Transaction Currency",
33
        "Fee Debit or Credit",
34
        "Fee Amount",
35
        "Fee Currency",
36
        "Transactional Status",
37
        "Insurance Amount",
38
        "Sales Tax Amount",
39
        "Shipping Amount",
40
        "Transaction Subject",
41
        "Transaction Note",
42
        "Payer's Account ID",
43
        "Payer Address Status",
44
        "Item Name",
45
        "Item ID",
46
        "Option 1 Name",
47
        "Option 1 Value",
48
        "Option 2 Name",
49
        "Option 2 Value",
50
        "Auction Site",
51
        "Auction Buyer ID",
52
        "Auction Closing Date",
53
        "Shipping Address Line1",
54
        "Shipping Address Line2",
55
        "Shipping Address City",
56
        "Shipping Address State",
57
        "Shipping Address Zip",
58
        "Shipping Address Country",
59
        "Shipping Method",
60
        "Custom Field",
61
        "Billing Address Line1",
62
        "Billing Address Line2",
63
        "Billing Address City",
64
        "Billing Address State",
65
        "Billing Address Zip",
66
        "Billing Address Country",
67
        "Consumer ID",
68
        "First Name",
69
        "Last Name",
70
        "Consumer Business Name",
71
        "Card Type",
72
        "Payment Source",
73
        "Shipping Name",
74
        "Authorization Review Status",
75
        "Protection Eligibility",
76
        "Payment Tracking ID",
77
    ]
78
79
    @staticmethod
80
    def handle(path):
81
        obj = TrrFile(path)
82
        obj.parse()
83
84
    def __init__(self, path):
85
        self.path = path
86
        self.config = process.globals.get_config()
87
        self.crm = civicrm.civicrm.Civicrm(self.config.civicrm_db)
88
89
    def parse(self):
90
        # FIXME: encapsulation issues
91
        ppreport.read(self.path, self.VERSION, self.parse_line, self.column_headers)
92
93
    def parse_line(self, row):
94
        if row['Billing Address Line1']:
95
            addr_prefix = 'Billing Address '
96
        else:
97
            addr_prefix = 'Shipping Address '
98
99
        # FIXME: Accept an empty or malformed date, or keep the exception?
100
        out = {
101
            'gateway_txn_id': row['Transaction ID'],
102
            'date': ppreport.parse_date(row['Transaction Initiation Date']),
103
            'settled_date': ppreport.parse_date(row['Transaction Completion Date']),
104
            'gross': float(row['Gross Transaction Amount']) / 100.0,
105
            'currency': row['Gross Transaction Currency'],
106
            'gateway_status': row['Transactional Status'],
107
            'note': row['Transaction Note'],
108
            'email': row['Payer\'s Account ID'],
109
110
            'street_address': row[addr_prefix + 'Line1'],
111
            'supplemental_address_1': row[addr_prefix + 'Line2'],
112
            'city': row[addr_prefix + 'City'],
113
            'state_province': row[addr_prefix + 'State'],
114
            'postal_code': row[addr_prefix + 'Zip'],
115
            'country': row[addr_prefix + 'Country'],
116
        }
117
118
        # FIXME: This is weasly, see that we're also sending the raw payment
119
        # source value as payment_method.
120
        if row['Payment Source'] == 'Express Checkout':
121
            out['gateway'] = 'paypal_ec'
122
        else:
123
            out['gateway'] = 'paypal'
124
125
        if row['Fee Amount']:
126
            out['fee'] = float(row['Fee Amount']) / 100.0
127
128
            if row['Fee Currency'] and row['Gross Transaction Currency'] != row['Fee Currency']:
129
                raise RuntimeError("Failed to import because multiple currencies for one transaction is not handled.")
130
131
        if 'First Name' in row:
132
            out['first_name'] = row['First Name']
133
134
        if 'Last Name' in row:
135
            out['last_name'] = row['Last Name']
136
137
        if 'Payment Source' in row:
138
            out['payment_method'] = row['Payment Source']
139
140
        if 'Card Type' in row:
141
            out['payment_submethod'] = row['Card Type']
142
143
        if row['PayPal Reference ID Type'] == 'SUB':
144
            out['subscr_id'] = row['PayPal Reference ID']
145
146
        # Look in all the places we might have stuck a ct_id
147
        if re.search('^[0-9]+$', row['Transaction Subject']):
148
            out['contribution_tracking_id'] = row['Transaction Subject']
149
        elif row['Custom Field']:
150
            out['contribution_tracking_id'] = row['Custom Field']
151
        elif row['Invoice ID']:
152
            # Here it can be the ct_id.attempt format
153
            out['contribution_tracking_id'] = row['Invoice ID'].split('.')[0]
154
155
        if out['contribution_tracking_id']:
156
            out['order_id'] = out['contribution_tracking_id']
157
158
        event_type = row['Transaction Event Code'][0:3]
159
160
        queue = None
161
        if event_type in ('T00', 'T03', 'T05', 'T07', 'T22'):
162
            if row['Transaction Event Code'] == 'T0002':
163
                queue = 'recurring'
164
                out['txn_type'] = 'subscr_payment'
165
                if 'subscr_id' not in out or not out['subscr_id']:
166
                    raise Exception('Missing field subscr_id')
167
            elif row['Transaction  Debit or Credit'] == 'DR':
168
                # sic: double-space is coming from the upstream
169
                log.info("-Debit\t{id}\t{date}\tPayment to".format(id=out['gateway_txn_id'], date=out['date']))
170
                # This payment is from us!  Do not send to the CRM.
171
                return
172
            else:
173
                queue = 'donations'
174
        elif event_type in ('T11', 'T12'):
175
            out['gateway_refund_id'] = out['gateway_txn_id']
176
            out['gross_currency'] = out['currency']
177
178
            if row['PayPal Reference ID Type'] == 'TXN':
179
                out['gateway_parent_id'] = row['PayPal Reference ID']
180
181
            if row['Transaction Event Code'] == 'T1106':
182
                out['type'] = 'reversal'
183
            elif row['Transaction Event Code'] == 'T1107':
184
                out['type'] = 'refund'
185
            elif row['Transaction Event Code'] == 'T1201':
186
                out['type'] = 'chargeback'
187
            else:
188
                log.info("-Unknown\t{id}\t{date}\t(Refundish type {type})".format(id=out['gateway_txn_id'], date=out['date'], type=row['Transaction Event Code']))
189
                return
190
191
            queue = 'refund'
192
193
        if not queue:
194
            log.info("-Unknown\t{id}\t{date}\t(Type {type})".format(id=out['gateway_txn_id'], date=out['date'], type=event_type))
195
            return
196
197
        if queue == 'donations' and self.crm.transaction_exists(gateway_txn_id=out['gateway_txn_id'], gateway=out['gateway']):
198
            log.info("-Duplicate\t{id}\t{date}\t{type}".format(id=out['gateway_txn_id'], date=row['Transaction Initiation Date'], type=queue))
199
            return
200
201
        if queue == 'refund' and self.crm.transaction_refunded(gateway_txn_id=out['gateway_parent_id'], gateway=out['gateway']):
202
            log.info("-Duplicate\t{id}\t{date}\t{type}".format(id=out['gateway_txn_id'], date=row['Transaction Initiation Date'], type=queue))
203
            return
204
205
        if 'last_name' not in out and queue != 'refund':
206
            out['first_name'], out['last_name'] = paypal_api.PaypalApiClassic().fetch_donor_name(out['gateway_txn_id'])
207
208
        if self.config.no_thankyou:
209
            out['thankyou_date'] = 0
210
211
        log.info("+Sending\t{id}\t{date}\t{type}".format(id=out['gateway_txn_id'], date=row['Transaction Initiation Date'], type=queue))
212
        self.send(queue, out)
213
214
    def send(self, queue_name, msg):
215
        if not self.redis:
216
            self.redis = queue.redis_wrap.Redis()
217
218
        self.redis.send(queue_name, msg)
219