{
...
"owningUserName": "...",
"owningUserEmailAddress": "...",
"shippingAddress": {...},
"billingAddress": {...},
...
}
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.
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?
Use Stripe Elements for your frontend integration.
Provide the following data points when defining your Stripe Payment in PaymentTransactionServices:
{
...
"owningUserName": "...",
"owningUserEmailAddress": "...",
"shippingAddress": {...},
"billingAddress": {...},
...
}
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
|
|
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
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
|
|
To understand & record the results of a manual fraud review, we leverage webhook notifications sent to PaymentTransactionServices from Stripe.
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.
|
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"
}
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.
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"
}
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.
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"
}
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
.
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
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
.