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

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

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

View File

@@ -16,6 +16,7 @@ export const carts = {
title: "merge line",
description: "This is a new line",
thumbnail: "test-img-yeah.com/thumb",
tax_lines: [],
content: [
{
unit_price: 8,
@@ -45,6 +46,7 @@ export const carts = {
title: "merge line",
description: "This is a new line",
thumbnail: "test-img-yeah.com/thumb",
tax_lines: [],
content: {
unit_price: 10,
variant: {
@@ -62,6 +64,7 @@ export const carts = {
{
id: IdMap.getId("freeShipping"),
name: "Free shipping",
tax_lines: [],
data: {
name: "test",
},

View File

@@ -2,27 +2,10 @@ export default async (req, res) => {
// In Medusa, we store the cart id in merchant_data
const { shipping_address, merchant_data } = req.body
try {
const manager = req.scope.resolve("manager")
const cartService = req.scope.resolve("cartService")
const klarnaProviderService = req.scope.resolve("pp_klarna")
const shippingProfileService = req.scope.resolve("shippingProfileService")
const result = await manager.transaction("SERIALIZABLE", async (m) => {
const cart = await cartService.retrieve(merchant_data, {
select: ["subtotal"],
relations: [
"shipping_address",
"billing_address",
"region",
"items",
"shipping_methods",
"shipping_methods.shipping_option",
"items.variant",
"items.variant.product",
],
})
if (shipping_address) {
const shippingAddress = {
first_name: shipping_address.given_name,
@@ -46,38 +29,13 @@ export default async (req, res) => {
phone: shipping_address.phone,
}
await cartService.update(cart.id, {
await cartService.update(merchant_data, {
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",
],
let cart = await cartService.retrieveWithTotals(merchant_data, {
relations: [
"shipping_address",
"billing_address",
@@ -89,20 +47,33 @@ export default async (req, res) => {
"items.variant.product",
],
})
return klarnaProviderService.cartToKlarnaOrder(updatedCart)
} else {
return null
}
})
const shippingOptions = await shippingProfileService.fetchCartOptions(cart)
if (result) {
res.json(result)
return
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",
],
})
}
}
const order = await klarnaProviderService.cartToKlarnaOrder(cart)
res.json(order)
} else {
res.sendStatus(400)
return
}
} catch (error) {
throw error
}
}

View File

@@ -7,8 +7,9 @@ export default async (req, res) => {
const klarnaProviderService = req.scope.resolve("pp_klarna")
const shippingProfileService = req.scope.resolve("shippingProfileService")
const cart = await cartService.retrieve(merchant_data, {
select: ["subtotal"],
const cart = await cartService.retrieveWithTotals(
merchant_data,
{
relations: [
"shipping_address",
"billing_address",
@@ -20,7 +21,9 @@ export default async (req, res) => {
"items.variant",
"items.variant.product",
],
})
},
{ force_taxes: true }
)
let shippingOptions = await shippingProfileService.fetchCartOptions(cart)
shippingOptions = shippingOptions.filter(
@@ -35,16 +38,9 @@ export default async (req, res) => {
}
}
const newCart = await cartService.retrieve(cart.id, {
select: [
"gift_card_total",
"subtotal",
"total",
"shipping_total",
"tax_total",
"discount_total",
"subtotal",
],
const newCart = await cartService.retrieveWithTotals(
cart.id,
{
relations: [
"shipping_address",
"billing_address",
@@ -56,7 +52,9 @@ export default async (req, res) => {
"items.variant",
"items.variant.product",
],
})
},
{ force_taxes: true }
)
const order = await klarnaProviderService.cartToKlarnaOrder(newCart)

View File

@@ -5,7 +5,7 @@ import { PaymentService } from "medusa-interfaces"
class KlarnaProviderService extends PaymentService {
static identifier = "klarna"
constructor({ shippingProfileService, totalsService }, options) {
constructor({ logger, shippingProfileService, totalsService }, options) {
super()
/**
@@ -23,6 +23,7 @@ class KlarnaProviderService extends PaymentService {
* }
*/
this.options_ = options
this.logger_ = logger
/** @private @const {Klarna} */
this.klarna_ = axios.create({
@@ -53,22 +54,16 @@ class KlarnaProviderService extends PaymentService {
// Withdraw discount from the total item amount
const quantity = item.quantity
const totals = await this.totalsService_.getLineItemTotals(item, cart, {
include_tax: true,
})
const tax =
totals.tax_lines.reduce((acc, next) => acc + next.rate, 0) / 100
const tax = item.tax_lines.reduce((acc, next) => acc + next.rate, 0) / 100
order_lines.push({
name: item.title,
tax_rate: tax * 10000,
quantity,
unit_price: Math.round(totals.original_total / item.quantity),
total_amount: totals.total - totals.gift_card_total,
total_tax_amount: totals.tax_total,
total_discount_amount:
totals.original_total - totals.total + totals.gift_card_total,
unit_price: Math.round(item.original_total / item.quantity),
total_amount: item.total,
total_tax_amount: item.tax_total,
total_discount_amount: item.original_total - item.total,
})
}
@@ -79,23 +74,15 @@ class KlarnaProviderService extends PaymentService {
let taxRate = 0
if (cart.shipping_total > 0) {
for (const next of cart.shipping_methods) {
const totals = await this.totalsService_.getShippingMethodTotals(
next,
cart,
{
include_tax: true,
}
)
for (const method of cart.shipping_methods) {
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
taxRate += (totals.price / cart.shipping_total) * methodTaxRate
tax += totals.tax_total
total += method.total
taxRate += (method.price / cart.shipping_total) * methodTaxRate
tax += method.tax_total
}
}
@@ -124,16 +111,17 @@ class KlarnaProviderService extends PaymentService {
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({
name: `Gift Card`,
name: "Gift Card",
quantity: 1,
type: "gift_card",
unit_price: 0,
total_discount_amount: gift_card_total,
tax_rate: 0,
total_amount: -gift_card_total,
total_tax_amount: 0,
unit_price: -1 * (cart.gift_card_total + cart.gift_card_tax_total),
tax_rate: Math.round(taxRate * 10000),
total_amount: -1 * (cart.gift_card_total + cart.gift_card_tax_total),
total_tax_amount: -1 * cart.gift_card_tax_total,
})
}
@@ -159,7 +147,7 @@ class KlarnaProviderService extends PaymentService {
}
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.merchant_urls = {
@@ -183,21 +171,13 @@ class KlarnaProviderService extends PaymentService {
// If the cart does not have shipping methods yet, preselect one from
// shipping_options and set the selected shipping method
if (cart.shipping_methods.length) {
const shipping_method = cart.shipping_methods[0]
const totals = await this.totalsService_.getShippingMethodTotals(
shipping_method,
cart,
{
include_tax: true,
}
)
const taxRate = totals.tax_total / (totals.total - totals.tax_total)
const method = cart.shipping_methods[0]
const taxRate = method.tax_total / (method.total - method.tax_total)
order.selected_shipping_option = {
id: shipping_method.shipping_option.id,
name: shipping_method.shipping_option.name,
price: totals.total,
tax_amount: total.tax_total,
id: method.shipping_option.id,
name: method.shipping_option.name,
price: method.total,
tax_amount: method.tax_total,
tax_rate: taxRate * 10000,
}
}
@@ -286,6 +266,7 @@ class KlarnaProviderService extends PaymentService {
return klarnaPayment
} catch (error) {
this.logger_.error(error)
throw error
}
}

View File

@@ -72,7 +72,6 @@ describe("PaypalProviderService", () => {
const paypalProviderService = new PayPalProviderService(
{
regionService: RegionServiceMock,
totalsService: TotalsServiceMock,
},
{
api_key: "test",
@@ -98,7 +97,6 @@ describe("PaypalProviderService", () => {
const paypalProviderService = new PayPalProviderService(
{
regionService: RegionServiceMock,
totalsService: TotalsServiceMock,
},
{
api_key: "test",
@@ -149,7 +147,6 @@ describe("PaypalProviderService", () => {
const paypalProviderService = new PayPalProviderService(
{
regionService: RegionServiceMock,
totalsService: TotalsServiceMock,
},
{
api_key: "test",
@@ -196,7 +193,6 @@ describe("PaypalProviderService", () => {
const paypalProviderService = new PayPalProviderService(
{
regionService: RegionServiceMock,
totalsService: TotalsServiceMock,
},
{
api_key: "test",
@@ -275,7 +271,6 @@ describe("PaypalProviderService", () => {
const paypalProviderService = new PayPalProviderService(
{
regionService: RegionServiceMock,
totalsService: TotalsServiceMock,
},
{
api_key: "test",
@@ -349,9 +344,13 @@ describe("PaypalProviderService", () => {
data: { id: "test-voided" },
})
expect(PayPalMock.payments.AuthorizationsVoidRequest).not.toHaveBeenCalled()
expect(
PayPalMock.payments.AuthorizationsVoidRequest
).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(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.orders.OrdersGetRequest).toHaveBeenCalledWith("test-refund")
expect(PayPalMock.orders.OrdersGetRequest).toHaveBeenCalledWith(
"test-refund"
)
expect(PayPalClientMock.execute).toHaveBeenCalledTimes(1)
expect(result.id).toEqual("test-refund")

View File

@@ -12,7 +12,7 @@ function roundToTwo(num, currency) {
class PayPalProviderService extends PaymentService {
static identifier = "paypal"
constructor({ totalsService, regionService }, options) {
constructor({ regionService }, options) {
super()
/**
@@ -44,9 +44,6 @@ class PayPalProviderService extends PaymentService {
/** @private @const {RegionService} */
this.regionService_ = regionService
/** @private @const {TotalsService} */
this.totalsService_ = totalsService
}
/**
@@ -94,7 +91,7 @@ class PayPalProviderService extends PaymentService {
const { region_id } = cart
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()
request.requestBody({
@@ -289,7 +286,8 @@ class PayPalProviderService extends PaymentService {
async cancelPayment(payment) {
const order = await this.retrievePayment(payment.data)
const isAlreadyCanceled = order.status === "VOIDED"
const isCanceledAndFullyRefund = order.status === "COMPLETED" && !!order.invoice_id
const isCanceledAndFullyRefund =
order.status === "COMPLETED" && !!order.invoice_id
if (isAlreadyCanceled || isCanceledAndFullyRefund) {
return order
}

View File

@@ -135,9 +135,7 @@ class StripeProviderService extends AbstractPaymentService {
.withTransaction(this.manager_)
.retrieve(region_id)
const amount = await this.totalsService_
.withTransaction(this.manager_)
.getTotal(cart)
const amount = cart.total
const intentRequest = {
description: