fix(medusa-payment-klarna, medusa-payment-stripe, medusa-payment-paypal): Totals calculation (#2381)

This commit is contained in:
Sebastian Rindom authored and GitHub committed 2022-10-08 10:53:19 +02:00
1 parent 5f2bc975cb
commit a908a7716c
8 files changed
+157 -198

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
---
"medusa-payment-klarna": patch
"medusa-payment-paypal": patch
"medusa-payment-stripe": patch
---
Rely on cart totals in payment providers
@@ -16,6 +16,7 @@ export const carts = {
title: "merge line", title: "merge line",
description: "This is a new line", description: "This is a new line",
thumbnail: "test-img-yeah.com/thumb", thumbnail: "test-img-yeah.com/thumb",
tax_lines: [],
content: [ content: [
{ {
unit_price: 8, unit_price: 8,
@@ -45,6 +46,7 @@ export const carts = {
title: "merge line", title: "merge line",
description: "This is a new line", description: "This is a new line",
thumbnail: "test-img-yeah.com/thumb", thumbnail: "test-img-yeah.com/thumb",
tax_lines: [],
content: { content: {
unit_price: 10, unit_price: 10,
variant: { variant: {
@@ -62,6 +64,7 @@ export const carts = {
{ {
id: IdMap.getId("freeShipping"), id: IdMap.getId("freeShipping"),
name: "Free shipping", name: "Free shipping",
tax_lines: [],
data: { data: {
name: "test", name: "test",
}, },
@@ -2,107 +2,78 @@ export default async (req, res) => {
// In Medusa, we store the cart id in merchant_data // In Medusa, we store the cart id in merchant_data
const { shipping_address, merchant_data } = req.body const { shipping_address, merchant_data } = req.body
try { const cartService = req.scope.resolve("cartService")
const manager = req.scope.resolve("manager") const klarnaProviderService = req.scope.resolve("pp_klarna")
const cartService = req.scope.resolve("cartService") const shippingProfileService = req.scope.resolve("shippingProfileService")
const klarnaProviderService = req.scope.resolve("pp_klarna")
const shippingProfileService = req.scope.resolve("shippingProfileService")
const result = await manager.transaction("SERIALIZABLE", async (m) => { if (shipping_address) {
const cart = await cartService.retrieve(merchant_data, { const shippingAddress = {
select: ["subtotal"], first_name: shipping_address.given_name,
relations: [ last_name: shipping_address.family_name,
"shipping_address", address_1: shipping_address.street_address,
"billing_address", address_2: shipping_address.street_address2,
"region", city: shipping_address.city,
"items", country_code: shipping_address.country,
"shipping_methods", postal_code: shipping_address.postal_code,
"shipping_methods.shipping_option", phone: shipping_address.phone,
"items.variant", }
"items.variant.product",
],
})
if (shipping_address) { let billingAddress = {
const shippingAddress = { first_name: shipping_address.given_name,
first_name: shipping_address.given_name, last_name: shipping_address.family_name,
last_name: shipping_address.family_name, address_1: shipping_address.street_address,
address_1: shipping_address.street_address, address_2: shipping_address.street_address2,
address_2: shipping_address.street_address2, city: shipping_address.city,
city: shipping_address.city, country_code: shipping_address.country,
country_code: shipping_address.country, postal_code: shipping_address.postal_code,
postal_code: shipping_address.postal_code, phone: shipping_address.phone,
phone: shipping_address.phone, }
}
let billingAddress = { await cartService.update(merchant_data, {
first_name: shipping_address.given_name, shipping_address: shippingAddress,
last_name: shipping_address.family_name, billing_address: billingAddress,
address_1: shipping_address.street_address, email: shipping_address.email,
address_2: shipping_address.street_address2,
city: shipping_address.city,
country_code: shipping_address.country,
postal_code: shipping_address.postal_code,
phone: shipping_address.phone,
}
await cartService.update(cart.id, {
shipping_address: shippingAddress,
billing_address: billingAddress,
email: shipping_address.email,
})
const shippingOptions = await shippingProfileService.fetchCartOptions(
cart
)
if (shippingOptions?.length) {
const option = shippingOptions.find(
(o) => o.data && !o.data.require_drop_point
)
await cartService
.withTransaction(m)
.addShippingMethod(cart.id, option.id, option.data)
}
// Fetch and return updated Klarna order
const updatedCart = await cartService
.withTransaction(m)
.retrieve(cart.id, {
select: [
"gift_card_total",
"subtotal",
"total",
"shipping_total",
"tax_total",
"discount_total",
"subtotal",
],
relations: [
"shipping_address",
"billing_address",
"region",
"shipping_methods",
"shipping_methods.shipping_option",
"items",
"items.variant",
"items.variant.product",
],
})
return klarnaProviderService.cartToKlarnaOrder(updatedCart)
} else {
return null
}
}) })
if (result) { let cart = await cartService.retrieveWithTotals(merchant_data, {
res.json(result) relations: [
return "shipping_address",
} else { "billing_address",
res.sendStatus(400) "region",
return "shipping_methods",
"shipping_methods.shipping_option",
"items",
"items.variant",
"items.variant.product",
],
})
const shippingOptions = await shippingProfileService.fetchCartOptions(cart)
if (shippingOptions?.length) {
const option = shippingOptions.find(
(o) => o.data && !o.data.require_drop_point
)
if (option) {
await cartService.addShippingMethod(cart.id, option.id, option.data)
cart = await cartService.retrieveWithTotals(cart.id, {
relations: [
"shipping_address",
"billing_address",
"region",
"shipping_methods",
"shipping_methods.shipping_option",
"items",
"items.variant",
"items.variant.product",
],
})
}
} }
} catch (error) {
throw error const order = await klarnaProviderService.cartToKlarnaOrder(cart)
res.json(order)
} else {
res.sendStatus(400)
return
} }
} }
@@ -7,20 +7,23 @@ export default async (req, res) => {
const klarnaProviderService = req.scope.resolve("pp_klarna") const klarnaProviderService = req.scope.resolve("pp_klarna")
const shippingProfileService = req.scope.resolve("shippingProfileService") const shippingProfileService = req.scope.resolve("shippingProfileService")
const cart = await cartService.retrieve(merchant_data, { const cart = await cartService.retrieveWithTotals(
select: ["subtotal"], merchant_data,
relations: [ {
"shipping_address", relations: [
"billing_address", "shipping_address",
"region", "billing_address",
"shipping_methods", "region",
"shipping_methods.shipping_option", "shipping_methods",
"items", "shipping_methods.shipping_option",
"items.adjustments", "items",
"items.variant", "items.adjustments",
"items.variant.product", "items.variant",
], "items.variant.product",
}) ],
},
{ force_taxes: true }
)
let shippingOptions = await shippingProfileService.fetchCartOptions(cart) let shippingOptions = await shippingProfileService.fetchCartOptions(cart)
shippingOptions = shippingOptions.filter( shippingOptions = shippingOptions.filter(
@@ -35,28 +38,23 @@ export default async (req, res) => {
} }
} }
const newCart = await cartService.retrieve(cart.id, { const newCart = await cartService.retrieveWithTotals(
select: [ cart.id,
"gift_card_total", {
"subtotal", relations: [
"total", "shipping_address",
"shipping_total", "billing_address",
"tax_total", "shipping_methods",
"discount_total", "shipping_methods.shipping_option",
"subtotal", "region",
], "items",
relations: [ "items.adjustments",
"shipping_address", "items.variant",
"billing_address", "items.variant.product",
"shipping_methods", ],
"shipping_methods.shipping_option", },
"region", { force_taxes: true }
"items", )
"items.adjustments",
"items.variant",
"items.variant.product",
],
})
const order = await klarnaProviderService.cartToKlarnaOrder(newCart) const order = await klarnaProviderService.cartToKlarnaOrder(newCart)
@@ -5,7 +5,7 @@ import { PaymentService } from "medusa-interfaces"
class KlarnaProviderService extends PaymentService { class KlarnaProviderService extends PaymentService {
static identifier = "klarna" static identifier = "klarna"
constructor({ shippingProfileService, totalsService }, options) { constructor({ logger, shippingProfileService, totalsService }, options) {
super() super()
/** /**
@@ -23,6 +23,7 @@ class KlarnaProviderService extends PaymentService {
* } * }
*/ */
this.options_ = options this.options_ = options
this.logger_ = logger
/** @private @const {Klarna} */ /** @private @const {Klarna} */
this.klarna_ = axios.create({ this.klarna_ = axios.create({
@@ -53,22 +54,16 @@ class KlarnaProviderService extends PaymentService {
// Withdraw discount from the total item amount // Withdraw discount from the total item amount
const quantity = item.quantity const quantity = item.quantity
const totals = await this.totalsService_.getLineItemTotals(item, cart, { const tax = item.tax_lines.reduce((acc, next) => acc + next.rate, 0) / 100
include_tax: true,
})
const tax =
totals.tax_lines.reduce((acc, next) => acc + next.rate, 0) / 100
order_lines.push({ order_lines.push({
name: item.title, name: item.title,
tax_rate: tax * 10000, tax_rate: tax * 10000,
quantity, quantity,
unit_price: Math.round(totals.original_total / item.quantity), unit_price: Math.round(item.original_total / item.quantity),
total_amount: totals.total - totals.gift_card_total, total_amount: item.total,
total_tax_amount: totals.tax_total, total_tax_amount: item.tax_total,
total_discount_amount: total_discount_amount: item.original_total - item.total,
totals.original_total - totals.total + totals.gift_card_total,
}) })
} }
@@ -79,23 +74,15 @@ class KlarnaProviderService extends PaymentService {
let taxRate = 0 let taxRate = 0
if (cart.shipping_total > 0) { if (cart.shipping_total > 0) {
for (const next of cart.shipping_methods) { for (const method of cart.shipping_methods) {
const totals = await this.totalsService_.getShippingMethodTotals(
next,
cart,
{
include_tax: true,
}
)
const methodTaxRate = const methodTaxRate =
totals.tax_lines.reduce((acc, next) => acc + next.rate, 0) / 100 method.tax_lines.reduce((acc, next) => acc + next.rate, 0) / 100
name.push(next?.shipping_option.name) name.push(method.shipping_option.name)
total += totals.total total += method.total
taxRate += (totals.price / cart.shipping_total) * methodTaxRate taxRate += (method.price / cart.shipping_total) * methodTaxRate
tax += totals.tax_total tax += method.tax_total
} }
} }
@@ -124,16 +111,17 @@ class KlarnaProviderService extends PaymentService {
order.order_lines = await this.lineItemsToOrderLines_(cart) order.order_lines = await this.lineItemsToOrderLines_(cart)
if (gift_card_total && !region.gift_cards_taxable) { if (gift_card_total) {
const taxRate = cart.gift_card_tax_total / cart.gift_card_total
order.order_lines.push({ order.order_lines.push({
name: `Gift Card`, name: "Gift Card",
quantity: 1, quantity: 1,
type: "gift_card", type: "gift_card",
unit_price: 0, unit_price: -1 * (cart.gift_card_total + cart.gift_card_tax_total),
total_discount_amount: gift_card_total, tax_rate: Math.round(taxRate * 10000),
tax_rate: 0, total_amount: -1 * (cart.gift_card_total + cart.gift_card_tax_total),
total_amount: -gift_card_total, total_tax_amount: -1 * cart.gift_card_tax_total,
total_tax_amount: 0,
}) })
} }
@@ -159,7 +147,7 @@ class KlarnaProviderService extends PaymentService {
} }
order.order_amount = total order.order_amount = total
order.order_tax_amount = tax_total order.order_tax_amount = tax_total - cart.gift_card_tax_total
order.purchase_currency = region.currency_code.toUpperCase() order.purchase_currency = region.currency_code.toUpperCase()
order.merchant_urls = { order.merchant_urls = {
@@ -183,21 +171,13 @@ class KlarnaProviderService extends PaymentService {
// If the cart does not have shipping methods yet, preselect one from // If the cart does not have shipping methods yet, preselect one from
// shipping_options and set the selected shipping method // shipping_options and set the selected shipping method
if (cart.shipping_methods.length) { if (cart.shipping_methods.length) {
const shipping_method = cart.shipping_methods[0] const method = cart.shipping_methods[0]
const totals = await this.totalsService_.getShippingMethodTotals( const taxRate = method.tax_total / (method.total - method.tax_total)
shipping_method,
cart,
{
include_tax: true,
}
)
const taxRate = totals.tax_total / (totals.total - totals.tax_total)
order.selected_shipping_option = { order.selected_shipping_option = {
id: shipping_method.shipping_option.id, id: method.shipping_option.id,
name: shipping_method.shipping_option.name, name: method.shipping_option.name,
price: totals.total, price: method.total,
tax_amount: total.tax_total, tax_amount: method.tax_total,
tax_rate: taxRate * 10000, tax_rate: taxRate * 10000,
} }
} }
@@ -286,6 +266,7 @@ class KlarnaProviderService extends PaymentService {
return klarnaPayment return klarnaPayment
} catch (error) { } catch (error) {
this.logger_.error(error)
throw error throw error
} }
} }
@@ -72,7 +72,6 @@ describe("PaypalProviderService", () => {
const paypalProviderService = new PayPalProviderService( const paypalProviderService = new PayPalProviderService(
{ {
regionService: RegionServiceMock, regionService: RegionServiceMock,
totalsService: TotalsServiceMock,
}, },
{ {
api_key: "test", api_key: "test",
@@ -98,7 +97,6 @@ describe("PaypalProviderService", () => {
const paypalProviderService = new PayPalProviderService( const paypalProviderService = new PayPalProviderService(
{ {
regionService: RegionServiceMock, regionService: RegionServiceMock,
totalsService: TotalsServiceMock,
}, },
{ {
api_key: "test", api_key: "test",
@@ -149,7 +147,6 @@ describe("PaypalProviderService", () => {
const paypalProviderService = new PayPalProviderService( const paypalProviderService = new PayPalProviderService(
{ {
regionService: RegionServiceMock, regionService: RegionServiceMock,
totalsService: TotalsServiceMock,
}, },
{ {
api_key: "test", api_key: "test",
@@ -196,7 +193,6 @@ describe("PaypalProviderService", () => {
const paypalProviderService = new PayPalProviderService( const paypalProviderService = new PayPalProviderService(
{ {
regionService: RegionServiceMock, regionService: RegionServiceMock,
totalsService: TotalsServiceMock,
}, },
{ {
api_key: "test", api_key: "test",
@@ -275,7 +271,6 @@ describe("PaypalProviderService", () => {
const paypalProviderService = new PayPalProviderService( const paypalProviderService = new PayPalProviderService(
{ {
regionService: RegionServiceMock, regionService: RegionServiceMock,
totalsService: TotalsServiceMock,
}, },
{ {
api_key: "test", api_key: "test",
@@ -349,9 +344,13 @@ describe("PaypalProviderService", () => {
data: { id: "test-voided" }, data: { id: "test-voided" },
}) })
expect(PayPalMock.payments.AuthorizationsVoidRequest).not.toHaveBeenCalled() expect(
PayPalMock.payments.AuthorizationsVoidRequest
).not.toHaveBeenCalled()
expect(PayPalMock.payments.CapturesRefundRequest).not.toHaveBeenCalled() expect(PayPalMock.payments.CapturesRefundRequest).not.toHaveBeenCalled()
expect(PayPalMock.orders.OrdersGetRequest).toHaveBeenCalledWith("test-voided") expect(PayPalMock.orders.OrdersGetRequest).toHaveBeenCalledWith(
"test-voided"
)
expect(PayPalClientMock.execute).toHaveBeenCalledTimes(1) expect(PayPalClientMock.execute).toHaveBeenCalledTimes(1)
expect(result.id).toEqual("test-voided") expect(result.id).toEqual("test-voided")
@@ -366,9 +365,13 @@ describe("PaypalProviderService", () => {
}, },
}) })
expect(PayPalMock.payments.AuthorizationsVoidRequest).not.toHaveBeenCalled() expect(
PayPalMock.payments.AuthorizationsVoidRequest
).not.toHaveBeenCalled()
expect(PayPalMock.payments.CapturesRefundRequest).not.toHaveBeenCalled() expect(PayPalMock.payments.CapturesRefundRequest).not.toHaveBeenCalled()
expect(PayPalMock.orders.OrdersGetRequest).toHaveBeenCalledWith("test-refund") expect(PayPalMock.orders.OrdersGetRequest).toHaveBeenCalledWith(
"test-refund"
)
expect(PayPalClientMock.execute).toHaveBeenCalledTimes(1) expect(PayPalClientMock.execute).toHaveBeenCalledTimes(1)
expect(result.id).toEqual("test-refund") expect(result.id).toEqual("test-refund")
@@ -12,7 +12,7 @@ function roundToTwo(num, currency) {
class PayPalProviderService extends PaymentService { class PayPalProviderService extends PaymentService {
static identifier = "paypal" static identifier = "paypal"
constructor({ totalsService, regionService }, options) { constructor({ regionService }, options) {
super() super()
/** /**
@@ -44,9 +44,6 @@ class PayPalProviderService extends PaymentService {
/** @private @const {RegionService} */ /** @private @const {RegionService} */
this.regionService_ = regionService this.regionService_ = regionService
/** @private @const {TotalsService} */
this.totalsService_ = totalsService
} }
/** /**
@@ -94,7 +91,7 @@ class PayPalProviderService extends PaymentService {
const { region_id } = cart const { region_id } = cart
const { currency_code } = await this.regionService_.retrieve(region_id) const { currency_code } = await this.regionService_.retrieve(region_id)
const amount = await this.totalsService_.getTotal(cart) const amount = cart.total
const request = new PayPal.orders.OrdersCreateRequest() const request = new PayPal.orders.OrdersCreateRequest()
request.requestBody({ request.requestBody({
@@ -289,7 +286,8 @@ class PayPalProviderService extends PaymentService {
async cancelPayment(payment) { async cancelPayment(payment) {
const order = await this.retrievePayment(payment.data) const order = await this.retrievePayment(payment.data)
const isAlreadyCanceled = order.status === "VOIDED" const isAlreadyCanceled = order.status === "VOIDED"
const isCanceledAndFullyRefund = order.status === "COMPLETED" && !!order.invoice_id const isCanceledAndFullyRefund =
order.status === "COMPLETED" && !!order.invoice_id
if (isAlreadyCanceled || isCanceledAndFullyRefund) { if (isAlreadyCanceled || isCanceledAndFullyRefund) {
return order return order
} }
@@ -135,9 +135,7 @@ class StripeProviderService extends AbstractPaymentService {
.withTransaction(this.manager_) .withTransaction(this.manager_)
.retrieve(region_id) .retrieve(region_id)
const amount = await this.totalsService_ const amount = cart.total
.withTransaction(this.manager_)
.getTotal(cart)
const intentRequest = { const intentRequest = {
description: description: