Broadleaf Microservices
  • v1.0.0-latest-prod

Fraud Prevention

The Big Picture

When Authorize or AuthorizeAndCapture transactions are executed with Stripe, we also automatically engage Stripe’s fraud prevention mechanisms (i.e. Stripe Radar). In simple terms, Stripe Radar leverages a risk score, rules, & lists to determine if the transaction should be allowed, blocked, or flagged for manual review. The goal of Stripe Radar is to reduce the number of fraudulent transactions, and ultimately the number of disputes/charge-backs.

Maximizing Stripe’s Fraud Prevention Tools

The key to making the most of Stripe Radar is to pass as much information as much information as possible, so that the risk scoring engine, rules, & lists have more data to work with. Stripes documentation optimizing the fraud integration is great, but what does this mean in a Broadleaf context?

  1. Use Stripe Elements for your frontend integration.

  2. Provide the following data points when defining your Stripe Payment in PaymentTransactionServices:

{
...
  "owningUserName": "...",
  "owningUserEmailAddress": "...",
  "shippingAddress": {...},
  "billingAddress": {...},
...
}

Manual Reviews

Allowed & blocked transactions are fairly straight-forward, & can generally be treated like normal transaction success or failure results. On the other hand, transactions flagged for manual review require significantly more attention.

When a transaction is flagged for manual review, this means that someone must navigate to the transaction in the Stripe dashboard, review the transaction data, & decide if the transaction is fraudulent or not. During that time, it’s best to restrict business processes related to this transaction. For example, if this is a checkout transaction, then you’ll likely want to avoid fulfilling the order if the payment is suspected fraud. If the payment is indeed fraudulent & you shipped the items, then you’re lost inventory & are required to return the funds.

Note
  • Flagging for manual review occurs after the transaction was executed. This means that the customer’s funds will be held while the review takes place.

  • If the review is not processed within 7 days, the review is automatically rejected & the transaction is reversed (i.e. reverse-authorized or refunded).

Recognizing Transactions Flagged for Manual Review

When a transaction is flagged for manual review, the following data points will declare this fact:

  • Coming out of the Stripe integration module…​

    • PaymentResponse#flaggedForManualReview.

  • In PaymentTransactionServices…​

    • PaymentTransaction#flaggedForManualReview

    • PaymentSummary#hasTransactionFlaggedForManualReview

  • In the PaymentTransactionServices database (i.e. the paymenttransaction schema)…​

    • BLC_PAYMENT_TRANSACTION.FLAGGED_FOR_MANUAL_REVIEW

  • Coming out of PaymentTransactionServices when an Authorize or AuthorizeAndCapture transaction has been executed…​

    • TransactionExecutionResponse#hasTransactionFlaggedForManualReview

    • TransactionExecutionDetail#flaggedForManualReview

Reviewing Transactions in Stripe Dashboard

All transactions flagged for manual review are placed into the review queue in the Stripe dashboard. From this view, you’ll have the ability to approve or reject the transaction, & execute subsequent Capture or Refund transactions as needed.

Note
  • It’s strongly suggested that these reviews are executed in a timely manner, since they likely delay later business processes.

  • Based on your post-checkout order management requirements, take time to consider what (if any) transactions you want your business executing from the Stripe dashboard & the implications of these interactions on your business processes. PaymentTransactionServices includes webhooks to become aware of these transactions, which will help to maintain an accurate representation of the state of the payment, but it’s up to you to decide how to manage business practices around these transactions.

  • When capturing funds, remember that Stripe doesn’t support multiple partial captures, so if you execute a partial capture, then the remaining authorized amount will be automatically released.

Gathering & Notifying Broadleaf Ecosystem of Review Results

To understand & record the results of a manual fraud review, we leverage webhook notifications sent to PaymentTransactionServices from Stripe.

Webhook Configuration

  • What needs to be setup in Stripe?

  • What needs to be setup in BLC?

Review Result Notifications (review.closed events)

When a review is approved or rejected, we’ll receive a review.closed event from Stripe. This allows us to declare flaggedForManualReview = false on the original Authorize or AuthorizeAndCapture transaction, & record a manualReviewResult (i.e. APPROVED or REJECTED).

Note
Rejected reviews are often paired with a charge.refunded event notifying us of the reversal of the original transaction. For Stripe, a charge.refunded event can represent a ReverseAuthorize or a Refund.
Example review.closed event
{
  "id": "evt_1MguVeA9wKNWChx1rWrJf0po",
  "object": "event",
  "api_version": "2022-11-15",
  "created": 1677695774,
  "data": {
    "object": {
      "id": "prv_1MguV7A9wKNWChx14KGnvc3g",
      "object": "review",
      "billing_zip": null,
      "charge": null,
      "closed_reason": "approved",
      "created": 1677695741,
      "ip_address": null,
      "ip_address_location": null,
      "livemode": false,
      "open": false,
      "opened_reason": "rule",
      "payment_intent": "pi_3MguV7A9wKNWChx11WIiaOOT",
      "reason": "approved",
      "session": null
    }
  },
  "livemode": false,
  "pending_webhooks": 3,
  "request": {
    "id": "req_ZajQM143CfsAuW",
    "idempotency_key": "3c6823b9-3620-4616-b20f-fedd98cdc890"
  },
  "type": "review.closed"
}

Capture Notifications (charge.captured events)

When a capture is triggered as part of the fraud review (i.e. capturing a payment that was originally only authorized), we’ll receive a charge.captured event. In this case, we’ll record an entirely new Capture PaymentTransaction in PaymentTransactionServices, tied to the same Payment.

Example charge.captured event
{
  "id": "evt_3Mgu4kA9wKNWChx10gqcPqj2",
  "object": "event",
  "api_version": "2022-11-15",
  "created": 1677694143,
  "data": {
    "object": {
      "id": "ch_3Mgu4kA9wKNWChx10svxcXyJ",
      "object": "charge",
      "amount": 100100,
      "amount_captured": 100100,
      "amount_refunded": 0,
      "application": null,
      "application_fee": null,
      "application_fee_amount": null,
      "balance_transaction": "txn_3Mgu4kA9wKNWChx10u0d3DM6",
      "billing_details": {
        "address": {
          "city": null,
          "country": null,
          "line1": null,
          "line2": null,
          "postal_code": null,
          "state": null
        },
        "email": null,
        "name": null,
        "phone": null
      },
      "calculated_statement_descriptor": "Stripe",
      "captured": true,
      "created": 1677694106,
      "currency": "usd",
      "customer": null,
      "description": null,
      "destination": null,
      "dispute": null,
      "disputed": false,
      "failure_balance_transaction": null,
      "failure_code": null,
      "failure_message": null,
      "fraud_details": {
      },
      "invoice": null,
      "livemode": false,
      "metadata": {
        "BROADLEAF_TRANSACTION_REF_ID": "01GTF5RYCE75TR0SBDV2TD0X1E",
        "BROADLEAF_TENANT_ID": "mytenant"
      },
      "on_behalf_of": null,
      "order": null,
      "outcome": {
        "network_status": "approved_by_network",
        "reason": "rule",
        "risk_level": "normal",
        "risk_score": 24,
        "rule": "ssr_1MgBZJA9wKNWChx1bmx7wB8R",
        "seller_message": "One of your rules placed this payment in manual review.",
        "type": "manual_review"
      },
      "paid": true,
      "payment_intent": "pi_3Mgu4kA9wKNWChx108g95UCp",
      "payment_method": "pm_1Mgu4jA9wKNWChx1fqClwL90",
      "payment_method_details": {
        "card": {
          "brand": "visa",
          "checks": {
            "address_line1_check": null,
            "address_postal_code_check": null,
            "cvc_check": "pass"
          },
          "country": "US",
          "exp_month": 7,
          "exp_year": 2023,
          "fingerprint": "O1UWb7kl3q7CTowj",
          "funding": "credit",
          "installments": null,
          "last4": "4242",
          "mandate": null,
          "network": "visa",
          "three_d_secure": null,
          "wallet": null
        },
        "type": "card"
      },
      "receipt_email": null,
      "receipt_number": null,
      "receipt_url": "https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xRW1NMDlBOXdLTldDaHgxKL-p_p8GMgY2myn8dI46LBaXnBfLkV_X8DHmhyMmZMC94QIvGf-dS2M7XKGSGWzDOQrcmYBl5NGLkdN0",
      "refunded": false,
      "review": null,
      "shipping": null,
      "source": null,
      "source_transfer": null,
      "statement_descriptor": null,
      "statement_descriptor_suffix": null,
      "status": "succeeded",
      "transfer_data": null,
      "transfer_group": null
    },
    "previous_attributes": {
      "amount_captured": 0,
      "balance_transaction": null,
      "captured": false,
      "receipt_url": "https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xRW1NMDlBOXdLTldDaHgxKL-p_p8GMgbl_L_FSvo6LBazCGjpSxR1QOU3OrhDeBCT6klgo-vc4lV6Z_iFI79iACFnJMbeBPwJdgSM"
    }
  },
  "livemode": false,
  "pending_webhooks": 1,
  "request": {
    "id": "req_HR2WEXhc36Ri3K",
    "idempotency_key": "795ad4c0-885f-4c26-b19d-7c049f015ab7"
  },
  "type": "charge.captured"
}

Refund Notifications (charge.refunded events)

When a refund is triggered as part of the fraud review, we’ll receive a charge.refunded event. In this case, we’ll record an entirely new Refund PaymentTransaction in PaymentTransactionServices, tied to the same Payment.

Example charge.refunded event
{
  "id": "evt_3Mgu4kA9wKNWChx10QZMMgfs",
  "object": "event",
  "api_version": "2022-11-15",
  "created": 1677695640,
  "data": {
    "object": {
      "id": "ch_3Mgu4kA9wKNWChx10svxcXyJ",
      "object": "charge",
      "amount": 100100,
      "amount_captured": 100100,
      "amount_refunded": 100100,
      "application": null,
      "application_fee": null,
      "application_fee_amount": null,
      "balance_transaction": "txn_3Mgu4kA9wKNWChx10u0d3DM6",
      "billing_details": {
        "address": {
          "city": null,
          "country": null,
          "line1": null,
          "line2": null,
          "postal_code": null,
          "state": null
        },
        "email": null,
        "name": null,
        "phone": null
      },
      "calculated_statement_descriptor": "Stripe",
      "captured": true,
      "created": 1677694106,
      "currency": "usd",
      "customer": null,
      "description": null,
      "destination": null,
      "dispute": null,
      "disputed": false,
      "failure_balance_transaction": null,
      "failure_code": null,
      "failure_message": null,
      "fraud_details": {
      },
      "invoice": null,
      "livemode": false,
      "metadata": {
        "BROADLEAF_TRANSACTION_REF_ID": "01GTF5RYCE75TR0SBDV2TD0X1E",
        "BROADLEAF_TENANT_ID": "mytenant"
      },
      "on_behalf_of": null,
      "order": null,
      "outcome": {
        "network_status": "approved_by_network",
        "reason": "rule",
        "risk_level": "normal",
        "risk_score": 24,
        "rule": "ssr_1MgBZJA9wKNWChx1bmx7wB8R",
        "seller_message": "One of your rules placed this payment in manual review.",
        "type": "manual_review"
      },
      "paid": true,
      "payment_intent": "pi_3Mgu4kA9wKNWChx108g95UCp",
      "payment_method": "pm_1Mgu4jA9wKNWChx1fqClwL90",
      "payment_method_details": {
        "card": {
          "brand": "visa",
          "checks": {
            "address_line1_check": null,
            "address_postal_code_check": null,
            "cvc_check": "pass"
          },
          "country": "US",
          "exp_month": 7,
          "exp_year": 2023,
          "fingerprint": "O1UWb7kl3q7CTowj",
          "funding": "credit",
          "installments": null,
          "last4": "4242",
          "mandate": null,
          "network": "visa",
          "three_d_secure": null,
          "wallet": null
        },
        "type": "card"
      },
      "receipt_email": null,
      "receipt_number": null,
      "receipt_url": "https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xRW1NMDlBOXdLTldDaHgxKJm1_p8GMgb9DV89lcU6LBYhgpHUZG-rWPKY5DPUZo6GB3LK8AoEHGMlpXUxbg3yQBHHRZFE6_jkUYHf",
      "refunded": true,
      "review": null,
      "shipping": null,
      "source": null,
      "source_transfer": null,
      "statement_descriptor": null,
      "statement_descriptor_suffix": null,
      "status": "succeeded",
      "transfer_data": null,
      "transfer_group": null
    },
    "previous_attributes": {
      "amount_refunded": 0,
      "receipt_url": "https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xRW1NMDlBOXdLTldDaHgxKJi1_p8GMgYsbQnf9PM6LBasVfzMitBR0AKbCKNoyfvKwooXKQP0QZ1s9bc__XcC6JZzzxLVTDO94IkC",
      "refunded": false
    }
  },
  "livemode": false,
  "pending_webhooks": 3,
  "request": {
    "id": "req_0LGpIdzkWPkoCP",
    "idempotency_key": "7840bfa9-71dd-49a0-bb84-566b98fdd1dd"
  },
  "type": "charge.refunded"
}

Notifying the Broadleaf Ecosystem of Review Results

Once the review.closed event is finished processing, a message containing the PaymentTransaction on the paymentTransactionWebhookOutput message channel. In this case, PaymentTransaction#flaggedForManualReview should be false & PaymentTransaction#manualReviewResult should have a value of APPROVED or REJECTED.

Local webhook testing

For the local testing, we recommend using the Ngrok to allow Stripe to post the request to your local environment.

  • Start up the supporting utility services with the docker compose - docker-compose up -d. See Starting up Supporting Utility Services

  • Run ngrok http https://heatclinic.localhost:8456/. This will create the tunnel to your local environment with the URL like https://91f3-176-115-97-245.eu.ngrok.io

  • Open the Stripe Webhooks and click Add an endpoint button

  • In the Endpoint URL paste the URL to the webhook - https://91f3-176-115-97-245.eu.ngrok.io/api/payment/webhooks/STRIPE?tenantId={tenantId}. Replace tenantId with an actual ID

  • Select next events to listen to: review.closed, charge.captured and charge.refunded

  • When the webhook is created you will be redirected to the webhook details page. Under the Signing secret click Reveal and copy the opened value.

  • Add the Signing secret via the next property broadleaf.stripe.rest.webhook-endpoint-secret=secret

  • Run Backend APIs

After that, your webhook is ready for testing. To check that it works create the payment with the card number 4000000000009235. This payment will be flagged for manual review. Got to the Stripe dashboard, find this payment and execute any actions. For example, Approve will send the review.closed event. Check that this event was successfully sent to your webhook endpoint. Check the transaction in the database, the column FLAGGED_FOR_MANUAL_REVIEW should have the value N.

Miscellaneous Notes

  • Transactions triggered by Broadleaf will have an idempotency key prepended with BLC-STRIPE-, while those triggered via the Stripe dashboard will not.