Stripe Connect: limit the number of steps during the account linking onboarding - stripe-payments

I'm testing Stripe Connect to allow users to pay other users. I'm testing it with the Stripe Java library (in Kotlin).
As the documentation suggest I need the users that will receive money to create a Stripe account with the Account.create API, and then to link the account with the AccountLink.create API.
When the user go through the Stripe onboarding flow (the Stripe url received by the AccountLink.create API) they are required to input a lot of data in many steps.
This steps are:
01: "Get started with Stripe": user is asked to enater the email
02: "Create your free Stripe account": user need to enter email and password
03: "Secure your account with a mobile number": enable the 2FA
04: "Let’s start with some basics": user is asket to enter the Business location and the Type of business
05: "Verify your personal details": user is asked to enter Name, Address, Email address and Phone number
06: "Professional details": the user have to enter VAT number (optional) and select an Industry
07: "Add your bank to receive payouts": the user have to enter Currency and Bank details
08: "Add public details for customers": the user have to enter the "Statement descriptor", "Shortened descriptor" and the "Customer support phone number"
09: "Show customers your climate commitment": the user need to select, or skip, a climate contribution
10: "Review and finish up": the user have to review and confirm the data
I would like to only keep steps 1, 2, 3, 5 (without address and phone number), 7 and 10 during the initial onboarding, and remove the other steps, to decrease friction.
I saw other websites using Stripe Connect, and I know for a fact that it is possible.
This is the code I'm using:
Account.create request:
suspend fun stripeCreateNewAccount(
email: String,
localID: String,
productDescription: String,
onSuccess: suspend (Account?) -> Unit,
) {
try {
Stripe.apiKey = Constants.STRIPE_TOKEN_TEST
val metadata: MutableMap<String, String> = HashMap()
metadata["id"] = localID
val createAccountParams = AccountCreateParams.Builder()
.setType(AccountCreateParams.Type.STANDARD)
.setEmail(email.lowercase().trim())
.setMetadata(metadata)
.setBusinessType(AccountCreateParams.BusinessType.INDIVIDUAL)
.setBusinessProfile(
AccountCreateParams.BusinessProfile.builder()
.setUrl(url)
.setName("Testing Stripe connect")
.setProductDescription(productDescription)
.build()
)
.setCompany(
AccountCreateParams.Company.Builder()
.setName("Test Company Name")
.build()
)
.setIndividual(
AccountCreateParams.Individual.Builder()
.setFirstName("John")
.setLastName("Smith")
.setEmail(email.lowercase().trim())
.build()
)
.setBusinessProfile(
AccountCreateParams.BusinessProfile.builder()
.setUrl("${Constants.BASE_URL}/list/$localID" )
.setProductDescription(productDescription)
.build()
)
.build()
onSuccess(Account.create(createAccountParams))
} catch (e: Exception) {
e.printStackTrace()
onSuccess(null)
}
}
and this is the AccountLink.create request:
suspend fun stripeLinkExistingAccount(
idAccountToLink: String,
onCompleted: suspend (String?) -> Unit,
) {
try {
Stripe.apiKey = Constants.STRIPE_TOKEN_TEST
val params = AccountLinkCreateParams
.builder()
.setAccount(idAccountToLink)
.setRefreshUrl("${Constants.BASE_URL}/profile")
.setReturnUrl("${Constants.BASE_URL}/profile")
.setType(AccountLinkCreateParams.Type.ACCOUNT_ONBOARDING)
.build()
AccountLink.create(params)?.let { accountLink ->
onCompleted(accountLink.url) // The Stripe onboarding url
} ?: run {
// Something went wrong
onCompleted(null)
}
} catch (e: Exception) {
e.printStackTrace()
onCompleted(null)
}
}

Unfortunately, this is not something you can control. You are creating a Standard account which means the person signs up directly with Stripe and opens a "full account" with them. Stripe will need to collect all the bits of information they require for identity verification, but they will also collect additional details they need or want to offer (like the Climate screen). You can't skip this.
One potential alternative would be to switch to Express accounts instead for example where you control more of the onboarding. But it comes with different risks and restrictions as covered in the docs here.

Even with Express, you are NOT going to accomplish what you want. By Law and Regulations, Stripe MUST follow KYC (Know Your Customer) and AML (Anti-Money Laundering) identification and verification for ANY account that is sent money.
ANY.
The reason you think other sites don't is they have ALREADY established KYC and AML verification with the customer.
Stripe doesn't want to slow down accounts any more than you do. They (and you) are bound by law and regulation.

Related

How to set payment details with store-api in shopware 6

i want to use shopware as a headless shop with stripe payment provider. The payment works in shopware without problems.
Now im testing the order steps with api only. the last step is to handle the payment through the provider (stripe in this case).
in the shopware documentation its handled with the api call /store-api/handle-payment.
the payload looks like this:
{
"orderId": "string",
"finishUrl": "string",
"errorUrl": "string"
}
now when i request the api i get 500 error with message:
No credit card selected
My question is, how to send credit card data through this api so that Stripe can handle the payment. Is there anyone to solved this problem?
With the advice from Alex I was able to find the following solution:
Find error credit card not selected: This happens only when you try to pay per api request. The reason i found was, that Stripe saves the payment details (credit card id) in the session object. Per api you have no access to this as default and thatswhy u get the message credit card not selected
Take a look at the stripe plugin, especially in your PaymentMethods/Card/CardPaymentConfigurator.
i put the following in the configure method
from Line 46 - 62
$requestDataBag = $stripePaymentContext;
$paymentDetails = $requestDataBag->requestDataBag->get('paymentDetails');
if(!null == $paymentDetails) {
$card = $paymentDetails->get('creditCardId');
} else {
$card = null;
}
$selectedCard = $this->stripePaymentMethodSettings->getSelectedCard();
if ($selectedCard || isset($selectedCard['id'])) {
$selectedCard = $selectedCard['id'];
} elseif ($card) {
$selectedCard = $card;
} else {
throw PaymentIntentPaymentConfiguratorException::noCreditCardSelected();
}
send payment data per handle-payment request:
let payload = {
"orderId": event,
"finishUrl": "https://www.myfinishurl.de",
"errorUrl": "https://www.myurl.de/order/error",
"paymentDetails": {
"creditCardId": "creditcardid"
}
Now do this for all methods you need. It works. Maybe Stripe can implement this in the future.
You have the following options:
Check the local API documentation - it might have more information than the public one, because it honors installed modules (see https://stackoverflow.com/a/67649883/288568)
Contact their support for more information as this is not covered in the API Docs
Make a test-payment via the normal storefront and look at the requests which are made in the network panel of your browser's development tools

how to solve the error Unsupported Gateway Type for Storage in whmcs using stripe?

I want to save the credit card details for future use and i want to save the data in whmcs database credit card last 4 digit or card_data in blobformat. I am using stripe payment gateway and whmcs.
<?php
$command = 'AddPayMethod';//save the data in api format
$postData = array(
'clientid' => $ca->getUserID(),//client id
'type' => 'CreditCard', //credit card type
'description' => 'Mohit - check',//account holder name
'card_number' => '4640823519584356',//credit card number
'card_expiry' => '0521',//credit card expiry date
'gateway_module_name' => 'stripe'//payment method
);
$adminUsername = '';
$results = localAPI($command, $postData, $adminUsername); //show the api result or error
echo '<pre>';
print_r($results);
echo '</pre>';
?>
You haven't used any models, so you won't be able to use $ca->getUserID(). You also need to include WHMCS' init.php file. Try this instead:
<?php
use WHMCS\ClientArea;
require __DIR__ . '/init.php';
$ca = new ClientArea();
$command = 'AddPayMethod';//save the data in api format
$postData = array(
'clientid' => $ca->getUserID(),//client id
'type' => 'CreditCard', //credit card type
'description' => 'Mohit - check',//account holder name
'card_number' => '4640823519584356',//credit card number
'card_expiry' => '0521',//credit card expiry date
'gateway_module_name' => 'stripe'//payment method
);
$adminUsername = '';
$results = localAPI($command, $postData, $adminUsername); //show the api result or error
echo '<pre>';
print_r($results);
echo '</pre>';
?>
Also a little late, but just in case someone is Googling for this..
These days, credit cards are not saved this way; rather they're saved by a WHMCS payment gateway in tokenized form - that is, instead of saving the credit card number itself (as in your example) your payment gateway would call Stripe and it gives you a code (eg: "AH998232187E41392HB0471") which your WHMCS Stripe payment gateway would then save for you as a payment method. Here you're trying to do something that would normally be done by the payment gateway.
Saving actual literal credit card details is really frowned on, as if your WHMCS is successfully attacked, bad guys could steal and misuse the credit card info from the database. A token can't be stolen as it onluy allows a transaction between your business and the customer - it can't be used to pay other businesses, so is ideal from a security point of view.
You should also be able to save the payment card details as a payment method without doing a transaction, under WHMCS's customer menu option "Manage Payment Methods", and there are similar options at the admin level.
I'm sure you solved this two years ago, but just wanted to make sure there was a response here for others.

How to get PaymentIntent next_action.type = redirect_to_url instead of use_stripe_sdk for Subscription

I am working on the implementing Subscription*(which is SCA ready) using Stripe.
I try to handle https://stripe.com/docs/billing/subscriptions/payment#handling-action-required.
After the subscription is created on Stripe side, I have got the answer like in documentation above:
{
"id": "sub_XXXXXXXXXXXXXXXXXXX",
"object": "subscription",
"status": "incomplete",
...
"latest_invoice": {
...
"payment_intent": {
"status": "requires_action",
...
"next_action": {
"type": "use_stripe_sdk",
...
},
...
}
}
}
According to the documentation https://stripe.com/docs/api/payment_intents/object#payment_intent_object-next_action-type
next_action.type can have two values redirect_to_url and use_stripe_sdk
So my question is how to get next_action.type = redirect_to_url(instead of use_stripe_sdk) and how to force stripe to fill next_action.redirect_to_url(Because I want to handle it on my own in my UI)?
*There is already a similar question on SO:
https://stackoverflow.com/questions/56490033/how-to-handle-use-stripe-sdk-through-php
but my case is to create Subscription where I don't have control over PaymentIntent
In my understanding, the next_action.type will be equal to redirect_to_url only if you choose to manually handle 3D Secure authentication https://stripe.com/docs/payments/payment-intents/verifying-status#manual-3ds-auth
As per documentation:
To handle 3D Secure authentication manually, you can redirect the customer. This approach is used when you manually confirm the PaymentIntent and provide a return_url destination to indicate where the customer should be sent once authentication is complete. Manual PaymentIntent confirmation can be performed on the server or on the client with Stripe.js.
Example using Stripe.js:
stripe.confirmPaymentIntent(
'{PAYMENT_INTENT_CLIENT_SECRET}',
{
payment_method: '{PAYMENT_METHOD_ID}',
return_url: 'https://example.com/return_url'
}
).then(function(result) {
// Handle result.error or result.paymentIntent
});
Example using Stripe Python:
intent = stripe.PaymentIntent.confirm(
'{PAYMENT_INTENT_ID}',
payment_method='{PAYMENT_METHOD_ID}',
return_url='https://example.com/return_url'
)
EDIT: as per #karllekko's comment the {PAYMENT_INTENT_ID} will in your case be latest_invoice.payment_intent.id.
Please read https://stripe.com/docs/payments/3d-secure-iframe - it gives more details about the "return_url" flow - and describes also posibilities to customize it inside "iframe" etc
Probably this document is fairly recent - so at the time of this question (July) it was not existing yet

nlapiSendEmail returns SSS_AUTHOR_MUST_BE_EMPLOYEE from correct employee id (on Sandbox)

in a Sandbox environment nlapiSendEmail (defined inside a suitelet) returns SSS_AUTHOR_MUST_BE_EMPLOYEE even when the sender id is correct
My distribution is Kilimanjaro, with SuiteScript 1.0. I have an administrator role, when calling nlapiSenEmail() directly from the backend model with my employee id, the email was sent to my employee profile, but not to the specified email, which is really a company distribution list. Even when I did not specify the logged customer email, a copy was sent to the logged customer email, a gmail account. The backend model operates only for the MyAccount application. It's worth noting that in this scenario nlapiSendEmail() return value was undefined. In my experience, Netsuite is really ambiguous in its behavior returning values or just functioning in an expected way, due to the "execution context". So, with the same data I put my call inside a suitelet, and now I am having the return SSS_AUTHOR_MUST_BE_EMPLOYEE.
function sendEmailWithAPI(request, response)
{
var senderId = request.getParameter('senderId');
var to = request.getParameter('emailTo');
var subject = request.getParameter('subject');
var body = request.getParameter('body');
var cc = request.getParameter('emailCC');
var result = {success:false, errorInfo:''};
try
{
var sendingResult = nlapiSendEmail(senderId, to, subject, body, cc);
result.success = true;
}
catch (errorOnMailSending)
{
result.returnValue = sendingResult;
result.errorInfo = errorOnMailSending.details;
}
response.write(JSON.stringify(result));
}
What is the record type of the senderId? NetSuite only accepts Employee records as sender of script generated emails. Also in Sandbox accounts, the emails are re-routed to the logged in user, specific list, or not at all. This is actually based on the Company preference in your Sandbox account. The reason for this is Sandbox is usually used for testing and you don't want to send test emails to actual customers.
In the end the "problem" was that I was just working in the Sandbox, as I prepared a snippet to test email sending in production everything went right. In the sandbox you can still send emails specifying a list at the subtab "Email options"
with the option "SEND EMAIL TO (SEPARATE ADDRESSES WITH COMMAS)"
this is located at Setup > Company > Email Preferences.

DDD modelisation issue (entity accessing repository)

I am designing the model of the following business needs :
The application must be able to register Users
The steps of the User registration are :
The user enters an email address and confirm
A verification code is sent to the provided email address.
The user must enter the correct verification code to continue
Repeat steps 1-3 for a phone number with verification code by SMS (optional)
The user then enters some personal information and confirm => the account is created
After registration, the user can update his email address or mobile phone number, but must go through the same verification process (code sent which must be entered to confirm the modification)
I ended up with the following model :
Verifiable (interface)
User (entity)
EmailAddress (value type, is a Verifiable)
MobilePhoneNumber (value type, is a Verifiable)
RandomCode (value type)
VerificationCode (entity containing a Verifiable, a RandomCode and a generationDateTime)
VerificationEmail (aggregate containing a VerificationCode, an EmailAddress and a Locale)
VerificationSms (aggregate containing a VerificationCode, a MobilePhoneNumber and a Locale)
Then here come the questions !!
Is it correct to have the Verifiable interface in order to have a VerificationCode instead of having EmailVerificationCode and SmsVerificationCode ? (Although it's not really a part of the ubiquitous language)
As I must persist somewhere the tuple emailAddress/mobilePhoneNumber + randomCode + generationDateTime to be able to retrieve it for verification, is it ok to have a specific entity for this ?
When the user wants to update his email address I was expecting to do something like :
// In the application service
User u = userRepository.findByUid(uid);
u.updateEmailAddress(newEmailAddress, enteredCode);
userRepository.save(u);
// In the User class
public void updateEmailAddress(EmailAddress newEmailAddress, String code) {
// Here comes the direct repository access
VerificationCode v = verificationCodeRepository.findByVerifiable(newEmailAddress);
if (v != null && v.hasNotExpired() && v.equalsToCode(code)) {
this.emailAddress = newEmailAddress;
verificationCodeRepository.delete(v);
}
else {
throw new IncorrectVerificationCodeException();
}
}
but to prevent my entity accessing a repository I ended up with the following code :
// In the application service
User u = userRepository.findByUid(uid);
VerificationCode v = verificationCodeRepository.findByVerifiable(newEmailAddress);
if (v != null && v.hasNotExpired() && v.equalsToCode(code)) {
verificationCodeRepository.delete(v);
u.updateEmailAddress(newEmailAddress);
userRepository.save(u);
}
else {
throw new IncorrectVerificationCodeException();
}
// In the User class
public void updateEmailAddress(EmailAddress newEmailAddress) {
this.emailAddress = newEmailAddress;
}
But it looks like an anemic model and the business logic is now in the application layer...
I am really struggling to correctly design the model as this is my first DDD project, any advice, modelisation suggestion is welcomed...
There is nothing wrong passing a repository as an argument in your updateEmailAddress() method.
But there is a better alternative, a domain service:
Your domain service depends on the repository and encapsulates the logic bound to your verification. You then pass this service to the user entity which is in charge of calling the correct method.
Here is how it could looks like:
class EmailVerificationService {
VerificationCodeRepository repository;
boolean isCodeVerified(EmailAddress emailAddress, String code) {
// do your things with the repository
// return true or false
}
}
Then in the user class:
class User {
// ...
public void updateEmailAddress(EmailVerificationService service, EmailAddress emailAddress, String code) {
if (service.isCodeVerified(emailAddress, code)) {
this.emailAddress = emailAddress;
} else {
// throw business Exception ?
}
}
}
In your application service, you inject the domain service and wire everything, catching the eventual exception and returning an error message to the user.
This is a suggestion of modeling, if you want to take it into account. Hope it could help you. I would model it this way:
User (aggregate root entity)
id
emailAddress (not null and unique)
mobilePhoneNumber (optional)
personalInfo
enabled (a user is created disabled when the registration process starts, and it is enabled when the process ends successfully)
VerificationCode (aggregate root entity) ===> it is associated to a user
id
randomCode
expirationDate
userId
smsOption (boolean) ===> if sms option is true, this verification code will be sent in a SMS to the user (otherwise it will be sent by email to the user)
Static Factory meethods:
forSendingByEmail ==> creates an instance with smsOption false
forSendingBySMS ===> creates and instance with smsOption true
Domain Service: sendVerificationCodeToUser ( verificationCodeId ) ===> checks smsOption to send either an SMS or an email (to the mobilePhoneNumber/emailAddress of the associated userId)
DomainEvent: VerificationCodeWasCreated ===> it has the id of the verification code that has been created
Raised by the VerificationCode constructor
The listener will call the domain service: sendVerificationCodeToUser(verificationCodeWasCreated.verificationCodeId())
THE REGISTRATION PROCESS (application service methods):
(1) The user enters an email address and confirm
public void registerUser ( String email ):
checks that doesn't exists any enabled user with the given email
if exist a disable user with the email, delete it
creates and persist a new disabled user with the email
creates and persist a new verification code associated to the created user for sending by email
(2) A verification code is sent to the provided email address ===> it is done by the domain event listener
(3) The user must enter the correct verification code to continue ===> the user who was sent the email in step (1) has to enter the email again, and the code he received)
public boolean isARandomCodeCorrectForUserEmail ( String randomCode, String email ) {
User user = userRepository.findByEmail(email);
if (user==null) {
return false;
}
VerificationCode vcode = verificationCodeRepository.findByRandomCodeAndUserId(randomCode,user.id());
if ( vcode==null) {
return false;
}
return vcode.hasNotExpired();
}
(4) Repeat steps 1-3 for a phone number with verification code by SMS (optional)
(4.1) The user of step (3) enters mobile phone number (we know the user id):
public void generateCodeForSendingBySmsToUser ( String mobilePhoneNumber, String userId ):
update user of userId with the given mobilePhoneNumber
creates and persist a new verification code associated to the user for sending by SMS
(4.2) The event listener sends the SMS
(4.3) The user who was sent the SMS in step (4.2) has to enter the email of step (1) again, and the code he received by SMS ===> isARandomCodeCorrectForUserEmail(randomCode,email)
(5) The user then enters some personal information and confirm ===> the account is created ===> what I do is enabling the user, since the user is already created, and we know the userId from step (3) or (4.3)
public void confirmRegistration ( PersonalInfo personalInfo, String userId ):
update user of userId with the given personalInfo
enables de the user
THE EMAIL/MOBILEPHONENUMBER MODIFICATION PROCESS:
It is similar to the registration, but the email/mobilePhoneNumber entered at the beginning must belongs to an existing enabled user, and at the end an update of the user is performed, instead of enabling.
ENABLED/DISABLED USERS:
Having enabled and disabled users, makes you taking it into account in authentication and authorization methods. If you don't want to or you're not allowed to have enabled/disabled users, you would have to model another aggregate that it would be UserCandidate or something like that, just with id, email and mobilePhoneNumber. And at the end of the process, create the real user with those values.

Resources