Commit 11a8624f authored by anarcat's avatar anarcat
Browse files

Merge branch 'webhook-ipn-dedupe-coordination' into 'main'

feat: Deduplicate IPN and webhook messages carrying the same legacy-series recurring donation data

See merge request tpo/web/donate-neo!179
parents 41587337 76fbf6f9
Loading
Loading
Loading
Loading
+15 −6
Original line number Diff line number Diff line
@@ -355,7 +355,7 @@ If this error persists, please reach out to us via one of the methods
        # that we do from webhooks, so we pick and choose the particular information from it
        # which will allow us to hand this info off to the rest of the donation-processing
        # pipeline.
        # (IPN messages always, and only, relate to ongoing subscriptions, so we can
        # (IPN messages we care about always, and only, relate to ongoing subscriptions, so we can
        # hardcode the message and make some other assumptions about what's in `args`.)
        message = "Tor\\Donation\\RecurringContributionOngoing"

@@ -388,8 +388,8 @@ If this error persists, please reach out to us via one of the methods
        args = {
            "payment_instrument_id": "PayPal",
            "receive_date": timestamp,
            "trxn_id": param_dict["txn_id"],
            "handshake_id": param_dict["txn_id"],
            "trxn_id": param_dict["recurring_payment_id"],
            "handshake_id": param_dict["recurring_payment_id"],
            "recurring_contribution_transaction_id": param_dict["recurring_payment_id"],
            "currency": param_dict["mc_currency"],
            "total_amount": param_dict["amount"],
@@ -406,8 +406,17 @@ If this error persists, please reach out to us via one of the methods
            billing_result, "recurring", "paypal"
        ).inc()

        # Ensure that IPN messages are deduped before being handed off for processing.
        if await self._civi.donation_exists(message, args["trxn_id"]) is False:
        # Ensure that donations are deduped before being handed off for processing.
        # In the event that PayPal sends both an IPN message and a webhook message about the
        # same legacy transaction, we need to use a deduping value available to both message
        # types; since `txn_id` is an IPN-exclusive value, we instead use `recurring_payment_id`.
        # Note that this ID is unique to this transaction series, and not this individual
        # transaction; since recurring transactions only bill once a month, and since we only
        # check for duplicate transaction messages for an hour after the first message, this
        # value is both unique enough and broadly-available enough for our purposes.
        if await self._civi.donation_exists(
            message, args["trxn_id"]
        ) is False:
            try:
                await self._civi.report_donation(message, args)
            except ValueError:
@@ -517,7 +526,7 @@ If this error persists, please reach out to us via one of the methods
            # A subscription's donation has succeeded
            case "PAYMENT.SALE.COMPLETED":
                message = "Tor\\Donation\\RecurringContributionOngoing"
                args["trxn_id"] = event["id"]
                args["trxn_id"] = event["resource"]["id"]
                # In the case of new Paypal subscriptions, the only ID string found both in the
                # post-donation success data _and_ the webhook confirming the payment went through
                # is the billing agreement ID. This works fine in practice, but it is the one-off
+4 −4
Original line number Diff line number Diff line
@@ -588,8 +588,8 @@ class AsyncPaypalControllerTests(IsolatedAsyncioTestCase):
                {
                    "payment_instrument_id": "PayPal",
                    "receive_date": "2024-10-30 14:57:11",
                    "trxn_id": "61B760829R389225R",
                    "handshake_id": "61B760829R389225R",
                    "trxn_id": "I-JXHVH701URKS",
                    "handshake_id": "I-JXHVH701URKS",
                    "recurring_contribution_transaction_id": "I-JXHVH701URKS",
                    "currency": "USD",
                    "total_amount": "5.00",
@@ -646,8 +646,8 @@ class AsyncPaypalControllerTests(IsolatedAsyncioTestCase):
                {
                    "payment_instrument_id": "PayPal",
                    "receive_date": "2024-10-30 14:57:11",
                    "trxn_id": "61B760829R389225R",
                    "handshake_id": "61B760829R389225R",
                    "trxn_id": "I-JXHVH701URKS",
                    "handshake_id": "I-JXHVH701URKS",
                    "recurring_contribution_transaction_id": "I-JXHVH701URKS",
                    "currency": "USD",
                    "total_amount": "5.00",