velocity-python 0.2.0__py3-none-any.whl → 0.2.1__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.
- velocity/__init__.py +1 -1
- velocity/aws/tests/test_sms.py +7 -4
- velocity/misc/pdf.py +10 -3
- velocity/payment/__init__.py +2 -0
- velocity/payment/authorizenet_adapter.py +306 -14
- velocity/payment/base_adapter.py +207 -2
- velocity/payment/braintree_adapter.py +150 -13
- velocity/payment/stripe_adapter.py +190 -7
- {velocity_python-0.2.0.dist-info → velocity_python-0.2.1.dist-info}/METADATA +1 -1
- {velocity_python-0.2.0.dist-info → velocity_python-0.2.1.dist-info}/RECORD +14 -14
- {velocity_python-0.2.0.dist-info → velocity_python-0.2.1.dist-info}/WHEEL +0 -0
- {velocity_python-0.2.0.dist-info → velocity_python-0.2.1.dist-info}/entry_points.txt +0 -0
- {velocity_python-0.2.0.dist-info → velocity_python-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {velocity_python-0.2.0.dist-info → velocity_python-0.2.1.dist-info}/top_level.txt +0 -0
velocity/__init__.py
CHANGED
velocity/aws/tests/test_sms.py
CHANGED
|
@@ -55,10 +55,13 @@ class TestParseInboundEvent(unittest.TestCase):
|
|
|
55
55
|
)
|
|
56
56
|
messages = parse_inbound_event(event)
|
|
57
57
|
self.assertEqual(len(messages), 2)
|
|
58
|
-
self.assertEqual(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
self.assertEqual(
|
|
59
|
+
messages[1].media_urls,
|
|
60
|
+
[
|
|
61
|
+
"s3://bucket/media/one.jpg",
|
|
62
|
+
"s3://bucket/media/two.jpg",
|
|
63
|
+
],
|
|
64
|
+
)
|
|
62
65
|
|
|
63
66
|
def test_bare_payload_dict(self):
|
|
64
67
|
messages = parse_inbound_event(_payload())
|
velocity/misc/pdf.py
CHANGED
|
@@ -214,7 +214,10 @@ def page_css(page: PageOptions) -> str:
|
|
|
214
214
|
size = _css_page_size(page)
|
|
215
215
|
top, right, bottom, left = page.margins()
|
|
216
216
|
parts = [f"size: {size};", f"margin: {top} {right} {bottom} {left};"]
|
|
217
|
-
for slot, text in (
|
|
217
|
+
for slot, text in (
|
|
218
|
+
("bottom-right", page.footer_right),
|
|
219
|
+
("bottom-center", page.footer_center),
|
|
220
|
+
):
|
|
218
221
|
if text:
|
|
219
222
|
parts.append(
|
|
220
223
|
f"@{slot} {{ content: {_footer_content(text)}; "
|
|
@@ -258,7 +261,9 @@ _RENDERERS: Dict[str, BasePdfRenderer] = {
|
|
|
258
261
|
}
|
|
259
262
|
|
|
260
263
|
|
|
261
|
-
def register_renderer(
|
|
264
|
+
def register_renderer(
|
|
265
|
+
name: str, renderer: BasePdfRenderer, *, preferred: bool = False
|
|
266
|
+
) -> None:
|
|
262
267
|
"""Add (or replace) a backend. ``preferred=True`` makes it the first choice
|
|
263
268
|
for auto-selection."""
|
|
264
269
|
key = name.strip().lower()
|
|
@@ -331,7 +336,9 @@ def html_to_pdf(
|
|
|
331
336
|
footer_center=footer_center,
|
|
332
337
|
footer_font_size=footer_font_size,
|
|
333
338
|
)
|
|
334
|
-
return get_renderer(engine).render(
|
|
339
|
+
return get_renderer(engine).render(
|
|
340
|
+
html, page, base_url=base_url, extra_css=extra_css
|
|
341
|
+
)
|
|
335
342
|
|
|
336
343
|
|
|
337
344
|
def _inject_css(html: str, css: str) -> str:
|
velocity/payment/__init__.py
CHANGED
|
@@ -21,6 +21,7 @@ StripeAdapter`` still works and raises ImportError when the SDK is missing;
|
|
|
21
21
|
|
|
22
22
|
from .base_adapter import (
|
|
23
23
|
OUTCOME_UNKNOWN,
|
|
24
|
+
ChargeReferenceAnomaly,
|
|
24
25
|
PaymentProcessorAdapter,
|
|
25
26
|
ProcessorError,
|
|
26
27
|
ProcessorOutcomeUnknown,
|
|
@@ -38,6 +39,7 @@ __all__ = [
|
|
|
38
39
|
"PaymentProcessorAdapter",
|
|
39
40
|
"ProcessorError",
|
|
40
41
|
"ProcessorOutcomeUnknown",
|
|
42
|
+
"ChargeReferenceAnomaly",
|
|
41
43
|
"OUTCOME_UNKNOWN",
|
|
42
44
|
# Adapters
|
|
43
45
|
"StripeAdapter",
|
|
@@ -30,11 +30,14 @@ from authorizenet import apicontractsv1, apicontrollers
|
|
|
30
30
|
from .base_adapter import (
|
|
31
31
|
PaymentProcessorAdapter,
|
|
32
32
|
ProcessorError,
|
|
33
|
+
_as_utc,
|
|
33
34
|
_require_amount,
|
|
34
35
|
_validate_partial_amount,
|
|
36
|
+
fit_reference,
|
|
35
37
|
mark_external_side_effect,
|
|
38
|
+
payment_reference,
|
|
36
39
|
)
|
|
37
|
-
from .money import cents_to_decimal
|
|
40
|
+
from .money import cents_to_decimal, to_cents
|
|
38
41
|
from .redact import normalize_card_number, scrub
|
|
39
42
|
from .authorizenet_mirror import (
|
|
40
43
|
build_payment_account_fields_from_authorize_account,
|
|
@@ -52,6 +55,75 @@ logger = logging.getLogger(__name__)
|
|
|
52
55
|
logging.getLogger("authorizenet.sdk").setLevel(logging.CRITICAL + 1)
|
|
53
56
|
logging.getLogger("pyxb").setLevel(logging.WARNING)
|
|
54
57
|
|
|
58
|
+
#: ``order.invoiceNumber`` and the request ``refId`` are each at most 20
|
|
59
|
+
#: characters (AnetApiSchema.xsd; the SDK rejects longer values).
|
|
60
|
+
INVOICE_NUMBER_MAX_LENGTH = 20
|
|
61
|
+
REF_ID_MAX_LENGTH = 20
|
|
62
|
+
|
|
63
|
+
#: getTransactionList / getUnsettledTransactionList page size (schema max 1000).
|
|
64
|
+
_PAGE_LIMIT = 1000
|
|
65
|
+
#: getSettledBatchList accepts at most 31 days per request.
|
|
66
|
+
_BATCH_WINDOW = timedelta(days=30)
|
|
67
|
+
#: Look-back when find_charge_by_reference gets no ``since``.
|
|
68
|
+
DEFAULT_REFERENCE_LOOKBACK = timedelta(days=31)
|
|
69
|
+
|
|
70
|
+
#: transactionStatus values meaning "no money moved / none will": ignored by a
|
|
71
|
+
#: lookup by reference. Refund statuses are skipped too (they are credits).
|
|
72
|
+
_DEAD_STATUSES = frozenset(
|
|
73
|
+
{
|
|
74
|
+
"declined",
|
|
75
|
+
"voided",
|
|
76
|
+
"expired",
|
|
77
|
+
"generalError",
|
|
78
|
+
"failedReview",
|
|
79
|
+
"settlementError",
|
|
80
|
+
"communicationError",
|
|
81
|
+
"refundSettledSuccessfully",
|
|
82
|
+
"refundPendingSettlement",
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
#: Live but undecided (fraud review): returned with ``success=False``.
|
|
86
|
+
_PENDING_STATUSES = frozenset(
|
|
87
|
+
{"FDSPendingReview", "FDSAuthorizedPendingReview", "underReview"}
|
|
88
|
+
)
|
|
89
|
+
#: An authOnlyTransaction not yet captured (``authorize_payment``'s result).
|
|
90
|
+
_AUTHORIZED_STATUSES = frozenset({"authorizedPendingCapture"})
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _lookup_status(transaction_status: Any) -> str:
|
|
94
|
+
"""A live ``transactionStatus`` in the charge path's vocabulary.
|
|
95
|
+
|
|
96
|
+
Fraud review -> ``"Held for Review"`` (response code 4), an uncaptured
|
|
97
|
+
authOnly -> ``"authorized"`` (as ``authorize_payment`` reports it), every
|
|
98
|
+
other live status (captured, settled, approved after review...) ->
|
|
99
|
+
``"Approved"`` (response code 1).
|
|
100
|
+
"""
|
|
101
|
+
if transaction_status in _PENDING_STATUSES:
|
|
102
|
+
return RESPONSE_CODES[4]
|
|
103
|
+
if transaction_status in _AUTHORIZED_STATUSES:
|
|
104
|
+
return "authorized"
|
|
105
|
+
return RESPONSE_CODES[1]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _invoice_number(payment_data: Dict[str, Any]) -> Optional[str]:
|
|
109
|
+
"""The ``order.invoiceNumber`` for ``payment_data``'s reference, or None.
|
|
110
|
+
|
|
111
|
+
The same value is sent as the request ``refId`` (also 20 characters).
|
|
112
|
+
"""
|
|
113
|
+
reference = payment_reference(payment_data)
|
|
114
|
+
return fit_reference(reference, INVOICE_NUMBER_MAX_LENGTH) if reference else None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _as_list(container: Any, key: str) -> List[Dict[str, Any]]:
|
|
118
|
+
"""xmltodict gives one child as a dict and several as a list."""
|
|
119
|
+
if not isinstance(container, dict):
|
|
120
|
+
return []
|
|
121
|
+
value = container.get(key)
|
|
122
|
+
if value is None:
|
|
123
|
+
return []
|
|
124
|
+
return value if isinstance(value, list) else [value]
|
|
125
|
+
|
|
126
|
+
|
|
55
127
|
RESPONSE_CODES = {
|
|
56
128
|
1: "Approved",
|
|
57
129
|
2: "Declined",
|
|
@@ -567,7 +639,9 @@ class AuthorizeNetAdapter(PaymentProcessorAdapter):
|
|
|
567
639
|
Pre-authorize a payment using authOnlyTransaction on a stored profile.
|
|
568
640
|
|
|
569
641
|
Authorize.Net authorizations expire after 30 days. No HTTP response
|
|
570
|
-
raises ``ProcessorOutcomeUnknown`` with the side effect marked.
|
|
642
|
+
raises ``ProcessorOutcomeUnknown`` with the side effect marked. The
|
|
643
|
+
reference, when given, is sent as ``order.invoiceNumber`` and ``refId``
|
|
644
|
+
as in :meth:`charge_stored_payment_method`.
|
|
571
645
|
"""
|
|
572
646
|
amount_cents = _require_amount(
|
|
573
647
|
"authorize_net", payment_data["amount"], "amount"
|
|
@@ -584,17 +658,25 @@ class AuthorizeNetAdapter(PaymentProcessorAdapter):
|
|
|
584
658
|
tx_request.amount = amount_dollars
|
|
585
659
|
tx_request.profile = profile
|
|
586
660
|
|
|
587
|
-
|
|
661
|
+
invoice_number = _invoice_number(payment_data)
|
|
662
|
+
if (
|
|
663
|
+
payment_data.get("client_id")
|
|
664
|
+
or payment_data.get("description")
|
|
665
|
+
or invoice_number
|
|
666
|
+
):
|
|
588
667
|
order = apicontractsv1.orderType()
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
668
|
+
if invoice_number:
|
|
669
|
+
order.invoiceNumber = invoice_number
|
|
670
|
+
if payment_data.get("client_id") or payment_data.get("description"):
|
|
671
|
+
client_id = payment_data.get("client_id", "")
|
|
672
|
+
desc = payment_data.get("description", str(client_id))
|
|
673
|
+
order.description = str(desc)[:255]
|
|
592
674
|
tx_request.order = order
|
|
593
675
|
|
|
594
676
|
request = apicontractsv1.createTransactionRequest()
|
|
595
677
|
request.merchantAuthentication = self._merchant_auth()
|
|
596
678
|
request.transactionRequest = tx_request
|
|
597
|
-
request.refId = f"ref{int(time.time() * 1000)}"
|
|
679
|
+
request.refId = invoice_number or f"ref{int(time.time() * 1000)}"
|
|
598
680
|
|
|
599
681
|
controller = apicontrollers.createTransactionController(request)
|
|
600
682
|
except Exception as e:
|
|
@@ -614,6 +696,7 @@ class AuthorizeNetAdapter(PaymentProcessorAdapter):
|
|
|
614
696
|
"customer_profile_id": payment_data.get("customer_profile_id"),
|
|
615
697
|
"payment_profile_id": payment_data.get("payment_method"),
|
|
616
698
|
"ref_id": request.refId,
|
|
699
|
+
"processor_reference": invoice_number,
|
|
617
700
|
},
|
|
618
701
|
)
|
|
619
702
|
trans = response.get("transactionResponse", {})
|
|
@@ -649,6 +732,8 @@ class AuthorizeNetAdapter(PaymentProcessorAdapter):
|
|
|
649
732
|
}
|
|
650
733
|
if mirror_error:
|
|
651
734
|
metadata["mirror_error"] = mirror_error
|
|
735
|
+
if invoice_number:
|
|
736
|
+
metadata["processor_reference"] = invoice_number
|
|
652
737
|
return {
|
|
653
738
|
"processor_transaction_id": trans.get("transId"),
|
|
654
739
|
"status": "authorized",
|
|
@@ -667,7 +752,11 @@ class AuthorizeNetAdapter(PaymentProcessorAdapter):
|
|
|
667
752
|
"authorization_code": None,
|
|
668
753
|
"expires_at": None,
|
|
669
754
|
"error_message": _transaction_error_text(trans, code),
|
|
670
|
-
"metadata":
|
|
755
|
+
"metadata": (
|
|
756
|
+
{"response_code": code, "processor_reference": invoice_number}
|
|
757
|
+
if invoice_number
|
|
758
|
+
else {"response_code": code}
|
|
759
|
+
),
|
|
671
760
|
}
|
|
672
761
|
|
|
673
762
|
def charge_stored_payment_method(
|
|
@@ -680,7 +769,17 @@ class AuthorizeNetAdapter(PaymentProcessorAdapter):
|
|
|
680
769
|
and spot donations. No HTTP response raises ``ProcessorOutcomeUnknown``
|
|
681
770
|
(as before, this path raises rather than returns on transport errors)
|
|
682
771
|
with the side effect marked.
|
|
772
|
+
|
|
773
|
+
Authorize.Net has no idempotency keys. The stable business reference
|
|
774
|
+
(``payment_data["reference"]``, else ``["idempotency_key"]``) is sent
|
|
775
|
+
as ``order.invoiceNumber`` and as the request ``refId`` (both hashed
|
|
776
|
+
to 20 characters when longer) and returned in
|
|
777
|
+
``metadata["processor_reference"]`` (and in the
|
|
778
|
+
``ProcessorOutcomeUnknown`` metadata); :meth:`find_charge_by_reference`
|
|
779
|
+
finds the charge by it. Without a reference the ``refId`` stays
|
|
780
|
+
time-based.
|
|
683
781
|
"""
|
|
782
|
+
invoice_number = _invoice_number(payment_data)
|
|
684
783
|
try:
|
|
685
784
|
amount_cents = _require_amount(
|
|
686
785
|
"authorize_net", payment_data.get("amount"), "amount"
|
|
@@ -702,13 +801,16 @@ class AuthorizeNetAdapter(PaymentProcessorAdapter):
|
|
|
702
801
|
client_id = payment_data.get("client_id", "")
|
|
703
802
|
campaign = metadata.get("campaign", str(client_id))
|
|
704
803
|
order_description = None
|
|
705
|
-
if client_id or campaign:
|
|
804
|
+
if client_id or campaign or invoice_number:
|
|
706
805
|
order = apicontractsv1.orderType()
|
|
806
|
+
if invoice_number:
|
|
807
|
+
order.invoiceNumber = invoice_number
|
|
707
808
|
if client_id and campaign:
|
|
708
809
|
order_description = f"{client_id}::{campaign}"[:255]
|
|
709
|
-
|
|
810
|
+
elif client_id or campaign:
|
|
710
811
|
order_description = str(client_id or campaign)[:255]
|
|
711
|
-
|
|
812
|
+
if order_description:
|
|
813
|
+
order.description = order_description
|
|
712
814
|
tx_request.order = order
|
|
713
815
|
|
|
714
816
|
# Line items
|
|
@@ -734,7 +836,7 @@ class AuthorizeNetAdapter(PaymentProcessorAdapter):
|
|
|
734
836
|
request = apicontractsv1.createTransactionRequest()
|
|
735
837
|
request.merchantAuthentication = self._merchant_auth()
|
|
736
838
|
request.transactionRequest = tx_request
|
|
737
|
-
request.refId = f"ref{int(time.time() * 1000)}"
|
|
839
|
+
request.refId = invoice_number or f"ref{int(time.time() * 1000)}"
|
|
738
840
|
|
|
739
841
|
controller = apicontrollers.createTransactionController(request)
|
|
740
842
|
except ProcessorError:
|
|
@@ -752,7 +854,14 @@ class AuthorizeNetAdapter(PaymentProcessorAdapter):
|
|
|
752
854
|
"platform_fee": 0,
|
|
753
855
|
"client_amount": 0,
|
|
754
856
|
"error_message": scrub(str(e)),
|
|
755
|
-
"metadata":
|
|
857
|
+
"metadata": (
|
|
858
|
+
{
|
|
859
|
+
"error_type": type(e).__name__,
|
|
860
|
+
"processor_reference": invoice_number,
|
|
861
|
+
}
|
|
862
|
+
if invoice_number
|
|
863
|
+
else {"error_type": type(e).__name__}
|
|
864
|
+
),
|
|
756
865
|
}
|
|
757
866
|
|
|
758
867
|
response = self._execute(
|
|
@@ -764,6 +873,7 @@ class AuthorizeNetAdapter(PaymentProcessorAdapter):
|
|
|
764
873
|
"customer_profile_id": payment_data.get("customer_profile_id"),
|
|
765
874
|
"payment_profile_id": payment_data.get("payment_method"),
|
|
766
875
|
"ref_id": request.refId,
|
|
876
|
+
"processor_reference": invoice_number,
|
|
767
877
|
},
|
|
768
878
|
)
|
|
769
879
|
trans = response.get("transactionResponse", {})
|
|
@@ -807,6 +917,8 @@ class AuthorizeNetAdapter(PaymentProcessorAdapter):
|
|
|
807
917
|
}
|
|
808
918
|
if mirror_error:
|
|
809
919
|
result_metadata["mirror_error"] = mirror_error
|
|
920
|
+
if invoice_number:
|
|
921
|
+
result_metadata["processor_reference"] = invoice_number
|
|
810
922
|
return {
|
|
811
923
|
"processor_transaction_id": trans.get("transId"),
|
|
812
924
|
"processor_charge_id": trans.get("transId"),
|
|
@@ -831,7 +943,187 @@ class AuthorizeNetAdapter(PaymentProcessorAdapter):
|
|
|
831
943
|
"platform_fee": 0,
|
|
832
944
|
"client_amount": 0,
|
|
833
945
|
"error_message": _transaction_error_text(trans, code),
|
|
834
|
-
"metadata":
|
|
946
|
+
"metadata": (
|
|
947
|
+
{"response_code": code, "processor_reference": invoice_number}
|
|
948
|
+
if invoice_number
|
|
949
|
+
else {"response_code": code}
|
|
950
|
+
),
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
# ========================================================================
|
|
954
|
+
# LOOKUP BY REFERENCE
|
|
955
|
+
# ========================================================================
|
|
956
|
+
|
|
957
|
+
def find_charge_by_reference(
|
|
958
|
+
self,
|
|
959
|
+
tx,
|
|
960
|
+
reference: str,
|
|
961
|
+
*,
|
|
962
|
+
amount: Optional[int] = None,
|
|
963
|
+
since: Optional[datetime] = None,
|
|
964
|
+
) -> Optional[Dict[str, Any]]:
|
|
965
|
+
"""Find the live transaction whose ``invoiceNumber`` is ``reference``.
|
|
966
|
+
|
|
967
|
+
Authorize.Net cannot search by invoice number, so this scans the
|
|
968
|
+
documented reporting lists and filters on ``invoiceNumber`` (the same
|
|
969
|
+
20-character fitting the charge applied):
|
|
970
|
+
|
|
971
|
+
* ``getUnsettledTransactionListRequest`` (status ``any``, so held
|
|
972
|
+
transactions are included), sorted by ``submitTimeUTC``, paged 1000
|
|
973
|
+
at a time;
|
|
974
|
+
* ``getSettledBatchListRequest`` from ``since`` to now (in 30-day
|
|
975
|
+
windows; the API allows 31), then ``getTransactionListRequest`` per
|
|
976
|
+
batch, sorted and paged the same way.
|
|
977
|
+
|
|
978
|
+
``since`` bounds the scan (default: 31 days back) -- pass the attempt's
|
|
979
|
+
creation time. Each request is a Transaction Details API call, so the
|
|
980
|
+
scan costs one call per batch page. Declined, voided, expired, error
|
|
981
|
+
and refund transactions are ignored; a transaction under fraud review
|
|
982
|
+
is returned with ``success=False``. The amount is the summary's
|
|
983
|
+
``settleAmount``. Any API error or missing response raises
|
|
984
|
+
``ProcessorOutcomeUnknown``. Transaction Details API access must be
|
|
985
|
+
enabled for the API login.
|
|
986
|
+
|
|
987
|
+
``status`` uses the charge path's vocabulary: ``"Approved"``,
|
|
988
|
+
``"Held for Review"`` (fraud review, ``success=False``) or
|
|
989
|
+
``"authorized"`` (an uncaptured authOnly); the raw value is in
|
|
990
|
+
``metadata["transaction_status"]``. The found transaction is mirrored
|
|
991
|
+
to ``authorize_transactions`` with the charge path's writer (an
|
|
992
|
+
idempotent upsert keyed by ``remote_transid``); a mirror failure does
|
|
993
|
+
not hide the charge and sets ``metadata["mirror_error"]``.
|
|
994
|
+
"""
|
|
995
|
+
reference = self._require_reference(reference)
|
|
996
|
+
invoice_number = fit_reference(reference, INVOICE_NUMBER_MAX_LENGTH)
|
|
997
|
+
now = datetime.now(timezone.utc)
|
|
998
|
+
start = _as_utc(since) or now - DEFAULT_REFERENCE_LOOKBACK
|
|
999
|
+
found: Dict[str, Dict[str, Any]] = {}
|
|
1000
|
+
try:
|
|
1001
|
+
for summary in self._unsettled_transactions():
|
|
1002
|
+
if summary.get("invoiceNumber") == invoice_number:
|
|
1003
|
+
found.setdefault(str(summary.get("transId")), summary)
|
|
1004
|
+
for batch_id in self._settled_batch_ids(start, now):
|
|
1005
|
+
for summary in self._batch_transactions(batch_id):
|
|
1006
|
+
if summary.get("invoiceNumber") == invoice_number:
|
|
1007
|
+
found.setdefault(str(summary.get("transId")), summary)
|
|
1008
|
+
except Exception as exc:
|
|
1009
|
+
raise self._reference_lookup_failed(reference, exc) from None
|
|
1010
|
+
|
|
1011
|
+
matches = [
|
|
1012
|
+
self._normalize_summary(summary, invoice_number)
|
|
1013
|
+
for summary in found.values()
|
|
1014
|
+
if summary.get("transactionStatus") not in _DEAD_STATUSES
|
|
1015
|
+
]
|
|
1016
|
+
match = self._select_reference_match(reference, matches, amount)
|
|
1017
|
+
if match is None:
|
|
1018
|
+
return None
|
|
1019
|
+
summary = match["metadata"]["transaction_summary"]
|
|
1020
|
+
status = match["status"]
|
|
1021
|
+
mirror_error = self._mirror_transaction(
|
|
1022
|
+
tx,
|
|
1023
|
+
{
|
|
1024
|
+
"processor_transaction_id": match["processor_transaction_id"],
|
|
1025
|
+
"amount": (
|
|
1026
|
+
cents_to_decimal(match["amount"])
|
|
1027
|
+
if match["amount"] is not None
|
|
1028
|
+
else None
|
|
1029
|
+
),
|
|
1030
|
+
"status": status,
|
|
1031
|
+
"remote_responsecode": 4 if status == RESPONSE_CODES[4] else 1,
|
|
1032
|
+
"raw_payload": summary,
|
|
1033
|
+
},
|
|
1034
|
+
"find_charge_by_reference",
|
|
1035
|
+
)
|
|
1036
|
+
if mirror_error:
|
|
1037
|
+
match["metadata"]["mirror_error"] = mirror_error
|
|
1038
|
+
return match
|
|
1039
|
+
|
|
1040
|
+
def _paged_transactions(self, build_request, make_controller):
|
|
1041
|
+
"""Yield transaction summaries page by page (sorted, 1000 per page)."""
|
|
1042
|
+
page = 1
|
|
1043
|
+
while True:
|
|
1044
|
+
request = build_request()
|
|
1045
|
+
request.merchantAuthentication = self._merchant_auth()
|
|
1046
|
+
sorting = apicontractsv1.TransactionListSorting()
|
|
1047
|
+
sorting.orderBy = apicontractsv1.TransactionListOrderFieldEnum.submitTimeUTC
|
|
1048
|
+
sorting.orderDescending = False
|
|
1049
|
+
request.sorting = sorting
|
|
1050
|
+
paging = apicontractsv1.Paging()
|
|
1051
|
+
paging.limit = _PAGE_LIMIT
|
|
1052
|
+
paging.offset = page
|
|
1053
|
+
request.paging = paging
|
|
1054
|
+
response = self._execute(make_controller(request))
|
|
1055
|
+
items = _as_list(response.get("transactions"), "transaction")
|
|
1056
|
+
yield from items
|
|
1057
|
+
if len(items) < _PAGE_LIMIT:
|
|
1058
|
+
return
|
|
1059
|
+
page += 1
|
|
1060
|
+
|
|
1061
|
+
def _unsettled_transactions(self):
|
|
1062
|
+
def build():
|
|
1063
|
+
request = apicontractsv1.getUnsettledTransactionListRequest()
|
|
1064
|
+
request.status = apicontractsv1.TransactionGroupStatusEnum.any
|
|
1065
|
+
return request
|
|
1066
|
+
|
|
1067
|
+
return self._paged_transactions(
|
|
1068
|
+
build, apicontrollers.getUnsettledTransactionListController
|
|
1069
|
+
)
|
|
1070
|
+
|
|
1071
|
+
def _batch_transactions(self, batch_id: str):
|
|
1072
|
+
def build():
|
|
1073
|
+
request = apicontractsv1.getTransactionListRequest()
|
|
1074
|
+
request.batchId = batch_id
|
|
1075
|
+
return request
|
|
1076
|
+
|
|
1077
|
+
return self._paged_transactions(
|
|
1078
|
+
build, apicontrollers.getTransactionListController
|
|
1079
|
+
)
|
|
1080
|
+
|
|
1081
|
+
def _settled_batch_ids(self, start: datetime, end: datetime) -> List[str]:
|
|
1082
|
+
"""Settled batch ids from ``start`` to ``end``, 30 days per request."""
|
|
1083
|
+
batch_ids: List[str] = []
|
|
1084
|
+
window_start = start
|
|
1085
|
+
while window_start < end:
|
|
1086
|
+
window_end = min(window_start + _BATCH_WINDOW, end)
|
|
1087
|
+
request = apicontractsv1.getSettledBatchListRequest()
|
|
1088
|
+
request.merchantAuthentication = self._merchant_auth()
|
|
1089
|
+
request.firstSettlementDate = window_start
|
|
1090
|
+
request.lastSettlementDate = window_end
|
|
1091
|
+
response = self._execute(
|
|
1092
|
+
apicontrollers.getSettledBatchListController(request)
|
|
1093
|
+
)
|
|
1094
|
+
for batch in _as_list(response.get("batchList"), "batch"):
|
|
1095
|
+
batch_id = batch.get("batchId")
|
|
1096
|
+
if batch_id and batch_id not in batch_ids:
|
|
1097
|
+
batch_ids.append(batch_id)
|
|
1098
|
+
window_start = window_end
|
|
1099
|
+
return batch_ids
|
|
1100
|
+
|
|
1101
|
+
@staticmethod
|
|
1102
|
+
def _normalize_summary(
|
|
1103
|
+
summary: Dict[str, Any], invoice_number: str
|
|
1104
|
+
) -> Dict[str, Any]:
|
|
1105
|
+
"""A transactionSummaryType (as a dict) in the charge result shape."""
|
|
1106
|
+
raw_status = summary.get("transactionStatus")
|
|
1107
|
+
settle_amount = summary.get("settleAmount")
|
|
1108
|
+
amount_cents = to_cents(str(settle_amount)) if settle_amount else None
|
|
1109
|
+
return {
|
|
1110
|
+
"processor_transaction_id": summary.get("transId"),
|
|
1111
|
+
"processor_charge_id": summary.get("transId"),
|
|
1112
|
+
"status": _lookup_status(raw_status),
|
|
1113
|
+
"success": raw_status not in _PENDING_STATUSES,
|
|
1114
|
+
"amount": amount_cents,
|
|
1115
|
+
"currency": "usd",
|
|
1116
|
+
"authorization_code": None, # not in the summary; see get_transaction_details
|
|
1117
|
+
"platform_fee": 0,
|
|
1118
|
+
"client_amount": amount_cents,
|
|
1119
|
+
"created_at": summary.get("submitTimeUTC"),
|
|
1120
|
+
"error_message": None,
|
|
1121
|
+
"metadata": {
|
|
1122
|
+
"processor_reference": invoice_number,
|
|
1123
|
+
"found_by_reference": True,
|
|
1124
|
+
"transaction_status": raw_status,
|
|
1125
|
+
"transaction_summary": summary,
|
|
1126
|
+
},
|
|
835
1127
|
}
|
|
836
1128
|
|
|
837
1129
|
# ========================================================================
|