[IMP] payment_stripe: allow separate authorization and capture
Users who select Stripe as their payment acquirer will now have the possibility to capture manually the amount later (e.g. when delivering the product). task-2278434 closes odoo/odoo#69598 Related: odoo/upgrade#3290 Signed-off-by: Antoine Vandevenne (anv) <anv@odoo.com>
This commit is contained in:
@@ -320,6 +320,7 @@
|
||||
<li class="list-inline-item"><i class="fa fa-check"/>Payment Status Tracking</li>
|
||||
<li class="list-inline-item"><i class="fa fa-check"/>Subscriptions</li>
|
||||
<li class="list-inline-item"><i class="fa fa-check"/>Save Cards</li>
|
||||
<li class="list-inline-item"><i class="fa fa-check"/>Manual Capture</li>
|
||||
</ul>
|
||||
</field>
|
||||
<!--
|
||||
|
||||
@@ -23,6 +23,7 @@ PAYMENT_METHOD_TYPES = [
|
||||
INTENT_STATUS_MAPPING = {
|
||||
'draft': ('requires_payment_method', 'requires_confirmation', 'requires_action'),
|
||||
'pending': ('processing',),
|
||||
'authorized': ('requires_capture',),
|
||||
'done': ('succeeded',),
|
||||
'cancel': ('canceled',),
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<record id="payment.payment_acquirer_stripe" model="payment.acquirer">
|
||||
<field name="provider">stripe</field>
|
||||
<field name="support_authorization">False</field>
|
||||
<field name="support_authorization">True</field>
|
||||
<field name="support_fees_computation">False</field>
|
||||
<field name="support_refund"></field>
|
||||
<field name="support_tokenization">True</field>
|
||||
|
||||
@@ -5,7 +5,7 @@ import pprint
|
||||
|
||||
from werkzeug import urls
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo import _, fields, models
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
|
||||
from odoo.addons.payment import utils as payment_utils
|
||||
@@ -88,6 +88,7 @@ class PaymentTransaction(models.Model):
|
||||
# 1. attach the payment method to the created customer
|
||||
# 2. trigger a 3DS check if one if required, while the customer is still present
|
||||
future_usage = 'off_session' if self.tokenize else None
|
||||
capture_method = 'manual' if self.acquirer_id.capture_manually else 'automatic'
|
||||
checkout_session = self.acquirer_id._stripe_make_request(
|
||||
'checkout/sessions', payload={
|
||||
**common_session_values,
|
||||
@@ -102,6 +103,7 @@ class PaymentTransaction(models.Model):
|
||||
'line_items[0][quantity]': 1,
|
||||
'payment_intent_data[description]': self.reference,
|
||||
'payment_intent_data[setup_future_usage]': future_usage,
|
||||
'payment_intent_data[capture_method]': capture_method,
|
||||
}
|
||||
)
|
||||
self.stripe_payment_intent = checkout_session['payment_intent']
|
||||
@@ -173,18 +175,77 @@ class PaymentTransaction(models.Model):
|
||||
if self.provider != 'stripe':
|
||||
return
|
||||
|
||||
# Make the payment request to Stripe
|
||||
if not self.token_id:
|
||||
raise UserError("Stripe: " + _("The transaction is not linked to a token."))
|
||||
|
||||
# Make the payment request to Stripe
|
||||
payment_intent = self._stripe_create_payment_intent()
|
||||
feedback_data = {'reference': self.reference}
|
||||
StripeController._include_payment_intent_in_notification_data(payment_intent, feedback_data)
|
||||
_logger.info(
|
||||
"payment request response for transaction with reference %s:\n%s",
|
||||
self.reference, pprint.pformat(feedback_data)
|
||||
self.reference, pprint.pformat(payment_intent)
|
||||
)
|
||||
self._handle_notification_data('stripe', feedback_data)
|
||||
self.stripe_payment_intent = payment_intent['id']
|
||||
|
||||
# Handle the payment request response
|
||||
notification_data = {'reference': self.reference}
|
||||
StripeController._include_payment_intent_in_notification_data(
|
||||
payment_intent, notification_data
|
||||
)
|
||||
self._handle_notification_data('stripe', notification_data)
|
||||
|
||||
def _send_capture_request(self):
|
||||
""" Override of payment to send a capture request to Stripe.
|
||||
|
||||
Note: self.ensure_one()
|
||||
|
||||
:return: None
|
||||
"""
|
||||
super()._send_capture_request()
|
||||
if self.provider != 'stripe':
|
||||
return
|
||||
|
||||
# Make the capture request to Stripe
|
||||
payment_intent = self.acquirer_id._stripe_make_request(
|
||||
f'payment_intents/{self.stripe_payment_intent}/capture'
|
||||
)
|
||||
_logger.info(
|
||||
"capture request response for transaction with reference %s:\n%s",
|
||||
self.reference, pprint.pformat(payment_intent)
|
||||
)
|
||||
|
||||
# Handle the capture request response
|
||||
notification_data = {'reference': self.reference}
|
||||
StripeController._include_payment_intent_in_notification_data(
|
||||
payment_intent, notification_data
|
||||
)
|
||||
self._handle_notification_data('stripe', notification_data)
|
||||
|
||||
def _send_void_request(self):
|
||||
""" Override of payment to send a void request to Stripe.
|
||||
|
||||
Note: self.ensure_one()
|
||||
|
||||
:return: None
|
||||
"""
|
||||
super()._send_void_request()
|
||||
if self.provider != 'stripe':
|
||||
return
|
||||
|
||||
# Make the void request to Stripe
|
||||
payment_intent = self.acquirer_id._stripe_make_request(
|
||||
f'payment_intents/{self.stripe_payment_intent}/cancel'
|
||||
)
|
||||
_logger.info(
|
||||
"void request response for transaction with reference %s:\n%s",
|
||||
self.reference, pprint.pformat(payment_intent)
|
||||
)
|
||||
|
||||
# Handle the void request response
|
||||
notification_data = {'reference': self.reference}
|
||||
StripeController._include_payment_intent_in_notification_data(
|
||||
payment_intent, notification_data
|
||||
)
|
||||
self._handle_notification_data('stripe', notification_data)
|
||||
|
||||
def _stripe_create_payment_intent(self):
|
||||
""" Create and return a PaymentIntent.
|
||||
@@ -231,6 +292,7 @@ class PaymentTransaction(models.Model):
|
||||
'off_session': True,
|
||||
'payment_method': self.token_id.stripe_payment_method,
|
||||
'description': self.reference,
|
||||
'capture_method': 'manual' if self.acquirer_id.capture_manually else 'automatic',
|
||||
}
|
||||
|
||||
def _get_tx_from_notification_data(self, provider, notification_data):
|
||||
@@ -292,6 +354,10 @@ class PaymentTransaction(models.Model):
|
||||
pass
|
||||
elif intent_status in INTENT_STATUS_MAPPING['pending']:
|
||||
self._set_pending()
|
||||
elif intent_status in INTENT_STATUS_MAPPING['authorized']:
|
||||
if self.tokenize:
|
||||
self._stripe_tokenize_from_notification_data(notification_data)
|
||||
self._set_authorized()
|
||||
elif intent_status in INTENT_STATUS_MAPPING['done']:
|
||||
if self.tokenize:
|
||||
self._stripe_tokenize_from_notification_data(notification_data)
|
||||
|
||||
@@ -30,6 +30,36 @@ class StripeTest(StripeCommon, PaymentHttpCommon):
|
||||
self.assertEqual(processing_values['publishable_key'], self.stripe.stripe_publishable_key)
|
||||
self.assertEqual(processing_values['session_id'], dummy_session_id)
|
||||
|
||||
@mute_logger('odoo.addons.payment_stripe.models.payment_transaction')
|
||||
def test_tx_state_after_send_capture_request(self):
|
||||
self.acquirer.capture_manually = True
|
||||
tx = self.create_transaction('redirect', state='authorized')
|
||||
|
||||
with patch(
|
||||
'odoo.addons.payment_stripe.models.payment_acquirer.PaymentAcquirer'
|
||||
'._stripe_make_request',
|
||||
return_value={'status': 'succeeded'},
|
||||
):
|
||||
tx._send_capture_request()
|
||||
self.assertEqual(
|
||||
tx.state, 'done', msg="The state should be 'done' after a successful capture."
|
||||
)
|
||||
|
||||
@mute_logger('odoo.addons.payment_stripe.models.payment_transaction')
|
||||
def test_tx_state_after_send_void_request(self):
|
||||
self.acquirer.capture_manually = True
|
||||
tx = self.create_transaction('redirect', state='authorized')
|
||||
|
||||
with patch(
|
||||
'odoo.addons.payment_stripe.models.payment_acquirer.PaymentAcquirer'
|
||||
'._stripe_make_request',
|
||||
return_value={'status': 'canceled'},
|
||||
):
|
||||
tx._send_void_request()
|
||||
self.assertEqual(
|
||||
tx.state, 'cancel', msg="The state should be 'cancel' after voiding the transaction."
|
||||
)
|
||||
|
||||
@mute_logger('odoo.addons.payment_stripe.controllers.main')
|
||||
def test_webhook_notification_confirms_transaction(self):
|
||||
""" Test the processing of a webhook notification. """
|
||||
|
||||
Reference in New Issue
Block a user