basic support for Stripe Custom accounts

This commit is contained in:
Changaco 2024-06-04 09:27:07 +02:00
parent 9aa647a6f0
commit 209600b217
No known key found for this signature in database
16 changed files with 540 additions and 55 deletions

View File

@ -16,6 +16,7 @@ Liberapay.init = function() {
Liberapay.lookup.init();
Liberapay.s3_uploader_init();
Liberapay.stripe_init();
Liberapay.stripe_connect.init();
$('div[href]').css('cursor', 'pointer').on('click auxclick', function(event) {
if (event.target.tagName == 'A') {

97
js/stripe-connect.js Normal file
View File

@ -0,0 +1,97 @@
Liberapay.stripe_connect = {};
Liberapay.stripe_connect.init = function() {
var container = document.getElementById('stripe-connect');
if (!container) return;
async function fetchClientSecret() {
try {
var response = await fetch('', {
headers: {
"Accept": "application/json",
"X-CSRF-TOKEN": container.dataset.csrfToken,
},
method: "POST",
});
if (response.ok) {
return (await response.json()).client_secret;
} else {
Liberapay.error(response.status);
}
} catch(exc) {
Liberapay.error(exc);
return undefined;
}
};
const self = Liberapay.stripe_connect;
self.components = {};
const component_nav = document.getElementById('stripe-component-nav');
var target_component_name;
if (component_nav) {
target_component_name = 'account-management';
component_nav.querySelector(
'a[data-component="' + target_component_name + '"]'
).parentElement.classList.add('active');
component_nav.classList.remove('hidden');
const component_nav_links = component_nav.querySelectorAll('a');
component_nav_links.forEach((a) => {
a.addEventListener('click', (e) => {
e.preventDefault();
component_nav_links.forEach((a) => {
a.parentElement.classList.remove('active')
});
a.parentElement.classList.add('active');
const component_name = a.dataset.component;
if (self.components[component_name]) {
self.components[component_name].classList.remove('hidden');
} else {
self.components[component_name] = self.instance.create(
component_name
);
container.appendChild(self.components[component_name]);
}
self.current_component.classList.add('hidden');
self.current_component = self.components[component_name];
container.scrollIntoView();
});
});
} else {
target_component_name = 'account-onboarding';
}
if (!window.StripeConnect) {
alert(container.dataset.msgStripeMissing);
return;
}
StripeConnect.onLoad = () => {
self.instance = StripeConnect.init({
appearance: {
variables: {
colorPrimary: '#337ab7',
colorText: rgb_to_hex($(container).css('color')),
fontFamily: $(container).css('font-family'),
fontSizeBase: $(container).css('font-size'),
},
},
fetchClientSecret: fetchClientSecret,
fonts: [{cssSrc: 'https://liberapay.com/assets/fonts.css'}],
locale: document.documentElement.getAttribute('lang'),
publishableKey: container.dataset.stripePubKey,
});
const notification_div = document.getElementById('stripe-notification');
if (notification_div) {
notification_div.appendChild(self.instance.create('notification-banner'));
}
var component = self.instance.create(target_component_name);
self.current_component = self.components[target_component_name] = component;
if (target_component_name == 'account-onboarding') {
component.setOnExit(() => {
window.location = location.href.replace(
/\/stripe\/onboard\/?\?.+$/, '/'
);
});
}
container.appendChild(self.current_component);
}
};

View File

@ -1,11 +1,6 @@
Liberapay.stripe_init = function() {
var $form = $('form#stripe');
if ($form.length === 1) Liberapay.stripe_form_init($form);
var $next_action = $('#stripe_next_action');
if ($next_action.length === 1) Liberapay.stripe_next_action($next_action);
};
Liberapay.stripe_form_init = function($form) {
if ($form.length !== 1) return;
$('fieldset.hidden').prop('disabled', true);
$('button[data-modify]').click(function() {
var $btn = $(this);
@ -13,6 +8,15 @@ Liberapay.stripe_form_init = function($form) {
$btn.parent().addClass('hidden');
});
var $errorElement = $('#stripe-errors');
var stripe = null;
if (window.Stripe) {
stripe = Stripe($form.data('stripe-pk'));
} else {
$errorElement.text($form.attr('data-msg-stripe-missing'));
$errorElement.hide().fadeIn()[0].scrollIntoView();
}
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"]');
@ -32,10 +36,7 @@ Liberapay.stripe_form_init = function($form) {
);
}
var $errorElement = $('#stripe-errors');
var stripe = null;
if (window.Stripe) {
stripe = Stripe($form.data('stripe-pk'));
if ($container.length === 1) {
var elements = stripe.elements({
onBehalfOf: $form.data('stripe-on-behalf-of') || undefined,
});
@ -66,14 +67,29 @@ Liberapay.stripe_form_init = function($form) {
}
}
});
} else {
$errorElement.text($form.attr('data-msg-stripe-missing'));
}
Liberapay.stripe_before_submit = async function() {
Liberapay.stripe_before_account_submit = async function() {
const response = await stripe.createToken('account', {
tos_shown_and_accepted: true,
});
if (response.token) {
$form.find('input[name="account_token"]').remove();
var $pm_id_input = $('<input type="hidden" name="account_token">');
$pm_id_input.val(response.token.id);
$pm_id_input.appendTo($form);
return true;
} else {
$errorElement.text(response.error || response);
$errorElement.hide().fadeIn()[0].scrollIntoView();
return false;
}
}
Liberapay.stripe_before_element_submit = async function() {
// If the Payment Element is hidden, simply let the browser submit the form
if ($container.parents('.hidden').length > 0) {
console.debug("stripe_before_submit: ignoring hidden payment element");
console.debug("stripe_before_element_submit: ignoring hidden payment element");
return true;
}
// If Stripe.js is missing, stop the submission
@ -125,14 +141,4 @@ Liberapay.stripe_form_init = function($form) {
return false;
}
};
Liberapay.stripe_next_action = function ($next_action) {
stripe.handleCardAction($next_action.data('client_secret')).then(function (result) {
if (result.error) {
$next_action.addClass('alert alert-danger').text(result.error.message);
} else {
window.location.reload();
}
})
};
};

View File

@ -48,7 +48,7 @@ from liberapay.payin.cron import (
send_upcoming_debit_notifications,
)
from liberapay.security import authentication, csrf, set_default_security_headers
from liberapay.security.csp import csp_allow, csp_allow_stripe
from liberapay.security.csp import csp_allow, csp_allow_stripe, csp_allow_stripe_connect
from liberapay.utils import (
b64decode_s, b64encode_s, erase_cookie, http_caching, set_cookie,
)
@ -412,6 +412,10 @@ if hasattr(pando.Response, 'csp_allow_stripe'):
raise Warning('pando.Response.csp_allow_stripe() already exists')
pando.Response.csp_allow_stripe = csp_allow_stripe
if hasattr(pando.Response, 'csp_allow_stripe_connect'):
raise Warning('pando.Response.csp_allow_stripe_connect() already exists')
pando.Response.csp_allow_stripe_connect = csp_allow_stripe_connect
if hasattr(pando.Response, 'encode_url'):
raise Warning('pando.Response.encode_url() already exists')
def _encode_url(url):

View File

@ -462,6 +462,7 @@ def resolve_team_donation(
AND a.country IN %(SEPA)s
ORDER BY a.participant
, a.default_currency = %(currency)s DESC
, a.country = %(payer_country)s DESC
, a.connection_ts
""", dict(locals(), SEPA=SEPA, member_ids={t.member for t in takes}))}
if sepa_only or len(sepa_accounts) > 1 and takes[0].member in sepa_accounts:

View File

@ -43,3 +43,14 @@ def csp_allow_stripe(response) -> None:
(b'frame-src', b"*.js.stripe.com js.stripe.com hooks.stripe.com"),
(b'script-src', b"*.js.stripe.com js.stripe.com"),
)
def csp_allow_stripe_connect(response) -> None:
# https://docs.stripe.com/security/guide?csp=csp-connect#content-security-policy
csp_allow(
response,
(b'frame-src', b"connect-js.stripe.com js.stripe.com"),
(b'img-src', b"*.stripe.com"),
(b'script-src', b"connect-js.stripe.com js.stripe.com"),
(b'style-src', b"sha256-0hAheEzaMe6uXIKV4EehS9pu1am1lj/KnnzrOYqckXk="),
)

View File

@ -398,15 +398,17 @@ class Harness(unittest.TestCase):
data.setdefault('verified', True)
data.setdefault('display_name', None)
data.setdefault('token', None)
data.setdefault('independent', True)
data.setdefault('loss_taker', 'provider')
data.update(p_id=participant.id, provider=provider, country=country)
r = self.db.one("""
INSERT INTO payment_accounts
(participant, provider, country, id,
default_currency, charges_enabled, verified,
display_name, token)
display_name, token, independent, loss_taker)
VALUES (%(p_id)s, %(provider)s, %(country)s, %(id)s,
%(default_currency)s, %(charges_enabled)s, %(verified)s,
%(display_name)s, %(token)s)
%(display_name)s, %(token)s, %(independent)s, %(loss_taker)s)
RETURNING *
""", data)
participant.set_attributes(payment_providers=self.db.one("""

14
sql/branch.sql Normal file
View File

@ -0,0 +1,14 @@
BEGIN;
CREATE TYPE loss_taker AS ENUM ('provider', 'platform');
ALTER TABLE payment_accounts
ADD COLUMN independent boolean DEFAULT true,
ADD COLUMN loss_taker loss_taker DEFAULT 'provider',
ADD COLUMN details_submitted boolean,
ADD COLUMN allow_payout boolean;
END;
SELECT 'after deployment';
BEGIN;
ALTER TABLE payment_accounts
ALTER COLUMN independent DROP DEFAULT,
ALTER COLUMN loss_taker DROP DEFAULT;
END;

View File

@ -165,8 +165,6 @@ elif payin_id:
except NextAction as act:
if act.type == 'redirect_to_url':
response.refresh(state, url=act.redirect_to_url.url)
elif act.type == 'use_stripe_sdk':
client_secret = act.client_secret
else:
raise NotImplementedError(act.type)
tell_payer_to_fill_profile = (
@ -256,13 +254,7 @@ title = _("Funding your donations")
% block thin_content
% if client_secret is defined
<p>{{ _("More information is required in order to process this payment.") }}</p>
<noscript><div class="alert alert-danger">{{ _("JavaScript is required") }}</div></noscript>
<div id="stripe_next_action" data-client_secret="{{ client_secret }}"></div>
% elif payin is defined
% if payin is defined
% set status = payin.status
% if status == 'succeeded'
<div class="alert alert-success">{{ _(
@ -354,7 +346,7 @@ title = _("Funding your donations")
<noscript><div class="alert alert-danger">{{ _("JavaScript is required") }}</div></noscript>
<form action="javascript:" method="POST" id="stripe"
data-before-submit="call:Liberapay.stripe_before_submit"
data-before-submit="call:Liberapay.stripe_before_element_submit"
data-msg-stripe-missing='{{ _(
"The initialization of a required component has failed. If you use a "
"browser extension that blocks requests, for example NoScript, please "

View File

@ -18,7 +18,7 @@ if request.method == 'POST':
AND pk = %s
RETURNING *
""", (participant.id, account_pk))
if account and account.provider == 'stripe':
if account and account.provider == 'stripe' and account.independent:
try:
stripe.oauth.OAuth.deauthorize(stripe_user_id=account.id)
except stripe.oauth_error.InvalidClientError as e:
@ -190,17 +190,41 @@ subhead = _("Payment Processors")
% endif
</p>
<p class="text-muted">{{ _("Added on {date}", date=account.connection_ts.date()) }}</p>
% if not account.charges_enabled
<p class="text-warning">{{ icon('exclamation-sign') }} {{ _(
"This account cannot receive payments. To fix this, log in to the "
"account and complete the verification process. After that, reconnect "
"the account if you still see this message."
) }}</p>
% if account.independent
% if not account.charges_enabled
<p class="text-warning">{{ icon('exclamation-sign') }} {{ _(
"This account cannot receive payments. To fix this, log in to the "
"account and complete the verification process. After that, reconnect "
"the account if you still see this message."
) }}</p>
% endif
<a href="https://dashboard.stripe.com/{{ account.id }}" target="_blank" rel="noopener noreferrer">{{
icon("external-link") }} {{ _(
"Manage this {platform} account", platform="Stripe"
) }}</a>
% elif account.details_submitted
% if not account.charges_enabled
<p class="text-warning">{{ icon('exclamation-sign') }} {{ _(
"This account cannot receive payments. To fix this, click "
"on the link below and complete the verification process."
) }}</p>
% endif
<a href="{{ participant.path('payment/stripe/manage?sn=%s' % account.pk) }}">{{
icon("gear") }} {{ _(
"Manage this {platform} account", platform="Stripe"
) }}</a>
% else
% if not account.charges_enabled
<p class="text-warning">{{ icon('exclamation-sign') }} {{ _(
"This account cannot receive payments. To fix this, click "
"on the link below and complete the activation process."
) }}</p>
% endif
<a href="{{ participant.path('payment/stripe/onboard?sn=%s' % account.pk) }}">{{
icon("manual") }} {{ _(
"Activate this {platform} account", platform="Stripe"
) }}</a>
% endif
<a href="https://dashboard.stripe.com/{{ account.id }}" target="_blank" rel="noopener noreferrer">{{
icon("external-link") }} {{ _(
"Manage this {platform} account", platform="Stripe"
) }}</a>
</form>
% endfor
</div>

View File

@ -0,0 +1,146 @@
import stripe
from liberapay.utils import get_participant
[---]
participant = get_participant(state, restrict=True, allow_member=False)
if request.method == 'POST':
if user != participant:
raise response.error(403)
country = request.body.get_choice('country', constants.PAYOUT_COUNTRIES['stripe'])
country_in_sepa = country in constants.SEPA
account_token_id = request.body['account_token']
if website.app_conf.stripe_secret_key.startswith('sk_test_'):
profile_url = f"https://fake.liberapay.com/{participant.username}"
else:
profile_url = participant.url()
account = stripe.Account.create(
account_token=account_token_id,
business_profile={
"url": profile_url,
},
controller={
"fees": {
"payer": "application",
},
"losses": {
"payments": "application",
},
"requirement_collection": "application",
"stripe_dashboard": {
"type": "none",
},
},
capabilities={
"transfers": {"requested": True},
} if country_in_sepa else {
"card_payments": {"requested": True},
"transfers": {"requested": True},
},
country=country,
metadata={
"participant_id": str(participant.id),
},
settings={
"payouts": {
"debit_negative_balances": True,
"schedule": {
"interval": "manual",
},
},
},
idempotency_key=f"create_{country}_account_for_{participant.id}",
)
independent = (
account.type == 'standard' or
account.controller.stripe_dashboard.type != "none"
)
if account.type == 'standard' or account.controller.losses.payments == 'stripe':
loss_taker = 'provider'
else:
loss_taker = 'platform'
serial_number = website.db.one("""
UPDATE payment_accounts
SET is_current = NULL
WHERE participant = %(p_id)s
AND provider = 'stripe'
AND country = %(country)s;
INSERT INTO payment_accounts
(participant, provider, country, id,
default_currency, charges_enabled, verified,
display_name, independent, loss_taker)
VALUES (%(p_id)s, 'stripe', %(country)s, %(account_id)s,
%(default_currency)s, %(charges_enabled)s, true,
%(display_name)s, %(independent)s, %(loss_taker)s)
ON CONFLICT (provider, id, participant) DO UPDATE
SET is_current = true
, country = excluded.country
, default_currency = excluded.default_currency
, charges_enabled = excluded.charges_enabled
, verified = true
, display_name = excluded.display_name
, independent = excluded.independent
, loss_taker = excluded.loss_taker
RETURNING pk;
""", dict(
p_id=participant.id,
country=country,
account_id=account.id,
default_currency=account.default_currency.upper(),
charges_enabled=account.charges_enabled,
display_name=account.settings.dashboard.display_name,
independent=independent,
loss_taker=loss_taker,
))
raise response.redirect(participant.path(
f'payment/stripe/onboard?sn={serial_number}'
))
title = _("Create a {provider} account", provider='Stripe')
[---] text/html
% extends "templates/layouts/settings.html"
% block content
<noscript><div class="alert alert-danger">{{ _("JavaScript is required") }}</div></noscript>
<output id="stripe-errors" class="alert alert-danger"></output>
<form action="javascript:" method="POST" id="stripe"
data-before-submit="call:Liberapay.stripe_before_account_submit"
data-msg-submitting="{{ _('Request in progress, please wait…') }}"
data-stripe-pk="{{ website.app_conf.stripe_publishable_key }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
<p>{{ _(
"Please select the country you live in or have a registered business "
"in, and confirm that you agree to Stripe's terms."
) }}</p>
<div class="form-group form-inline">
<select name="country" class="form-control country" required>
% set country = user.guessed_country
% if country not in constants.PAYOUT_COUNTRIES['stripe']
<option></option>
% endif
% for code, name in locale.countries.items() if code in constants.PAYOUT_COUNTRIES['stripe']
<option value="{{ code }}" {{ 'selected' if code == country }}>{{ name }}</option>
% endfor
</select>
</div>
<p class="form-group"><label>
<input type="checkbox" name="tos_shown_and_accepted" value="true" required />
{{ _("I agree to the {link_start}Stripe Connected Account Agreement{link_end}.",
link_start='<a href="https://stripe.com/connect-account/legal" target="_blank" rel="noopener noreferrer">'|safe,
link_end='</a>'|safe) }}
</label></p>
<div class="buttons">
<button class="btn btn-primary">{{ _("Create account") }}</button>
<a class="btn btn-default" href="{{ participant.path('payment/') }}">{{ _("Go back") }}</a>
</div>
</form>
% endblock
% block scripts
% do response.csp_allow_stripe()
<script src="https://js.stripe.com/v3/"></script>
% endblock

View File

@ -0,0 +1,106 @@
import stripe
from liberapay.utils import get_participant
[---]
participant = get_participant(state, restrict=True, allow_member=False)
user.require_write_permission()
if user != participant:
raise response.error(403)
payment_account = website.db.one("""
SELECT *
FROM payment_accounts
WHERE participant = %s
AND pk = %s
""", (participant.id, request.qs.get_int('sn')))
if not payment_account:
raise response.error(400, "invalid `sn` value in querystring")
if not payment_account.details_submitted:
account = stripe.Account.retrieve(payment_account.id)
if account.details_submitted:
payment_account = website.db.one("""
UPDATE payment_accounts
SET details_submitted = true
, charges_enabled = %s
, display_name = %s
WHERE pk = %s
RETURNING *
""", (
account.charges_enabled,
account.settings.dashboard.display_name,
payment_account.pk
))
else:
response.redirect(participant.path(f'payment/stripe/onboard?sn={payment_account.pk}'))
if request.method == 'POST':
allow_payout = bool(user.has_privilege('admin')) or payment_account.allow_payout
account_session = stripe.AccountSession.create(
account=payment_account.id,
components={
"account_management": {
"enabled": True,
"features": {
"disable_stripe_user_authentication": True,
},
},
"notification_banner": {
"enabled": True,
"features": {
"disable_stripe_user_authentication": True,
},
},
"payments": {
"enabled": True,
"features": {
"capture_payments": False,
"dispute_management": False,
"refund_management": False,
},
},
"payouts": {
"enabled": True,
"features": {
"disable_stripe_user_authentication": True,
"edit_payout_schedule": allow_payout,
"instant_payouts": allow_payout,
"standard_payouts": allow_payout,
},
},
},
)
raise response.json({"client_secret": account_session.client_secret})
title = _("Manage a {provider} account", provider='Stripe')
[---] text/html
% extends "templates/layouts/settings.html"
% block content
<noscript><p class="alert alert-danger">{{ _("JavaScript is required") }}</p></noscript>
<div id="stripe-notification"></div>
<nav id="stripe-component-nav" class="hidden">
<ul class="nav nav-pills">
<li><a data-component="account-management" href="javascript:">{{ _("Account") }}</a></li>
<li><a data-component="payments" href="javascript:">{{ _("Payments") }}</a></li>
<li><a data-component="payouts" href="javascript:">{{ _("Payouts") }}</a></li>
</ul>
</nav>
<br>
<div id="stripe-connect" data-csrf-token="{{ csrf_token }}"
data-msg-stripe-missing='{{ _(
"The initialization of a required component has failed. If you use a "
"browser extension that blocks requests, for example NoScript, please "
"make sure its allowing requests to the “stripe.com” domain."
) }}'
data-stripe-pub-key="{{ website.app_conf.stripe_publishable_key }}"></div>
<br><br>
<a class="btn btn-default" href="{{ participant.path('payment') }}">{{ _("Go back") }}</a>
% endblock
% block scripts
% do response.csp_allow_stripe_connect()
<script src="https://connect-js.stripe.com/v1.0/connect.js"></script>
% endblock

View File

@ -0,0 +1,61 @@
import stripe
from liberapay.utils import get_participant
[---]
participant = get_participant(state, restrict=True, allow_member=False)
user.require_write_permission()
if user != participant:
raise response.error(403)
payment_account = website.db.one("""
SELECT *
FROM payment_accounts
WHERE participant = %s
AND pk = %s
""", (participant.id, request.qs.get_int('sn')))
if not payment_account:
raise response.error(400, "invalid `sn` value in querystring")
account = stripe.Account.retrieve(payment_account.id)
if account.details_submitted:
response.redirect(participant.path(f'payment/stripe/manage?sn={payment_account.pk}'))
if request.method == 'POST':
account_session = stripe.AccountSession.create(
account=payment_account.id,
components={
"account_onboarding": {
"enabled": True,
"features": {
"disable_stripe_user_authentication": True,
},
},
},
)
raise response.json({"client_secret": account_session.client_secret})
title = _("Activate a {provider} account", provider='Stripe')
[---] text/html
% from 'templates/macros/nav.html' import querystring_nav with context
% extends "templates/layouts/settings.html"
% block content
<noscript><p class="alert alert-danger">{{ _("JavaScript is required") }}</p></noscript>
<div id="stripe-connect" data-csrf-token="{{ csrf_token }}"
data-msg-stripe-missing='{{ _(
"The initialization of a required component has failed. If you use a "
"browser extension that blocks requests, for example NoScript, please "
"make sure its allowing requests to the “stripe.com” domain."
) }}'
data-stripe-pub-key="{{ website.app_conf.stripe_publishable_key }}"></div>
<br><br>
<a class="btn btn-default" href="{{ participant.path('payment') }}">{{ _("Go back") }}</a>
% endblock
% block scripts
% do response.csp_allow_stripe_connect()
<script src="https://connect-js.stripe.com/v1.0/connect.js"></script>
% endblock

View File

@ -52,7 +52,7 @@ title = _("Add a payment instrument")
<noscript><div class="alert alert-danger">{{ _("JavaScript is required") }}</div></noscript>
<form action="javascript:" method="POST" id="stripe"
data-before-submit="call:Liberapay.stripe_before_submit"
data-before-submit="call:Liberapay.stripe_before_element_submit"
data-msg-stripe-missing='{{ _(
"The initialization of a required component has failed. If you use a "
"browser extension that blocks requests, for example NoScript, please "

View File

@ -130,6 +130,8 @@ elif 'state' in request.qs:
charges_enabled=None,
display_name=user_info['name'],
token=token_response_data,
independent=True,
loss_taker='provider',
)
elif provider_name == 'stripe':
data_from_stripe = token_response.json()
@ -139,6 +141,16 @@ elif 'state' in request.qs:
))
account_id = data_from_stripe['stripe_user_id']
account = stripe.Account.retrieve(account_id)
independent = (
account.type == 'standard' or
account.controller.stripe_dashboard.type != "none"
)
assert independent
if account.type == 'standard' or account.controller.losses.payments == 'stripe':
loss_taker = 'provider'
else:
loss_taker = 'platform'
assert loss_taker == 'provider'
account_data.update(
country=account.country,
account_id=account_id,
@ -146,6 +158,8 @@ elif 'state' in request.qs:
charges_enabled=account.charges_enabled,
display_name=account.settings.dashboard.display_name,
token=None,
independent=independent,
loss_taker=loss_taker,
)
else:
raise ValueError(provider_name)
@ -180,10 +194,10 @@ elif 'state' in request.qs:
INSERT INTO payment_accounts
(participant, provider, country, id,
default_currency, charges_enabled, verified,
display_name, token)
display_name, token, independent, loss_taker)
VALUES (%(p_id)s, %(provider)s, %(country)s, %(account_id)s,
%(default_currency)s, %(charges_enabled)s, true,
%(display_name)s, %(token)s)
%(display_name)s, %(token)s, %(independent)s, %(loss_taker)s)
ON CONFLICT (provider, id, participant) DO UPDATE
SET is_current = true
, country = excluded.country
@ -192,7 +206,9 @@ elif 'state' in request.qs:
, verified = true
, authorized = true
, display_name = excluded.display_name
, token = excluded.token;
, token = excluded.token
, independent = excluded.independent
, loss_taker = excluded.loss_taker;
""", account_data)
response.erase_cookie(cookie_name)

View File

@ -32,15 +32,19 @@ if request.method == 'POST':
AND country = %(country)s;
INSERT INTO payment_accounts
(participant, provider, country, id, verified)
VALUES (%(p_id)s, 'paypal', %(country)s, %(account_id)s, %(verified)s)
(participant, provider, country, id, verified,
independent, loss_taker)
VALUES (%(p_id)s, 'paypal', %(country)s, %(account_id)s, %(verified)s,
true, 'provider')
ON CONFLICT (provider, id, participant) DO UPDATE
SET is_current = true
, country = excluded.country
, default_currency = excluded.default_currency
, charges_enabled = excluded.charges_enabled
, display_name = excluded.display_name
, token = excluded.token;
, token = excluded.token
, independent = excluded.independent
, loss_taker = excluded.loss_taker;
""", account_data)
response.redirect(participant.path('payment'))