glaring 0.2__tar.gz → 0.3__tar.gz

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.
@@ -0,0 +1,3 @@
1
+ include README.md glaring/version.txt dev.txt glaring/svs/wsdl/**/*
2
+ recursive-exclude * test
3
+ prune test*
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: glaring
3
- Version: 0.2
3
+ Version: 0.3
4
4
  Summary: glaring
5
5
  Home-page: https://github.com/angry-penguins/glaring
6
6
  Author: dev
File without changes
@@ -0,0 +1,431 @@
1
+ from base64 import b64encode, b64decode
2
+ from datetime import datetime
3
+ from hashlib import sha1
4
+ import json
5
+ import os
6
+ from threading import Lock
7
+ from cryptography.hazmat.primitives import padding as sym_padding
8
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
9
+ from deceit.async_api_client import ApiClient
10
+ from raft.tasks import task
11
+ from httpx import Client
12
+ from pytz import utc
13
+ from .rest import CsmCrypto, Stan
14
+
15
+
16
+ class AsyncRestApi(ApiClient):
17
+ def __init__(self, base_url, auth_id, root_key, username, password, merchant_id, store_number=None):
18
+ super().__init__(base_url=base_url)
19
+ self.base_url = base_url
20
+ self.auth_id = auth_id
21
+ self.root_key = root_key
22
+ self.username = username
23
+ self.password = password
24
+ self.merchant_id = int(merchant_id)
25
+ self.root_crypto = CsmCrypto(root_key)
26
+ self.crypto = None
27
+ self.store_number = store_number
28
+ self.kyn = None
29
+ self.active_key = None
30
+ self.encrypted_auth_message = None
31
+ self.tvb = None
32
+ self.session_id = None
33
+
34
+ def build_csm(self, **kwargs):
35
+ csm = (
36
+ f'CSM(MCL/SMA RCV/Comdata ORG/{self.merchant_id} TVB/{self.tvb} TVA/d9b94d17828b2853 '
37
+ f'SID/{self.session_id} USR/{self.username} PWD/{self.password})'
38
+ )
39
+ return csm
40
+
41
+ def headers(self, **kwargs):
42
+ if not self.session_id:
43
+ return {}
44
+ csm = self.build_csm()
45
+ auth_message = self.crypto.encrypt(csm)
46
+ # this is stupid but important -- httpx wants header keys
47
+ # to be all lowercase. if you set `Content-Type`
48
+ # and then somewhere else set `content-type`, you will
49
+ # get duplicated conent-types in your headers: e.g.,
50
+ # content-type: application/json, application/json
51
+ return {
52
+ 'content-type': 'application/json',
53
+ 'auth-id': self.auth_id,
54
+ 'auth-message': auth_message,
55
+ 'accept': 'application/json',
56
+ }
57
+
58
+ async def authenticate_root(self):
59
+ # Step 1: Get KEX from SVS
60
+ route = 'oltp-gift/authenticate/root'
61
+ headers = {
62
+ 'Auth-Id': self.auth_id,
63
+ 'Name': 'root',
64
+ 'Key': self.root_key,
65
+ }
66
+ response = await self.get(route, headers=headers, raw=True)
67
+ headers = response.headers
68
+ auth_message = headers.get('Auth-Message')
69
+ auth_message = self.root_crypto.decrypt(auth_message)
70
+ csm = self.parse_csm(auth_message)
71
+ self.kyn = csm.get('kyn')
72
+ self.active_key = csm.get('key')
73
+ self.crypto = CsmCrypto(self.active_key)
74
+ self.encrypted_auth_message = self.crypto.encrypt(auth_message)
75
+ return response
76
+
77
+ # async def presend(self, request):
78
+ # body = request.content.decode()
79
+ # if body:
80
+ # body = json.loads(body)
81
+ # body = json.dumps(body, indent=2)
82
+ # self.log.info(f'presend: {request.method} {request.url}')
83
+ # self.log.info('body=%s', body)
84
+ # headers = request.headers
85
+ # headers = json.dumps(dict(headers), indent=2)
86
+ # self.log.info('headers=%s', headers)
87
+
88
+ async def postsend(self, request, response):
89
+ headers = response.headers
90
+ if 'Auth-Message' in headers:
91
+ if not self.crypto:
92
+ return
93
+ auth_message = headers.get('Auth-Message')
94
+ auth_message = self.crypto.decrypt(auth_message)
95
+ # print(f'received csm: {auth_message}')
96
+ csm = self.parse_csm(auth_message)
97
+ self.tvb = csm.get('tvb')
98
+
99
+ async def establish_session(self):
100
+ """
101
+ establishes a session with svs
102
+ """
103
+ route = f'oltp-gift/authenticate/{self.kyn}'
104
+ headers = {
105
+ 'Auth-Id': self.auth_id,
106
+ 'Name': self.kyn,
107
+ 'Key': self.active_key,
108
+ 'Auth-Message': self.encrypted_auth_message,
109
+ }
110
+ response = await self.get(route, headers=headers, raw=True)
111
+ headers = response.headers
112
+ auth_message = headers.get('Auth-Message')
113
+ auth_message = self.crypto.decrypt(auth_message)
114
+ # print(auth_message)
115
+ csm = self.parse_csm(auth_message)
116
+ self.tvb = csm.get('tvb')
117
+ self.session_id = csm.get('sid')
118
+ return response
119
+
120
+ async def post_it(self, route, payload, retry=False, retry_403=False):
121
+ """
122
+ we do a lot of generic processing for every call -- handle it here
123
+ instead.
124
+ """
125
+ payload = self.base_payload(**payload)
126
+ stan = payload['stan']
127
+ headers = None
128
+ if retry:
129
+ # only use the retry flag when recovering from a
130
+ # connect timeout
131
+ headers = { 'Retry': 'Y', }
132
+ response = await self.post(route, json_data=payload, headers=headers, raw=True)
133
+ if response.status_code == 403 and not retry_403:
134
+ # if we immediately get a 403 response, svs
135
+ # expects us to retry with the updated tvb
136
+ response = await self.post_it(route, payload, retry=retry, retry_403=True)
137
+ result = await self.handle_response(response)
138
+ if isinstance(result, dict):
139
+ result['stan'] = stan
140
+ return result
141
+
142
+ async def balance_inquiry(self, data, enhanced=False, retry=False):
143
+ """
144
+ performs a balance inquiry request to svs
145
+ expected payload:
146
+ {
147
+ 'currency': 'USD',
148
+ 'card_number': '600...2449',
149
+ 'pin': '1234',
150
+ 'invoice_number': '00000000',
151
+ }
152
+ """
153
+ route = 'oltp-gift/v1/balance-inquiry'
154
+ if enhanced: # pragma: no cover
155
+ route = 'oltp-gift/v1/enhanced-balance-inquiry'
156
+ response = await self.post_it(route, data, retry=retry)
157
+ return response
158
+
159
+ def now(self):
160
+ return datetime.now(utc)
161
+
162
+ @classmethod
163
+ def lpad(cls, st, n=10):
164
+ if len(st) < n:
165
+ l = '0' * (n - len(st))
166
+ return f'{l}{st}'
167
+ return st
168
+
169
+ def base_payload(
170
+ self,
171
+ card_number: str = None,
172
+ pin: str = '',
173
+ currency = 'USD',
174
+ amount = None,
175
+ invoice_number: str = '00000000',
176
+ stan: int = None,
177
+ ) -> dict:
178
+ """Builds the standard SVS request payload for card operations."""
179
+ now = self.now()
180
+ payload = {
181
+ 'merchantId': self.merchant_id,
182
+ 'storeNumber': self.lpad(self.store_number),
183
+ 'invoiceNumber': self.lpad(invoice_number),
184
+ 'stan': Stan.get(card_number) if not stan else '',
185
+ 'currencyCode': currency,
186
+ 'posTime': now.strftime('%H:%M:%S'),
187
+ 'posDate': now.strftime('%Y-%m-%d'),
188
+ }
189
+ if card_number:
190
+ payload['cardNumber'] = card_number
191
+ if pin:
192
+ payload['pin'] = pin
193
+ if amount is not None:
194
+ payload['amount'] = amount
195
+ if stan is not None:
196
+ payload['stan'] = stan
197
+ return payload
198
+
199
+ @classmethod
200
+ def parse_csm(cls, csm):
201
+ """
202
+ Parses a CSM string into a dictionary.
203
+
204
+ Example input:
205
+ CSM(MCL/SMA RCV/Comdata ORG/10001095 TVB/abc TVA/def SID/xxx USR/u PWD/p)
206
+
207
+ Example output:
208
+ {
209
+ 'MCL': 'SMA',
210
+ 'RCV': 'Comdata',
211
+ 'ORG': '10001095',
212
+ 'TVB': 'abc',
213
+ 'TVA': 'def',
214
+ 'SID': 'xxx',
215
+ 'USR': 'u',
216
+ 'PWD': 'p'
217
+ }
218
+ """
219
+ if not csm.startswith('CSM(') or not csm.endswith(')'): # pragma: no cover
220
+ raise ValueError('Invalid CSM format')
221
+ content = csm[4:-1] # Strip off 'CSM(' and ')'
222
+ parts = content.split()
223
+ result = {}
224
+ for part in parts:
225
+ key, value = part.split('/', 1)
226
+ result[key.lower()] = value
227
+ return result
228
+
229
+ async def reload(self, data, retry=False, **kwargs):
230
+ """
231
+ reloads a card with svs
232
+ expected payload:
233
+ {
234
+ 'currency': 'USD',
235
+ 'card_number': '600...244',
236
+ 'pin': '1234',
237
+ 'amount': 25,
238
+ 'invoice_number': '00000000',
239
+ }
240
+ """
241
+ route = 'oltp-gift/v1/reload'
242
+ response = await self.post_it(route, data, retry=retry)
243
+ return response
244
+
245
+ async def issue(self, data, retry=False, **kwargs):
246
+ """
247
+ initial activation of a **physical** gift card with a certain balance
248
+ """
249
+ route = 'oltp-gift/v1/issue'
250
+ response = await self.post_it(route, data, retry=retry)
251
+ return response
252
+
253
+ async def preissue(self, data, retry=False, **kwargs):
254
+ """
255
+ checks whether a card is eligible to be issued
256
+ """
257
+ route = 'oltp-gift/v1/pre-issue'
258
+ response = await self.post_it(route, data, retry=retry)
259
+ return response
260
+
261
+ async def preactivate(self, data, retry=False, **kwargs): # pragma: no cover
262
+ """
263
+ checks whether a card is eligible to be activated
264
+ """
265
+ route = 'oltp-gift/v1/pre-activate'
266
+ response = await self.post_it(route, data, retry=retry)
267
+ return response
268
+
269
+ async def activate(self, data, retry=False, **kwargs): # pragma: no cover
270
+ """
271
+ activates a fixed value (i.e., a conversion bin) card
272
+ """
273
+ route = 'oltp-gift/v1/activate'
274
+ response = await self.post_it(route, data, retry=retry)
275
+ return response
276
+
277
+ async def issue_virtual(self, data, retry=False, **kwargs):
278
+ """
279
+ issues a virtual gift card and pin from a pool allocated on svs
280
+
281
+ sample payload:
282
+ {
283
+ 'invoice_number': '00004321',
284
+ 'stan': 123456,
285
+ 'amount': 12.50,
286
+ }
287
+ """
288
+ route = 'oltp-gift/v1/issue-virtual'
289
+ response = await self.post_it(route, data, retry=retry)
290
+ return response
291
+
292
+ async def redeem(self, data, retry=False, **kwargs):
293
+ """
294
+ This transaction is used when purchasing goods with a gift card.
295
+
296
+ sample payload:
297
+ {
298
+ 'invoice_umber': '00004321',
299
+ 'stan': 123456,
300
+ 'amount': 12.50,
301
+ 'card_number':'6006...123',
302
+ 'pin':'0123',
303
+ }
304
+ """
305
+ route = 'oltp-gift/v1/redeem'
306
+ response = await self.post_it(route, data, retry=retry)
307
+ return response
308
+
309
+ async def preauth(self, data, retry=False, **kwargs):
310
+ """
311
+ Used to place a reserve on funds on a card for a fixed period of time or
312
+ until a completion message is approved. This message type should
313
+ always be followed by a Pre-Authorization Completion transaction.
314
+
315
+ The amount sent in the REQ should be an amount defined by the
316
+ client's POS. Amount of funds held is determined by:
317
+
318
+ If amount sent in the REQ is greater than the available funds,
319
+ the available funds on the card are held.
320
+
321
+ If the amount sent in the REQ is less than the available balance,
322
+ the requested amount is held.
323
+
324
+ sample payload:
325
+ {
326
+ 'invoice_number': '00004321',
327
+ 'stan':123456,
328
+ 'amount': 12.50,
329
+ 'card_number':'6006...123',
330
+ 'pin':'0123',
331
+ 'ssc':'0234',
332
+ 'track1':'B6006...123^ ^4912110ABC',
333
+ 'track2':'6006...123=491211012345678',
334
+ 'auxiliaryData':'',
335
+ 'cardType':''
336
+ }
337
+ """
338
+ route = 'oltp-gift/v1/pre-authorization'
339
+ response = await self.post_it(route, data, retry=retry, **kwargs)
340
+ return response
341
+
342
+ async def completion(self, data, retry=False, **kwargs):
343
+ """
344
+ This transaction is used remove the funds and holds placed by
345
+ the pre-authorization transaction These two transactions must
346
+ use the same STAN. In some cases, a pre-authorization will expire
347
+ before the transaction is completed or it will be necessary to
348
+ cancel out of a pre-authorization transaction. In order to release
349
+ the funds held by the pre-authorization, send a zero-dollar ($0)
350
+ Completion transaction.
351
+ sample payload:
352
+ {
353
+ 'invoice_number': '00004321',
354
+ 'stan': 123456,
355
+ 'amount': 12.50,
356
+ 'card_number':'6006...123',
357
+ 'pin':'0123',
358
+ }
359
+ """
360
+ route = 'oltp-gift/v1/completion'
361
+ response = await self.post_it(route, data, retry=retry, **kwargs)
362
+ return response
363
+
364
+ async def cancel(self, data, retry=False, **kwargs):
365
+ """
366
+ Used to manually void or post void a previous transaction. It is
367
+ possible to cancel a transaction until it is
368
+ reconciled or reversed.
369
+ Notes:
370
+
371
+ * In order to cancel funds held by the pre-authorization,
372
+ send a zero-dollar ($0) Completion transaction.
373
+ * A cancellation is NOT a reversal.
374
+ * Cancel transactions can be sent for any original transaction
375
+ type except Cancels, Reversals, Balance Inquiry, and Pre-Authorizations.
376
+ * On a time out, resend the Cancel transaction up to 3 times,
377
+ with a delay between each attempt, until a response is received
378
+ to the Cancellation.
379
+
380
+ sample payload:
381
+ {
382
+ 'invoice_number': '00004321',
383
+ 'stan': 123456,
384
+ 'amount': 12.50,
385
+ 'card_number':'6006...123',
386
+ 'pin':'0123',
387
+ }
388
+ """
389
+ route = 'oltp-gift/v1/cancel'
390
+ response = await self.post_it(route, data, retry=retry, **kwargs)
391
+ return response
392
+
393
+ async def reversal(self, data, retry=False, **kwargs):
394
+ """
395
+ Used to resolve situations arising from a POS timeout or
396
+ network outage. This includes the case when SVS is
397
+ switching authorizations to a third party and a
398
+ response code of 98 (timeout) is returned.
399
+ Notes:
400
+ * A reversal is not a cancellation.
401
+ * Reversal transactions can be sent for any original
402
+ transaction types, excluding Cancels, Reversals,
403
+ Balance Inquiry, and Pre-Authorizations.
404
+ * Do not send a Reversal transaction to back
405
+ out of a Pre-Authorization transaction when the
406
+ pre-authorization times out or must be cancelled.
407
+ In order to release the funds held by the pre-authorization,
408
+ send a zero-dollar ($0) Pre-Authorization Completion
409
+ transaction instead.
410
+ * When a response to the reversal transaction is not
411
+ received, the reversal should be retransmitted. There
412
+ should be a total of 3 reversal attempts with a delay
413
+ between each transaction.
414
+ * Another transaction from the same device should not
415
+ be sent to SVS until a response is received to the reversal.
416
+ * An Issue Virtual Card transaction, which has timed out
417
+ cannot be reversed when the card number is not known.
418
+ However, a reversal is automatically generated by the
419
+ client's system when the response was not received.
420
+ sample payload:
421
+ {
422
+ 'invoice_number': '00004321',
423
+ 'stan': 123456,
424
+ 'amount': 12.50,
425
+ 'card_number':'6006...123',
426
+ 'pin':'0123',
427
+ }
428
+ """
429
+ route = 'oltp-gift/v1/reversal'
430
+ response = await self.post_it(route, data, retry=retry, **kwargs)
431
+ return response
@@ -0,0 +1,2 @@
1
+ def hello():
2
+ return 'hello'
@@ -0,0 +1,43 @@
1
+ response_code_map = {
2
+ '01': 'approval',
3
+ '02': 'inactive card',
4
+ '03': 'invalid card number',
5
+ '04': 'invalid transaction code',
6
+ '05': 'insufficient funds',
7
+ '06': 'no previous authorizations',
8
+ '07': 'invalid message',
9
+ '08': 'no card found',
10
+ '09': 'insufficient funds due to outstanding pre-authorization.',
11
+ '10': 'denial - no previous authorization',
12
+ '11': 'no authorization number',
13
+ '12': 'invalid authorization number',
14
+ '13': 'maximum single recharge exceeded',
15
+ '14': 'maximum working balance exceeded',
16
+ '15': 'host unavailable',
17
+ '16': 'invalid card status',
18
+ '17': 'unknown dealer/store code - special edit',
19
+ '18': 'maximum number of recharges exceeded',
20
+ '19': 'invalid card verification value (cvv) or secondary security code (ssc)',
21
+ '20': 'invalid pin number or pin locked',
22
+ '21': 'card already issued',
23
+ '22': 'card not issued',
24
+ '23': 'card already used',
25
+ '24': 'manual transaction not allowed',
26
+ '25': 'mag stripe read not valid',
27
+ '26': 'transaction type unknown',
28
+ '27': 'invalid tender type',
29
+ '28': 'invalid customer type',
30
+ '29': 'n/a',
31
+ '30': 'max number of redemption exceeded',
32
+ '31': 'invalid currency code',
33
+ '32': 'invalid server id (restaurant only - server id code is invalid)',
34
+ '33': 'frozen card or unknown',
35
+ '34': 'invalid amount (transaction amount does not match the pre-valued card dollar amount)',
36
+ '35': 'n/a',
37
+ '36': 'transaction rejected - invalid transaction promotion-wide or invalid for originating merchant and store. (dps)',
38
+ '37': 'transaction rejected - invalid merchant and store combination for promotion. (dps)',
39
+ '38': 'transaction rejected - exceeded maximum number of promotion cards allowed for a single pos transaction. (dps fraud control)',
40
+ '42': 'transaction declined - transaction amount less than the required minimum. (dps)',
41
+ '98': 'timeout/error communicating with third party authorizer',
42
+ '99': 'failure in retrieving data (enhance balance inquiry only)',
43
+ }