Skip to content

Commit

Permalink
use Stripe Intent API for Stripe Klarna payments
Browse files Browse the repository at this point in the history
  • Loading branch information
vstudenichnik-insoft committed May 30, 2023
1 parent 3211975 commit 48c6d80
Show file tree
Hide file tree
Showing 6 changed files with 291 additions and 149 deletions.
5 changes: 5 additions & 0 deletions mocks/payment.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ const mockPayment = {
return {
redirect_url: 'https://www.amazon.com/',
};
case 'stripe':
return {
id: 'test_stripe_intent_id',
client_secret: 'test_stripe_client_secret',
};
default:
throw new Error(`Unknown gateway: ${gateway}`);
}
Expand Down
46 changes: 39 additions & 7 deletions src/payment/klarna/stripe.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import Payment from '../payment';
import { createKlarnaSource } from '../../utils/stripe';
import {
getKlarnaIntentDetails,
getKlarnaConfirmationDetails,
} from '../../utils/stripe';
import {
PaymentMethodDisabledError,
LibraryNotLoadedError,
UnableAuthenticatePaymentMethodError,
} from '../../utils/errors';

export default class StripeKlarnaPayment extends Payment {
Expand Down Expand Up @@ -43,21 +47,49 @@ export default class StripeKlarnaPayment extends Payment {

async tokenize() {
const cart = await this.getCart();
const { source, error: sourceError } = await createKlarnaSource(
this.stripe,
cart,
const intent = await this.createIntent({
gateway: 'stripe',
intent: getKlarnaIntentDetails(cart),
});
const { error } = await this.stripe.confirmKlarnaPayment(
intent.client_secret,
getKlarnaConfirmationDetails(cart),
);

if (error) {
throw new Error(error.message);
}
}

async handleRedirect(queryParams) {
const { redirect_status, payment_intent_client_secret } = queryParams;

if (redirect_status !== 'succeeded') {
throw new UnableAuthenticatePaymentMethodError();
}

const { paymentIntent, error } = await this.stripe.retrievePaymentIntent(
payment_intent_client_secret,
);

if (sourceError) {
throw new Error(sourceError.message);
if (error) {
throw new Error(error.message);
}

await this.updateCart({
billing: {
method: 'klarna',
klarna: {
token: paymentIntent.payment_method,
},
intent: {
stripe: {
id: paymentIntent.id,
},
},
},
});

window.location.replace(source.redirect.url);
this.onSuccess();
}
}
148 changes: 32 additions & 116 deletions src/utils/stripe.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { get, map, reduce, toNumber, toLower, isEmpty } from './index';
import { get, reduce, toLower, isEmpty } from './index';

// https://stripe.com/docs/currencies#minimum-and-maximum-charge-amounts
const MINIMUM_CHARGE_AMOUNT = {
Expand Down Expand Up @@ -71,102 +71,6 @@ function getBillingDetails(cart) {
return details;
}

function getKlarnaItems(cart) {
const currency = toLower(get(cart, 'currency', 'eur'));
const items = map(cart.items, (item) => ({
type: 'sku',
description: item.product.name,
quantity: item.quantity,
currency,
amount: Math.round(toNumber(item.price_total - item.discount_total) * 100),
}));

const tax = get(cart, 'tax_included_total');
if (tax) {
items.push({
type: 'tax',
description: 'Taxes',
currency,
amount: Math.round(toNumber(tax) * 100),
});
}

const shipping = get(cart, 'shipping', {});
const shippingTotal = get(cart, 'shipment_total', {});
if (shipping.price) {
items.push({
type: 'shipping',
description: shipping.service_name,
currency,
amount: Math.round(toNumber(shippingTotal) * 100),
});
}

return items;
}

function setKlarnaBillingShipping(source, data) {
const shippingNameFieldsMap = {
shipping_first_name: 'first_name',
shipping_last_name: 'last_name',
};
const shippingFieldsMap = {
phone: 'phone',
};
const billingNameFieldsMap = {
first_name: 'first_name',
last_name: 'last_name',
};
const billingFieldsMap = {
email: 'email',
};

const fillValues = (fieldsMap, data) =>
reduce(
fieldsMap,
(acc, srcKey, destKey) => {
const value = data[srcKey];
if (value) {
acc[destKey] = value;
}
return acc;
},
{},
);

source.klarna = {
...source.klarna,
...fillValues(shippingNameFieldsMap, data.shipping),
};
const shipping = fillValues(shippingFieldsMap, data.shipping);
const shippingAddress = fillValues(addressFieldsMap, data.shipping);
if (shipping || shippingAddress) {
source.source_order.shipping = {
...(shipping ? shipping : {}),
...(shippingAddress ? { address: shippingAddress } : {}),
};
}

source.klarna = {
...source.klarna,
...fillValues(
billingNameFieldsMap,
data.billing || get(data, 'account.billing') || data.shipping,
),
};
const billing = fillValues(billingFieldsMap, data.account);
const billingAddress = fillValues(
addressFieldsMap,
data.billing || get(data, 'account.billing') || data.shipping,
);
if (billing || billingAddress) {
source.owner = {
...(billing ? billing : {}),
...(billingAddress ? { address: billingAddress } : {}),
};
}
}

function setBancontactOwner(source, data) {
const fillValues = (fieldsMap, data) =>
reduce(
Expand Down Expand Up @@ -249,26 +153,37 @@ async function createIDealPaymentMethod(stripe, element, cart) {
});
}

async function createKlarnaSource(stripe, cart) {
const sourceObject = {
type: 'klarna',
flow: 'redirect',
amount: Math.round(get(cart, 'grand_total', 0) * 100),
currency: toLower(get(cart, 'currency', 'eur')),
klarna: {
product: 'payment',
purchase_country: get(cart, 'settings.country', 'DE'),
},
source_order: {
items: getKlarnaItems(cart),
},
redirect: {
return_url: window.location.href,
},
function getKlarnaIntentDetails(cart) {
const { account, currency, capture_total } = cart;
const stripeCustomer = account && account.stripe_customer;
const stripeCurrency = (currency || 'USD').toLowerCase();
const stripeAmount = stripeAmountByCurrency(currency, capture_total);
const details = {
payment_method_types: 'klarna',
amount: stripeAmount,
currency: stripeCurrency,
capture_method: 'manual',
};
setKlarnaBillingShipping(sourceObject, cart);

return await stripe.createSource(sourceObject);
if (stripeCustomer) {
details.customer = stripeCustomer;
}

return details;
}

function getKlarnaConfirmationDetails(cart) {
const billingDetails = getBillingDetails(cart);
const returnUrl = `${
window.location.origin + window.location.pathname
}?gateway=stripe`;

return {
payment_method: {
billing_details: billingDetails,
},
return_url: returnUrl,
};
}

async function createBancontactSource(stripe, cart) {
Expand Down Expand Up @@ -319,7 +234,8 @@ export {
createElement,
createPaymentMethod,
createIDealPaymentMethod,
createKlarnaSource,
getKlarnaIntentDetails,
getKlarnaConfirmationDetails,
createBancontactSource,
stripeAmountByCurrency,
isStripeChargeableAmount,
Expand Down
108 changes: 107 additions & 1 deletion src/utils/stripe.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { createPaymentMethod, createIDealPaymentMethod } from './stripe';
import {
createPaymentMethod,
createIDealPaymentMethod,
getKlarnaIntentDetails,
getKlarnaConfirmationDetails,
} from './stripe';

describe('utils/stripe', () => {
describe('#createPaymentMethod', () => {
Expand Down Expand Up @@ -182,4 +187,105 @@ describe('utils/stripe', () => {
expect(paymentMethod.error).toBe('Card error');
});
});

describe('#getKlarnaIntentDetails', () => {
it('should return intent details', () => {
const cart = {
account: { stripe_customer: 'cus_test' },
currency: 'USD',
capture_total: 100,
};
const result = getKlarnaIntentDetails(cart);

expect(result).toEqual({
payment_method_types: 'klarna',
currency: 'usd',
amount: 10000,
capture_method: 'manual',
customer: 'cus_test',
});
});

it('should return intent details without customer when cart account does not have a Stripe Customer', () => {
const cart = {
account: {},
currency: 'USD',
capture_total: 100,
};
const result = getKlarnaIntentDetails(cart);

expect(result).toEqual({
payment_method_types: 'klarna',
currency: 'usd',
amount: 10000,
capture_method: 'manual',
});
});

it('should return intent details without customer when cart account is not defined', () => {
const cart = {
currency: 'USD',
capture_total: 100,
};
const result = getKlarnaIntentDetails(cart);

expect(result).toEqual({
payment_method_types: 'klarna',
currency: 'usd',
amount: 10000,
capture_method: 'manual',
});
});
});

describe('#getKlarnaConfirmationDetails', () => {
beforeEach(() => {
global.window = {
location: {
origin: 'http://test.swell.test',
pathname: '/checkout',
},
};
});

afterEach(() => {
global.window = undefined;
});

it('should return confirmation details', () => {
const cart = {
account: { email: 'test@swell.is' },
billing: {
name: 'Test Person-us',
phone: '3106683312',
city: 'Beverly Hills',
country: 'US',
address1: 'Lombard St 10',
address2: 'Apt 214',
zip: '90210',
state: 'CA',
},
};
const result = getKlarnaConfirmationDetails(cart);

expect(result).toEqual({
payment_method: {
billing_details: {
address: {
city: 'Beverly Hills',
country: 'US',
line1: 'Lombard St 10',
line2: 'Apt 214',
postal_code: '90210',
state: 'CA',
},
email: 'test@swell.is',
name: 'Test Person-us',
phone: '3106683312',
},
},
return_url: 'http://test.swell.test/checkout?gateway=stripe',
});
});
});
});
Loading

0 comments on commit 48c6d80

Please sign in to comment.