Skip to content

Commit

Permalink
use the PaymenMethod API for SEPA Direct Debit (#2376)
Browse files Browse the repository at this point in the history
first step of dealing with #2374
  • Loading branch information
Changaco authored May 23, 2024
1 parent cab4cad commit b1886c5
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 74 deletions.
111 changes: 58 additions & 53 deletions js/stripe.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,22 @@ Liberapay.stripe_form_init = function($form) {
});

var $container = $('#stripe-element');
var $postal_address_alert = $form.find('.msg-postal-address-required');
var $postal_address_country = $form.find('select[name="postal_address.country"]');
var $postal_address_region = $form.find('input[name="postal_address.region"]');
var $postal_address_city = $form.find('input[name="postal_address.city"]');
var $postal_address_code = $form.find('input[name="postal_address.postal_code"]');
var $postal_address_local = $form.find('textarea[name="postal_address.local_address"]');
function is_postal_address_filled() {
return $postal_address_country.val() > '' &&
$postal_address_city.val() > '' &&
$postal_address_code.val() > '' &&
$postal_address_local.val() > '';
}
function is_postal_address_required() {
return /AD|BL|CH|GB|GG|GI|IM|JE|MC|NC|PF|PM|SM|TF|VA|WF/.test($container.data('country'));
}

var stripe = Stripe($form.data('stripe-pk'));
var elements = stripe.elements();
var element_type = $container.data('type');
Expand All @@ -37,6 +53,12 @@ Liberapay.stripe_form_init = function($form) {
} else {
$errorElement.text('');
}
if (event.country) {
$container.data('country', event.country);
if (!is_postal_address_required()) {
$postal_address_alert.hide();
}
}
});

var submitting = false;
Expand Down Expand Up @@ -64,63 +86,46 @@ Liberapay.stripe_form_init = function($form) {
$form.submit();
return;
}
var local_address = $form.find('input[name="postal_address.local_address"]').val();
var pmType = element_type;
if (element_type == 'iban') {
pmType = 'sepa_debit';
if (is_postal_address_required() && !is_postal_address_filled()) {
$postal_address_alert.removeClass('hidden').hide().fadeIn()[0].scrollIntoView();
return;
}
}
var local_address = $postal_address_local.val();
local_address = !!local_address ? local_address.split(/(?:\r\n?|\n)/g) : [null];
if (local_address.length === 1) {
local_address.push(null);
}
if (element_type == 'iban') {
var tokenData = {};
tokenData.currency = 'EUR';
tokenData.account_holder_name = $form.find('input[name="owner.name"]').val();
tokenData.address_country = $form.find('input[name="postal_address.country"]').val();
tokenData.address_state = $form.find('input[name="postal_address.region"]').val();
tokenData.address_city = $form.find('input[name="postal_address.city"]').val();
tokenData.address_zip = $form.find('input[name="postal_address.postal_code"]').val();
tokenData.address_line1 = local_address[0];
tokenData.address_line2 = local_address[1];
stripe.createToken(element, tokenData).then(Liberapay.wrap(function(result) {
if (result.error) {
$errorElement.text(result.error.message);
} else {
submitting = true;
$form.find('input[name="route"]').remove();
$form.find('input[name="token"]').remove();
var $hidden_input = $('<input type="hidden" name="token">');
$hidden_input.val(result.token.id);
$form.append($hidden_input);
$form.submit();
}
}));
} else if (element_type == 'card') {
var pmData = {
billing_details: {
address: {
city: $form.find('input[name="postal_address.city"]').val(),
country: $form.find('input[name="postal_address.country"]').val(),
line1: local_address[0],
line2: local_address[1],
postal_code: $form.find('input[name="postal_address.postal_code"]').val(),
state: $form.find('input[name="postal_address.region"]').val(),
},
email: $form.find('input[name="owner.email"]').val(),
name: $form.find('input[name="owner.name"]').val(),
}
};
stripe.createPaymentMethod('card', element, pmData).then(Liberapay.wrap(function(result) {
if (result.error) {
$errorElement.text(result.error.message);
} else {
submitting = true;
$form.find('input[name="route"]').remove();
$form.find('input[name="stripe_pm_id"]').remove();
var $hidden_input = $('<input type="hidden" name="stripe_pm_id">');
$hidden_input.val(result.paymentMethod.id);
$form.append($hidden_input);
$form.submit();
}
}));
}
var pmData = {
billing_details: {
address: {
city: $postal_address_city.val(),
country: $postal_address_country.val(),
line1: local_address[0],
line2: local_address[1],
postal_code: $postal_address_code.val(),
state: $postal_address_region.val(),
},
email: $form.find('input[name="owner.email"]').val(),
name: $form.find('input[name="owner.name"]').val(),
}
};
stripe.createPaymentMethod(pmType, element, pmData).then(Liberapay.wrap(function(result) {
if (result.error) {
$errorElement.text(result.error.message);
} else {
submitting = true;
$form.find('input[name="route"]').remove();
$form.find('input[name="stripe_pm_id"]').remove();
var $hidden_input = $('<input type="hidden" name="stripe_pm_id">');
$hidden_input.val(result.paymentMethod.id);
$form.append($hidden_input);
$form.submit();
}
}));
}));
$form.attr('action', '');
};
Expand Down
38 changes: 35 additions & 3 deletions liberapay/models/exchange_route.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

from ..constants import CARD_BRANDS
from ..exceptions import InvalidId, TooManyAttempts
from ..utils import utcnow
from ..website import website


class ExchangeRoute(Model):
Expand Down Expand Up @@ -144,6 +146,35 @@ def attach_stripe_payment_method(cls, participant, pm, one_off):
country=pm_country, currency=pm_currency,
)
route.stripe_payment_method = pm
if network == 'stripe-sdd':
state = website.state.get()
request, response = state['request'], state['response']
user_agent = request.headers.get(b'User-Agent', b'')
try:
user_agent = user_agent.decode('ascii', 'backslashreplace')
except UnicodeError:
raise response.error(400, "User-Agent must be ASCII only")
si = stripe.SetupIntent.create(
confirm=True,
customer=route.remote_user_id,
mandate_data={
"customer_acceptance": {
"type": "online",
"accepted_at": int(utcnow().timestamp()),
"online": {
"ip_address": str(request.source),
"user_agent": user_agent,
},
},
},
metadata={"route_id": route.id},
payment_method=pm.id,
payment_method_types=[pm.type],
usage='off_session',
idempotency_key='create_SI_for_route_%i' % route.id,
)
route.set_mandate(si.mandate)
assert not si.next_action, si.next_action
return route

@classmethod
Expand Down Expand Up @@ -268,7 +299,7 @@ def get_brand(self):
return self.stripe_source.card.brand
elif self.network == 'stripe-sdd':
if self.address.startswith('pm_'):
raise NotImplementedError()
return getattr(self.stripe_payment_method.sepa_debit, 'bank_name', '')
else:
return getattr(self.stripe_source.sepa_debit, 'bank_name', '')
else:
Expand All @@ -292,7 +323,8 @@ def get_mandate_url(self):
return
elif self.network == 'stripe-sdd':
if self.address.startswith('pm_'):
raise NotImplementedError()
mandate = stripe.Mandate.retrieve(self.mandate)
return mandate.payment_method_details.sepa_debit.url
else:
return self.stripe_source.sepa_debit.mandate_url
else:
Expand All @@ -307,7 +339,7 @@ def get_partial_number(self):
elif self.network == 'stripe-sdd':
from ..payin.stripe import get_partial_iban
if self.address.startswith('pm_'):
raise NotImplementedError()
return get_partial_iban(self.stripe_payment_method.sepa_debit)
else:
return get_partial_iban(self.stripe_source.sepa_debit)
else:
Expand Down
11 changes: 8 additions & 3 deletions liberapay/payin/stripe.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,9 +367,14 @@ def send_payin_notification(db, payin, payer, charge, route):
"""
if route.network == 'stripe-sdd' and charge.status != 'failed':
if route.address.startswith('pm_'):
raise NotImplementedError()
sepa_debit = stripe.PaymentMethod.retrieve(route.address).sepa_debit
mandate = stripe.Mandate.retrieve(route.mandate)
mandate_url = mandate.payment_method_details.sepa_debit.url
mandate_reference = mandate.payment_method_details.sepa_debit.reference
else:
sepa_debit = stripe.Source.retrieve(route.address).sepa_debit
mandate_url = sepa_debit.mandate_url
mandate_reference = sepa_debit.mandate_reference
tippees = db.all("""
SELECT DISTINCT tippee_p.id AS tippee_id, tippee_p.username AS tippee_username
FROM payin_transfers pt
Expand All @@ -384,8 +389,8 @@ def send_payin_notification(db, payin, payer, charge, route):
payin_amount=payin.amount,
bank_name=getattr(sepa_debit, 'bank_name', None),
partial_bank_account_number=get_partial_iban(sepa_debit),
mandate_url=sepa_debit.mandate_url,
mandate_id=sepa_debit.mandate_reference,
mandate_url=mandate_url,
mandate_id=mandate_reference,
mandate_creation_date=route.ctime.date(),
creditor_identifier=website.app_conf.sepa_creditor_identifier,
average_settlement_seconds=PAYIN_SETTLEMENT_DELAYS['stripe-sdd'].total_seconds(),
Expand Down
5 changes: 5 additions & 0 deletions www/%username/giving/pay/stripe/%payin_id.spt
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,7 @@ title = _("Funding your donations")
</div>
% endif

<input type="hidden" name="owner.email" value="{{ payer.get_email_address() }}" />
% if payment_type == 'card'
<fieldset id="card-form" class="form-group {{ 'hidden' if routes else '' }}">
<p>{{ _("Please input your name and card number:") }}</p>
Expand Down Expand Up @@ -576,6 +577,10 @@ title = _("Funding your donations")
% endif

<br>
<output class="alert alert-danger hidden msg-postal-address-required">{{ _(
"Please fill in your postal address. It's required because the IBAN "
"you've provided emanates from outside the European Union."
) }}</output>
<button class="btn btn-primary btn-lg btn-block">{{ _(
"Initiate the payment"
) }}</button>
Expand Down
20 changes: 5 additions & 15 deletions www/%username/routes/add.spt
Original file line number Diff line number Diff line change
Expand Up @@ -15,33 +15,19 @@ if request.method == 'POST':
raise AccountSuspended()
body = request.body
one_off = body.get('one_off') == 'true'
return_url = participant.url('routes/')
try:
if 'token' in body:
owner_info = {
'email': participant.get_email_address(),
'name': body.get('owner.name'),
}
source = create_source_from_token(
body.word('token'), one_off, None, owner_info, return_url
body.word('token'), one_off, None, owner_info, participant.url('routes/')
)
route = ExchangeRoute.attach_stripe_source(participant, source, one_off)
else:
pm = stripe.PaymentMethod.retrieve(body.word('stripe_pm_id'))
route = ExchangeRoute.attach_stripe_payment_method(participant, pm, one_off)
si = stripe.SetupIntent.create(
confirm=True,
customer=route.remote_user_id,
payment_method=pm.id,
metadata={"route_id": route.id},
return_url=return_url,
usage='off_session',
idempotency_key='create_SI_for_route_%i' % route.id,
)
if si.next_action:
if si.next_action.type != 'redirect_to_url':
raise NotImplementedError(si.next_action.type)
raise response.redirect(si.next_action.redirect_to_url.url)
except stripe.error.StripeError as e:
raise response.error(e.http_status or 500, _(
"The payment processor {name} returned an error: “{error_message}”.",
Expand Down Expand Up @@ -143,6 +129,10 @@ title = _("Add a payment instrument")
postal_address_form_v2(saved=identity.get('postal_address'), required=False)
}}</div>
<br>
<output class="alert alert-danger hidden msg-postal-address-required">{{ _(
"Please fill in your postal address. It's required because the IBAN "
"you've provided emanates from outside the European Union."
) }}</output>
<button class="btn btn-primary btn-lg">{{ _("Save") }}</button>
</form>

Expand Down

0 comments on commit b1886c5

Please sign in to comment.